mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
feat: Passkey (WebAuthn) login (#1111)
* feat(auth): passkey (WebAuthn) login — server endpoints, schema + admin toggle Add @simplewebauthn/server registration and primary (discoverable) login ceremonies under /api/auth/passkey, a webauthn_credentials + single-use webauthn_challenges schema (migration), the instance-wide passkey_login toggle (default off) enforced before auth by a guard, and require_mfa satisfaction via a verified passkey. RP ID/origin come only from server config (webauthn_rp_id/origins -> APP_URL), never request headers. * 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. * i18n(auth): passkey strings across all locales Add login/settings/admin passkey keys to en and all 19 translated locales.
This commit is contained in:
@@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useSettingsStore, hasStoredLanguage } from '../../store/settingsStore'
|
||||
import { useTranslation, detectBrowserLanguage } from '../../i18n'
|
||||
import { startAuthentication } from '@simplewebauthn/browser'
|
||||
import { authApi, configApi } from '../../api/client'
|
||||
import { getApiErrorMessage } from '../../types'
|
||||
|
||||
@@ -18,6 +19,8 @@ interface AppConfig {
|
||||
password_registration: boolean
|
||||
oidc_login: boolean
|
||||
oidc_registration: boolean
|
||||
passkey_login?: boolean
|
||||
passkey_configured?: boolean
|
||||
env_override_oidc_only: boolean
|
||||
}
|
||||
|
||||
@@ -196,6 +199,28 @@ export function useLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasskeyLogin = async (): Promise<void> => {
|
||||
setError('')
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const options = await authApi.passkey.loginOptions()
|
||||
const assertion = await startAuthentication({ optionsJSON: options })
|
||||
await authApi.passkey.loginVerify(assertion)
|
||||
await loadUser({ silent: true })
|
||||
setShowTakeoff(true)
|
||||
setTimeout(() => navigate(redirectTarget), 2600)
|
||||
} catch (err: unknown) {
|
||||
// The user dismissing the native prompt isn't an error worth surfacing.
|
||||
const name = (err as { name?: string })?.name
|
||||
if (name === 'NotAllowedError' || name === 'AbortError') {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
setError(getApiErrorMessage(err, t('login.passkey.failed')))
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
@@ -270,6 +295,6 @@ export function useLogin() {
|
||||
showTakeoff, mfaStep, setMfaStep, mfaToken, setMfaToken, mfaCode, setMfaCode,
|
||||
passwordChangeStep, newPassword, setNewPassword, confirmPassword, setConfirmPassword,
|
||||
noRedirect, showRegisterOption, oidcOnly,
|
||||
handleDemoLogin, handleSubmit,
|
||||
handleDemoLogin, handleSubmit, handlePasskeyLogin,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user