feat(web): account security page and MFA enrolment wizard

This commit is contained in:
2026-09-16 09:26:54 +00:00
parent 26825841fa
commit 32fd11cde7
5 changed files with 409 additions and 18 deletions
+64
View File
@@ -790,6 +790,70 @@ export const auth = {
},
};
export interface MePasskey {
id: string;
name: string;
created_at: string;
last_used_at?: string;
transports?: string[];
}
export interface MeMfaStatus {
totp_enabled: boolean;
passkeys: MePasskey[];
recovery_remaining: number;
/** The instance's require_mfa policy: a removal leaving no factor is refused. */
require_mfa: boolean;
/** False when an identity provider owns this user's authentication. */
applicable: boolean;
}
/**
* The signed-in user's own factors, at /api/me/*. Several of these are
* guarded by step-up and answer 403 `step_up_required`; that interception is
* handled globally elsewhere, not here.
*/
export const me = {
mfa(): Promise<MeMfaStatus> {
return request<MeMfaStatus>("/me/mfa");
},
totpSetup(): Promise<{ secret: string; otpauth_uri: string }> {
return request("/me/mfa/totp/setup", { method: "POST" });
},
totpConfirm(code: string): Promise<{ ok: true; recovery_codes?: string[] }> {
return request("/me/mfa/totp/confirm", { method: "POST", body: JSON.stringify({ code }) });
},
removeTotp(): Promise<void> {
return request<void>("/me/mfa/totp", { method: "DELETE" });
},
regenerateRecovery(): Promise<{ recovery_codes: string[] }> {
return request("/me/mfa/recovery/regenerate", { method: "POST" });
},
passkeyRegisterBegin(): Promise<{ publicKey: any; ceremony_id: string }> {
return request("/me/passkeys/begin", { method: "POST" });
},
passkeyRegisterFinish(ceremonyId: string, credential: unknown, name?: string): Promise<{ ok: true; recovery_codes?: string[] }> {
return request("/me/passkeys/finish", {
method: "POST",
body: JSON.stringify({ ceremony_id: ceremonyId, credential, name }),
});
},
renamePasskey(id: string, name: string): Promise<void> {
return request<void>(`/me/passkeys/${id}`, { method: "PATCH", body: JSON.stringify({ name }) });
},
deletePasskey(id: string): Promise<void> {
return request<void>(`/me/passkeys/${id}`, { method: "DELETE" });
},
};
export const api = {
listInstanceUsers(): Promise<InstanceUser[]> {
return request<InstanceUser[]>("/instance/users");