feat: implement API security measures phase 1
This commit is contained in:
+126
-1
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user