diff --git a/packages/frontend/src/stores/authStore.ts b/packages/frontend/src/stores/authStore.ts new file mode 100644 index 0000000..1a77b50 --- /dev/null +++ b/packages/frontend/src/stores/authStore.ts @@ -0,0 +1,61 @@ +import { create } from 'zustand'; +import type { User } from '@flashcard/shared'; +import { authApi } from '../api/auth.js'; +import { ApiClientError } from '../api/client.js'; + +interface AuthState { + user: User | null; + loading: boolean; + ready: boolean; + hydrate: () => Promise; + refreshMe: () => Promise; + login: (email: string, password: string) => Promise; + logout: () => Promise; + setUserFromAuthResponse: (publicUser: { id: number; email: string; displayName: string; role: 'user' | 'sysadmin' }) => void; +} + +export const useAuth = create((set, get) => ({ + user: null, + loading: false, + ready: false, + + hydrate: async () => { + set({ loading: true }); + try { + const user = await authApi.me(); + set({ user, ready: true }); + } catch (e) { + if (e instanceof ApiClientError && e.status === 401) { + set({ user: null, ready: true }); + return; + } + set({ user: null, ready: true }); + } finally { + set({ loading: false }); + } + }, + + refreshMe: async () => { + try { set({ user: await authApi.me() }); } catch { set({ user: null }); } + }, + + login: async (email, password) => { + await authApi.login({ email, password }); + await get().refreshMe(); + }, + + logout: async () => { + await authApi.logout(); + set({ user: null }); + }, + + setUserFromAuthResponse: (pu) => { + set({ + user: { + id: pu.id, email: pu.email, displayName: pu.displayName, role: pu.role, + isActive: true, emailVerifiedAt: Math.floor(Date.now() / 1000), + pendingEmail: null, createdAt: 0, updatedAt: 0, + }, + }); + }, +}));