feat: implement API security measures phase 1

This commit is contained in:
Marko (Hermes Implementer)
2026-05-27 00:24:09 +00:00
parent a0b3ae7e34
commit c2fc7bc15f
21 changed files with 1050 additions and 44 deletions
+126 -1
View File
@@ -16,6 +16,97 @@ apiClient.interceptors.request.use((config) => {
return config;
});
// Response interceptor: auto-refresh on 401, retry once
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (err: unknown) => void;
}> = [];
function processQueue(error: unknown, token: string | null = null): void {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else if (token) {
prom.resolve(token);
}
});
failedQueue = [];
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Only handle 401s that aren't already refresh/login/register attempts
if (
error.response?.status !== 401 ||
originalRequest._retry ||
originalRequest.url?.includes("/api/auth/token/refresh/") ||
originalRequest.url?.includes("/api/auth/login/") ||
originalRequest.url?.includes("/api/auth/register/") ||
originalRequest.url?.includes("/api/auth/logout/")
) {
return Promise.reject(error);
}
if (isRefreshing) {
// Queue this request until the refresh completes
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem("refresh_token");
if (!refreshToken) {
isRefreshing = false;
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
// Redirect to login
window.location.href = "/login";
return Promise.reject(error);
}
try {
const response = await axios.post(
`${
import.meta.env.VITE_API_URL || "http://localhost:8000"
}/api/auth/token/refresh/`,
{ refresh: refreshToken }
);
const newAccessToken = response.data.access;
const newRefreshToken = response.data.refresh;
localStorage.setItem("access_token", newAccessToken);
localStorage.setItem("refresh_token", newRefreshToken);
processQueue(null, newAccessToken);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
window.location.href = "/login";
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
// ── Types ──────────────────────────────────────────────────────────────
export interface RegisterPayload {
email: string;
password: string;
@@ -31,7 +122,6 @@ export interface LoginPayload {
export interface UserProfile {
id: number;
email: string;
first_name: string;
last_name: string;
}
@@ -42,6 +132,8 @@ export interface LoginResponse {
refresh: string;
}
// ── API Functions ──────────────────────────────────────────────────────
export function registerUser(payload: RegisterPayload): Promise<UserProfile> {
return apiClient
.post<UserProfile>("/api/auth/register/", payload)
@@ -53,3 +145,36 @@ export function loginUser(payload: LoginPayload): Promise<LoginResponse> {
.post<LoginResponse>("/api/auth/login/", payload)
.then((res) => res.data);
}
export function getProfile(): Promise<UserProfile> {
return apiClient
.get<UserProfile>("/api/auth/me/")
.then((res) => res.data);
}
export function logoutUser(refreshToken: string): Promise<void> {
return apiClient
.post("/api/auth/logout/", { refresh: refreshToken })
.then(() => {});
}
export function refreshAccessToken(
refreshToken: string
): Promise<{ access: string; refresh: string }> {
return apiClient
.post<{ access: string; refresh: string }>(
"/api/auth/token/refresh/",
{ refresh: refreshToken }
)
.then((res) => res.data);
}
export function setTokens(access: string, refresh: string): void {
localStorage.setItem("access_token", access);
localStorage.setItem("refresh_token", refresh);
}
export function clearTokens(): void {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
}