profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
auth.tsx 3,285 bytes
1 import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
2 import { useQueryClient } from "@tanstack/react-query";
3 import { api, clearSessionToken, getSessionToken, jsonBody, setSessionToken } from "./api";
4 import type { Account, AuthSession } from "../types";
5
6 interface AuthContextValue {
7 user: Account | null;
8 loading: boolean;
9 login: (email: string, password: string) => Promise<void>;
10 register: (displayName: string, email: string, password: string) => Promise<void>;
11 logout: () => Promise<void>;
12 deleteAccount: (password: string) => Promise<void>;
13 refresh: () => Promise<void>;
14 }
15
16 const AuthContext = createContext<AuthContextValue | null>(null);
17
18 export function AuthProvider({ children }: { children: ReactNode }) {
19 const queryClient = useQueryClient();
20 const [user, setUser] = useState<Account | null>(null);
21 const [loading, setLoading] = useState(true);
22
23 const refresh = useCallback(async () => {
24 if (!getSessionToken()) {
25 setUser(null);
26 setLoading(false);
27 return;
28 }
29 try {
30 setUser(await api<Account>("/api/v1/auth/me"));
31 } catch {
32 clearSessionToken();
33 queryClient.clear();
34 setUser(null);
35 } finally {
36 setLoading(false);
37 }
38 }, [queryClient]);
39
40 useEffect(() => {
41 void refresh();
42 }, [refresh]);
43
44 useEffect(() => {
45 function handleUnauthorized() {
46 queryClient.clear();
47 setUser(null);
48 }
49 window.addEventListener("tasteprint:unauthorized", handleUnauthorized);
50 return () => window.removeEventListener("tasteprint:unauthorized", handleUnauthorized);
51 }, [queryClient]);
52
53 const login = useCallback(async (email: string, password: string) => {
54 const session = await api<AuthSession>("/api/v1/auth/login", {
55 method: "POST",
56 ...jsonBody({ email, password })
57 });
58 queryClient.clear();
59 setSessionToken(session.token);
60 setUser(session.user);
61 }, [queryClient]);
62
63 const register = useCallback(async (displayName: string, email: string, password: string) => {
64 const session = await api<AuthSession>("/api/v1/auth/register", {
65 method: "POST",
66 ...jsonBody({ displayName, email, password })
67 });
68 queryClient.clear();
69 setSessionToken(session.token);
70 setUser(session.user);
71 }, [queryClient]);
72
73 const logout = useCallback(async () => {
74 try {
75 await api<void>("/api/v1/auth/logout", { method: "POST" });
76 } finally {
77 clearSessionToken();
78 queryClient.clear();
79 setUser(null);
80 }
81 }, [queryClient]);
82
83 const deleteAccount = useCallback(async (password: string) => {
84 await api<void>("/api/v1/auth/me", {
85 method: "DELETE",
86 ...jsonBody({ password })
87 });
88 clearSessionToken();
89 queryClient.clear();
90 setUser(null);
91 }, [queryClient]);
92
93 const value = useMemo(() => ({ user, loading, login, register, logout, deleteAccount, refresh }),
94 [user, loading, login, register, logout, deleteAccount, refresh]);
95
96 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
97 }
98
99 export function useAuth(): AuthContextValue {
100 const context = useContext(AuthContext);
101 if (!context) {
102 throw new Error("useAuth must be used inside AuthProvider");
103 }
104 return context;
105 }
106