Skip to content
File

Blob: src/client/stores/auth-store.ts

typescript98 lines
1import { create } from "zustand";
2import { User as UserSchema, type User } from "@/shared/types";
3import { SESSION_MODES, STORAGE_KEYS, type SessionMode } from "@/client/lib/constants";
4import { readVersionedStorageJson, writeVersionedStorageJson, removeStorageItem } from "@/client/lib/storage";
5 
6const STORED_USER_VERSION = 1;
7 
8export type SessionRefreshState = "idle" | "refreshing";
9 
10export interface AuthState {
11 accessToken: string | null;
12 user: User | null;
13 sessionMode: SessionMode;
14 refreshState: SessionRefreshState;
15 setAuth(token: string, user: User): void;
16 /** Full clear -- only on explicit logout. */
17 clearAuth(): void;
18 /** Drop remote auth but keep cached user identity. */
19 markExpired(): void;
20 /** Drop live auth but keep cached user identity for offline surfaces. */
21 markLocalOnly(): void;
22 setUser(user: User): void;
23 setSessionMode(mode: SessionMode): void;
24 setRefreshState(state: SessionRefreshState): void;
25}
26 
27export const selectIsAuthenticated = (s: AuthState): boolean => s.sessionMode === SESSION_MODES.AUTHENTICATED;
28 
29export const selectHasLocalSession = (s: AuthState): boolean =>
30 s.sessionMode === SESSION_MODES.AUTHENTICATED ||
31 s.sessionMode === SESSION_MODES.LOCAL_ONLY ||
32 s.sessionMode === SESSION_MODES.EXPIRED;
33 
34const storedUser = (() => {
35 return readVersionedStorageJson(STORAGE_KEYS.USER, STORED_USER_VERSION, (value) => {
36 const parsed = UserSchema.safeParse(value);
37 return parsed.success ? parsed.data : null;
38 });
39})();
40 
41export const useAuthStore = create<AuthState>((set) => ({
42 accessToken: null,
43 user: storedUser,
44 sessionMode: storedUser ? SESSION_MODES.LOCAL_ONLY : SESSION_MODES.ANONYMOUS,
45 refreshState: "idle",
46 
47 setAuth(token, user) {
48 writeVersionedStorageJson(STORAGE_KEYS.USER, STORED_USER_VERSION, user);
49 set({
50 accessToken: token,
51 user,
52 sessionMode: SESSION_MODES.AUTHENTICATED,
53 refreshState: "idle",
54 });
55 },
56 
57 clearAuth() {
58 removeStorageItem(STORAGE_KEYS.USER);
59 set({
60 accessToken: null,
61 user: null,
62 sessionMode: SESSION_MODES.ANONYMOUS,
63 refreshState: "idle",
64 });
65 },
66 
67 markExpired() {
68 set((state) => ({
69 accessToken: null,
70 sessionMode: SESSION_MODES.EXPIRED,
71 user: state.user,
72 refreshState: "idle",
73 }));
74 },
75 
76 markLocalOnly() {
77 set((state) => ({
78 accessToken: null,
79 sessionMode: SESSION_MODES.LOCAL_ONLY,
80 user: state.user,
81 refreshState: "idle",
82 }));
83 },
84 
85 setUser(user) {
86 writeVersionedStorageJson(STORAGE_KEYS.USER, STORED_USER_VERSION, user);
87 set({ user });
88 },
89 
90 setSessionMode(mode) {
91 set({ sessionMode: mode });
92 },
93 
94 setRefreshState(refreshState) {
95 set({ refreshState });
96 },
97}));