Refactor authentication API service to TypeScript

This commit is contained in:
2025-04-18 19:37:39 +02:00
parent 0769aa2bac
commit e789025cd1
3 changed files with 190 additions and 98 deletions

View File

@@ -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();
};

138
app/src/services/authApi.ts Normal file
View File

@@ -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<string, string> = {
'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<Response> => {
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<LoginResponse> => {
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<void> => {
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<boolean> => {
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<User> => {
const response = await apiCall(`${API_BASE_URL}/auth/me`);
return response.json();
};

View File

@@ -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;
}