M4-T08: add two-step login and TOTP settings panel (qrcode.react)
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* TOTP API helpers (M4-T08).
|
||||
*
|
||||
* All calls go through the typed apiClient (openapi-fetch + generated schema).
|
||||
* CSRF tokens are injected automatically by the client middleware for write
|
||||
* endpoints; GET /api/auth/totp needs no CSRF (read-only).
|
||||
*
|
||||
* Security notes:
|
||||
* - The TotpSetupResponse contains secret + recovery_codes in plaintext.
|
||||
* These are kept only in component state; callers must NOT write them to
|
||||
* localStorage, sessionStorage, or log them.
|
||||
* - After the setup phase is over the data leaves memory when the component
|
||||
* re-renders (one-time display only).
|
||||
*/
|
||||
|
||||
import apiClient, { ApiError } from '../api/client'
|
||||
import type { components } from '../api/schema.d.ts'
|
||||
|
||||
export type TotpSetupResponse = components['schemas']['TotpSetupResponse']
|
||||
export type TotpStatusResponse = components['schemas']['TotpStatusResponse']
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getTotpStatus — GET /api/auth/totp
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Return the current TOTP enabled state for the authenticated user. */
|
||||
export async function getTotpStatus(): Promise<TotpStatusResponse> {
|
||||
const res = await apiClient.GET('/api/auth/totp')
|
||||
if (!res.data) {
|
||||
throw new ApiError(res.response.status, null)
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setupTotp — POST /api/auth/totp/setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate a new pending TOTP secret + recovery codes.
|
||||
* Returns the one-time plaintext values; caller is responsible for displaying
|
||||
* them exactly once and never persisting them outside component state.
|
||||
*/
|
||||
export async function setupTotp(): Promise<TotpSetupResponse> {
|
||||
const res = await apiClient.POST('/api/auth/totp/setup')
|
||||
if (!res.data) {
|
||||
throw new ApiError(res.response.status, null)
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// enableTotp — POST /api/auth/totp/enable
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Confirm TOTP setup with the current 6-digit code from the authenticator app. */
|
||||
export async function enableTotp(code: string): Promise<void> {
|
||||
// The endpoint returns 204 on success; openapi-fetch yields { data: undefined }.
|
||||
await apiClient.POST('/api/auth/totp/enable', { body: { code } })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// disableTotp — POST /api/auth/totp/disable
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Disable TOTP. Exactly one of password or code must be provided.
|
||||
* On success the backend clears the secret and all recovery codes.
|
||||
*/
|
||||
export async function disableTotp(opts: {
|
||||
password?: string | null
|
||||
code?: string | null
|
||||
}): Promise<void> {
|
||||
await apiClient.POST('/api/auth/totp/disable', {
|
||||
body: { password: opts.password ?? null, code: opts.code ?? null },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user