diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index e219e94..f7f7752 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; -import { api, User, ApiError } from '@/lib/api'; +import { api, User, ApiError, tokenManager } from '@/lib/api'; interface AuthContextType { user: User | null; @@ -30,9 +30,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, []); - // Check session on mount + // Check for existing token on mount useEffect(() => { - const checkSession = async () => { + const checkAuth = async () => { + // Only attempt to fetch user if we have a valid token + if (!tokenManager.hasValidToken()) { + setUser(null); + setIsLoading(false); + return; + } + try { const response = await api.users.me(); setUser(response.user); @@ -43,7 +50,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }; - checkSession(); + checkAuth(); }, []); const login = useCallback(async (email: string, password: string, rememberMe = false) => { diff --git a/src/lib/api.ts b/src/lib/api.ts index eebbe6b..1d2dabb 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,5 @@ // API Client for Gatehouse Backend -// Uses session-based authentication with cookies +// Uses Bearer token authentication import { config } from '@/config'; @@ -44,14 +44,10 @@ export interface OrganizationsResponse { count: number; } -export interface Session { - id: string; - expires_at: string; -} - export interface LoginResponse { user: User; - session: Session; + token: string; + expires_at: string; } export interface ProfileResponse { @@ -72,22 +68,75 @@ class ApiError extends Error { } } +// Token storage keys +const TOKEN_KEY = 'gatehouse_token'; +const TOKEN_EXPIRY_KEY = 'gatehouse_token_expiry'; + +// Token management +export const tokenManager = { + getToken: (): string | null => { + const token = localStorage.getItem(TOKEN_KEY); + const expiry = localStorage.getItem(TOKEN_EXPIRY_KEY); + + // Check if token is expired + if (token && expiry) { + const expiryDate = new Date(expiry); + if (expiryDate <= new Date()) { + tokenManager.clearToken(); + return null; + } + } + + return token; + }, + + setToken: (token: string, expiresAt: string): void => { + localStorage.setItem(TOKEN_KEY, token); + localStorage.setItem(TOKEN_EXPIRY_KEY, expiresAt); + }, + + clearToken: (): void => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(TOKEN_EXPIRY_KEY); + }, + + hasValidToken: (): boolean => { + return tokenManager.getToken() !== null; + }, +}; + +// Central request function - all API calls go through here async function request( endpoint: string, - options: RequestInit = {} + options: RequestInit = {}, + requiresAuth = true ): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record), + }; + + // Add Authorization header if we have a token and auth is required + if (requiresAuth) { + const token = tokenManager.getToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + } + const response = await fetch(`${config.api.baseUrl}${endpoint}`, { ...options, - credentials: 'include', // Important: include session cookies - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, + headers, }); const json: ApiResponse = await response.json(); if (!json.success) { + // Clear token on 401 errors + if (json.code === 401) { + tokenManager.clearToken(); + } + throw new ApiError( json.message || 'An error occurred', json.code, @@ -99,18 +148,31 @@ async function request( return json.data as T; } +// Centralized API client - all routes defined here export const api = { auth: { - login: (email: string, password: string, remember_me = false) => - request('/auth/login', { + login: async (email: string, password: string, remember_me = false): Promise => { + const response = await request('/auth/login', { method: 'POST', body: JSON.stringify({ email, password, remember_me }), - }), + }, false); // Login doesn't require auth + + // Store token on successful login + tokenManager.setToken(response.token, response.expires_at); + + return response; + }, - logout: () => - request('/auth/logout', { - method: 'POST', - }), + logout: async (): Promise => { + try { + await request('/auth/logout', { + method: 'POST', + }); + } finally { + // Always clear token on logout + tokenManager.clearToken(); + } + }, }, users: {