import { useSignal } from '@preact/signals'; import { startRegistration } from '@simplewebauthn/browser'; import { MultiFactorAuthMethodType } from '/lib/types.ts'; import { RequestBody as PasskeySetupBeginRequestBody, ResponseBody as PasskeySetupBeginResponseBody, } from '/routes/api/auth/multi-factor/passkey/setup-begin.ts'; import { RequestBody as PasskeySetupCompleteRequestBody, ResponseBody as PasskeySetupCompleteResponseBody, } from '/routes/api/auth/multi-factor/passkey/setup-complete.ts'; import { RequestBody as TOTPSetupRequestBody, ResponseBody as TOTPSetupResponseBody, } from '/routes/api/auth/multi-factor/totp/setup.ts'; import { RequestBody as EmailSetupRequestBody, ResponseBody as EmailSetupResponseBody, } from '/routes/api/auth/multi-factor/email/setup.ts'; import { RequestBody as MultiFactorAuthEnableRequestBody, ResponseBody as MultiFactorAuthEnableResponseBody, } from '/routes/api/auth/multi-factor/enable.ts'; import { RequestBody as MultiFactorAuthDisableRequestBody, ResponseBody as MultiFactorAuthDisableResponseBody, } from '/routes/api/auth/multi-factor/disable.ts'; interface MultiFactorAuthMethod { type: MultiFactorAuthMethodType; id: string; name: string; enabled: boolean; backupCodesCount?: number; } interface MultiFactorAuthSettingsProps { methods: MultiFactorAuthMethod[]; } interface TOTPSetupData { type: 'totp'; secret: string; qrCodeUrl: string; backupCodes: string[]; methodId: string; } interface PasskeySetupData { methodId: string; type: 'passkey'; } interface EmailSetupData { methodId: string; type: 'email'; } const methodTypeLabels: Record = { totp: 'Authenticator App', passkey: 'Passkey', email: 'Email', }; const methodTypeDescriptions: Record = { totp: 'Use an authenticator app like Aegis Authenticator or Google Authenticator to generate codes.', passkey: 'Use biometric authentication or security keys.', email: 'Receive codes in your email.', }; const availableMethodTypes = ['totp', 'passkey', 'email'] as MultiFactorAuthMethodType[]; export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSettingsProps) { const setupData = useSignal(null); const isLoading = useSignal(false); const error = useSignal(null); const success = useSignal(null); const showDisableForm = useSignal<'all' | string | null>(null); const verificationToken = useSignal(''); const disablePassword = useSignal(''); const enabledMethods = methods.filter((method) => method.enabled); const hasMultiFactorAuthEnabled = enabledMethods.length > 0; const setupPasskey = async () => { const beginRequestBody: PasskeySetupBeginRequestBody = {}; const beginResponse = await fetch('/api/auth/multi-factor/passkey/setup-begin', { method: 'POST', body: JSON.stringify(beginRequestBody), }); if (!beginResponse.ok) { throw new Error( `Failed to begin passkey registration! ${beginResponse.statusText} ${await beginResponse.text()}`, ); } const beginData = await beginResponse.json() as PasskeySetupBeginResponseBody; if (!beginData.success) { throw new Error(beginData.error || 'Failed to begin passkey registration'); } const registrationResponse = await startRegistration({ optionsJSON: beginData.options! }); const completeRequestBody: PasskeySetupCompleteRequestBody = { methodId: beginData.sessionData!.methodId, challenge: beginData.sessionData!.challenge, registrationResponse, }; const completeResponse = await fetch('/api/auth/multi-factor/passkey/setup-complete', { method: 'POST', body: JSON.stringify(completeRequestBody), }); if (!completeResponse.ok) { throw new Error( `Failed to complete passkey registration! ${completeResponse.statusText} ${await completeResponse.text()}`, ); } const completeData = await completeResponse.json() as PasskeySetupCompleteResponseBody; if (!completeData.success) { throw new Error(completeData.error || 'Failed to complete passkey registration'); } setupData.value = { methodId: beginData.sessionData!.methodId, type: 'passkey', }; }; const setupTOTP = async () => { const requestBody: TOTPSetupRequestBody = {}; const response = await fetch('/api/auth/multi-factor/totp/setup', { method: 'POST', body: JSON.stringify(requestBody), }); const data = await response.json() as TOTPSetupResponseBody; if (!data.success || !data.data) { throw new Error(data.error || 'Failed to setup TOTP multi-factor authentication'); } setupData.value = { type: 'totp', secret: data.data.secret!, qrCodeUrl: data.data.qrCodeUrl!, backupCodes: data.data.backupCodes!, methodId: data.data.methodId!, }; }; const setupEmail = async () => { const requestBody: EmailSetupRequestBody = {}; const response = await fetch('/api/auth/multi-factor/email/setup', { method: 'POST', body: JSON.stringify(requestBody), }); const data = await response.json() as EmailSetupResponseBody; if (!data.success || !data.data) { throw new Error(data.error || 'Failed to setup email multi-factor authentication'); } setupData.value = { type: 'email', methodId: data.data.methodId!, }; }; const setupMultiFactorAuth = async (type: MultiFactorAuthMethodType) => { isLoading.value = true; error.value = null; try { if (type === 'totp') { await setupTOTP(); } else if (type === 'passkey') { await setupPasskey(); } else if (type === 'email') { await setupEmail(); } } catch (setupError) { error.value = (setupError as Error).message; } finally { isLoading.value = false; } }; const enableMultiFactorAuth = async () => { if (!setupData.value) { error.value = 'No setup data available'; return; } if (setupData.value.type !== 'passkey' && !verificationToken.value) { error.value = 'Please enter a verification code/token'; return; } isLoading.value = true; error.value = null; try { const requestBody: MultiFactorAuthEnableRequestBody = { methodId: setupData.value.methodId, code: setupData.value.type === 'passkey' ? 'passkey-verified' : verificationToken.value, }; const response = await fetch('/api/auth/multi-factor/enable', { method: 'POST', body: JSON.stringify(requestBody), }); const data = await response.json() as MultiFactorAuthEnableResponseBody; if (!data.success) { throw new Error(data.error || 'Failed to enable multi-factor authentication'); } success.value = 'Multi-factor authentication has been enabled successfully! Reloading...'; setupData.value = null; verificationToken.value = ''; setTimeout(() => { window.location.reload(); }, 2000); } catch (enableError) { error.value = (enableError as Error).message; } finally { isLoading.value = false; } }; const disableMultiFactorAuth = async (methodId?: string, disableAll = false) => { if (!disablePassword.value) { error.value = 'Please enter your password'; return; } isLoading.value = true; error.value = null; try { const requestBody: MultiFactorAuthDisableRequestBody = { methodId, password: disablePassword.value, disableAll, }; const response = await fetch('/api/auth/multi-factor/disable', { method: 'POST', body: JSON.stringify(requestBody), }); const data = await response.json() as MultiFactorAuthDisableResponseBody; if (!data.success) { throw new Error(data.error || 'Failed to disable multi-factor authentication'); } success.value = 'Multi-factor authentication has been disabled successfully! Reloading...'; showDisableForm.value = null; disablePassword.value = ''; setTimeout(() => { window.location.reload(); }, 2000); } catch (disableError) { error.value = (disableError as Error).message; } finally { isLoading.value = false; } }; const cancelSetup = () => { setupData.value = null; verificationToken.value = ''; error.value = null; }; const cancelDisable = () => { showDisableForm.value = null; disablePassword.value = ''; error.value = null; }; return (

Multi-Factor Authentication (MFA)

{error.value ? (

{error.value}

) : null} {success.value ? (

{success.value}

) : null}

Multi-factor authentication adds an extra layer of security to your account by requiring additional verification beyond your password.

{availableMethodTypes .filter((type) => !enabledMethods.some((method) => method.type === type)).length > 0 ? (

Available Authentication Methods

{availableMethodTypes .filter((type) => !enabledMethods.some((method) => method.type === type) && setupData.value?.type !== type ) .map((type) => (

{methodTypeLabels[type]}

{methodTypeDescriptions[type]}

))}
) : null} {setupData.value && setupData.value.type === 'totp' ? (

Setup Authenticator App

1. Scan this QR code with your authenticator app (Aegis Authenticator, Google Authenticator, etc.):

TOTP QR Code

Or manually enter this secret:{' '} {setupData.value.secret}

2. Save these backup codes NOW in a safe place:

{setupData.value.backupCodes.map((code, index) =>
{code}
)}

These codes can be used to access your account if you lose your authenticator device.{' '} They won't be visible again.

verificationToken.value = (event.target as HTMLInputElement).value} placeholder='123456' class='mt-1 input-field' maxLength={6} />
) : null} {setupData.value && setupData.value.type === 'passkey' ? (

Passkey Setup Complete

Your passkey has been successfully registered! You can now enable it for multi-factor authentication.

) : null} {setupData.value && setupData.value.type === 'email' ? (

Setup Email

verificationToken.value = (event.target as HTMLInputElement).value} placeholder='123456' class='mt-1 input-field' maxLength={6} />
) : null} {hasMultiFactorAuthEnabled && !showDisableForm.value ? (

Active Authentication Methods

{enabledMethods.map((method) => (
{method.name}
{method.type === 'totp' && typeof method.backupCodesCount !== 'undefined' ? (

{method.backupCodesCount > 0 ? `${method.backupCodesCount} backup codes remaining` : 'No backup codes remaining'}

) : null}
))}
) : null} {showDisableForm.value ? (

{showDisableForm.value === 'all' ? 'Disable All Multi-Factor Authentication' : 'Disable Authentication Method'}

{showDisableForm.value === 'all' ? 'This will disable all multi-factor authentication methods and make your account less secure.' : 'This will disable this authentication method.'} Please enter your password to confirm.

disablePassword.value = (event.target as HTMLInputElement).value} placeholder='Enter your password' class='mt-1 input-field' />
) : null}
); }