diff --git a/backend/package.json b/backend/package.json index ffc3fe1..369b22a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,4 +20,4 @@ "overrides": { "esbuild": ">=0.25.0" } -} \ No newline at end of file +} diff --git a/backend/src/features/auth/authRoutes.ts b/backend/src/features/auth/authRoutes.ts index f7af343..8eac1c1 100644 --- a/backend/src/features/auth/authRoutes.ts +++ b/backend/src/features/auth/authRoutes.ts @@ -62,7 +62,7 @@ auth.post('/callback/:provider', async (c) => { const clientState = body.state; // 前端传回的回调 state 参数 if (!serverState || !clientState || serverState !== clientState) { - throw new AppError('Invalid or missing OAuth state. Security validation failed.', 403); + throw new AppError('oauth_state_invalid', 403); } // 校验通过,清理一次性 State Cookie diff --git a/backend/src/features/auth/authService.ts b/backend/src/features/auth/authService.ts index e6fa1aa..894a899 100644 --- a/backend/src/features/auth/authService.ts +++ b/backend/src/features/auth/authService.ts @@ -41,7 +41,7 @@ export class AuthService { }); params = searchParams; } else { - if (!body.code) throw new AppError('Missing OAuth code', 400); + if (!body.code) throw new AppError('oauth_code_missing', 400); params = body.code; } @@ -104,11 +104,11 @@ export class AuthService { } if (!isAllowed) { - throw new AppError(`Unauthorized user. Email: ${userEmail}, Username: ${userName}`, 403); + throw new AppError('unauthorized_user', 403); } } else { // 默认安全策略:如果未配置白名单,未开启 ALLOW_ALL,则拒绝所有人(或者也可以记录警告,但为了安全通常应拒绝) - throw new AppError(`No authorization whitelist configured. Login denied.`, 403); + throw new AppError('not_whitelisted', 403); } } @@ -127,7 +127,7 @@ export class AuthService { }; if (!this.env.JWT_SECRET) { - throw new AppError('Server configuration error: JWT_SECRET is missing', 500); + throw new AppError('missing_jwt_secret', 500); } return await generateSecureJWT(payload, this.env.JWT_SECRET); diff --git a/backend/src/features/auth/providers/cloudflareAccessProvider.ts b/backend/src/features/auth/providers/cloudflareAccessProvider.ts index 8d2aee0..c7b59a3 100644 --- a/backend/src/features/auth/providers/cloudflareAccessProvider.ts +++ b/backend/src/features/auth/providers/cloudflareAccessProvider.ts @@ -43,7 +43,7 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { private getBaseUrl(): string { const orgDomain = this.env.OAUTH_CLOUDFLARE_ORG_DOMAIN; if (!orgDomain) { - throw new AppError('Cloudflare Access configuration incomplete: Missing OAUTH_CLOUDFLARE_ORG_DOMAIN', 500); + throw new AppError('Cloudflare Access 配置不完整:缺少 OAUTH_CLOUDFLARE_ORG_DOMAIN', 500); } let baseUrl = orgDomain.replace(/\/$/, ''); @@ -58,7 +58,7 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_CLOUDFLARE_REDIRECT_URI; if (!clientId || !redirectUri) { - throw new AppError('Cloudflare Access configuration incomplete', 500); + throw new AppError('Cloudflare Access 配置不完整', 500); } const baseUrl = this.getBaseUrl(); @@ -83,7 +83,7 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { const code = typeof params === 'string' ? params : params.get('code'); if (!code) { - throw new AppError('Cloudflare Access OAuth callback missing code', 400); + throw new AppError('oauth_code_missing', 400); } const clientId = this.env.OAUTH_CLOUDFLARE_CLIENT_ID; @@ -91,7 +91,7 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_CLOUDFLARE_REDIRECT_URI; if (!clientId || !clientSecret || !redirectUri) { - throw new AppError('Cloudflare Access configuration incomplete', 500); + throw new AppError('Cloudflare Access 配置不完整', 500); } const baseUrl = this.getBaseUrl(); @@ -116,7 +116,7 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { // 增加更详细的错误日志,方便调试 const errorText = await tokenResponse.text(); console.error(`Cloudflare Access Token Error: ${tokenResponse.status} - ${errorText}`); - throw new AppError(`Cloudflare Access Token Exchange failed: ${tokenResponse.status}`, 502); + throw new AppError(`oauth_token_exchange_failed: Cloudflare Access | ${tokenResponse.status}`, 502); } const tokenData: any = await tokenResponse.json(); @@ -128,7 +128,7 @@ export class CloudflareAccessProvider extends BaseOAuthProvider { headers: { 'Authorization': `Bearer ${accessToken}` } }); - if (!userResponse.ok) throw new AppError(`Cloudflare Access API error: ${userResponse.status}`, 502); + if (!userResponse.ok) throw new AppError(`oauth_api_error: Cloudflare Access | ${userResponse.status}`, 502); const userData: any = await userResponse.json(); return { diff --git a/backend/src/features/auth/providers/giteeProvider.ts b/backend/src/features/auth/providers/giteeProvider.ts index bbb8a35..a15aa8b 100644 --- a/backend/src/features/auth/providers/giteeProvider.ts +++ b/backend/src/features/auth/providers/giteeProvider.ts @@ -17,7 +17,7 @@ export class GiteeProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_GITEE_REDIRECT_URI; if (!clientId || !redirectUri) { - throw new AppError('Gitee OAuth configuration incomplete', 500); + throw new AppError('oauth_config_incomplete', 500); } const params = new URLSearchParams({ @@ -36,7 +36,7 @@ export class GiteeProvider extends BaseOAuthProvider { const code = typeof params === 'string' ? params : params.get('code'); if (!code) { - throw new AppError('Gitee OAuth callback missing code', 400); + throw new AppError('oauth_code_missing', 400); } const clientId = this.env.OAUTH_GITEE_CLIENT_ID; @@ -44,7 +44,7 @@ export class GiteeProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_GITEE_REDIRECT_URI; if (!clientId || !clientSecret || !redirectUri) { - throw new AppError('Gitee OAuth configuration incomplete', 500); + throw new AppError('oauth_config_incomplete', 500); } // 1. Exchange Code for Token @@ -64,7 +64,7 @@ export class GiteeProvider extends BaseOAuthProvider { }); if (!tokenResponse.ok) { - throw new AppError(`Gitee Token Exchange failed: ${tokenResponse.status}`, 502); + throw new AppError(`oauth_token_exchange_failed: Gitee | ${tokenResponse.status}`, 502); } const tokenData: any = await tokenResponse.json(); @@ -72,7 +72,7 @@ export class GiteeProvider extends BaseOAuthProvider { // 2. Get User Info const userResponse = await fetch(`https://gitee.com/api/v5/user?access_token=${accessToken}`); - if (!userResponse.ok) throw new AppError(`Gitee API error: ${userResponse.status}`, 502); + if (!userResponse.ok) throw new AppError(`oauth_api_error: Gitee | ${userResponse.status}`, 502); const userData: any = await userResponse.json(); // 3. Get Email (if not in basic profile) diff --git a/backend/src/features/auth/providers/githubProvider.ts b/backend/src/features/auth/providers/githubProvider.ts index 1c1268e..c970615 100644 --- a/backend/src/features/auth/providers/githubProvider.ts +++ b/backend/src/features/auth/providers/githubProvider.ts @@ -31,7 +31,7 @@ export class GitHubProvider extends BaseOAuthProvider { const code = typeof params === 'string' ? params : params.get('code'); if (!code) { - throw new AppError('GitHub OAuth callback missing code', 400); + throw new AppError('oauth_code_missing', 400); } // 1. 使用 Code 换取 Access Token @@ -51,13 +51,13 @@ export class GitHubProvider extends BaseOAuthProvider { }); if (!tokenResponse.ok) { - throw new AppError(`GitHub Token Exchange failed: ${tokenResponse.status}`, 502); + throw new AppError(`oauth_token_exchange_failed: GitHub | ${tokenResponse.status}`, 502); } const tokenData: any = await tokenResponse.json(); if (tokenData.error) { - throw new AppError(`GitHub OAuth Error: ${tokenData.error_description || tokenData.error}`, 400); + throw new AppError(`oauth_token_exchange_failed: GitHub | ${tokenData.error_description || tokenData.error}`, 400); } const accessToken = tokenData.access_token; @@ -71,7 +71,7 @@ export class GitHubProvider extends BaseOAuthProvider { } }); - if (!userResponse.ok) throw new AppError(`GitHub API error: ${userResponse.status}`, 502); + if (!userResponse.ok) throw new AppError(`oauth_api_error: GitHub | ${userResponse.status}`, 502); const userData: any = await userResponse.json(); // 3. 补充获取 Email (如果公开资料中没有) diff --git a/backend/src/features/auth/providers/googleProvider.ts b/backend/src/features/auth/providers/googleProvider.ts index d959a9e..62bde51 100644 --- a/backend/src/features/auth/providers/googleProvider.ts +++ b/backend/src/features/auth/providers/googleProvider.ts @@ -17,7 +17,7 @@ export class GoogleProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_GOOGLE_REDIRECT_URI; if (!clientId || !redirectUri) { - throw new AppError('Google OAuth configuration incomplete', 500); + throw new AppError('oauth_config_incomplete', 500); } const params = new URLSearchParams({ @@ -37,7 +37,7 @@ export class GoogleProvider extends BaseOAuthProvider { const code = typeof params === 'string' ? params : params.get('code'); if (!code) { - throw new AppError('Google OAuth callback missing code', 400); + throw new AppError('oauth_code_missing', 400); } const clientId = this.env.OAUTH_GOOGLE_CLIENT_ID; @@ -45,7 +45,7 @@ export class GoogleProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_GOOGLE_REDIRECT_URI; if (!clientId || !clientSecret || !redirectUri) { - throw new AppError('Google OAuth configuration incomplete', 500); + throw new AppError('oauth_config_incomplete', 500); } // 1. Exchange Code for Token @@ -65,7 +65,7 @@ export class GoogleProvider extends BaseOAuthProvider { if (!tokenResponse.ok) { const errText = await tokenResponse.text(); - throw new AppError(`Google Token Exchange failed: ${tokenResponse.status} - ${errText}`, 502); + throw new AppError(`oauth_token_exchange_failed: Google | ${tokenResponse.status} - ${errText}`, 502); } const tokenData: any = await tokenResponse.json(); @@ -78,7 +78,7 @@ export class GoogleProvider extends BaseOAuthProvider { } }); - if (!userResponse.ok) throw new AppError(`Google API error: ${userResponse.status}`, 502); + if (!userResponse.ok) throw new AppError(`oauth_api_error: Google | ${userResponse.status}`, 502); const userData: any = await userResponse.json(); return { diff --git a/backend/src/features/auth/providers/index.ts b/backend/src/features/auth/providers/index.ts index e04a346..c504a33 100644 --- a/backend/src/features/auth/providers/index.ts +++ b/backend/src/features/auth/providers/index.ts @@ -22,13 +22,13 @@ export function getOAuthProvider(providerId: string, env: EnvBindings): BaseOAut case 'google': return new GoogleProvider(env); default: - throw new AppError(`Provider '${providerId}' is not supported`, 400); + throw new AppError(`unsupported_provider: ${providerId}`, 400); } } export function getAvailableProviders(env: EnvBindings) { const providers = []; - + const githubProvider = new GitHubProvider(env); const cloudflareProvider = new CloudflareAccessProvider(env); const nodelocProvider = new NodeLocProvider(env); diff --git a/backend/src/features/auth/providers/nodeLocProvider.ts b/backend/src/features/auth/providers/nodeLocProvider.ts index 347dee0..deb98e8 100644 --- a/backend/src/features/auth/providers/nodeLocProvider.ts +++ b/backend/src/features/auth/providers/nodeLocProvider.ts @@ -17,7 +17,7 @@ export class NodeLocProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_NODELOC_REDIRECT_URI; if (!clientId || !redirectUri) { - throw new AppError('NodeLoc OAuth configuration incomplete', 500); + throw new AppError('oauth_config_incomplete', 500); } const params = new URLSearchParams({ @@ -36,7 +36,7 @@ export class NodeLocProvider extends BaseOAuthProvider { const code = typeof params === 'string' ? params : params.get('code'); if (!code) { - throw new AppError('NodeLoc OAuth callback missing code', 400); + throw new AppError('oauth_code_missing', 400); } const clientId = this.env.OAUTH_NODELOC_CLIENT_ID; @@ -44,7 +44,7 @@ export class NodeLocProvider extends BaseOAuthProvider { const redirectUri = this.env.OAUTH_NODELOC_REDIRECT_URI; if (!clientId || !clientSecret || !redirectUri) { - throw new AppError('NodeLoc OAuth configuration incomplete', 500); + throw new AppError('oauth_config_incomplete', 500); } // 1. Exchange Code for Token @@ -64,7 +64,7 @@ export class NodeLocProvider extends BaseOAuthProvider { }); if (!tokenResponse.ok) { - throw new AppError(`NodeLoc Token Exchange failed: ${tokenResponse.status}`, 502); + throw new AppError(`oauth_token_exchange_failed: NodeLoc | ${tokenResponse.status}`, 502); } const tokenData: any = await tokenResponse.json(); @@ -78,7 +78,7 @@ export class NodeLocProvider extends BaseOAuthProvider { } }); - if (!userResponse.ok) throw new AppError(`NodeLoc API error: ${userResponse.status}`, 502); + if (!userResponse.ok) throw new AppError(`oauth_api_error: NodeLoc | ${userResponse.status}`, 502); const userData: any = await userResponse.json(); return { diff --git a/backend/src/features/auth/providers/telegramProvider.ts b/backend/src/features/auth/providers/telegramProvider.ts index f4ac8a0..b168acb 100644 --- a/backend/src/features/auth/providers/telegramProvider.ts +++ b/backend/src/features/auth/providers/telegramProvider.ts @@ -16,7 +16,7 @@ export class TelegramProvider extends BaseOAuthProvider { // 例如:https://t.me/MyAuthBot?start=state_value getAuthorizeUrl(state: string) { const botName = this.env.OAUTH_TELEGRAM_BOT_NAME; - if (!botName) throw new AppError('Telegram Bot Name not configured', 500); + if (!botName) throw new AppError('telegram_missing_bot_name', 500); // 注意:Telegram start 参数只允许 [A-Za-z0-9_-],UUID 通常符合,但最好移除连字符以防万一 // 这里我们直接使用 state,但在前端生成 state 时最好确保它是 URL 安全的 @@ -25,15 +25,15 @@ export class TelegramProvider extends BaseOAuthProvider { async handleCallback(params: string | URLSearchParams, _codeVerifier?: string): Promise { if (typeof params === 'string') { - throw new AppError('Telegram provider requires full query parameters, not just code', 400); + throw new AppError('telegram_missing_query', 400); } const botToken = this.env.OAUTH_TELEGRAM_BOT_TOKEN; - if (!botToken) throw new AppError('Telegram Bot Token not configured', 500); + if (!botToken) throw new AppError('telegram_missing_bot_token', 500); // 1. 提取并验证必要字段 const hash = params.get('hash'); - if (!hash) throw new AppError('Missing hash signature', 400); + if (!hash) throw new AppError('telegram_missing_hash', 400); const dataCheckArr: string[] = []; const allowedKeys = ['auth_date', 'first_name', 'id', 'last_name', 'photo_url', 'username']; @@ -69,14 +69,14 @@ export class TelegramProvider extends BaseOAuthProvider { ); if (!isValid) { - throw new AppError('Telegram signature verification failed', 403); + throw new AppError('telegram_signature_failed', 403); } // 4. 检查时效性 (Telegram 建议检查 auth_date) const authDate = parseInt(params.get('auth_date') || '0'); const now = Math.floor(Date.now() / 1000); if (now - authDate > 86400) { // 24小时过期 - throw new AppError('Telegram login data expired', 401); + throw new AppError('telegram_login_expired', 401); } // 5. 返回用户信息 diff --git a/backend/src/features/backup/backupService.ts b/backend/src/features/backup/backupService.ts index 34ec603..37fe9e0 100644 --- a/backend/src/features/backup/backupService.ts +++ b/backend/src/features/backup/backupService.ts @@ -24,7 +24,7 @@ export class BackupService { case 's3': return new S3Provider(config); default: - throw new AppError(`Unknown provider type: ${type}`, 400); + throw new AppError('provider_not_found', 400); } } @@ -77,17 +77,17 @@ export class BackupService { async addProvider(data: any): Promise { const { type, name, config, autoBackup, autoBackupPassword, autoBackupRetain } = data; - if (!type || !name || !config) throw new AppError('Missing required fields', 400); + if (!type || !name || !config) throw new AppError('missing_fields', 400); const key = this.env.ENCRYPTION_KEY || this.env.JWT_SECRET; const encryptedConfig = await this.processConfigForStorage(type, config, key); let encryptedAutoBackupPwd = null; if (autoBackup && autoBackupPassword) { - if (autoBackupPassword.length < 12) throw new AppError('Auto-backup password must be at least 12 characters', 400); + if (autoBackupPassword.length < 12) throw new AppError('backup_password_length', 400); encryptedAutoBackupPwd = JSON.stringify(await encryptData(autoBackupPassword, key)); } else if (autoBackup) { - throw new AppError('Auto-backup password is required (min 12 chars)', 400); + throw new AppError('backup_password_required', 400); } const res = await this.db.insert(backupProviders).values({ @@ -126,10 +126,10 @@ export class BackupService { let finalAutoPwd = current?.autoBackupPassword; if (autoBackupPassword) { - if (autoBackupPassword.length < 12) throw new AppError('Auto-backup password must be at least 12 characters', 400); + if (autoBackupPassword.length < 12) throw new AppError('backup_password_length', 400); finalAutoPwd = JSON.stringify(await encryptData(autoBackupPassword, key)); } else if (autoBackup && !finalAutoPwd) { - throw new AppError('Auto-backup password is required (min 12 chars)', 400); + throw new AppError('backup_password_required', 400); } await this.db.update(backupProviders).set({ @@ -167,7 +167,7 @@ export class BackupService { const provider = await this.getProvider(type, config); await provider.testConnection(); } catch (e: any) { - throw new AppError(`Connection failed: ${e.message}`, 400); + throw new AppError(`connection_failed: ${e.message}`, 400); } } @@ -186,13 +186,13 @@ export class BackupService { if (password === '') { if (!providerRow || !providerRow.autoBackupPassword) { - throw new AppError('Manual backup with auto-password requires auto-backup to be configured with a password.', 400); + throw new AppError('manual_backup_password_needed', 400); } backupPassword = await decryptData(JSON.parse(providerRow.autoBackupPassword), key); } if (!backupPassword || backupPassword.length < 12) { - throw new AppError('Backup password must be at least 12 characters.', 400); + throw new AppError('backup_password_length', 400); } const vaultResults = await this.db.select().from(vaultTable).all(); @@ -223,11 +223,11 @@ export class BackupService { finalFilename = `2fa-backup-manual-${new Date().toISOString().replace(/[:.]/g, '-')}.json`; finalContent = fileContent; } else { - throw new AppError('Request must include {filename, content} or {password}', 400); + throw new AppError('missing_fields', 400); } const providerRow = await this.db.select().from(backupProviders).where(eq(backupProviders.id, id)).get(); - if (!providerRow) throw new AppError('Provider not found', 404); + if (!providerRow) throw new AppError('provider_not_found', 404); const configObj = await this.processConfigForUsage(providerRow.type, providerRow.config, key); const provider = await this.getProvider(providerRow.type, configObj); @@ -240,13 +240,13 @@ export class BackupService { }).where(eq(backupProviders.id, id)).run(); } catch (e: any) { await this.db.update(backupProviders).set({ lastBackupStatus: 'failed' }).where(eq(backupProviders.id, id)).run(); - throw new AppError(`Backup failed: ${e.message}`, 500); + throw new AppError(`backup_failed: ${e.message}`, 500); } } async getFiles(id: number) { const providerRow = await this.db.select().from(backupProviders).where(eq(backupProviders.id, id)).get(); - if (!providerRow) throw new AppError('Provider not found', 404); + if (!providerRow) throw new AppError('provider_not_found', 404); const key = this.env.ENCRYPTION_KEY || this.env.JWT_SECRET; const config = await this.processConfigForUsage(providerRow.type, providerRow.config, key); @@ -256,10 +256,10 @@ export class BackupService { } async downloadFile(id: number, filename: string) { - if (!filename) throw new AppError('Filename is required', 400); + if (!filename) throw new AppError('filename_required', 400); const providerRow = await this.db.select().from(backupProviders).where(eq(backupProviders.id, id)).get(); - if (!providerRow) throw new AppError('Provider not found', 404); + if (!providerRow) throw new AppError('provider_not_found', 404); const key = this.env.ENCRYPTION_KEY || this.env.JWT_SECRET; const config = await this.processConfigForUsage(providerRow.type, providerRow.config, key); @@ -268,7 +268,7 @@ export class BackupService { try { return await provider.downloadBackup(filename); } catch (e: any) { - throw new AppError(`Download failed: ${e.message}`, 500); + throw new AppError(`download_failed: ${e.message}`, 500); } } diff --git a/backend/src/features/vault/vaultService.ts b/backend/src/features/vault/vaultService.ts index 3bf0445..e8b74c9 100644 --- a/backend/src/features/vault/vaultService.ts +++ b/backend/src/features/vault/vaultService.ts @@ -14,7 +14,7 @@ export class VaultService { this.repository = repository; if (!env.ENCRYPTION_KEY) { - throw new AppError('FATAL ERROR: ENCRYPTION_KEY is not configured.', 500); + throw new AppError('missing_encryption_key', 500); } this.encryptionKey = env.ENCRYPTION_KEY; @@ -55,14 +55,14 @@ export class VaultService { */ // normalize a service+account pair for comparison private normalizeSignature(service: string, account: string) { - return `${(service || '').toString().trim().toLowerCase()}:${(account||'').toString().trim().toLowerCase()}`; + return `${(service || '').toString().trim().toLowerCase()}:${(account || '').toString().trim().toLowerCase()}`; } async createAccount(userId: string, data: any) { let { service, account, category, secret, digits, period, algorithm } = data; if (!service || !account || !secret || !validateBase32Secret(secret)) { - throw new AppError('Invalid account data or secret format', 400); + throw new AppError('invalid_secret_format', 400); } // 入库清洗:去掉 account 中可能带有的 Issuer: 前缀 @@ -73,7 +73,7 @@ export class VaultService { // duplicate check (case‑insensitive & trimmed) using repository helper const existing = await this.repository.findByServiceAccount(service, account); if (existing) { - throw new AppError('Account already exists', 409); + throw new AppError('account_exists', 409); } const normalizedSecret = secret.replace(/\s/g, '').toUpperCase(); @@ -100,7 +100,7 @@ export class VaultService { let { service, account, category, secret, digits, period, algorithm } = data; if (!service || !account) { - throw new AppError('Service and account are required', 400); + throw new AppError('missing_service_account', 400); } // 入库清洗:去掉 account 中可能带有的 Issuer: 前缀 @@ -112,14 +112,14 @@ export class VaultService { let encryptedSecret: string; if (secret) { if (!validateBase32Secret(secret)) { - throw new AppError('Invalid secret format (must be Base32, min 16 chars)', 400); + throw new AppError('invalid_secret_format', 400); } const normalizedSecret = secret.replace(/\s/g, '').toUpperCase(); encryptedSecret = await encryptField(normalizedSecret, this.encryptionKey); } else { // 取出现有记录,复用已有加密值 const existing = await this.repository.findById(id); - if (!existing) throw new AppError('Account not found', 404); + if (!existing) throw new AppError('account_not_found', 404); encryptedSecret = existing.secret; } @@ -133,7 +133,7 @@ export class VaultService { ...(period && { period }), }); - if (!updated) throw new AppError('Account not found', 404); + if (!updated) throw new AppError('account_not_found', 404); return updated; } @@ -142,11 +142,11 @@ export class VaultService { */ async deleteAccount(id: string) { const success = await this.repository.delete(id); - if (!success) throw new AppError('Account not found', 404); + if (!success) throw new AppError('account_not_found', 404); } async batchDeleteAccounts(ids: string[]) { - if (!ids || ids.length === 0) throw new AppError('No IDs provided', 400); + if (!ids || ids.length === 0) throw new AppError('no_account_ids', 400); const count = await this.repository.batchDelete(ids); return { count }; } @@ -157,12 +157,12 @@ export class VaultService { async exportAccounts(type: string, password?: string) { const SECURITY_CONFIG = { MIN_EXPORT_PASSWORD_LENGTH: 5 }; if (!['encrypted', 'json', '2fas', 'text'].includes(type)) { - throw new AppError('Invalid export type', 400); + throw new AppError('export_type_invalid', 400); } if (type === 'encrypted') { if (!password || password.length < SECURITY_CONFIG.MIN_EXPORT_PASSWORD_LENGTH) { - throw new AppError(`导出密码至少需要 ${SECURITY_CONFIG.MIN_EXPORT_PASSWORD_LENGTH} 个字符`, 400); + throw new AppError('export_password_length', 400); } } @@ -208,20 +208,20 @@ export class VaultService { return { data: lines.join('\n'), isText: true }; } - throw new AppError('Unhandled export type', 500); + throw new AppError('export_type_invalid', 500); } /** * 处理导入 */ async importAccounts(userId: string, type: string, content: string, password?: string) { - if (!content || !type) throw new AppError('内容和类型不能为空', 400); + if (!content || !type) throw new AppError('missing_content_type', 400); let validAccounts: any[] = []; try { if (type === 'encrypted') { - if (!password) throw new AppError('加密导入需要密码', 400); + if (!password) throw new AppError('import_password_required', 400); const encryptedFile = JSON.parse(content); const decryptedData = await decryptData(encryptedFile.data, password); validAccounts = decryptedData.accounts || []; @@ -305,7 +305,7 @@ export class VaultService { } } } catch (e) { - throw new AppError('解析失败,请检查文件格式或密码是否正确', 400); + throw new AppError('parse_failed', 400); } if (type === 'raw') validAccounts = JSON.parse(content); diff --git a/backend/src/shared/middleware/auth.ts b/backend/src/shared/middleware/auth.ts index 5cec53b..877ac74 100644 --- a/backend/src/shared/middleware/auth.ts +++ b/backend/src/shared/middleware/auth.ts @@ -7,21 +7,21 @@ export async function authMiddleware(c: Context<{ Bindings: EnvBindings, Variabl // 1. 从 Cookie 获取 JWT const token = getCookie(c, 'auth_token'); if (!token) { - throw new AppError('Unauthorized: Missing session cookie', 401); + throw new AppError('no_session', 401); } // 2. CSRF 校验 (Double Submit Cookie) const csrfCookie = getCookie(c, 'csrf_token'); const csrfHeader = c.req.header('X-CSRF-Token'); if (!csrfCookie || !csrfHeader || csrfCookie !== csrfHeader) { - throw new AppError('Forbidden: CSRF token mismatch', 403); + throw new AppError('csrf_mismatch', 403); } // 3. 验证 JWT const payload = await verifySecureJWT(token, c.env.JWT_SECRET); if (!payload || !payload.userInfo) { - throw new AppError('Unauthorized: Token expired or invalid', 401); + throw new AppError('token_expired', 401); } // 验证通过,将用户信息挂载到上下文,供后续路由使用 diff --git a/frontend/package.json b/frontend/package.json index e2bc54f..b1d7e8e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "scrypt-js": "^3.0.1", "sql.js": "^1.10.3", "vue": "^3.5.25", + "vue-i18n": "^9.14.5", "vue-router": "^5.0.3", "wa-sqlite": "^1.0.0" }, diff --git a/frontend/src/app/app.vue b/frontend/src/app/app.vue index 9ee8454..c862b68 100644 --- a/frontend/src/app/app.vue +++ b/frontend/src/app/app.vue @@ -5,6 +5,15 @@ import { Warning } from '@element-plus/icons-vue' import TheHeader from '@/shared/components/theHeader.vue' import TheFooter from '@/shared/components/theFooter.vue' import { useLayoutStore } from '@/shared/stores/layoutStore' +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import en from 'element-plus/es/locale/lang/en' + +const { locale } = useI18n() +const elementLocale = computed(() => { + return locale.value === 'zh-CN' ? zhCn : en +}) const route = useRoute() const layoutStore = useLayoutStore() @@ -33,10 +42,11 @@ onBeforeUnmount(() => { diff --git a/frontend/src/features/auth/composables/useOAuthCallback.js b/frontend/src/features/auth/composables/useOAuthCallback.js index 5576913..6fdcfa5 100644 --- a/frontend/src/features/auth/composables/useOAuthCallback.js +++ b/frontend/src/features/auth/composables/useOAuthCallback.js @@ -1,4 +1,4 @@ -import { ref, onMounted } from 'vue' +import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' import { useAuthUserStore } from '@/features/auth/store/authUserStore' import { setIdbItem } from '@/shared/utils/idb' @@ -10,8 +10,9 @@ import { authService } from '@/features/auth/service/authService' export function useOAuthCallback() { const route = useRoute() const router = useRouter() + const { t } = useI18n() const errorMsg = ref('') - const providerName = ref('身份提供商') + const providerName = ref(t('auth.default_provider')) onMounted(async () => { const code = route.query.code @@ -43,7 +44,7 @@ export function useOAuthCallback() { } if (error) { - errorMsg.value = route.query.error_description || `您拒绝了 ${providerName.value} 的授权请求` + errorMsg.value = route.query.error_description || t('auth.oauth_declined', { provider: providerName.value }) return } @@ -52,21 +53,21 @@ export function useOAuthCallback() { if (providerId === 'telegram') { if (!hash) { - errorMsg.value = 'Telegram 登录缺少签名参数' + errorMsg.value = t('auth.telegram_missing_hash') return } const savedState = localStorage.getItem('oauth_state') payload = { ...route.query, state: savedState } } else { if (!code || !state) { - errorMsg.value = 'URL 缺少必要的授权凭证参数' + errorMsg.value = t('auth.missing_auth_params') return } // 防御 CSRF:必须检查 State 匹不匹配 const savedState = localStorage.getItem('oauth_state') if (!savedState || savedState !== state) { - errorMsg.value = '安全警告:State 校验失败,请求可能被篡改' + errorMsg.value = t('auth.state_mismatch') return } @@ -103,12 +104,12 @@ export function useOAuthCallback() { await authUserStore.fetchUserInfo() router.push('/') } else { - errorMsg.value = data.error || '登录验证被后端拒绝' + errorMsg.value = data.error || t('auth.login_rejected') } } catch (err) { console.error('OAuth Callback Error:', err) // Error Managed by request interceptor and authError mapping - errorMsg.value = err.message || '网络请求异常,请稍后再试' + errorMsg.value = err.message || t('auth.network_abnormal') } }) diff --git a/frontend/src/features/auth/views/login.vue b/frontend/src/features/auth/views/login.vue index b70b40b..d2ec504 100644 --- a/frontend/src/features/auth/views/login.vue +++ b/frontend/src/features/auth/views/login.vue @@ -4,7 +4,7 @@

2FAuth Worker

-

A Secure 2FA Management Tool

+

{{ $t('auth.subtitle') }}

@@ -23,16 +23,16 @@ - 通过 {{ provider.name }} 授权登录 + {{ $t('auth.login_with', { provider: provider.name }) }}
@@ -179,6 +179,7 @@ import iconAegis from '@/shared/components/icons/iconAegis.vue' import iconGoogleAuth from '@/shared/components/icons/iconGoogleAuth.vue' import iconBitwarden from '@/shared/components/icons/iconBitwarden.vue' import { useDataExport } from '@/features/migration/composables/useDataExport' +import { i18n } from '@/locales' const { showPasswordDialog, @@ -206,10 +207,11 @@ const { // 动态计算“全选”按钮文案 const selectAllText = computed(() => { + const { t } = i18n.global if (!searchKeyword.value || searchKeyword.value.trim() === '') { - return '全选所有账号' + return t('migration.select_all_accounts') } else { - return `全选搜索出的 ${filteredVault.value.length} 个账号` + return t('migration.select_all_search', { count: filteredVault.value.length }) } }) diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue index f365c6f..c4e8094 100644 --- a/frontend/src/features/migration/views/dataImport.vue +++ b/frontend/src/features/migration/views/dataImport.vue @@ -2,8 +2,8 @@
-

数据导入

-

支持从各类 2FA 软件或本系统的备份文件中恢复数据。

+

{{ $t('migration.center_title') }}

+

{{ $t('migration.center_desc') }}

@@ -20,23 +20,23 @@ >
-

💡 自动识别文件类型导入

-

将备份文件或迁移二维码截图拖到此处,或 点击上传

+

{{ $t('migration.auto_identify_tip') }}

+