apply security audit

This commit is contained in:
Matthias Hochmeister
2026-03-11 13:51:01 +01:00
parent 93a87a7ae9
commit 3c9b7d3446
19 changed files with 247 additions and 341 deletions

View File

@@ -1,10 +1,27 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { API_URL } from '../utils/config';
import { getToken, removeToken, removeUser } from '../utils/storage';
import { getToken, setToken, removeToken, removeUser, getRefreshToken, removeRefreshToken } from '../utils/storage';
let authInitialized = false;
let isRedirectingToLogin = false;
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: any) => void;
}> = [];
function processQueue(error: any, token: string | null = null) {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token!);
}
});
failedQueue = [];
}
export function setAuthInitialized(value: boolean): void {
authInitialized = value;
if (value === true) {
@@ -54,15 +71,71 @@ class ApiService {
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
if (authInitialized && !isRedirectingToLogin) {
isRedirectingToLogin = true;
// Clear tokens and redirect to login
console.warn('Unauthorized request, redirecting to login');
removeToken();
removeUser();
window.location.href = '/login';
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// Don't attempt refresh during auth initialization or if already retried
if (!authInitialized || originalRequest._retry) {
if (authInitialized && !isRedirectingToLogin) {
isRedirectingToLogin = true;
removeToken();
removeRefreshToken();
removeUser();
window.location.href = '/login';
}
return Promise.reject(this.handleError(error));
}
if (isRefreshing) {
return new Promise<AxiosResponse>((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${token}` };
resolve(this.axiosInstance.request(originalRequest));
},
reject: (err: any) => {
reject(err);
},
});
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = getRefreshToken();
if (!refreshToken) {
isRefreshing = false;
if (!isRedirectingToLogin) {
isRedirectingToLogin = true;
removeToken();
removeRefreshToken();
removeUser();
window.location.href = '/login';
}
return Promise.reject(this.handleError(error));
}
try {
// Use a raw axios call (not the intercepted instance) to avoid loops
const response = await axios.post(`${API_URL}/api/auth/refresh`, { refreshToken });
const newToken = response.data.data.accessToken;
setToken(newToken);
processQueue(null, newToken);
originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${newToken}` };
return this.axiosInstance.request(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
if (!isRedirectingToLogin) {
isRedirectingToLogin = true;
removeToken();
removeRefreshToken();
removeUser();
window.location.href = '/login';
}
return Promise.reject(this.handleError(error));
} finally {
isRefreshing = false;
}
// During initialization, silently reject without redirecting
}
// Retry on 429 (Too Many Requests) with exponential backoff

View File

@@ -6,6 +6,7 @@ const REDIRECT_URI = `${window.location.origin}/auth/callback`;
export interface AuthCallbackResponse {
token: string;
refreshToken: string;
user: User;
}
@@ -51,6 +52,7 @@ export const authService = {
});
return {
token: response.data.data.accessToken,
refreshToken: response.data.data.refreshToken,
user: mapBackendUser(response.data.data.user),
};
},