Add backend policy tests and API client split
All checks were successful
Build and Deploy / build-and-push (push) Successful in 3m8s

This commit is contained in:
Cauê Faleiros
2026-05-28 16:00:30 -03:00
parent 5648dc7986
commit aa59e642af
11 changed files with 298 additions and 171 deletions

93
src/services/apiClient.ts Normal file
View File

@@ -0,0 +1,93 @@
// Em produção, usa caminho relativo porque o backend serve o frontend.
// Em desenvolvimento, aponta para o backend local.
export const API_URL = import.meta.env.PROD ? '/api' : 'http://localhost:3001/api';
export const getHeaders = (customToken?: string) => {
const token = customToken || localStorage.getItem('ctms_token');
if (!token || token === 'undefined' || token === 'null') return { 'Content-Type': 'application/json' };
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
};
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];
let sessionExpiredHandler: (() => void) | null = null;
export const setSessionExpiredHandler = (handler: () => void) => {
sessionExpiredHandler = handler;
};
const onRefreshed = (token: string) => {
refreshSubscribers.forEach(cb => cb(token));
refreshSubscribers = [];
};
const addRefreshSubscriber = (cb: (token: string) => void) => {
refreshSubscribers.push(cb);
};
const expireSession = () => {
if (sessionExpiredHandler) {
sessionExpiredHandler();
return;
}
localStorage.removeItem('ctms_token');
localStorage.removeItem('ctms_refresh_token');
localStorage.removeItem('ctms_user_id');
localStorage.removeItem('ctms_tenant_id');
};
export const apiFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
let response = await fetch(url, options);
if (response.status === 401 && !url.includes('/auth/login') && !url.includes('/auth/refresh')) {
const refreshToken = localStorage.getItem('ctms_refresh_token');
if (!refreshToken) {
expireSession();
return response;
}
if (isRefreshing) {
return new Promise(resolve => {
addRefreshSubscriber((newToken) => {
options.headers = getHeaders(newToken);
resolve(fetch(url, options));
});
});
}
isRefreshing = true;
try {
const refreshResponse = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (!refreshResponse.ok) {
throw new Error('Refresh token invalid');
}
const data = await refreshResponse.json();
localStorage.setItem('ctms_token', data.token);
options.headers = getHeaders(data.token);
response = await fetch(url, options);
onRefreshed(data.token);
} catch (err) {
console.error("Session expired or refresh failed:", err);
expireSession();
} finally {
isRefreshing = false;
}
}
return response;
};

View File

@@ -1,88 +1,6 @@
import { Attendance, DashboardFilter, User } from '../types';
// URL do Backend
// Em produção (import.meta.env.PROD), usa caminho relativo '/api' pois o backend serve o frontend
// Em desenvolvimento, aponta para o localhost:3001
const API_URL = import.meta.env.PROD ? '/api' : 'http://localhost:3001/api';
const getHeaders = (customToken?: string) => {
const token = customToken || localStorage.getItem('ctms_token');
// Evitar enviar "undefined" ou "null" como strings se o localStorage estiver corrompido
if (!token || token === 'undefined' || token === 'null') return { 'Content-Type': 'application/json' };
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
};
// Global flag to prevent multiple simultaneous refresh attempts
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];
const onRefreshed = (token: string) => {
refreshSubscribers.forEach(cb => cb(token));
refreshSubscribers = [];
};
const addRefreshSubscriber = (cb: (token: string) => void) => {
refreshSubscribers.push(cb);
};
export const apiFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
let response = await fetch(url, options);
// If unauthorized, attempt to refresh the token
if (response.status === 401 && !url.includes('/auth/login') && !url.includes('/auth/refresh')) {
const refreshToken = localStorage.getItem('ctms_refresh_token');
if (!refreshToken) {
logout();
return response;
}
if (isRefreshing) {
// If a refresh is already in progress, wait for it to finish and retry
return new Promise(resolve => {
addRefreshSubscriber((newToken) => {
options.headers = getHeaders(newToken);
resolve(fetch(url, options));
});
});
}
isRefreshing = true;
try {
const refreshResponse = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (!refreshResponse.ok) {
throw new Error('Refresh token invalid');
}
const data = await refreshResponse.json();
localStorage.setItem('ctms_token', data.token);
// Retry the original request
options.headers = getHeaders(data.token);
response = await fetch(url, options);
onRefreshed(data.token);
} catch (err) {
console.error("Session expired or refresh failed:", err);
logout();
} finally {
isRefreshing = false;
}
}
return response;
};
import { API_URL, apiFetch, getHeaders, setSessionExpiredHandler } from './apiClient';
export const getNotifications = async (): Promise<any[]> => {
try {
@@ -693,6 +611,8 @@ export const logout = () => {
}
};
setSessionExpiredHandler(logout);
export const login = async (credentials: any): Promise<any> => {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',