Phase 1C: @bytelyst/auth-client + @bytelyst/react-auth Google Sign-In
- loginWithGoogle/Microsoft/Apple(idToken) → POST /auth/oauth/:provider
- getProviders/linkProvider/unlinkProvider → provider management
- React context: loginWithGoogle, providers state, refreshProviders
Phase 2D: MFA + Social Login SDK + Auth UI
- verifyMfa/setupTotp/verifyTotpSetup/disableMfa/getMfaStatus
- regenerateRecoveryCodes → recovery code management
- React context: mfaRequired/mfaChallenge/mfaMethods state, verifyMfa action
- login() handles MfaLoginResult (returns false, sets MFA state)
- NEW @bytelyst/auth-ui: LoginForm, MfaChallenge, SocialButtons components
Phase 3: Passkeys + Device SDK
- getPasskeyRegisterOptions/verifyPasskeyRegistration
- getPasskeyAuthOptions/verifyPasskeyAuth/listPasskeys/deletePasskey
- listDevices/trustDevice/revokeDevice/revokeAllDevices
Phase 4C: @bytelyst/auth RS256 support
- createJwtUtils({ algorithm: 'RS256', rsaPrivateKey, rsaPublicKey })
- Dual verification: RS256 first, HS256 fallback (migration-safe)
- Remote JWKS support via jwksUrl option
- Backward-compatible: HS256 remains default
Phase 5B: Admin security endpoints
- getSecurityOverview/unlockUser/exportAuthData/cancelDeletion
Tests: 101 total (36 auth-client + 21 react-auth + 13 auth-ui + 31 auth)
Builds: all 4 packages pass tsc
115 lines
3.1 KiB
TypeScript
115 lines
3.1 KiB
TypeScript
import { useState, type FormEvent } from 'react';
|
|
import type { MfaChallengeProps } from './types.js';
|
|
|
|
/**
|
|
* MFA code entry form (6-digit TOTP or recovery code).
|
|
* Styled via CSS custom properties (inherits --bl-* from host app).
|
|
*/
|
|
export function MfaChallenge({
|
|
onSubmit,
|
|
onUseRecovery,
|
|
methods,
|
|
isLoading = false,
|
|
error,
|
|
className,
|
|
}: MfaChallengeProps) {
|
|
const [code, setCode] = useState('');
|
|
|
|
function handleSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
onSubmit(code);
|
|
}
|
|
|
|
return (
|
|
<div className={className} data-testid="bl-mfa-challenge">
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}
|
|
>
|
|
<div style={{ fontSize: '14px', color: 'var(--bl-text, #333)' }}>
|
|
Enter your authentication code
|
|
</div>
|
|
|
|
{methods && methods.length > 0 && (
|
|
<div
|
|
data-testid="bl-mfa-methods"
|
|
style={{ fontSize: '12px', color: 'var(--bl-muted, #999)' }}
|
|
>
|
|
Available methods: {methods.join(', ')}
|
|
</div>
|
|
)}
|
|
|
|
<input
|
|
type="text"
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
placeholder="000000"
|
|
value={code}
|
|
onChange={e => setCode(e.target.value)}
|
|
required
|
|
disabled={isLoading}
|
|
maxLength={8}
|
|
data-testid="bl-mfa-code"
|
|
style={{
|
|
padding: '12px',
|
|
border: '1px solid var(--bl-border, #ccc)',
|
|
borderRadius: 'var(--bl-radius, 6px)',
|
|
fontSize: '24px',
|
|
textAlign: 'center',
|
|
letterSpacing: '4px',
|
|
fontFamily: 'monospace',
|
|
}}
|
|
/>
|
|
|
|
{error && (
|
|
<div
|
|
data-testid="bl-mfa-error"
|
|
style={{ color: 'var(--bl-error, #dc3545)', fontSize: '13px' }}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || code.length < 6}
|
|
data-testid="bl-mfa-submit"
|
|
style={{
|
|
padding: '10px 16px',
|
|
border: 'none',
|
|
borderRadius: 'var(--bl-radius, 6px)',
|
|
background: 'var(--bl-primary, #0066ff)',
|
|
color: '#fff',
|
|
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
fontSize: '14px',
|
|
fontWeight: 600,
|
|
opacity: isLoading || code.length < 6 ? 0.6 : 1,
|
|
}}
|
|
>
|
|
{isLoading ? 'Verifying...' : 'Verify'}
|
|
</button>
|
|
|
|
{onUseRecovery && (
|
|
<button
|
|
type="button"
|
|
onClick={onUseRecovery}
|
|
disabled={isLoading}
|
|
data-testid="bl-mfa-recovery"
|
|
style={{
|
|
padding: '8px',
|
|
border: 'none',
|
|
background: 'transparent',
|
|
color: 'var(--bl-link, #0066ff)',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
textDecoration: 'underline',
|
|
}}
|
|
>
|
|
Use a recovery code
|
|
</button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|