78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
/**
|
|||
|
|
* 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 },
|
||
|
|
})
|
||
|
|
}
|