From 4da5525a1a902eeeda2159c8bc82f3afd4d5eced Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Mon, 2 Mar 2026 22:36:10 +0800 Subject: [PATCH] fix: update 2FA support descriptions and improve error handling in TOTP actions --- README.md | 2 +- README_EN.md | 2 +- src/handlers/accounts.ts | 2 +- src/handlers/identity.ts | 9 +++------ src/handlers/sync.ts | 3 +-- webapp/src/App.tsx | 6 ++++-- webapp/src/components/SettingsPage.tsx | 12 ++++++++---- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f57f57a..7b7038a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ English:[`README_EN.md`](./README_EN.md) | Send | ✅ | ✅ | 已支持文本 Send 与文件 Send | | 多用户 | ✅ | ✅ | 完整的用户管理,邀请机制 | | 组织/集合/成员权限 | ✅ | ❌ | 没必要实现 | -| 登录 2FA(TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ 部分支持 | 仅支持 TOTP(通过 `TOTP_SECRET`) | +| 登录 2FA(TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ 部分支持 | 仅支持用户级 TOTP | | SSO / SCIM / 企业目录 | ✅ | ❌ | 没必要实现 | | 紧急访问 | ✅ | ❌ | 没必要实现 | | 管理后台 / 计费订阅 | ✅ | ❌ | 纯免费 | diff --git a/README_EN.md b/README_EN.md index 63989f7..98dd706 100644 --- a/README_EN.md +++ b/README_EN.md @@ -36,7 +36,7 @@ | Multi-user | ✅ | ✅ | Full user management with invitation mechanism | | Send | ✅ | ✅ | Text Send and File Send are supported | | Organizations / Collections / Member roles | ✅ | ❌ | Not necessary to implement | -| Login 2FA (TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ Partial | TOTP-only via `TOTP_SECRET` | +| Login 2FA (TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ Partial | User-level TOTP only | | SSO / SCIM / Enterprise directory | ✅ | ❌ | Not necessary to implement | | Emergency access | ✅ | ❌ | Not necessary to implement | | Admin console / Billing & subscription | ✅ | ❌ | Free only | diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index b756076..f61b0ba 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -71,7 +71,7 @@ function toProfile(user: User, env: Env): ProfileResponse { usesKeyConnector: false, masterPasswordHint: null, culture: 'en-US', - twoFactorEnabled: !!user.totpSecret || isTotpEnabled(env.TOTP_SECRET), + twoFactorEnabled: !!user.totpSecret, key: user.key, privateKey: user.privateKey, accountKeys: null, diff --git a/src/handlers/identity.ts b/src/handlers/identity.ts index 105d7c6..a704b8f 100644 --- a/src/handlers/identity.ts +++ b/src/handlers/identity.ts @@ -19,13 +19,10 @@ const TWO_FACTOR_PROVIDER_RECOVERY_CODE_RESPONSE = '-1'; const TWO_FACTOR_PROVIDER_RECOVERY_CODE_LEGACY = 8; const TWO_FACTOR_PROVIDER_RECOVERY_CODE_ANDROID_REQUEST = 100; -function resolveTotpSecret(userSecret: string | null, envSecret: string | undefined): string | null { +function resolveTotpSecret(userSecret: string | null): string | null { if (userSecret && isTotpEnabled(userSecret)) { return userSecret; } - if (isTotpEnabled(envSecret)) { - return envSecret!; - } return null; } @@ -155,9 +152,9 @@ export async function handleToken(request: Request, env: Env): Promise ); } - // Optional 2FA: enabled per-user secret first, then falls back to global env secret for compatibility. + // Optional 2FA: enabled only by per-user secret. let trustedTwoFactorTokenToReturn: string | undefined; - const effectiveTotpSecret = resolveTotpSecret(user.totpSecret, env.TOTP_SECRET); + const effectiveTotpSecret = resolveTotpSecret(user.totpSecret); if (effectiveTotpSecret) { const canUseRecoveryCode = !!user.totpRecoveryCode; const normalizedTwoFactorProvider = String(twoFactorProvider ?? '').trim(); diff --git a/src/handlers/sync.ts b/src/handlers/sync.ts index 5e30d91..67cf434 100644 --- a/src/handlers/sync.ts +++ b/src/handlers/sync.ts @@ -4,7 +4,6 @@ import { errorResponse } from '../utils/response'; import { cipherToResponse } from './ciphers'; import { sendToResponse } from './sends'; import { LIMITS } from '../config/limits'; -import { isTotpEnabled } from '../utils/totp'; interface SyncCacheEntry { body: string; @@ -76,7 +75,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr usesKeyConnector: false, masterPasswordHint: null, culture: 'en-US', - twoFactorEnabled: !!user.totpSecret || isTotpEnabled(env.TOTP_SECRET), + twoFactorEnabled: !!user.totpSecret, key: user.key, privateKey: user.privateKey, accountKeys: null, diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index f7b7161..bd98688 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -611,14 +611,16 @@ export default function App() { async function enableTotpAction(secret: string, token: string) { if (!secret.trim() || !token.trim()) { - pushToast('error', t('txt_secret_and_code_are_required')); - return; + const error = new Error(t('txt_secret_and_code_are_required')); + pushToast('error', error.message); + throw error; } try { await setTotp(authedFetch, { enabled: true, secret: secret.trim(), token: token.trim() }); pushToast('success', t('txt_totp_enabled')); } catch (error) { pushToast('error', error instanceof Error ? error.message : t('txt_enable_totp_failed')); + throw error; } } diff --git a/webapp/src/components/SettingsPage.tsx b/webapp/src/components/SettingsPage.tsx index 77ebc73..d03a5b9 100644 --- a/webapp/src/components/SettingsPage.tsx +++ b/webapp/src/components/SettingsPage.tsx @@ -55,10 +55,14 @@ export default function SettingsPage(props: SettingsPageProps) { }, [props.profile.email, secret]); async function enableTotp(): Promise { - await props.onEnableTotp(secret, token); - // Secret is now stored on the server; remove plaintext copy from localStorage. - localStorage.removeItem(totpSecretStorageKey); - setTotpLocked(true); + try { + await props.onEnableTotp(secret, token); + // Secret is now stored on the server; remove plaintext copy from localStorage. + localStorage.removeItem(totpSecretStorageKey); + setTotpLocked(true); + } catch { + // Keep inputs editable after a failed attempt. + } } async function loadRecoveryCode(): Promise {