import { create } from 'zustand'; import { api } from '../api/client'; interface AuthState { accessToken: string | null; refreshToken: string | null; username: string | null; roles: string[]; isAuthenticated: boolean; error: string | null; loading: boolean; login: (username: string, password: string) => Promise; loginWithOidcCode: (code: string, redirectUri: string) => Promise; refresh: () => Promise; logout: () => void; } function parseRolesFromJwt(token: string): string[] { try { const payload = JSON.parse(atob(token.split('.')[1])); return Array.isArray(payload.roles) ? payload.roles : []; } catch { return []; } } function loadTokens() { return { accessToken: localStorage.getItem('cameleer-access-token'), refreshToken: localStorage.getItem('cameleer-refresh-token'), username: localStorage.getItem('cameleer-username'), }; } function persistTokens(access: string, refresh: string, username: string) { localStorage.setItem('cameleer-access-token', access); localStorage.setItem('cameleer-refresh-token', refresh); localStorage.setItem('cameleer-username', username); } function clearTokens() { localStorage.removeItem('cameleer-access-token'); localStorage.removeItem('cameleer-refresh-token'); localStorage.removeItem('cameleer-username'); } const initial = loadTokens(); export const useAuthStore = create((set, get) => ({ accessToken: initial.accessToken, refreshToken: initial.refreshToken, username: initial.username, roles: initial.accessToken ? parseRolesFromJwt(initial.accessToken) : [], isAuthenticated: !!initial.accessToken, error: null, loading: false, login: async (username, password) => { set({ loading: true, error: null }); try { const { data, error } = await api.POST('/auth/login', { body: { username, password }, }); if (error || !data) { throw new Error('Invalid credentials'); } const { accessToken, refreshToken, displayName } = data; localStorage.removeItem('cameleer-oidc-end-session'); localStorage.removeItem('cameleer-oidc-id-token'); const name = displayName ?? username; persistTokens(accessToken, refreshToken, name); set({ accessToken, refreshToken, username: name, roles: parseRolesFromJwt(accessToken), isAuthenticated: true, loading: false, }); } catch (e: unknown) { set({ error: e instanceof Error ? e.message : 'Login failed', loading: false, }); } }, loginWithOidcCode: async (code, redirectUri) => { set({ loading: true, error: null }); try { const { data, error } = await api.POST('/auth/oidc/callback', { body: { code, redirectUri }, }); if (error || !data) { throw new Error('OIDC login failed'); } const { accessToken, refreshToken, displayName, idToken } = data; const username = displayName ?? 'oidc-user'; persistTokens(accessToken, refreshToken, username); if (idToken) { localStorage.setItem('cameleer-oidc-id-token', idToken); } set({ accessToken, refreshToken, username, roles: parseRolesFromJwt(accessToken), isAuthenticated: true, loading: false, }); } catch (e: unknown) { set({ error: e instanceof Error ? e.message : 'OIDC login failed', loading: false, }); } }, refresh: async () => { const { refreshToken } = get(); if (!refreshToken) return false; try { const { data, error } = await api.POST('/auth/refresh', { body: { refreshToken }, }); if (error || !data) return false; const username = data.displayName ?? get().username ?? ''; persistTokens(data.accessToken, data.refreshToken, username); set({ accessToken: data.accessToken, refreshToken: data.refreshToken, username, roles: parseRolesFromJwt(data.accessToken), isAuthenticated: true, }); return true; } catch { return false; } }, logout: () => { const endSessionEndpoint = localStorage.getItem('cameleer-oidc-end-session'); const idToken = localStorage.getItem('cameleer-oidc-id-token'); clearTokens(); localStorage.removeItem('cameleer-oidc-end-session'); localStorage.removeItem('cameleer-oidc-id-token'); set({ accessToken: null, refreshToken: null, username: null, roles: [], isAuthenticated: false, error: null, }); if (endSessionEndpoint && idToken) { const postLogoutRedirect = `${window.location.origin}/login`; const params = new URLSearchParams({ id_token_hint: idToken, post_logout_redirect_uri: postLogoutRedirect, }); window.location.href = `${endSessionEndpoint}?${params}`; } }, }));