diff --git a/app/src/services/authApi.js b/app/src/services/authApi.js deleted file mode 100644 index 9a65633..0000000 --- a/app/src/services/authApi.js +++ /dev/null @@ -1,98 +0,0 @@ -import { API_BASE_URL } from '../utils/constants'; - -export const apiCall = async (url, options = {}) => { - try { - const headers = { - 'Content-Type': 'application/json', - ...options.headers, - }; - - if (options.method && options.method !== 'GET') { - const csrfToken = document.cookie - .split('; ') - .find((row) => row.startsWith('csrf_token=')) - ?.split('=')[1]; - if (csrfToken) { - headers['X-CSRF-Token'] = csrfToken; - } - } - - const response = await fetch(url, { - ...options, - headers, - credentials: 'include', - }); - - if (response.status === 429) { - throw new Error('Rate limit exceeded'); - } - - // Handle 401 responses - if (response.status === 401) { - const isRefreshEndpoint = url.endsWith('/auth/refresh'); - if (!isRefreshEndpoint) { - // Attempt token refresh and retry the request - const refreshSuccess = await refreshToken(); - if (refreshSuccess) { - // Retry the original request - return apiCall(url, options); - } - } - throw new Error('Authentication failed'); - } - - // Handle other error responses - if (!response.ok && response.status !== 204) { - const errorData = await response.json().catch(() => null); - throw new Error( - errorData?.message || `HTTP error! status: ${response.status}` - ); - } - - // Return null for 204 responses - if (response.status === 204) { - return null; - } - - return response; - } catch (error) { - console.error(`API call failed: ${error.message}`); - throw error; - } -}; - -// Authentication endpoints -export const login = async (email, password) => { - const response = await apiCall(`${API_BASE_URL}/auth/login`, { - method: 'POST', - body: JSON.stringify({ email, password }), - }); - - const data = await response.json(); - // No need to store tokens as they're in cookies now - return data; -}; - -export const logout = async () => { - await apiCall(`${API_BASE_URL}/auth/logout`, { - method: 'POST', - }); - return; -}; - -export const refreshToken = async () => { - try { - const response = await apiCall(`${API_BASE_URL}/auth/refresh`, { - method: 'POST', - }); - return response.status === 200; - } catch (error) { - console.error('Token refresh failed:', error); - return false; - } -}; - -export const getCurrentUser = async () => { - const response = await apiCall(`${API_BASE_URL}/auth/me`); - return response.json(); -}; diff --git a/app/src/services/authApi.ts b/app/src/services/authApi.ts new file mode 100644 index 0000000..1bed5a9 --- /dev/null +++ b/app/src/services/authApi.ts @@ -0,0 +1,138 @@ +import { + API_BASE_URL, + User, + LoginRequest, + LoginResponse, + ApiCallOptions, + ErrorResponse +} from '../types/api'; + +let authToken: string | null = null; + +/** + * Sets the authentication token for API requests + */ +export const setAuthToken = (token: string): void => { + authToken = token; +}; + +/** + * Clears the authentication token + */ +export const clearAuthToken = (): void => { + authToken = null; +}; + +/** + * Gets headers for API requests including auth token if present + */ +export const getAuthHeaders = (): HeadersInit => { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + return headers; +}; + +/** + * Makes an API call with authentication and error handling + */ +export const apiCall = async ( + url: string, + options: ApiCallOptions = {} +): Promise => { + try { + const headers = { + ...getAuthHeaders(), + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers: headers as HeadersInit, + }); + + // Handle 401 responses + if (response.status === 401) { + const isRefreshEndpoint = url.endsWith('/auth/refresh'); + if (!isRefreshEndpoint) { + // Attempt token refresh and retry the request + const refreshSuccess = await refreshToken(); + if (refreshSuccess) { + // Retry the original request with the new token + return apiCall(url, options); + } + } + throw new Error('Authentication failed'); + } + + if (!response.ok && response.status !== 204) { + const errorData = await response.json() as ErrorResponse; + throw new Error( + errorData?.message || `HTTP error! status: ${response.status}` + ); + } + + return response; + } catch (error) { + console.error(`API call failed: ${(error as Error).message}`); + throw error; + } +}; + +/** + * Logs in a user with email and password + */ +export const login = async ( + email: string, + password: string +): Promise => { + const loginData: LoginRequest = { email, password }; + const response = await apiCall(`${API_BASE_URL}/auth/login`, { + method: 'POST', + body: JSON.stringify(loginData), + }); + return response.json(); +}; + +/** + * Logs out the current user + */ +export const logout = async (): Promise => { + const sessionId = localStorage.getItem('sessionId'); + await apiCall(`${API_BASE_URL}/auth/logout`, { + method: 'POST', + headers: { + 'X-Session-ID': sessionId || '', + }, + }); +}; + +/** + * Refreshes the auth token using a refresh token + */ +export const refreshToken = async (): Promise => { + const refreshToken = localStorage.getItem('refreshToken'); + try { + const response = await apiCall(`${API_BASE_URL}/auth/refresh`, { + method: 'POST', + body: JSON.stringify({ refreshToken }), + }); + const data = await response.json(); + return !!data.accessToken; + } catch (error) { + return false; + } +}; + +/** + * Gets the currently authenticated user + */ +export const getCurrentUser = async (): Promise => { + const response = await apiCall(`${API_BASE_URL}/auth/me`); + return response.json(); +}; \ No newline at end of file diff --git a/app/src/types/api.ts b/app/src/types/api.ts index 77996bd..d556639 100644 --- a/app/src/types/api.ts +++ b/app/src/types/api.ts @@ -5,3 +5,55 @@ declare global { } export const API_BASE_URL = window.API_BASE_URL; + +/** + * User role in the system + */ +export enum UserRole { + Admin = 'admin', + Editor = 'editor', + Viewer = 'viewer' +} + +/** + * User model from the API + */ +export interface User { + id: number; + email: string; + displayName?: string; + role: UserRole; + createdAt: string; + lastWorkspaceId: number; +} + +/** + * Error response from the API + */ +export interface ErrorResponse { + message: string; +} + +/** + * Login request parameters + */ +export interface LoginRequest { + email: string; + password: string; +} + +/** + * Login response from the API + */ +export interface LoginResponse { + user: User; + sessionId: string; + expiresAt: string; +} + +/** + * API call options extending the standard RequestInit + */ +export interface ApiCallOptions extends RequestInit { + headers?: HeadersInit; +} \ No newline at end of file