Migrate to Bearer tokens
Switch API client to use Bearer token authentication with centralized api.ts, remove session-token logic, and update AuthContext to rely on token-based flow. Introduce token storage, propagate token via Authorization header, and keep login/logout flows intact. X-Lovable-Edit-ID: edt-da01510b-c831-4a48-9b71-708de445097d
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { api, User, ApiError } from '@/lib/api';
|
import { api, User, ApiError, tokenManager } from '@/lib/api';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
@@ -30,9 +30,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Check session on mount
|
// Check for existing token on mount
|
||||||
useEffect(() => {
|
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 {
|
try {
|
||||||
const response = await api.users.me();
|
const response = await api.users.me();
|
||||||
setUser(response.user);
|
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) => {
|
const login = useCallback(async (email: string, password: string, rememberMe = false) => {
|
||||||
|
|||||||
+82
-20
@@ -1,5 +1,5 @@
|
|||||||
// API Client for Gatehouse Backend
|
// API Client for Gatehouse Backend
|
||||||
// Uses session-based authentication with cookies
|
// Uses Bearer token authentication
|
||||||
|
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
|
|
||||||
@@ -44,14 +44,10 @@ export interface OrganizationsResponse {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Session {
|
|
||||||
id: string;
|
|
||||||
expires_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
user: User;
|
user: User;
|
||||||
session: Session;
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileResponse {
|
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<T>(
|
async function request<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestInit = {}
|
options: RequestInit = {},
|
||||||
|
requiresAuth = true
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(options.headers as Record<string, string>),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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}`, {
|
const response = await fetch(`${config.api.baseUrl}${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
credentials: 'include', // Important: include session cookies
|
headers,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const json: ApiResponse<T> = await response.json();
|
const json: ApiResponse<T> = await response.json();
|
||||||
|
|
||||||
if (!json.success) {
|
if (!json.success) {
|
||||||
|
// Clear token on 401 errors
|
||||||
|
if (json.code === 401) {
|
||||||
|
tokenManager.clearToken();
|
||||||
|
}
|
||||||
|
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
json.message || 'An error occurred',
|
json.message || 'An error occurred',
|
||||||
json.code,
|
json.code,
|
||||||
@@ -99,18 +148,31 @@ async function request<T>(
|
|||||||
return json.data as T;
|
return json.data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Centralized API client - all routes defined here
|
||||||
export const api = {
|
export const api = {
|
||||||
auth: {
|
auth: {
|
||||||
login: (email: string, password: string, remember_me = false) =>
|
login: async (email: string, password: string, remember_me = false): Promise<LoginResponse> => {
|
||||||
request<LoginResponse>('/auth/login', {
|
const response = await request<LoginResponse>('/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ email, password, remember_me }),
|
body: JSON.stringify({ email, password, remember_me }),
|
||||||
}),
|
}, false); // Login doesn't require auth
|
||||||
|
|
||||||
logout: () =>
|
// Store token on successful login
|
||||||
request<void>('/auth/logout', {
|
tokenManager.setToken(response.token, response.expires_at);
|
||||||
method: 'POST',
|
|
||||||
}),
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await request<void>('/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// Always clear token on logout
|
||||||
|
tokenManager.clearToken();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
users: {
|
users: {
|
||||||
|
|||||||
Reference in New Issue
Block a user