Fix: hide passwordless toggle when disabled, remove lock auto-trigger
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) <noreply@anthropic.com>
This commit is contained in:
parent
42d73526f5
commit
1b868ba503
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'}
|
||||
</Button>
|
||||
|
||||
{/* Passwordless login section */}
|
||||
<Separator />
|
||||
{/* Passwordless login section — hidden when admin hasn't enabled the feature */}
|
||||
{(allowPasswordless || passwordlessEnabled) && <Separator />}
|
||||
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Fingerprint className="h-4 w-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">Passwordless Login</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Skip the password prompt and unlock the app using a passkey only.
|
||||
</p>
|
||||
{passkeyCount < 2 && (
|
||||
<p className="text-xs text-amber-400">
|
||||
Requires at least 2 registered passkeys as a fallback.
|
||||
{(allowPasswordless || passwordlessEnabled) && (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Fingerprint className="h-4 w-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">Passwordless Login</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Skip the password prompt and unlock the app using a passkey only.
|
||||
</p>
|
||||
)}
|
||||
{passkeyCount < 2 && !passwordlessEnabled && (
|
||||
<p className="text-xs text-amber-400">
|
||||
Requires at least 2 registered passkeys as a fallback.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={passwordlessEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setEnablePassword('');
|
||||
setEnableDialogOpen(true);
|
||||
} else {
|
||||
setDisableDialogOpen(true);
|
||||
disablePasswordlessMutation.mutate();
|
||||
}
|
||||
}}
|
||||
disabled={(!passwordlessEnabled && passkeyCount < 2) || enablePasswordlessMutation.isPending || disablePasswordlessMutation.isPending}
|
||||
aria-label="Toggle passwordless login"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={passwordlessEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setEnablePassword('');
|
||||
setEnableDialogOpen(true);
|
||||
} else {
|
||||
setDisableDialogOpen(true);
|
||||
disablePasswordlessMutation.mutate();
|
||||
}
|
||||
}}
|
||||
disabled={(!passwordlessEnabled && passkeyCount < 2) || enablePasswordlessMutation.isPending || disablePasswordlessMutation.isPending}
|
||||
aria-label="Toggle passwordless login"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enable passwordless dialog */}
|
||||
<Dialog open={enableDialogOpen} onOpenChange={(open) => {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -246,6 +246,7 @@ export interface AuthStatus {
|
||||
has_passkeys: boolean;
|
||||
passkey_count: number;
|
||||
passwordless_enabled: boolean;
|
||||
allow_passwordless: boolean;
|
||||
}
|
||||
|
||||
export interface PasskeyCredential {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user