From 1b868ba5036bea172a67890f4c9bc5b689278cd8 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Wed, 18 Mar 2026 00:32:03 +0800 Subject: [PATCH] Fix: hide passwordless toggle when disabled, remove lock auto-trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Passwordless toggle in Settings is now hidden when admin hasn't enabled allow_passwordless in system config (or when user already has it enabled — so they can still disable it). Backend exposes allow_passwordless in /auth/status response. 2. Remove auto-trigger passkey ceremony on lock screen — previously fired immediately when session locked for passwordless users. Now waits for user to click "Unlock with passkey" button. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/routers/auth.py | 1 + .../src/components/layout/LockOverlay.tsx | 8 --- .../components/settings/PasskeySection.tsx | 64 ++++++++++--------- frontend/src/hooks/useAuth.ts | 1 + frontend/src/types/index.ts | 1 + 5 files changed, 36 insertions(+), 39 deletions(-) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index afb7289..053ed04 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -553,6 +553,7 @@ async def auth_status( "has_passkeys": has_passkeys, "passkey_count": passkey_count, "passwordless_enabled": passwordless_enabled, + "allow_passwordless": config.allow_passwordless if config else False, } diff --git a/frontend/src/components/layout/LockOverlay.tsx b/frontend/src/components/layout/LockOverlay.tsx index 901b50d..9f39013 100644 --- a/frontend/src/components/layout/LockOverlay.tsx +++ b/frontend/src/components/layout/LockOverlay.tsx @@ -39,14 +39,6 @@ export default function LockOverlay() { } }, [isLocked, showPasswordForm]); - // Auto-trigger passkey unlock when passwordless mode is enabled - useEffect(() => { - if (isLocked && passwordlessEnabled && supportsWebAuthn && !isPasskeyUnlocking) { - handlePasskeyUnlock(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLocked, passwordlessEnabled]); - if (!isLocked) return null; const preferredName = settings?.preferred_name; diff --git a/frontend/src/components/settings/PasskeySection.tsx b/frontend/src/components/settings/PasskeySection.tsx index e1f7010..065178f 100644 --- a/frontend/src/components/settings/PasskeySection.tsx +++ b/frontend/src/components/settings/PasskeySection.tsx @@ -137,7 +137,7 @@ function PasskeyDeleteButton({ credential, onDelete, isDeleting }: DeleteConfirm export default function PasskeySection() { const queryClient = useQueryClient(); - const { passwordlessEnabled, passkeyCount } = useAuth(); + const { passwordlessEnabled, passkeyCount, allowPasswordless } = useAuth(); // Registration state const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -376,39 +376,41 @@ export default function PasskeySection() { {hasPasskeys ? 'Add another passkey' : 'Add a passkey'} - {/* Passwordless login section */} - + {/* Passwordless login section — hidden when admin hasn't enabled the feature */} + {(allowPasswordless || passwordlessEnabled) && } -
-
-
- - -
-

- Skip the password prompt and unlock the app using a passkey only. -

- {passkeyCount < 2 && ( -

- Requires at least 2 registered passkeys as a fallback. + {(allowPasswordless || passwordlessEnabled) && ( +

+
+
+ + +
+

+ Skip the password prompt and unlock the app using a passkey only.

- )} + {passkeyCount < 2 && !passwordlessEnabled && ( +

+ Requires at least 2 registered passkeys as a fallback. +

+ )} +
+ { + if (checked) { + setEnablePassword(''); + setEnableDialogOpen(true); + } else { + setDisableDialogOpen(true); + disablePasswordlessMutation.mutate(); + } + }} + disabled={(!passwordlessEnabled && passkeyCount < 2) || enablePasswordlessMutation.isPending || disablePasswordlessMutation.isPending} + aria-label="Toggle passwordless login" + />
- { - if (checked) { - setEnablePassword(''); - setEnableDialogOpen(true); - } else { - setDisableDialogOpen(true); - disablePasswordlessMutation.mutate(); - } - }} - disabled={(!passwordlessEnabled && passkeyCount < 2) || enablePasswordlessMutation.isPending || disablePasswordlessMutation.isPending} - aria-label="Toggle passwordless login" - /> -
+ )} {/* Enable passwordless dialog */} { diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index a8b6cb5..8910b1a 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -154,5 +154,6 @@ export function useAuth() { hasPasskeys: authQuery.data?.has_passkeys ?? false, passkeyCount: authQuery.data?.passkey_count ?? 0, passwordlessEnabled: authQuery.data?.passwordless_enabled ?? false, + allowPasswordless: authQuery.data?.allow_passwordless ?? false, }; } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f7b2f02..b93d7ed 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -246,6 +246,7 @@ export interface AuthStatus { has_passkeys: boolean; passkey_count: number; passwordless_enabled: boolean; + allow_passwordless: boolean; } export interface PasskeyCredential {