From dfd33f1dadc3531b958519dfcdaebdecf534ec8f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 11 Apr 2026 14:15:27 +0530 Subject: [PATCH] refactor: centralize API endpoints and token refresh logic --- frontend/public/robots.txt | 2 ++ frontend/src/api/apiClient.ts | 61 ++++++++++++++++++++------------ frontend/src/config/endpoints.ts | 21 +++++++++++ frontend/src/pages/Activate.tsx | 12 +++++-- frontend/src/pages/Register.tsx | 5 +-- frontend/src/store/useAuth.ts | 14 +++++--- 6 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 frontend/public/robots.txt create mode 100644 frontend/src/config/endpoints.ts diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 3707016..1559663 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -1,23 +1,50 @@ import axios, { type AxiosError } from "axios"; +import { endpoints } from "../config/endpoints"; import { useAuth } from "../store/useAuth"; -const authApiClient = axios.create({ - baseURL: `${import.meta.env.VITE_API_URL}/api/auth/`, +const baseURL = import.meta.env.VITE_API_URL; + +export const preAuthApiClient = axios.create({ + baseURL, withCredentials: true, headers: { "Content-Type": "application/json", }, }); -authApiClient.interceptors.response.use( +export const postAuthApiClient = axios.create({ + baseURL, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, +}); + +// automatically attach access token to requests +postAuthApiClient.interceptors.request.use((config) => { + const token = useAuth.getState().accessToken; + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// handle 401 & auto token refresh +postAuthApiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { + const originalRequest = error.config; + + // do not retry refresh request if ( error.response?.status === 401 && - !error.config?.url?.includes("refresh/") + originalRequest && + !originalRequest.url?.includes(endpoints.REFRESH) ) { try { - const response = await authApiClient.post("refresh/"); + // refresh the access token + const response = await preAuthApiClient.post(endpoints.REFRESH); + if (response.status === 200) { const newAccessToken = response.data.access; @@ -26,26 +53,16 @@ authApiClient.interceptors.response.use( accessToken: newAccessToken, isAuthenticated: true, }); - if (error.config) { - error.config.headers.Authorization = `Bearer ${newAccessToken}`; - return authApiClient(error.config); - } + + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; + return postAuthApiClient(originalRequest); } - } catch (error) { - return Promise.reject(error); + } catch (refreshError) { + useAuth.getState().logout(); + return Promise.reject(refreshError); } } + return Promise.reject(error); }, ); - -// automatically attach access token to request -authApiClient.interceptors.request.use((config) => { - const token = useAuth.getState().accessToken; - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - -export default authApiClient; diff --git a/frontend/src/config/endpoints.ts b/frontend/src/config/endpoints.ts new file mode 100644 index 0000000..07915aa --- /dev/null +++ b/frontend/src/config/endpoints.ts @@ -0,0 +1,21 @@ +export const endpoints = { + LOGIN: "/api/auth/login/", + REGISTER: "/api/auth/register/", + VERIFY_EMAIL: "/api/auth/verify-email/", + ACTIVATE: "/api/auth/activate/:uidb64/:token/", + ME: "/api/auth/me/", + REFRESH: "/api/auth/refresh/", + LOGOUT: "/api/auth/logout/", +}; + +// simple utility to handle path params +export const replacePathParams = ( + url: string, + params: Record, +): string => { + let result = url; + Object.entries(params).forEach(([key, value]) => { + result = result.replace(`:${key}`, value); + }); + return result; +}; diff --git a/frontend/src/pages/Activate.tsx b/frontend/src/pages/Activate.tsx index d96640b..908186d 100644 --- a/frontend/src/pages/Activate.tsx +++ b/frontend/src/pages/Activate.tsx @@ -1,8 +1,10 @@ import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import authApiClient from "../api/apiClient"; +import { preAuthApiClient } from "../api/apiClient"; import Logo from "../components/Logo"; +import { endpoints, replacePathParams } from "../config/endpoints"; +import { ROUTES } from "../config/routes"; export default function Activate() { const { uidb64, token } = useParams(); @@ -20,7 +22,11 @@ export default function Activate() { const activateAccount = async () => { try { - await authApiClient.get(`/activate/${uidb64}/${token}/`); + const url = replacePathParams(endpoints.ACTIVATE, { + uidb64, + token, + }); + await preAuthApiClient.get(url); setStatus("success"); } catch (err) { console.error("Activation error:", err); @@ -80,7 +86,7 @@ export default function Activate() { diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index c2a55cb..932674e 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -5,9 +5,10 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; -import authApiClient from "../api/apiClient"; +import { preAuthApiClient } from "../api/apiClient"; import Logo from "../components/Logo"; import FormField from "../components/ui/FormField"; +import { endpoints } from "../config/endpoints"; // validation logic const registerSchema = z @@ -41,7 +42,7 @@ export default function Register() { setIsLoading(true); setApiError(null); try { - await authApiClient.post("/register/", { + await preAuthApiClient.post(endpoints.REGISTER, { full_name: data.full_name, email: data.email, password: data.password, diff --git a/frontend/src/store/useAuth.ts b/frontend/src/store/useAuth.ts index 11a938b..cd997f5 100644 --- a/frontend/src/store/useAuth.ts +++ b/frontend/src/store/useAuth.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; -import authApiClient from "../api/apiClient"; +import { postAuthApiClient, preAuthApiClient } from "../api/apiClient"; +import { endpoints } from "../config/endpoints"; interface UserProfile { public_id: string; @@ -17,14 +18,14 @@ interface AuthState { checkAuth: () => Promise; } -export const useAuth = create((set) => ({ +export const useAuth = create((set, get) => ({ accessToken: null, isAuthenticated: false, user: null, isInitializing: true, login: async (credentials: any) => { - const response = await authApiClient.post("login/", credentials); + const response = await preAuthApiClient.post(endpoints.LOGIN, credentials); set({ accessToken: response.data.access, isAuthenticated: true, @@ -34,7 +35,10 @@ export const useAuth = create((set) => ({ logout: async () => { try { - await authApiClient.post("logout/"); + const token = get().accessToken; + if (token) { + await preAuthApiClient.post(endpoints.LOGOUT); + } } finally { set({ accessToken: null, @@ -46,7 +50,7 @@ export const useAuth = create((set) => ({ checkAuth: async () => { try { - const response = await authApiClient.get("me/"); + const response = await postAuthApiClient.get(endpoints.ME); set({ user: response.data, isAuthenticated: true,