feat(auth): passkey enrolment, login button + admin settings UI

PasskeysSection in account settings (add/rename/remove with a current-password step-up), a 'Sign in with a passkey' button on the login page, the admin enable + RP-ID/origins controls, and a per-user admin reset action.
This commit is contained in:
Maurice
2026-06-05 18:46:03 +02:00
parent 5b8c61d215
commit 7471976c9a
9 changed files with 474 additions and 4 deletions
@@ -23,6 +23,8 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration,
envOverrideOidcOnly, oidcConfigured, requireMfa,
passkeyLogin, setPasskeyLogin, passkeyConfigured,
webauthnRpId, setWebauthnRpId, webauthnOrigins, setWebauthnOrigins, savingWebauthn, handleSaveWebauthn,
allowedFileTypes, setAllowedFileTypes, savingFileTypes, setSavingFileTypes,
mapsKey, setMapsKey, showKeys, savingKeys, validating, validation,
setShowRotateJwtModal,
@@ -119,6 +121,71 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
</div>
</div>
{/* Passkey (WebAuthn) login */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.passkey.title')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('admin.passkey.cardHint')}</p>
</div>
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.passkey.login')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.passkey.loginHint')}</p>
</div>
<button
type="button"
onClick={() => handleToggleAuthSetting('passkey_login', !passkeyLogin, setPasskeyLogin)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${passkeyLogin ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: passkeyLogin ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
{passkeyLogin && !passkeyConfigured && (
<p className="flex items-start gap-2 text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
{t('admin.passkey.notConfigured')}
</p>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{t('admin.passkey.rpId')}</label>
<p className="text-xs text-slate-400 mb-1.5">{t('admin.passkey.rpIdHint')}</p>
<input
type="text"
value={webauthnRpId}
onChange={e => setWebauthnRpId(e.target.value)}
placeholder="trek.example.org"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{t('admin.passkey.origins')}</label>
<p className="text-xs text-slate-400 mb-1.5">{t('admin.passkey.originsHint')}</p>
<input
type="text"
value={webauthnOrigins}
onChange={e => setWebauthnOrigins(e.target.value)}
placeholder="https://trek.example.org"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<button
type="button"
onClick={handleSaveWebauthn}
disabled={savingWebauthn}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:opacity-50"
>
{savingWebauthn ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
{t('common.save')}
</button>
</div>
</div>
{/* Require 2FA for all users */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">