Fix QA review #2: backup code flow, audit filters, schema hardening

C-01: verifyTotp now sends backup_code field when in backup mode
C-02: Backup code input filter allows alphanumeric chars (not digits only)
W-01: Audit log ACTION_TYPES aligned with actual backend action strings
W-02: Added extra="forbid" to SetupRequest and LoginRequest schemas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-27 04:59:29 +08:00
parent 72ac1d53fb
commit 72e00f3a69
4 changed files with 28 additions and 22 deletions

View File

@ -32,6 +32,8 @@ def _validate_username(v: str) -> str:
class SetupRequest(BaseModel): class SetupRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
username: str username: str
password: str password: str
@ -69,6 +71,8 @@ class RegisterRequest(BaseModel):
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
username: str username: str
password: str password: str

View File

@ -16,21 +16,20 @@ import { cn } from '@/lib/utils';
import { actionColor } from './shared'; import { actionColor } from './shared';
const ACTION_TYPES = [ const ACTION_TYPES = [
'user.create', 'admin.user_created',
'user.login', 'admin.role_changed',
'user.logout', 'admin.password_reset',
'user.login_failed', 'admin.mfa_disabled',
'user.locked', 'admin.mfa_enforce_toggled',
'user.unlocked', 'admin.user_deactivated',
'user.role_changed', 'admin.user_activated',
'user.disabled', 'admin.sessions_revoked',
'user.enabled', 'admin.config_updated',
'user.password_reset', 'auth.login_success',
'user.totp_disabled', 'auth.login_failed',
'user.mfa_enforced', 'auth.setup_complete',
'user.mfa_enforcement_removed', 'auth.registration',
'user.sessions_revoked', 'auth.mfa_enforce_prompted',
'config.updated',
]; ];
function actionLabel(action: string): string { function actionLabel(action: string): string {

View File

@ -145,7 +145,7 @@ export default function LockScreen() {
const handleTotpSubmit = async (e: FormEvent) => { const handleTotpSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
await verifyTotp(totpCode); await verifyTotp({ code: totpCode, isBackup: useBackupCode });
} catch (error) { } catch (error) {
toast.error(getErrorMessage(error, 'Invalid verification code')); toast.error(getErrorMessage(error, 'Invalid verification code'));
setTotpCode(''); setTotpCode('');
@ -257,7 +257,7 @@ export default function LockScreen() {
onChange={(e) => onChange={(e) =>
setTotpCode( setTotpCode(
useBackupCode useBackupCode
? e.target.value.replace(/[^0-9-]/g, '') ? e.target.value.replace(/[^A-Za-z0-9-]/g, '').toUpperCase()
: e.target.value.replace(/\D/g, '') : e.target.value.replace(/\D/g, '')
) )
} }

View File

@ -57,11 +57,14 @@ export function useAuth() {
}); });
const totpVerifyMutation = useMutation({ const totpVerifyMutation = useMutation({
mutationFn: async (code: string) => { mutationFn: async ({ code, isBackup }: { code: string; isBackup: boolean }) => {
const { data } = await api.post('/auth/totp-verify', { const payload: Record<string, string> = { mfa_token: mfaToken! };
mfa_token: mfaToken, if (isBackup) {
code, payload.backup_code = code;
}); } else {
payload.code = code;
}
const { data } = await api.post('/auth/totp-verify', payload);
return data; return data;
}, },
onSuccess: () => { onSuccess: () => {