import { createContext, type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { configureHttpAuth } from "../api/http"; import type { AuthState, MeResponse } from "../api/types"; import { changePassword as changePasswordApi } from "../api/auth"; import { fetchCurrentEmployee, loginWithPassword, logoutSession, refreshSession, } from "./authService"; import { resolvePermissionsForRoles } from "./permissions"; type AuthContextValue = AuthState & { login: (identifier: string, password: string) => Promise<{ mustChangePassword: boolean }>; changePassword: (oldPassword: string, newPassword: string) => Promise; logout: () => Promise; bootstrapByRefresh: () => Promise; clearSession: () => void; }; const AuthContext = createContext(null); const EMPTY_STATE: AuthState = { accessToken: null, me: null, permissions: [], mustChangePassword: false, ready: false, }; const MUST_CHANGE_PASSWORD_STORAGE_KEY = "hyb-oa.must_change_password"; function readMustChangePasswordFromStorage() { return typeof window !== "undefined" && window.localStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY) === "1"; } function writeMustChangePasswordToStorage(value: boolean) { if (typeof window === "undefined") return; if (value) { window.localStorage.setItem(MUST_CHANGE_PASSWORD_STORAGE_KEY, "1"); } else { window.localStorage.removeItem(MUST_CHANGE_PASSWORD_STORAGE_KEY); } } function buildStateWithUser( accessToken: string, me: MeResponse, mustChangePassword: boolean, ): AuthState { return { accessToken, me, permissions: resolvePermissionsForRoles(me.roleCodes ?? []), mustChangePassword, ready: true, }; } export function AuthProvider({ children }: PropsWithChildren) { const [state, setState] = useState(EMPTY_STATE); const tokenRef = useRef(null); useEffect(() => { tokenRef.current = state.accessToken; }, [state.accessToken]); const clearSession = useCallback(() => { tokenRef.current = null; writeMustChangePasswordToStorage(false); setState((previous) => ({ ...previous, accessToken: null, me: null, permissions: [], mustChangePassword: false, ready: true, })); }, []); const commitAuthenticatedUser = useCallback( (accessToken: string, me: MeResponse, mustChangePassword: boolean) => { tokenRef.current = accessToken; writeMustChangePasswordToStorage(mustChangePassword); setState(buildStateWithUser(accessToken, me, mustChangePassword)); }, [], ); const bootstrapByRefresh = useCallback(async () => { try { const refreshed = await refreshSession(); const me = await fetchCurrentEmployee(refreshed.accessToken); const mustChangePassword = readMustChangePasswordFromStorage(); commitAuthenticatedUser(refreshed.accessToken, me, mustChangePassword); } catch { clearSession(); } }, [clearSession, commitAuthenticatedUser]); const login = useCallback( async (identifier: string, password: string) => { const result = await loginWithPassword({ identifier, password }); const mustChangePassword = Boolean(result.mustChangePassword); const me = await fetchCurrentEmployee(result.accessToken); commitAuthenticatedUser(result.accessToken, me, mustChangePassword); return { mustChangePassword }; }, [commitAuthenticatedUser], ); const changePassword = useCallback( async (oldPassword: string, newPassword: string) => { const result = await changePasswordApi({ oldPassword, newPassword }); const me = await fetchCurrentEmployee(result.accessToken); commitAuthenticatedUser(result.accessToken, me, false); }, [commitAuthenticatedUser], ); const logout = useCallback(async () => { try { await logoutSession(); } finally { clearSession(); } }, [clearSession]); useEffect(() => { configureHttpAuth({ getAccessToken: () => tokenRef.current, refreshAccessToken: async () => { try { const refreshed = await refreshSession(); tokenRef.current = refreshed.accessToken; setState((previous) => ({ ...previous, accessToken: refreshed.accessToken, ready: true, })); return refreshed.accessToken; } catch { clearSession(); return null; } }, clearSession, }); }, [clearSession]); useEffect(() => { void bootstrapByRefresh(); }, [bootstrapByRefresh]); const value = useMemo( () => ({ ...state, login, changePassword, logout, bootstrapByRefresh, clearSession, }), [bootstrapByRefresh, changePassword, clearSession, login, logout, state], ); return {children}; } export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within AuthProvider"); } return context; }