mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: Provider 异步删除、可配置密码策略、Hub 超时优化及多项改进
- 新增 Provider 异步删除任务系统,后台分阶段删除子资源并清理残留引用 - 新增可配置密码策略等级(weak/medium/strong),支持系统设置面板调整 - aether-hub 升级至 0.1.4,idle timeout 支持禁用(设为 0),worker 默认超时调整为 120s - OAuth 手动续期增加 Redis 分布式锁,防止并发刷新冲突 - ProxyNode 心跳检测改为 asyncio.to_thread,避免阻塞事件循环 - 删除 ModelMultiSelect 和 useInvalidModels,MultiSelect 组件通用化 - 明确 allowed_providers/allowed_api_formats 的 NULL 与空数组语义 - 前端 StandaloneKeyFormDialog、UserFormDialog 等多处 UI 优化 - 新增 Alembic 迁移脚本清理 Provider 删除后的残留引用 - 补充相关测试用例
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { OAuthOrganizationInfo } from '@/api/endpoints/types/provider'
|
||||
|
||||
export function formatOAuthIdentityShort(
|
||||
function formatOAuthIdentityShort(
|
||||
value: string | null | undefined,
|
||||
head = 8,
|
||||
tail = 6,
|
||||
@@ -11,33 +11,24 @@ export function formatOAuthIdentityShort(
|
||||
return `${normalized.slice(0, head)}...${normalized.slice(-tail)}`
|
||||
}
|
||||
|
||||
export function getPrimaryOAuthOrganizationTitle(
|
||||
function getPrimaryOAuthOrganizationId(
|
||||
value: { oauth_organizations?: OAuthOrganizationInfo[] | null } | null | undefined,
|
||||
): string | null {
|
||||
const organizations = Array.isArray(value?.oauth_organizations) ? value.oauth_organizations : []
|
||||
const defaultOrg = organizations.find(
|
||||
(org) => org?.is_default && typeof org?.title === 'string' && org.title.trim(),
|
||||
(org) => org?.is_default && typeof org?.id === 'string' && org.id.trim(),
|
||||
)
|
||||
if (defaultOrg?.title) return defaultOrg.title.trim()
|
||||
const firstWithTitle = organizations.find(
|
||||
(org) => typeof org?.title === 'string' && org.title.trim(),
|
||||
if (defaultOrg?.id) return defaultOrg.id.trim()
|
||||
const firstWithId = organizations.find(
|
||||
(org) => typeof org?.id === 'string' && org.id.trim(),
|
||||
)
|
||||
return firstWithTitle?.title?.trim() || null
|
||||
return firstWithId?.id?.trim() || null
|
||||
}
|
||||
|
||||
export function getOAuthOrganizationsTooltip(
|
||||
export function getOAuthOrgBadge(
|
||||
value: { oauth_organizations?: OAuthOrganizationInfo[] | null } | null | undefined,
|
||||
): string {
|
||||
const organizations = Array.isArray(value?.oauth_organizations) ? value.oauth_organizations : []
|
||||
if (organizations.length === 0) return ''
|
||||
return organizations
|
||||
.map((org) => {
|
||||
const title =
|
||||
typeof org?.title === 'string' && org.title.trim() ? org.title.trim() : '未命名组织'
|
||||
const role =
|
||||
typeof org?.role === 'string' && org.role.trim() ? ` (${org.role.trim()})` : ''
|
||||
const suffix = org?.is_default ? ' [default]' : ''
|
||||
return `${title}${role}${suffix}`
|
||||
})
|
||||
.join('\n')
|
||||
): { id: string; label: string } | null {
|
||||
const id = getPrimaryOAuthOrganizationId(value)
|
||||
if (!id) return null
|
||||
return { id, label: formatOAuthIdentityShort(id) }
|
||||
}
|
||||
|
||||
98
frontend/src/utils/passwordPolicy.ts
Normal file
98
frontend/src/utils/passwordPolicy.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
export type PasswordPolicyLevel = 'weak' | 'medium' | 'strong'
|
||||
|
||||
export const PASSWORD_POLICY_OPTIONS: Array<{
|
||||
value: PasswordPolicyLevel
|
||||
label: string
|
||||
description: string
|
||||
}> = [
|
||||
{
|
||||
value: 'weak',
|
||||
label: '弱密码',
|
||||
description: '至少 6 个字符',
|
||||
},
|
||||
{
|
||||
value: 'medium',
|
||||
label: '中等密码',
|
||||
description: '至少 8 个字符,且包含字母和数字',
|
||||
},
|
||||
{
|
||||
value: 'strong',
|
||||
label: '强密码',
|
||||
description: '至少 8 个字符,且包含大小写字母、数字和特殊字符',
|
||||
},
|
||||
]
|
||||
|
||||
export function normalizePasswordPolicyLevel(value: unknown): PasswordPolicyLevel {
|
||||
if (value === 'medium' || value === 'strong') {
|
||||
return value
|
||||
}
|
||||
return 'weak'
|
||||
}
|
||||
|
||||
export function getPasswordPolicyHint(level: unknown): string {
|
||||
switch (normalizePasswordPolicyLevel(level)) {
|
||||
case 'medium':
|
||||
return '至少 8 个字符,且需包含字母和数字'
|
||||
case 'strong':
|
||||
return '至少 8 个字符,且需包含大写字母、小写字母、数字和特殊字符'
|
||||
case 'weak':
|
||||
default:
|
||||
return '至少 6 个字符'
|
||||
}
|
||||
}
|
||||
|
||||
export function getPasswordPolicyPlaceholder(level: unknown): string {
|
||||
switch (normalizePasswordPolicyLevel(level)) {
|
||||
case 'medium':
|
||||
return '至少 8 位,含字母和数字'
|
||||
case 'strong':
|
||||
return '至少 8 位,含大小写字母、数字和特殊字符'
|
||||
case 'weak':
|
||||
default:
|
||||
return '至少 6 个字符'
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePasswordByPolicy(password: string, level: unknown): string {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const normalized = normalizePasswordPolicyLevel(level)
|
||||
|
||||
if (password.length < 6) {
|
||||
return '密码长度至少为6个字符'
|
||||
}
|
||||
|
||||
if (normalized === 'medium') {
|
||||
if (password.length < 8) {
|
||||
return '密码长度至少为8个字符'
|
||||
}
|
||||
if (!/[A-Za-z]/.test(password)) {
|
||||
return '密码必须包含至少一个字母'
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return '密码必须包含至少一个数字'
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized === 'strong') {
|
||||
if (password.length < 8) {
|
||||
return '密码长度至少为8个字符'
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return '密码必须包含至少一个大写字母'
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return '密码必须包含至少一个小写字母'
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return '密码必须包含至少一个数字'
|
||||
}
|
||||
if (!/[!@#$%^&*()_+\-=[\]{};:'",.<>?/\\|`~]/.test(password)) {
|
||||
return '密码必须包含至少一个特殊字符'
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
Reference in New Issue
Block a user