mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: implement account passkey functionality
- Added functions for managing account passkeys including creation, listing, updating, and deletion. - Introduced login methods using account passkeys with options for direct unlock and login-only modes. - Enhanced error handling and response parsing for passkey-related API calls. - Updated UI styles for account passkey management components. - Added new translations for account passkey features in multiple languages. - Modified network status handling to improve service reachability checks.
This commit is contained in:
+67
-1
@@ -37,13 +37,16 @@ import {
|
||||
bootstrapAppSession,
|
||||
type CompletedLogin,
|
||||
readInitialAppBootstrapState,
|
||||
completePasskeyPasswordLogin,
|
||||
performPasswordLogin,
|
||||
performPasskeyLogin,
|
||||
performRecoverTwoFactorLogin,
|
||||
performRegistration,
|
||||
performTotpLogin,
|
||||
hydrateLockedSession,
|
||||
performUnlock,
|
||||
type JwtUnsafeReason,
|
||||
type PendingPasskeyPassword,
|
||||
type PendingTotp,
|
||||
} from '@/lib/app-auth';
|
||||
import useAccountSecurityActions from '@/hooks/useAccountSecurityActions';
|
||||
@@ -170,7 +173,7 @@ export default function App() {
|
||||
[initialBootstrap]
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null);
|
||||
const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'passkey' | 'register' | 'unlock' | null>(null);
|
||||
const [location, navigate] = useLocation();
|
||||
const [phase, setPhase] = useState<AppPhase>(initialBootstrap.phase);
|
||||
const [session, setSessionState] = useState<SessionState | null>(initialBootstrap.session);
|
||||
@@ -201,6 +204,8 @@ export default function App() {
|
||||
const [unlockPassword, setUnlockPassword] = useState('');
|
||||
const [pendingTotp, setPendingTotp] = useState<PendingTotp | null>(null);
|
||||
const [pendingTotpMode, setPendingTotpMode] = useState<'login' | 'unlock' | null>(null);
|
||||
const [pendingPasskeyPassword, setPendingPasskeyPassword] = useState<PendingPasskeyPassword | null>(null);
|
||||
const [passkeyPassword, setPasskeyPassword] = useState('');
|
||||
const [totpCode, setTotpCode] = useState('');
|
||||
const [rememberDevice, setRememberDevice] = useState(true);
|
||||
const [totpSubmitting, setTotpSubmitting] = useState(false);
|
||||
@@ -480,7 +485,9 @@ export default function App() {
|
||||
setUnlockPreparing(false);
|
||||
setPendingTotp(null);
|
||||
setPendingTotpMode(null);
|
||||
setPendingPasskeyPassword(null);
|
||||
setTotpCode('');
|
||||
setPasskeyPassword('');
|
||||
setUnlockPassword('');
|
||||
setPhase('app');
|
||||
if (location === '/' || location === '/login' || location === '/register' || location === '/lock') {
|
||||
@@ -535,6 +542,51 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasskeyLogin() {
|
||||
if (pendingAuthAction) return;
|
||||
if (IS_DEMO_MODE) {
|
||||
pushToast('warning', t('txt_demo_readonly_message'));
|
||||
return;
|
||||
}
|
||||
setPendingAuthAction('passkey');
|
||||
try {
|
||||
const result = await performPasskeyLogin(defaultKdfIterations);
|
||||
if (result.kind === 'success') {
|
||||
await finalizeLogin(result.login);
|
||||
return;
|
||||
}
|
||||
if (result.kind === 'password') {
|
||||
setPendingPasskeyPassword(result.pendingPasskeyPassword);
|
||||
setLoginValues({ email: result.pendingPasskeyPassword.email, password: '' });
|
||||
setPasskeyPassword('');
|
||||
pushToast('warning', t('txt_passkey_requires_master_password'));
|
||||
return;
|
||||
}
|
||||
pushToast('error', result.message || t('txt_login_failed'));
|
||||
} catch (error) {
|
||||
pushToast('error', error instanceof Error ? error.message : t('txt_login_failed'));
|
||||
} finally {
|
||||
setPendingAuthAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasskeyPasswordLogin() {
|
||||
if (pendingAuthAction || !pendingPasskeyPassword) return;
|
||||
if (!passkeyPassword) {
|
||||
pushToast('error', t('txt_please_input_master_password'));
|
||||
return;
|
||||
}
|
||||
setPendingAuthAction('login');
|
||||
try {
|
||||
const login = await completePasskeyPasswordLogin(pendingPasskeyPassword, passkeyPassword);
|
||||
await finalizeLogin(login);
|
||||
} catch (error) {
|
||||
pushToast('error', error instanceof Error ? error.message : t('txt_unlock_failed_master_password_is_incorrect'));
|
||||
} finally {
|
||||
setPendingAuthAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTotpVerify() {
|
||||
if (totpSubmitting) return;
|
||||
if (!pendingTotp) return;
|
||||
@@ -1354,6 +1406,7 @@ export default function App() {
|
||||
const accountSecurityActions = useAccountSecurityActions({
|
||||
authedFetch,
|
||||
profile,
|
||||
session,
|
||||
defaultKdfIterations,
|
||||
disableTotpPassword,
|
||||
clearDisableTotpDialog: () => {
|
||||
@@ -1540,6 +1593,10 @@ export default function App() {
|
||||
onGetRecoveryCode: accountSecurityActions.getRecoveryCode,
|
||||
onGetApiKey: accountSecurityActions.getApiKey,
|
||||
onRotateApiKey: accountSecurityActions.rotateApiKey,
|
||||
onListAccountPasskeys: accountSecurityActions.listAccountPasskeys,
|
||||
onCreateAccountPasskey: accountSecurityActions.createAccountPasskey,
|
||||
onEnableAccountPasskeyDirectUnlock: accountSecurityActions.enableAccountPasskeyDirectUnlock,
|
||||
onDeleteAccountPasskey: accountSecurityActions.deleteAccountPasskey,
|
||||
onLockTimeoutChange: setLockTimeoutMinutes,
|
||||
onSessionTimeoutActionChange: setSessionTimeoutAction,
|
||||
onRefreshAuthorizedDevices: accountSecurityActions.refreshAuthorizedDevices,
|
||||
@@ -1650,18 +1707,25 @@ export default function App() {
|
||||
unlockReady={!!session?.email}
|
||||
unlockPreparing={unlockPreparing}
|
||||
loginValues={loginValues}
|
||||
pendingPasskeyPasswordEmail={pendingPasskeyPassword?.email || null}
|
||||
passkeyPassword={passkeyPassword}
|
||||
registerValues={registerValues}
|
||||
registrationInviteRequired={registrationInviteRequired}
|
||||
unlockPassword={unlockPassword}
|
||||
emailForLock={profile?.email || session?.email || ''}
|
||||
loginHintLoading={loginHintState.loading}
|
||||
onChangeLogin={setLoginValues}
|
||||
onChangePasskeyPassword={setPasskeyPassword}
|
||||
onChangeRegister={setRegisterValues}
|
||||
onChangeUnlock={setUnlockPassword}
|
||||
onSubmitLogin={() => void handleLogin()}
|
||||
onSubmitPasskey={() => void handlePasskeyLogin()}
|
||||
onSubmitPasskeyPassword={() => void handlePasskeyPasswordLogin()}
|
||||
onSubmitRegister={() => void handleRegister()}
|
||||
onSubmitUnlock={() => void handleUnlock()}
|
||||
onGotoLogin={() => {
|
||||
setPendingPasskeyPassword(null);
|
||||
setPasskeyPassword('');
|
||||
setPhase('login');
|
||||
navigate('/login');
|
||||
}}
|
||||
@@ -1673,6 +1737,8 @@ export default function App() {
|
||||
if (inviteCodeFromUrl) {
|
||||
setRegisterValues((prev) => ({ ...prev, inviteCode: inviteCodeFromUrl }));
|
||||
}
|
||||
setPendingPasskeyPassword(null);
|
||||
setPasskeyPassword('');
|
||||
setPhase('register');
|
||||
navigate('/register');
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user