Files
hope-events/frontend/src/lib/auth.ts
T
joshuaandClaude Opus 5.5 c3012cb3f6 Require two-factor authentication for admins and supervisors
Admins and supervisors without 2FA are sent a code at their next login,
and verifying it turns 2FA on. They can change the channel but not turn
it off. Older sessions without 2FA are rejected with TWO_FACTOR_REQUIRED,
and promoting a user ends their sessions so they enrol at next login.
The requirement is skipped when no email or WhatsApp channel can deliver
a code, and TWO_FACTOR_REQUIRED_ROLES=false switches it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:00:12 +02:00

60 lines
2.1 KiB
TypeScript

export type AuthUser = {
id: string;
name: string;
email: string;
role?: "admin" | "supervisor" | "staff" | "user";
phoneNumber?: string | null;
notificationPreference?: "email" | "whatsapp" | "both";
twoFactorEnabled?: boolean;
twoFactorMethod?: TwoFactorMethod | null;
// True for roles that must use 2FA (admin, supervisor) — it can't be turned off.
twoFactorRequired?: boolean;
};
export type AuthResponse = AuthUser & {
token: string;
// Only present when the user ticked "Trust this device" on the 2FA step.
deviceToken?: string;
};
export type TwoFactorMethod = "email" | "whatsapp";
// What the login endpoint returns instead of a token when the account has 2FA on:
// a code was sent to `destination` (masked) and must be exchanged for the token.
export type TwoFactorChallenge = {
twoFactorRequired?: true;
challengeId: string;
method: TwoFactorMethod;
destination: string;
// The role requires 2FA and it wasn't on yet: verifying this code turns it on.
enrolling?: boolean;
};
export function isTwoFactorChallenge(res: unknown): res is TwoFactorChallenge & { twoFactorRequired: true } {
return !!res && typeof res === "object" && (res as TwoFactorChallenge).twoFactorRequired === true;
}
const TOKEN_KEY = "hope_events_token";
// Remembers this browser after a 2FA code so the next login can skip it. Kept on
// logout on purpose — it identifies the device, not the session.
const DEVICE_KEY = "hope_events_device";
export function saveDeviceToken(token: string) {
if (typeof window !== "undefined") localStorage.setItem(DEVICE_KEY, token);
}
export function getDeviceToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(DEVICE_KEY);
}
export function saveToken(token: string) {
if (typeof window !== "undefined") localStorage.setItem(TOKEN_KEY, token);
}
export function getToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(TOKEN_KEY);
}
export function clearToken() {
if (typeof window !== "undefined") localStorage.removeItem(TOKEN_KEY);
}