mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: implement pending authentication actions for login, registration, and unlock flows
This commit is contained in:
+30
-15
@@ -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<AppPhase>('loading');
|
||||
const [session, setSessionState] = useState<SessionState | null>(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() {
|
||||
<>
|
||||
<AuthViews
|
||||
mode={phase}
|
||||
pendingAction={pendingAuthAction}
|
||||
loginValues={loginValues}
|
||||
registerValues={registerValues}
|
||||
unlockPassword={unlockPassword}
|
||||
|
||||
@@ -18,6 +18,7 @@ interface RegisterValues {
|
||||
|
||||
interface AuthViewsProps {
|
||||
mode: 'login' | 'register' | 'locked';
|
||||
pendingAction: 'login' | 'register' | 'unlock' | null;
|
||||
loginValues: LoginValues;
|
||||
registerValues: RegisterValues;
|
||||
unlockPassword: string;
|
||||
@@ -60,6 +61,10 @@ function PasswordField(props: {
|
||||
}
|
||||
|
||||
export default function AuthViews(props: AuthViewsProps) {
|
||||
const loginBusy = props.pendingAction === 'login';
|
||||
const registerBusy = props.pendingAction === 'register';
|
||||
const unlockBusy = props.pendingAction === 'unlock';
|
||||
|
||||
if (props.mode === 'locked') {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
@@ -77,12 +82,12 @@ export default function AuthViews(props: AuthViewsProps) {
|
||||
autoFocus
|
||||
onInput={props.onChangeUnlock}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary full">
|
||||
<button type="submit" className="btn btn-primary full" disabled={unlockBusy}>
|
||||
<Unlock size={16} className="btn-icon" />
|
||||
{t('txt_unlock')}
|
||||
{unlockBusy ? t('txt_unlocking') : t('txt_unlock')}
|
||||
</button>
|
||||
<div className="or">{t('txt_or')}</div>
|
||||
<button type="button" className="btn btn-secondary full" onClick={props.onLogout}>
|
||||
<button type="button" className="btn btn-secondary full" onClick={props.onLogout} disabled={unlockBusy}>
|
||||
<LogOut size={16} className="btn-icon" />
|
||||
{t('txt_log_out')}
|
||||
</button>
|
||||
@@ -143,12 +148,12 @@ export default function AuthViews(props: AuthViewsProps) {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="btn btn-primary full">
|
||||
<button type="submit" className="btn btn-primary full" disabled={registerBusy}>
|
||||
<UserPlus size={16} className="btn-icon" />
|
||||
{t('txt_create_account')}
|
||||
{registerBusy ? t('txt_registering') : t('txt_create_account')}
|
||||
</button>
|
||||
<div className="or">{t('txt_or')}</div>
|
||||
<button type="button" className="btn btn-secondary full" onClick={props.onGotoLogin}>
|
||||
<button type="button" className="btn btn-secondary full" onClick={props.onGotoLogin} disabled={registerBusy}>
|
||||
<ArrowLeft size={16} className="btn-icon" />
|
||||
{t('txt_back_to_login')}
|
||||
</button>
|
||||
@@ -182,12 +187,12 @@ export default function AuthViews(props: AuthViewsProps) {
|
||||
onInput={(v) => props.onChangeLogin({ ...props.loginValues, password: v })}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary full">
|
||||
<button type="submit" className="btn btn-primary full" disabled={loginBusy}>
|
||||
<LogIn size={16} className="btn-icon" />
|
||||
{t('txt_log_in')}
|
||||
{loginBusy ? t('txt_logging_in') : t('txt_log_in')}
|
||||
</button>
|
||||
<div className="or">{t('txt_or')}</div>
|
||||
<button type="button" className="btn btn-secondary full" onClick={props.onGotoRegister}>
|
||||
<button type="button" className="btn btn-secondary full" onClick={props.onGotoRegister} disabled={loginBusy}>
|
||||
<UserPlus size={16} className="btn-icon" />
|
||||
{t('txt_create_account')}
|
||||
</button>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -229,6 +229,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
||||
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<Locale, Record<string, string>> = {
|
||||
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<Locale, Record<string, string>> = {
|
||||
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<Locale, Record<string, string>> = {
|
||||
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<Locale, Record<string, string>> = {
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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 个字符',
|
||||
|
||||
Reference in New Issue
Block a user