From 9820c2ed44ac1360bfae487abd6ad41d77c03afc Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sun, 15 Mar 2026 18:32:30 +0800 Subject: [PATCH] feat: implement pending authentication actions for login, registration, and unlock flows --- webapp/src/App.tsx | 45 ++++++++++++------- webapp/src/components/AuthViews.tsx | 23 ++++++---- webapp/src/hooks/useAccountSecurityActions.ts | 39 +++++++++------- webapp/src/lib/i18n.ts | 10 +++++ 4 files changed, 77 insertions(+), 40 deletions(-) diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index cf5782f..995bb88 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -59,6 +59,7 @@ const SIGNALR_UPDATE_TYPE_LOG_OUT = 11; const SIGNALR_UPDATE_TYPE_DEVICE_STATUS = 12; export default function App() { + const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null); const [location, navigate] = useLocation(); const [phase, setPhase] = useState('loading'); const [session, setSessionState] = useState(null); @@ -210,10 +211,12 @@ export default function App() { } async function handleLogin() { + if (pendingAuthAction) return; if (!loginValues.email || !loginValues.password) { pushToast('error', t('txt_please_input_email_and_password')); return; } + setPendingAuthAction('login'); try { const result = await performPasswordLogin(loginValues.email, loginValues.password, defaultKdfIterations); if (result.kind === 'success') { @@ -229,6 +232,8 @@ export default function App() { pushToast('error', result.message || t('txt_login_failed')); } catch (error) { pushToast('error', error instanceof Error ? error.message : t('txt_login_failed')); + } finally { + setPendingAuthAction(null); } } @@ -273,6 +278,7 @@ export default function App() { } async function handleRegister() { + if (pendingAuthAction) return; if (!registerValues.email || !registerValues.password) { pushToast('error', t('txt_please_input_email_and_password')); return; @@ -285,29 +291,36 @@ export default function App() { pushToast('error', t('txt_passwords_do_not_match')); return; } - const resp = await performRegistration({ - email: registerValues.email, - name: registerValues.name, - password: registerValues.password, - inviteCode: registerValues.inviteCode, - fallbackIterations: defaultKdfIterations, - }); - if (!resp.ok) { - pushToast('error', resp.message); - return; + setPendingAuthAction('register'); + try { + const resp = await performRegistration({ + email: registerValues.email, + name: registerValues.name, + password: registerValues.password, + inviteCode: registerValues.inviteCode, + fallbackIterations: defaultKdfIterations, + }); + if (!resp.ok) { + pushToast('error', resp.message); + return; + } + setLoginValues({ email: registerValues.email.toLowerCase(), password: '' }); + setPhase('login'); + navigate('/login'); + pushToast('success', t('txt_registration_succeeded_please_sign_in')); + } finally { + setPendingAuthAction(null); } - setLoginValues({ email: registerValues.email.toLowerCase(), password: '' }); - setPhase('login'); - navigate('/login'); - pushToast('success', t('txt_registration_succeeded_please_sign_in')); } async function handleUnlock() { + if (pendingAuthAction) return; if (!session || !profile) return; if (!unlockPassword) { pushToast('error', t('txt_please_input_master_password')); return; } + setPendingAuthAction('unlock'); try { const nextSession = await performUnlock(session, profile, unlockPassword, defaultKdfIterations); setSession(nextSession); @@ -318,6 +331,8 @@ export default function App() { pushToast('success', t('txt_unlocked')); } catch { pushToast('error', t('txt_unlock_failed_master_password_is_incorrect')); + } finally { + setPendingAuthAction(null); } } @@ -749,7 +764,6 @@ export default function App() { setDisableTotpOpen(false); setDisableTotpPassword(''); }, - onPromptLogout: handleLogout, onLogoutNow: logoutNow, onNotify: pushToast, onSetConfirm: setConfirm, @@ -937,6 +951,7 @@ export default function App() { <> @@ -77,12 +82,12 @@ export default function AuthViews(props: AuthViewsProps) { autoFocus onInput={props.onChangeUnlock} /> -
{t('txt_or')}
- @@ -143,12 +148,12 @@ export default function AuthViews(props: AuthViewsProps) { } /> -
{t('txt_or')}
- @@ -182,12 +187,12 @@ export default function AuthViews(props: AuthViewsProps) { onInput={(v) => props.onChangeLogin({ ...props.loginValues, password: v })} autoFocus /> -
{t('txt_or')}
- diff --git a/webapp/src/hooks/useAccountSecurityActions.ts b/webapp/src/hooks/useAccountSecurityActions.ts index 54df722..9ede4c8 100644 --- a/webapp/src/hooks/useAccountSecurityActions.ts +++ b/webapp/src/hooks/useAccountSecurityActions.ts @@ -23,7 +23,6 @@ interface UseAccountSecurityActionsOptions { defaultKdfIterations: number; disableTotpPassword: string; clearDisableTotpDialog: () => void; - onPromptLogout: () => void; onLogoutNow: () => void; onNotify: Notify; onSetConfirm: (next: AppConfirmState | null) => void; @@ -38,7 +37,6 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct defaultKdfIterations, disableTotpPassword, clearDisableTotpDialog, - onPromptLogout, onLogoutNow, onNotify, onSetConfirm, @@ -62,19 +60,29 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct onNotify('error', t('txt_new_passwords_do_not_match')); return; } - try { - await changeMasterPassword(authedFetch, { - email: profile.email, - currentPassword, - newPassword: nextPassword, - currentIterations: defaultKdfIterations, - profileKey: profile.key, - }); - onPromptLogout(); - onNotify('success', t('txt_master_password_changed_please_login_again')); - } catch (error) { - onNotify('error', error instanceof Error ? error.message : t('txt_change_password_failed')); - } + onSetConfirm({ + title: t('txt_change_master_password'), + message: t('txt_change_password_confirm_and_sign_out_all_devices'), + danger: true, + onConfirm: () => { + onSetConfirm(null); + void (async () => { + try { + await changeMasterPassword(authedFetch, { + email: profile.email, + currentPassword, + newPassword: nextPassword, + currentIterations: defaultKdfIterations, + profileKey: profile.key, + }); + onNotify('success', t('txt_master_password_changed_signing_out_everywhere')); + onLogoutNow(); + } catch (error) { + onNotify('error', error instanceof Error ? error.message : t('txt_change_password_failed')); + } + })(); + }, + }); }, async enableTotp(secret: string, token: string) { @@ -200,7 +208,6 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct disableTotpPassword, onLogoutNow, onNotify, - onPromptLogout, onSetConfirm, profile, refetchAuthorizedDevices, diff --git a/webapp/src/lib/i18n.ts b/webapp/src/lib/i18n.ts index 1cf63be..2cb923e 100644 --- a/webapp/src/lib/i18n.ts +++ b/webapp/src/lib/i18n.ts @@ -229,6 +229,7 @@ const messages: Record> = { txt_change_master_password: "Change Master Password", txt_change_password: "Change Password", txt_change_password_failed: "Change password failed", + txt_change_password_confirm_and_sign_out_all_devices: "Changing the master password will sign out all devices, including this web session. Continue?", txt_checked: "Checked", txt_choose_destination_folder: "Choose destination folder.", txt_chrome_browser: "Chrome Browser", @@ -248,6 +249,7 @@ const messages: Record> = { txt_country: "Country", txt_create: "Create", txt_create_account: "Create Account", + txt_registering: "Creating account...", txt_create_folder: "Create Folder", txt_create_folder_failed: "Create folder failed", txt_create_item_failed: "Create item failed", @@ -380,6 +382,7 @@ const messages: Record> = { txt_random_secret_generator: "Random Secret Generator", txt_copied: "Copied", txt_log_in: "Log In", + txt_logging_in: "Logging in...", txt_log_out: "Log Out", txt_lock: "Lock", txt_menu: "Menu", @@ -394,6 +397,7 @@ const messages: Record> = { txt_manage_device_sessions_and_30_day_totp_trusted_sessions: "Manage device sessions and 30-day TOTP trusted sessions.", txt_master_password: "Master Password", txt_master_password_changed_please_login_again: "Master password changed. Please login again.", + txt_master_password_changed_signing_out_everywhere: "Master password changed. Signing out all devices.", txt_master_password_is_required: "Master password is required", txt_master_password_is_required_2: "Master password is required.", txt_master_password_must_be_at_least_12_chars: "Master password must be at least 12 chars", @@ -544,6 +548,7 @@ const messages: Record> = { txt_unchecked: "Unchecked", txt_unknown_device: "Unknown device", txt_unlock: "Unlock", + txt_unlocking: "Unlocking...", txt_unlock_details: "Unlock Details", txt_unlock_failed: "Unlock failed", txt_unlock_failed_master_password_is_incorrect: "Unlock failed. Master password is incorrect.", @@ -762,10 +767,13 @@ const zhCNOverrides: Record = { txt_backup_clear_and_restore: '清空后还原', txt_sign_out: '退出登录', txt_log_in: '登录', + txt_logging_in: '正在登录...', txt_log_out: '退出', txt_create_account: '创建账户', + txt_registering: '正在注册...', txt_back_to_login: '返回登录', txt_unlock: '解锁', + txt_unlocking: '正在解锁...', txt_unlock_vault: '解锁密码库', txt_master_password: '主密码', txt_email: '邮箱', @@ -845,6 +853,7 @@ const zhCNOverrides: Record = { txt_current_password: '当前密码', txt_new_password: '新密码', txt_change_password: '修改密码', + txt_change_password_confirm_and_sign_out_all_devices: '修改主密码后会强制退出所有设备,包括当前网页端。确认继续吗', txt_totp: 'TOTP', txt_enable_totp: '启用 TOTP', txt_disable_totp: '停用 TOTP', @@ -1037,6 +1046,7 @@ const zhCNOverrides: Record = { txt_login_success: '登录成功', txt_macos_desktop: 'macOS 桌面端', txt_master_password_changed_please_login_again: '主密码已修改,请重新登录', + txt_master_password_changed_signing_out_everywhere: '主密码已修改,正在退出所有设备', txt_master_password_is_required: '主密码不能为空', txt_master_password_is_required_2: '请输入主密码', txt_master_password_must_be_at_least_12_chars: '主密码至少需要 12 个字符',