refactor: centralize API endpoints and token refresh logic

This commit is contained in:
Your Name
2026-04-11 14:15:27 +05:30
parent 54d2021e81
commit dfd33f1dad
6 changed files with 83 additions and 32 deletions
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
+40 -23
View File
@@ -1,23 +1,50 @@
import axios, { type AxiosError } from "axios"; import axios, { type AxiosError } from "axios";
import { endpoints } from "../config/endpoints";
import { useAuth } from "../store/useAuth"; import { useAuth } from "../store/useAuth";
const authApiClient = axios.create({ const baseURL = import.meta.env.VITE_API_URL;
baseURL: `${import.meta.env.VITE_API_URL}/api/auth/`,
export const preAuthApiClient = axios.create({
baseURL,
withCredentials: true, withCredentials: true,
headers: { headers: {
"Content-Type": "application/json", "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, (response) => response,
async (error: AxiosError) => { async (error: AxiosError) => {
const originalRequest = error.config;
// do not retry refresh request
if ( if (
error.response?.status === 401 && error.response?.status === 401 &&
!error.config?.url?.includes("refresh/") originalRequest &&
!originalRequest.url?.includes(endpoints.REFRESH)
) { ) {
try { try {
const response = await authApiClient.post("refresh/"); // refresh the access token
const response = await preAuthApiClient.post(endpoints.REFRESH);
if (response.status === 200) { if (response.status === 200) {
const newAccessToken = response.data.access; const newAccessToken = response.data.access;
@@ -26,26 +53,16 @@ authApiClient.interceptors.response.use(
accessToken: newAccessToken, accessToken: newAccessToken,
isAuthenticated: true, isAuthenticated: true,
}); });
if (error.config) {
error.config.headers.Authorization = `Bearer ${newAccessToken}`; originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return authApiClient(error.config); return postAuthApiClient(originalRequest);
} }
} } catch (refreshError) {
} catch (error) { useAuth.getState().logout();
return Promise.reject(error); return Promise.reject(refreshError);
} }
} }
return Promise.reject(error); 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;
+21
View File
@@ -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, string>,
): string => {
let result = url;
Object.entries(params).forEach(([key, value]) => {
result = result.replace(`:${key}`, value);
});
return result;
};
+9 -3
View File
@@ -1,8 +1,10 @@
import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react"; import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import authApiClient from "../api/apiClient"; import { preAuthApiClient } from "../api/apiClient";
import Logo from "../components/Logo"; import Logo from "../components/Logo";
import { endpoints, replacePathParams } from "../config/endpoints";
import { ROUTES } from "../config/routes";
export default function Activate() { export default function Activate() {
const { uidb64, token } = useParams(); const { uidb64, token } = useParams();
@@ -20,7 +22,11 @@ export default function Activate() {
const activateAccount = async () => { const activateAccount = async () => {
try { try {
await authApiClient.get(`/activate/${uidb64}/${token}/`); const url = replacePathParams(endpoints.ACTIVATE, {
uidb64,
token,
});
await preAuthApiClient.get(url);
setStatus("success"); setStatus("success");
} catch (err) { } catch (err) {
console.error("Activation error:", err); console.error("Activation error:", err);
@@ -80,7 +86,7 @@ export default function Activate() {
<button <button
type="button" type="button"
className="btn btn-ghost w-full" className="btn btn-ghost w-full"
onClick={() => navigate("/onboard")} onClick={() => navigate(ROUTES.ONBOARD)}
> >
Back to Registration Back to Registration
</button> </button>
+3 -2
View File
@@ -5,9 +5,10 @@ import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { z } from "zod"; import { z } from "zod";
import authApiClient from "../api/apiClient"; import { preAuthApiClient } from "../api/apiClient";
import Logo from "../components/Logo"; import Logo from "../components/Logo";
import FormField from "../components/ui/FormField"; import FormField from "../components/ui/FormField";
import { endpoints } from "../config/endpoints";
// validation logic // validation logic
const registerSchema = z const registerSchema = z
@@ -41,7 +42,7 @@ export default function Register() {
setIsLoading(true); setIsLoading(true);
setApiError(null); setApiError(null);
try { try {
await authApiClient.post("/register/", { await preAuthApiClient.post(endpoints.REGISTER, {
full_name: data.full_name, full_name: data.full_name,
email: data.email, email: data.email,
password: data.password, password: data.password,
+9 -5
View File
@@ -1,5 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import authApiClient from "../api/apiClient"; import { postAuthApiClient, preAuthApiClient } from "../api/apiClient";
import { endpoints } from "../config/endpoints";
interface UserProfile { interface UserProfile {
public_id: string; public_id: string;
@@ -17,14 +18,14 @@ interface AuthState {
checkAuth: () => Promise<void>; checkAuth: () => Promise<void>;
} }
export const useAuth = create<AuthState>((set) => ({ export const useAuth = create<AuthState>((set, get) => ({
accessToken: null, accessToken: null,
isAuthenticated: false, isAuthenticated: false,
user: null, user: null,
isInitializing: true, isInitializing: true,
login: async (credentials: any) => { login: async (credentials: any) => {
const response = await authApiClient.post("login/", credentials); const response = await preAuthApiClient.post(endpoints.LOGIN, credentials);
set({ set({
accessToken: response.data.access, accessToken: response.data.access,
isAuthenticated: true, isAuthenticated: true,
@@ -34,7 +35,10 @@ export const useAuth = create<AuthState>((set) => ({
logout: async () => { logout: async () => {
try { try {
await authApiClient.post("logout/"); const token = get().accessToken;
if (token) {
await preAuthApiClient.post(endpoints.LOGOUT);
}
} finally { } finally {
set({ set({
accessToken: null, accessToken: null,
@@ -46,7 +50,7 @@ export const useAuth = create<AuthState>((set) => ({
checkAuth: async () => { checkAuth: async () => {
try { try {
const response = await authApiClient.get("me/"); const response = await postAuthApiClient.get(endpoints.ME);
set({ set({
user: response.data, user: response.data,
isAuthenticated: true, isAuthenticated: true,