Files
hope-events/frontend/src/lib/auth.ts
T
joshuaandClaude Opus 5.5 d265047c84 POPIA toolkit part 2: privacy notice editor, operators, consent and access logs
- Admin -> Privacy (POPIA): edit the privacy notice (built-in text as the
  start, placeholders for org/IO/operators), drafts, preview, numbered
  versions; earlier versions readable on /legal/privacy?version=N
- "Significant change" versions are accepted once after login
  (NoticePrompt); sign-up records the version accepted
- Operator register (seeded with the services the app uses), listed in
  the notice via {operators}
- Consent log (news opt-ins, notice acceptances, parental consent) and
  access log (report exports, registers, certificates, group lists, form
  answers, consent/medical lists, backups, impersonation)
- Information Officer on the contact page
- Unit tests (privacyNotice.test.js), e2e (privacy-notice.spec.ts); docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
2026-10-23 19:11:20 +02:00

64 lines
2.3 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;
// POPIA: hasn't answered the "news and announcements" opt-in question yet (asked once).
newsPromptPending?: boolean;
// POPIA: a privacy notice version they must accept has been published since they last did.
noticePromptPending?: 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 {
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);
}