User management
securityApi — name changes, 2FA toggle, and account deletion, built on the apiRequest helper.
User management client
securityApi is a compact wrapper around the self-service account management routes, built on the apiRequest helper. The User management guide remains the complete request and response reference.
UI components
All UI examples on this page use shadcn/ui with the default base variant. Only layout and styling change between projects — the component API stays the same. Install with npx shadcn@latest add button input card label alert.
The wrapper
// lib/api/user-management.ts
import { apiRequest } from "./client";
const API_BASE = "/api";
export const userManagementApi = {
changeName: (name: string) =>
apiRequest(API_BASE, "PUT", "/auth/name", { name }),
enableTwoFactor: (password: string) =>
apiRequest(API_BASE, "POST", "/auth/2fa/enable", { password }),
disableTwoFactor: (password: string) =>
apiRequest(API_BASE, "POST", "/auth/2fa/disable", { password }),
deleteAccount: (password: string) =>
apiRequest(API_BASE, "DELETE", "/auth/account", { password }),
requestDeleteAccount: () =>
apiRequest(API_BASE, "POST", "/auth/account/delete/request"),
confirmDeleteAccount: (code: string) =>
apiRequest(API_BASE, "POST", "/auth/account/delete/confirm", { code }),
};Each method returns the matching endpoint response without reshaping errors. For full request/response shapes and error codes, see the User management guide.
Error handling
Every API error returns the same shape. Handle it uniformly across all calls:
// lib/api/client.ts
export class AuthError extends Error {
code: string;
retryAfter?: number;
constructor(body: { error: string; message: string }, status: number, retryAfter?: number) {
super(body.message);
this.name = "AuthError";
this.code = body.error;
this.retryAfter = retryAfter;
}
}Map error codes to user-facing messages:
// lib/auth-errors.ts
const errorMessages: Record<string, string> = {
wrong_password: "Incorrect password",
validation_error: "Name cannot be empty",
two_factor_already_enforced: "Two-factor is already required",
password_required: "Password is required to delete your account",
password_account: "Use password deletion instead",
delete_code_invalid: "Invalid deletion code",
delete_code_already_used: "This code has already been used",
delete_code_expired: "This code has expired",
email_not_configured: "Email is not configured",
rate_limit_exceeded: "Too many attempts, please try again later",
};
export function getErrorMessage(err: { error?: string; message?: string }): string {
if (err.error && err.error in errorMessages) {
return errorMessages[err.error];
}
return err.message ?? "Something went wrong";
}Use it in forms:
import { getErrorMessage } from "../lib/auth-errors";
// inside catch block
} catch (err: any) {
setError(getErrorMessage(err));
}Change name
Change name form
// components/change-name-form.tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { userManagementApi } from "../lib/api/user-management";
import { getErrorMessage } from "../lib/auth-errors";
export function ChangeNameForm({ currentName }: { currentName: string }) {
const [name, setName] = useState(currentName);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setSuccess(false);
setLoading(true);
try {
await userManagementApi.changeName(name);
setSuccess(true);
} catch (err: any) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Change name</CardTitle>
<CardDescription>Update your display name</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert>
<AlertDescription>Name updated</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading || name === currentName}>
{loading ? "Saving..." : "Save"}
</Button>
</form>
</CardContent>
</Card>
);
}Two-factor authentication
Two-factor settings
// components/two-factor-settings.tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { userManagementApi } from "../lib/api/user-management";
import { getErrorMessage } from "../lib/auth-errors";
export function TwoFactorSettings({
isEnabled,
onToggle,
}: {
isEnabled: boolean;
onToggle: (enabled: boolean) => void;
}) {
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleToggle() {
setError(null);
setLoading(true);
try {
if (isEnabled) {
await userManagementApi.disableTwoFactor(password);
onToggle(false);
} else {
await userManagementApi.enableTwoFactor(password);
onToggle(true);
}
setPassword("");
} catch (err: any) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Two-factor authentication</CardTitle>
<CardDescription>
{isEnabled ? "Two-factor is currently enabled" : "Two-factor is currently disabled"}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<Label htmlFor="password">Current password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Button
variant={isEnabled ? "destructive" : "default"}
className="w-full"
onClick={handleToggle}
disabled={loading || !password}
>
{loading
? isEnabled
? "Disabling..."
: "Enabling..."
: isEnabled
? "Disable two-factor"
: "Enable two-factor"}
</Button>
</div>
</CardContent>
</Card>
);
}Delete account
Delete with password
For accounts that have a password. Immediate deletion.
// components/delete-account-form.tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { userManagementApi } from "../lib/api/user-management";
import { getErrorMessage } from "../lib/auth-errors";
export function DeleteAccountWithPassword() {
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleDelete() {
setError(null);
setLoading(true);
try {
await userManagementApi.deleteAccount(password);
window.location.href = "/login";
} catch (err: any) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Delete account</CardTitle>
<CardDescription>This action cannot be undone</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Button
variant="destructive"
className="w-full"
onClick={handleDelete}
disabled={loading || !password}
>
{loading ? "Deleting..." : "Delete account"}
</Button>
</div>
</CardContent>
</Card>
);
}Delete with emailed code (OAuth-only accounts)
For accounts without a password. Two-step: request a code, then confirm with it.
// components/delete-account-oauth.tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { userManagementApi } from "../lib/api/user-management";
import { getErrorMessage } from "../lib/auth-errors";
export function DeleteAccountOAuth() {
const [code, setCode] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [codeSent, setCodeSent] = useState(false);
const [sending, setSending] = useState(false);
async function handleRequestCode() {
setError(null);
setSending(true);
try {
await userManagementApi.requestDeleteAccount();
setCodeSent(true);
} catch (err: any) {
setError(getErrorMessage(err));
} finally {
setSending(false);
}
}
async function handleConfirm() {
setError(null);
setLoading(true);
try {
await userManagementApi.confirmDeleteAccount(code);
window.location.href = "/login";
} catch (err: any) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Delete account</CardTitle>
<CardDescription>This action cannot be undone</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{!codeSent ? (
<Button
variant="destructive"
className="w-full"
onClick={handleRequestCode}
disabled={sending}
>
{sending ? "Sending code..." : "Send deletion code"}
</Button>
) : (
<>
<Alert>
<AlertDescription>Check your email for a deletion code</AlertDescription>
</Alert>
<div className="space-y-2">
<Label htmlFor="code">Deletion code</Label>
<Input
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="ABCD1234"
required
/>
</div>
<Button
variant="destructive"
className="w-full"
onClick={handleConfirm}
disabled={loading || !code}
>
{loading ? "Deleting..." : "Confirm deletion"}
</Button>
</>
)}
</div>
</CardContent>
</Card>
);
}Next
- Setup — the
apiRequesthelper this is built on - User management — full request/response/error reference for each of these endpoints