From 069694e42b9492301a4023a8233b12ec2704fa07 Mon Sep 17 00:00:00 2001 From: ranbeer06052009 Date: Thu, 28 May 2026 11:43:54 +0530 Subject: [PATCH 1/6] feat(frontend): implement centralized axios api client and global 401 handling --- frontend/package-lock.json | 38 ++++++-- frontend/package.json | 2 +- frontend/src/App.tsx | 3 - .../interview/InterviewSessionPage.tsx | 8 +- .../interview/components/DsaPanel.tsx | 9 +- .../interview/hooks/useInterviewSession.ts | 13 ++- frontend/src/features/user/DashboardPage.tsx | 4 +- .../src/features/user/ProfileSetupPage.tsx | 8 +- .../src/features/user/hooks/useDashboard.ts | 7 +- .../features/user/hooks/useProfileSetup.ts | 4 +- frontend/src/services/apiClient.ts | 44 +++++++++ frontend/src/services/auth.service.ts | 20 ++-- frontend/src/services/interview.service.ts | 95 ++++++++----------- frontend/src/services/organization.service.ts | 45 ++++----- frontend/src/services/user.service.ts | 57 ++++------- 15 files changed, 177 insertions(+), 180 deletions(-) create mode 100644 frontend/src/services/apiClient.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8558e7..1feff06 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "@hookform/resolvers": "^5.2.2", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-query": "^5.99.2", - "axios": "^1.15.2", + "axios": "^1.16.1", "dotenv": "^17.4.2", "fuse.js": "^7.3.0", "react": "^19.2.5", @@ -1634,6 +1634,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1845,13 +1857,14 @@ } }, "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -2166,7 +2179,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3181,6 +3193,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4114,7 +4139,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { diff --git a/frontend/package.json b/frontend/package.json index c944088..b566bfc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "@hookform/resolvers": "^5.2.2", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-query": "^5.99.2", - "axios": "^1.15.2", + "axios": "^1.16.1", "dotenv": "^17.4.2", "fuse.js": "^7.3.0", "react": "^19.2.5", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c4cadfa..347e66e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -102,7 +102,6 @@ function App() { return ( @@ -113,7 +112,6 @@ function App() { return ( @@ -124,7 +122,6 @@ function App() { return ( ); diff --git a/frontend/src/features/interview/InterviewSessionPage.tsx b/frontend/src/features/interview/InterviewSessionPage.tsx index 5922a50..7ef1c00 100644 --- a/frontend/src/features/interview/InterviewSessionPage.tsx +++ b/frontend/src/features/interview/InterviewSessionPage.tsx @@ -10,13 +10,11 @@ import type { export interface InterviewSessionPageProps { interviewId: number; - token: string; onExit: () => void; } const InterviewSessionPage: React.FC = ({ interviewId, - token, onExit, }) => { const { @@ -28,7 +26,7 @@ const InterviewSessionPage: React.FC = ({ followUpIndex, answer, applyState, - } = useInterviewSession(interviewId, token); + } = useInterviewSession(interviewId); return (
= ({ error, onAnswer: answer, applyState, - token, followUpIndex, })} @@ -91,7 +88,6 @@ interface RenderArgs { error: string | null; onAnswer: (text: string) => Promise; applyState: (next: InterviewStateResponse) => void; - token: string; followUpIndex: number; } @@ -102,7 +98,6 @@ function renderRound({ error, onAnswer, applyState, - token, followUpIndex, }: RenderArgs) { if (question.type === "custom") { @@ -135,7 +130,6 @@ function renderRound({ diff --git a/frontend/src/features/interview/components/DsaPanel.tsx b/frontend/src/features/interview/components/DsaPanel.tsx index ca0941a..b0277ce 100644 --- a/frontend/src/features/interview/components/DsaPanel.tsx +++ b/frontend/src/features/interview/components/DsaPanel.tsx @@ -16,7 +16,6 @@ import type { interface Props { sessionId: number; - token: string; question: Extract; onAdvance: (next: InterviewStateResponse) => void; } @@ -91,7 +90,6 @@ type ConsoleState = export default function DsaPanel({ sessionId, - token, question, onAdvance, }: Props) { @@ -128,7 +126,6 @@ export default function DsaPanel({ const res = await dsaRun( sessionId, { source_code: source, language, stdin }, - token, ); setConsoleState({ kind: "run-result", result: res }); } catch (e) { @@ -145,8 +142,7 @@ export default function DsaPanel({ try { const res = await dsaTest( sessionId, - { source_code: source, language }, - token, + { source_code: source, language } ); setConsoleState({ kind: "test-result", result: res }); } catch (e) { @@ -164,8 +160,7 @@ export default function DsaPanel({ try { const res = await dsaSubmit( sessionId, - { source_code: source, language }, - token, + { source_code: source, language } ); setConsoleState({ kind: "submit-result", diff --git a/frontend/src/features/interview/hooks/useInterviewSession.ts b/frontend/src/features/interview/hooks/useInterviewSession.ts index 620d1cb..920c5f8 100644 --- a/frontend/src/features/interview/hooks/useInterviewSession.ts +++ b/frontend/src/features/interview/hooks/useInterviewSession.ts @@ -32,7 +32,6 @@ const HEARTBEAT_MS = 5000; export function useInterviewSession( interviewId: number, - token: string, ): UseInterviewSessionReturn { const [phase, setPhase] = useState("loading"); const [state, setState] = useState(null); @@ -59,7 +58,7 @@ export function useInterviewSession( heartbeatRef.current = setInterval(async () => { if (stoppedRef.current) return; try { - const res = await sendHeartbeat(sessionId, token); + const res = await sendHeartbeat(sessionId); if (res.status !== "ongoing") { stoppedRef.current = true; stopHeartbeat(); @@ -71,7 +70,7 @@ export function useInterviewSession( } }, HEARTBEAT_MS); }, - [token, stopHeartbeat], + [stopHeartbeat], ); const applyState = useCallback( @@ -109,7 +108,7 @@ export function useInterviewSession( (async () => { try { - const initial = await startInterview(interviewId, token); + const initial = await startInterview(interviewId); if (cancelled) return; setState(initial); if (initial.completed) { @@ -134,7 +133,7 @@ export function useInterviewSession( stoppedRef.current = true; stopHeartbeat(); }; - }, [interviewId, token, startHeartbeat, stopHeartbeat]); + }, [interviewId, startHeartbeat, stopHeartbeat]); const answer = useCallback( async (text: string) => { @@ -142,7 +141,7 @@ export function useInterviewSession( setIsSubmitting(true); setError(null); try { - const next = await submitAnswer(state.session_id, text, token); + const next = await submitAnswer(state.session_id, text); applyState(next); } catch (e) { if (e instanceof InterviewServiceError) { @@ -161,7 +160,7 @@ export function useInterviewSession( setIsSubmitting(false); } }, - [state, isSubmitting, token, applyState, stopHeartbeat], + [state, isSubmitting, applyState, stopHeartbeat], ); return { diff --git a/frontend/src/features/user/DashboardPage.tsx b/frontend/src/features/user/DashboardPage.tsx index eef129b..e1a5c90 100644 --- a/frontend/src/features/user/DashboardPage.tsx +++ b/frontend/src/features/user/DashboardPage.tsx @@ -8,7 +8,6 @@ import type { export interface DashboardPageProps { user: UserResponse; - token: string; onLogout: () => void; onAttemptInterview?: (interviewId: number) => void; } @@ -64,14 +63,13 @@ function timeRemaining(deadline: string) { const DashboardPage: React.FC = ({ user, - token, onLogout, onAttemptInterview, }) => { const [tab, setTab] = useState("available"); const [query, setQuery] = useState(""); const [selectedId, setSelectedId] = useState(null); - const { available, applied, isLoading, error, refetch } = useDashboard(token); + const { available, applied, isLoading, error, refetch } = useDashboard(); const displayName = user.username; const initials = displayName.slice(0, 2).toUpperCase(); diff --git a/frontend/src/features/user/ProfileSetupPage.tsx b/frontend/src/features/user/ProfileSetupPage.tsx index e2494e3..82d07f7 100644 --- a/frontend/src/features/user/ProfileSetupPage.tsx +++ b/frontend/src/features/user/ProfileSetupPage.tsx @@ -5,19 +5,17 @@ import type { UserResponse } from "../../services/user.service"; export interface ProfileSetupPageProps { userId: number; - token: string; username: string; onComplete: (user: UserResponse) => void; } -const ProfileSetupPage: React.FC = ({ +export function ProfileSetupPage({ userId, - token, username, onComplete, -}) => { +}: ProfileSetupPageProps) { const { form, isLoading, error, handleChange, handleSubmit, handleSkip } = - useProfileSetup(userId, token, onComplete); + useProfileSetup(userId, onComplete); return (
{ - if (!token) return; dispatch({ type: "FETCH_START" }); - Promise.all([fetchInterviews(token), fetchAppliedInterviews(token)]) + Promise.all([fetchInterviews(), fetchAppliedInterviews()]) .then(([av, ap]) => { dispatch({ type: "FETCH_SUCCESS", available: av, applied: ap }); }) @@ -89,7 +88,7 @@ export function useDashboard(token: string): UseDashboardReturn { : "Failed to load interviews. Please refresh."; dispatch({ type: "FETCH_ERROR", error: message }); }); - }, [token, state.tick]); + }, [state.tick]); const refetch = useCallback(() => dispatch({ type: "REFETCH" }), []); diff --git a/frontend/src/features/user/hooks/useProfileSetup.ts b/frontend/src/features/user/hooks/useProfileSetup.ts index 367fbdb..8b24261 100644 --- a/frontend/src/features/user/hooks/useProfileSetup.ts +++ b/frontend/src/features/user/hooks/useProfileSetup.ts @@ -31,7 +31,6 @@ export interface UseProfileSetupReturn { export function useProfileSetup( userId: number, - token: string, onComplete: (user: UserResponse) => void, ): UseProfileSetupReturn { const [form, setForm] = useState({ @@ -66,7 +65,6 @@ export function useProfileSetup( leetcode: form.leetcode || null, }, }, - token, ); onComplete(updated); } catch (err) { @@ -84,7 +82,7 @@ export function useProfileSetup( const handleSkip = () => { // We don't have the latest user object here — pass a signal for the caller // We'll call the backend with empty profile to keep things clean - void updateUserProfile(userId, { profile: {} }, token) + void updateUserProfile(userId, { profile: {} }) .then(onComplete) .catch(() => { /* silent skip */ diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts new file mode 100644 index 0000000..aa24323 --- /dev/null +++ b/frontend/src/services/apiClient.ts @@ -0,0 +1,44 @@ +import axios from "axios"; + +const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; + +export const apiClient = axios.create({ + baseURL: BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +// Request Interceptor: Attach token from localStorage +apiClient.interceptors.request.use( + (config) => { + // Check for user token first, then org token + const token = localStorage.getItem("token") || localStorage.getItem("org_token"); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response Interceptor: Handle global 401 errors +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && error.response.status === 401) { + // Clear tokens + localStorage.removeItem("token"); + localStorage.removeItem("org_token"); + + // Redirect to login page + // Using window.location to force a hard redirect and clear React state + if (!window.location.pathname.includes("/login")) { + window.location.href = "/login"; + } + } + return Promise.reject(error); + } +); diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 989b587..c65e1e9 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -4,7 +4,7 @@ * Mirrors the backend schemas in app/schemas/user.py */ -const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; +import { apiClient } from "./apiClient"; // ── Request / Response types (mirrors backend schemas) ────────────────────── @@ -47,19 +47,13 @@ export class AuthServiceError extends Error { export async function loginUser( credentials: LoginRequest, ): Promise { - const response = await fetch(`${BASE_URL}/users/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(credentials), - }); - - if (!response.ok) { - const data = await response.json().catch(() => ({})); + try { + const response = await apiClient.post("/users/login", credentials); + return response.data; + } catch (error: any) { throw new AuthServiceError( - response.status, - data?.detail ?? "Login failed. Please check your credentials.", + error.response?.status ?? 500, + error.response?.data?.detail ?? "Login failed. Please check your credentials.", ); } - - return response.json() as Promise; } diff --git a/frontend/src/services/interview.service.ts b/frontend/src/services/interview.service.ts index c0f2818..111f604 100644 --- a/frontend/src/services/interview.service.ts +++ b/frontend/src/services/interview.service.ts @@ -1,4 +1,4 @@ -const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; +import { apiClient } from "./apiClient"; export class InterviewServiceError extends Error { public readonly statusCode: number; @@ -87,94 +87,73 @@ export interface DsaSubmitResponse { next_state: InterviewStateResponse; } -function authHeaders(token: string) { - return { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }; -} - -async function handle(res: Response): Promise { - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new InterviewServiceError( - res.status, - data?.detail ?? "Request failed.", - ); - } - return res.json() as Promise; -} export async function startInterview( interviewId: number, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/interviews/${interviewId}/start`, { - method: "POST", - headers: authHeaders(token), - }); - return handle(res); + try { + const res = await apiClient.post(`/interviews/${interviewId}/start`); + return res.data; + } catch (error: any) { + throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } export async function sendHeartbeat( sessionId: number, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/sessions/${sessionId}/heartbeat`, { - method: "POST", - headers: authHeaders(token), - }); - return handle(res); + try { + const res = await apiClient.post(`/sessions/${sessionId}/heartbeat`); + return res.data; + } catch (error: any) { + throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } export async function submitAnswer( sessionId: number, answer: string, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/sessions/${sessionId}/answer`, { - method: "POST", - headers: authHeaders(token), - body: JSON.stringify({ answer }), - }); - return handle(res); + try { + const res = await apiClient.post(`/sessions/${sessionId}/answer`, { answer }); + return res.data; + } catch (error: any) { + throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } export async function dsaRun( sessionId: number, payload: DsaRunRequest, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/sessions/${sessionId}/dsa/run`, { - method: "POST", - headers: authHeaders(token), - body: JSON.stringify(payload), - }); - return handle(res); + try { + const res = await apiClient.post(`/sessions/${sessionId}/dsa/run`, payload); + return res.data; + } catch (error: any) { + throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } export async function dsaTest( sessionId: number, payload: DsaTestRequest, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/sessions/${sessionId}/dsa/test`, { - method: "POST", - headers: authHeaders(token), - body: JSON.stringify(payload), - }); - return handle(res); + try { + const res = await apiClient.post(`/sessions/${sessionId}/dsa/test`, payload); + return res.data; + } catch (error: any) { + throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } export async function dsaSubmit( sessionId: number, payload: DsaSubmitRequest, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/sessions/${sessionId}/dsa/submit`, { - method: "POST", - headers: authHeaders(token), - body: JSON.stringify(payload), - }); - return handle(res); + try { + const res = await apiClient.post(`/sessions/${sessionId}/dsa/submit`, payload); + return res.data; + } catch (error: any) { + throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } diff --git a/frontend/src/services/organization.service.ts b/frontend/src/services/organization.service.ts index fb444cd..82b8e50 100644 --- a/frontend/src/services/organization.service.ts +++ b/frontend/src/services/organization.service.ts @@ -4,7 +4,7 @@ * Mirrors app/schemas/organization.py and app/routers/organization.py */ -const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; +import { apiClient } from "./apiClient"; // ── Types (mirrors backend schemas) ───────────────────────────────────────── @@ -41,18 +41,6 @@ export class OrgServiceError extends Error { } } -// ── Helpers ────────────────────────────────────────────────────────────────── - -async function handleResponse(response: Response): Promise { - if (!response.ok) { - const data = await response.json().catch(() => ({})); - throw new OrgServiceError( - response.status, - data?.detail ?? "Request failed. Please try again.", - ); - } - return response.json() as Promise; -} // ── Endpoints ──────────────────────────────────────────────────────────────── @@ -60,12 +48,15 @@ async function handleResponse(response: Response): Promise { export async function signupOrganization( payload: OrgSignupRequest, ): Promise { - const response = await fetch(`${BASE_URL}/organizations/signup`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - return handleResponse(response); + try { + const response = await apiClient.post("/organizations/signup", payload); + return response.data; + } catch (error: any) { + throw new OrgServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed. Please try again.", + ); + } } /** @@ -76,11 +67,13 @@ export async function loginOrganization(credentials: { username: string; password: string; }): Promise<{ token: string }> { - const response = await fetch(`${BASE_URL}/users/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(credentials), - }); - const data = await handleResponse<{ token: string; user: unknown }>(response); - return { token: data.token }; + try { + const response = await apiClient.post<{ token: string; user: unknown }>("/users/login", credentials); + return { token: response.data.token }; + } catch (error: any) { + throw new OrgServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Login failed. Please check your credentials.", + ); + } } diff --git a/frontend/src/services/user.service.ts b/frontend/src/services/user.service.ts index 794ff3b..b2d22c7 100644 --- a/frontend/src/services/user.service.ts +++ b/frontend/src/services/user.service.ts @@ -4,7 +4,7 @@ * Mirrors backend schemas: app/schemas/user.py + app/schemas/interview.py */ -const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; +import { apiClient } from "./apiClient"; // ── Helpers ─────────────────────────────────────────────────────────────────── export class UserServiceError extends Error { @@ -16,20 +16,6 @@ export class UserServiceError extends Error { } } -function authHeaders(token: string) { - return { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }; -} - -async function handleResponse(res: Response): Promise { - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new UserServiceError(res.status, data?.detail ?? "Request failed."); - } - return res.json() as Promise; -} // ── Types (mirror backend schemas) ─────────────────────────────────────────── @@ -83,32 +69,31 @@ export interface AppliedInterview extends InterviewBasic { export async function updateUserProfile( userId: number, data: UserUpdate, - token: string, ): Promise { - const res = await fetch(`${BASE_URL}/users/${userId}`, { - method: "PUT", - headers: authHeaders(token), - body: JSON.stringify(data), - }); - return handleResponse(res); + try { + const res = await apiClient.put(`/users/${userId}`, data); + return res.data; + } catch (error: any) { + throw new UserServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } /** GET /interviews/ — available interviews for this user */ -export async function fetchInterviews( - token: string, -): Promise { - const res = await fetch(`${BASE_URL}/interviews/`, { - headers: { Authorization: `Bearer ${token}` }, - }); - return handleResponse(res); +export async function fetchInterviews(): Promise { + try { + const res = await apiClient.get("/interviews/"); + return res.data; + } catch (error: any) { + throw new UserServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } /** GET /interviews/applied — interviews the user has applied to */ -export async function fetchAppliedInterviews( - token: string, -): Promise { - const res = await fetch(`${BASE_URL}/interviews/applied`, { - headers: { Authorization: `Bearer ${token}` }, - }); - return handleResponse(res); +export async function fetchAppliedInterviews(): Promise { + try { + const res = await apiClient.get("/interviews/applied"); + return res.data; + } catch (error: any) { + throw new UserServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + } } From 095449269a8830cb79fc3ab128bc5bf275d94c7e Mon Sep 17 00:00:00 2001 From: ranbeer06052009 Date: Thu, 28 May 2026 11:51:39 +0530 Subject: [PATCH 2/6] fix(frontend): integrate auth client into new google oauth routes --- frontend/src/App.tsx | 2 +- frontend/src/services/auth.service.ts | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 65810e7..67f49b9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -69,7 +69,7 @@ function App() { if (!token) return; // error case already reflected in the initial page state localStorage.setItem("token", token); - fetchCurrentUser(token) + fetchCurrentUser() .then((user) => { const hasProfile = Boolean(user.profile?.bio || user.profile?.github); setAuth({ token, user, isNewUser: false }); diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 1b5df66..52059bd 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -6,6 +6,8 @@ import { apiClient } from "./apiClient"; +const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; + // ── Request / Response types (mirrors backend schemas) ────────────────────── export interface LoginRequest { @@ -73,18 +75,14 @@ export function startGoogleOAuth(): void { } /** GET /users/me — resolve the authenticated user from a bearer token. */ -export async function fetchCurrentUser(token: string): Promise { - const response = await fetch(`${BASE_URL}/users/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); - - if (!response.ok) { - const data = await response.json().catch(() => ({})); +export async function fetchCurrentUser(): Promise { + try { + const response = await apiClient.get("/users/me"); + return response.data; + } catch (error: any) { throw new AuthServiceError( - response.status, - data?.detail ?? "Could not load your account.", + error.response?.status ?? 500, + error.response?.data?.detail ?? "Could not load your account.", ); } - - return response.json() as Promise; } From 1e4a84781a7a7fa6f37c4cb12e5a23e61df801a8 Mon Sep 17 00:00:00 2001 From: ranbeer06052009 Date: Thu, 28 May 2026 11:59:53 +0530 Subject: [PATCH 3/6] style(frontend): fix prettier formatting issues --- .../interview/components/DsaPanel.tsx | 25 +++----- .../src/features/user/ProfileSetupPage.tsx | 2 +- .../features/user/hooks/useProfileSetup.ts | 17 +++--- frontend/src/services/apiClient.ts | 9 +-- frontend/src/services/auth.service.ts | 8 ++- frontend/src/services/interview.service.ts | 59 +++++++++++++++---- frontend/src/services/organization.service.ts | 14 +++-- frontend/src/services/user.service.ts | 16 +++-- 8 files changed, 95 insertions(+), 55 deletions(-) diff --git a/frontend/src/features/interview/components/DsaPanel.tsx b/frontend/src/features/interview/components/DsaPanel.tsx index b0277ce..322ccd3 100644 --- a/frontend/src/features/interview/components/DsaPanel.tsx +++ b/frontend/src/features/interview/components/DsaPanel.tsx @@ -88,11 +88,7 @@ type ConsoleState = } | { kind: "error"; message: string }; -export default function DsaPanel({ - sessionId, - question, - onAdvance, -}: Props) { +export default function DsaPanel({ sessionId, question, onAdvance }: Props) { const [language, setLanguage] = useState("python"); const [source, setSource] = useState( LANGUAGES.find((l) => l.value === "python")?.starter ?? "", @@ -123,10 +119,11 @@ export default function DsaPanel({ const handleRun = async () => { setConsoleState({ kind: "running", label: "Running…" }); try { - const res = await dsaRun( - sessionId, - { source_code: source, language, stdin }, - ); + const res = await dsaRun(sessionId, { + source_code: source, + language, + stdin, + }); setConsoleState({ kind: "run-result", result: res }); } catch (e) { setConsoleState({ @@ -140,10 +137,7 @@ export default function DsaPanel({ const handleTest = async () => { setConsoleState({ kind: "running", label: "Running hidden test cases…" }); try { - const res = await dsaTest( - sessionId, - { source_code: source, language } - ); + const res = await dsaTest(sessionId, { source_code: source, language }); setConsoleState({ kind: "test-result", result: res }); } catch (e) { setConsoleState({ @@ -158,10 +152,7 @@ export default function DsaPanel({ setShowConfirm(false); setConsoleState({ kind: "running", label: "Grading your submission…" }); try { - const res = await dsaSubmit( - sessionId, - { source_code: source, language } - ); + const res = await dsaSubmit(sessionId, { source_code: source, language }); setConsoleState({ kind: "submit-result", results: res.case_results, diff --git a/frontend/src/features/user/ProfileSetupPage.tsx b/frontend/src/features/user/ProfileSetupPage.tsx index 82d07f7..0eff5e3 100644 --- a/frontend/src/features/user/ProfileSetupPage.tsx +++ b/frontend/src/features/user/ProfileSetupPage.tsx @@ -301,7 +301,7 @@ export function ProfileSetupPage({
); -}; +} const BgBlobs = () => ( <> diff --git a/frontend/src/features/user/hooks/useProfileSetup.ts b/frontend/src/features/user/hooks/useProfileSetup.ts index 8b24261..9d73c2b 100644 --- a/frontend/src/features/user/hooks/useProfileSetup.ts +++ b/frontend/src/features/user/hooks/useProfileSetup.ts @@ -55,17 +55,14 @@ export function useProfileSetup( setIsLoading(true); setError(null); try { - const updated = await updateUserProfile( - userId, - { - profile: { - bio: form.bio || null, - github: form.github || null, - linkedin: form.linkedin || null, - leetcode: form.leetcode || null, - }, + const updated = await updateUserProfile(userId, { + profile: { + bio: form.bio || null, + github: form.github || null, + linkedin: form.linkedin || null, + leetcode: form.leetcode || null, }, - ); + }); onComplete(updated); } catch (err) { if (err instanceof UserServiceError) { diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts index aa24323..2aa5515 100644 --- a/frontend/src/services/apiClient.ts +++ b/frontend/src/services/apiClient.ts @@ -13,7 +13,8 @@ export const apiClient = axios.create({ apiClient.interceptors.request.use( (config) => { // Check for user token first, then org token - const token = localStorage.getItem("token") || localStorage.getItem("org_token"); + const token = + localStorage.getItem("token") || localStorage.getItem("org_token"); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } @@ -21,7 +22,7 @@ apiClient.interceptors.request.use( }, (error) => { return Promise.reject(error); - } + }, ); // Response Interceptor: Handle global 401 errors @@ -32,7 +33,7 @@ apiClient.interceptors.response.use( // Clear tokens localStorage.removeItem("token"); localStorage.removeItem("org_token"); - + // Redirect to login page // Using window.location to force a hard redirect and clear React state if (!window.location.pathname.includes("/login")) { @@ -40,5 +41,5 @@ apiClient.interceptors.response.use( } } return Promise.reject(error); - } + }, ); diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 52059bd..755ea66 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -50,12 +50,16 @@ export async function loginUser( credentials: LoginRequest, ): Promise { try { - const response = await apiClient.post("/users/login", credentials); + const response = await apiClient.post( + "/users/login", + credentials, + ); return response.data; } catch (error: any) { throw new AuthServiceError( error.response?.status ?? 500, - error.response?.data?.detail ?? "Login failed. Please check your credentials.", + error.response?.data?.detail ?? + "Login failed. Please check your credentials.", ); } } diff --git a/frontend/src/services/interview.service.ts b/frontend/src/services/interview.service.ts index 111f604..95c8228 100644 --- a/frontend/src/services/interview.service.ts +++ b/frontend/src/services/interview.service.ts @@ -87,15 +87,19 @@ export interface DsaSubmitResponse { next_state: InterviewStateResponse; } - export async function startInterview( interviewId: number, ): Promise { try { - const res = await apiClient.post(`/interviews/${interviewId}/start`); + const res = await apiClient.post( + `/interviews/${interviewId}/start`, + ); return res.data; } catch (error: any) { - throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new InterviewServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -103,10 +107,15 @@ export async function sendHeartbeat( sessionId: number, ): Promise { try { - const res = await apiClient.post(`/sessions/${sessionId}/heartbeat`); + const res = await apiClient.post( + `/sessions/${sessionId}/heartbeat`, + ); return res.data; } catch (error: any) { - throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new InterviewServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -115,10 +124,16 @@ export async function submitAnswer( answer: string, ): Promise { try { - const res = await apiClient.post(`/sessions/${sessionId}/answer`, { answer }); + const res = await apiClient.post( + `/sessions/${sessionId}/answer`, + { answer }, + ); return res.data; } catch (error: any) { - throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new InterviewServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -127,10 +142,16 @@ export async function dsaRun( payload: DsaRunRequest, ): Promise { try { - const res = await apiClient.post(`/sessions/${sessionId}/dsa/run`, payload); + const res = await apiClient.post( + `/sessions/${sessionId}/dsa/run`, + payload, + ); return res.data; } catch (error: any) { - throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new InterviewServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -139,10 +160,16 @@ export async function dsaTest( payload: DsaTestRequest, ): Promise { try { - const res = await apiClient.post(`/sessions/${sessionId}/dsa/test`, payload); + const res = await apiClient.post( + `/sessions/${sessionId}/dsa/test`, + payload, + ); return res.data; } catch (error: any) { - throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new InterviewServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -151,9 +178,15 @@ export async function dsaSubmit( payload: DsaSubmitRequest, ): Promise { try { - const res = await apiClient.post(`/sessions/${sessionId}/dsa/submit`, payload); + const res = await apiClient.post( + `/sessions/${sessionId}/dsa/submit`, + payload, + ); return res.data; } catch (error: any) { - throw new InterviewServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new InterviewServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } diff --git a/frontend/src/services/organization.service.ts b/frontend/src/services/organization.service.ts index 82b8e50..7b21a33 100644 --- a/frontend/src/services/organization.service.ts +++ b/frontend/src/services/organization.service.ts @@ -41,7 +41,6 @@ export class OrgServiceError extends Error { } } - // ── Endpoints ──────────────────────────────────────────────────────────────── /** POST /organizations/signup */ @@ -49,7 +48,10 @@ export async function signupOrganization( payload: OrgSignupRequest, ): Promise { try { - const response = await apiClient.post("/organizations/signup", payload); + const response = await apiClient.post( + "/organizations/signup", + payload, + ); return response.data; } catch (error: any) { throw new OrgServiceError( @@ -68,12 +70,16 @@ export async function loginOrganization(credentials: { password: string; }): Promise<{ token: string }> { try { - const response = await apiClient.post<{ token: string; user: unknown }>("/users/login", credentials); + const response = await apiClient.post<{ token: string; user: unknown }>( + "/users/login", + credentials, + ); return { token: response.data.token }; } catch (error: any) { throw new OrgServiceError( error.response?.status ?? 500, - error.response?.data?.detail ?? "Login failed. Please check your credentials.", + error.response?.data?.detail ?? + "Login failed. Please check your credentials.", ); } } diff --git a/frontend/src/services/user.service.ts b/frontend/src/services/user.service.ts index b2d22c7..d16a55d 100644 --- a/frontend/src/services/user.service.ts +++ b/frontend/src/services/user.service.ts @@ -16,7 +16,6 @@ export class UserServiceError extends Error { } } - // ── Types (mirror backend schemas) ─────────────────────────────────────────── export interface UserProfileUpdate { @@ -74,7 +73,10 @@ export async function updateUserProfile( const res = await apiClient.put(`/users/${userId}`, data); return res.data; } catch (error: any) { - throw new UserServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new UserServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -84,7 +86,10 @@ export async function fetchInterviews(): Promise { const res = await apiClient.get("/interviews/"); return res.data; } catch (error: any) { - throw new UserServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new UserServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } @@ -94,6 +99,9 @@ export async function fetchAppliedInterviews(): Promise { const res = await apiClient.get("/interviews/applied"); return res.data; } catch (error: any) { - throw new UserServiceError(error.response?.status ?? 500, error.response?.data?.detail ?? "Request failed."); + throw new UserServiceError( + error.response?.status ?? 500, + error.response?.data?.detail ?? "Request failed.", + ); } } From dc8da74fbcc2a7da3b34a065725631c6fa32837c Mon Sep 17 00:00:00 2001 From: ranbeer06052009 Date: Thu, 28 May 2026 12:03:27 +0530 Subject: [PATCH 4/6] fix(frontend): properly type caught axios errors for eslint --- frontend/src/services/auth.service.ts | 15 ++++--- frontend/src/services/interview.service.ts | 43 +++++++++++-------- frontend/src/services/organization.service.ts | 15 ++++--- frontend/src/services/user.service.ts | 22 ++++++---- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 755ea66..4451af5 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -4,6 +4,7 @@ * Mirrors the backend schemas in app/schemas/user.py */ +import axios, { AxiosError } from "axios"; import { apiClient } from "./apiClient"; const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; @@ -55,10 +56,11 @@ export async function loginUser( credentials, ); return response.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new AuthServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Login failed. Please check your credentials.", ); } @@ -83,10 +85,11 @@ export async function fetchCurrentUser(): Promise { try { const response = await apiClient.get("/users/me"); return response.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new AuthServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Could not load your account.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Could not load your account.", ); } } diff --git a/frontend/src/services/interview.service.ts b/frontend/src/services/interview.service.ts index 95c8228..8325143 100644 --- a/frontend/src/services/interview.service.ts +++ b/frontend/src/services/interview.service.ts @@ -1,3 +1,4 @@ +import axios, { AxiosError } from "axios"; import { apiClient } from "./apiClient"; export class InterviewServiceError extends Error { @@ -95,10 +96,11 @@ export async function startInterview( `/interviews/${interviewId}/start`, ); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new InterviewServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -111,10 +113,11 @@ export async function sendHeartbeat( `/sessions/${sessionId}/heartbeat`, ); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new InterviewServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -129,10 +132,11 @@ export async function submitAnswer( { answer }, ); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new InterviewServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -147,10 +151,11 @@ export async function dsaRun( payload, ); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new InterviewServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -165,10 +170,11 @@ export async function dsaTest( payload, ); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new InterviewServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -183,10 +189,11 @@ export async function dsaSubmit( payload, ); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new InterviewServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } diff --git a/frontend/src/services/organization.service.ts b/frontend/src/services/organization.service.ts index 7b21a33..0115cd4 100644 --- a/frontend/src/services/organization.service.ts +++ b/frontend/src/services/organization.service.ts @@ -4,6 +4,7 @@ * Mirrors app/schemas/organization.py and app/routers/organization.py */ +import axios, { AxiosError } from "axios"; import { apiClient } from "./apiClient"; // ── Types (mirrors backend schemas) ───────────────────────────────────────── @@ -53,10 +54,11 @@ export async function signupOrganization( payload, ); return response.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new OrgServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed. Please try again.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed. Please try again.", ); } } @@ -75,10 +77,11 @@ export async function loginOrganization(credentials: { credentials, ); return { token: response.data.token }; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new OrgServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Login failed. Please check your credentials.", ); } diff --git a/frontend/src/services/user.service.ts b/frontend/src/services/user.service.ts index d16a55d..531d319 100644 --- a/frontend/src/services/user.service.ts +++ b/frontend/src/services/user.service.ts @@ -4,6 +4,7 @@ * Mirrors backend schemas: app/schemas/user.py + app/schemas/interview.py */ +import axios, { AxiosError } from "axios"; import { apiClient } from "./apiClient"; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -72,10 +73,11 @@ export async function updateUserProfile( try { const res = await apiClient.put(`/users/${userId}`, data); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new UserServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -85,10 +87,11 @@ export async function fetchInterviews(): Promise { try { const res = await apiClient.get("/interviews/"); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new UserServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } @@ -98,10 +101,11 @@ export async function fetchAppliedInterviews(): Promise { try { const res = await apiClient.get("/interviews/applied"); return res.data; - } catch (error: any) { + } catch (error) { + const axiosError = error as AxiosError<{ detail: string }>; throw new UserServiceError( - error.response?.status ?? 500, - error.response?.data?.detail ?? "Request failed.", + axiosError.response?.status ?? 500, + axiosError.response?.data?.detail ?? "Request failed.", ); } } From 307f687c6bb294d1d0987aabfc308f6c90d580c5 Mon Sep 17 00:00:00 2001 From: ranbeer06052009 Date: Thu, 28 May 2026 12:04:32 +0530 Subject: [PATCH 5/6] fix(frontend): remove unused axios import --- frontend/src/services/auth.service.ts | 2 +- frontend/src/services/interview.service.ts | 2 +- frontend/src/services/organization.service.ts | 2 +- frontend/src/services/user.service.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 4451af5..5145b82 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -4,7 +4,7 @@ * Mirrors the backend schemas in app/schemas/user.py */ -import axios, { AxiosError } from "axios"; +import { AxiosError } from "axios"; import { apiClient } from "./apiClient"; const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; diff --git a/frontend/src/services/interview.service.ts b/frontend/src/services/interview.service.ts index 8325143..3025b43 100644 --- a/frontend/src/services/interview.service.ts +++ b/frontend/src/services/interview.service.ts @@ -1,4 +1,4 @@ -import axios, { AxiosError } from "axios"; +import { AxiosError } from "axios"; import { apiClient } from "./apiClient"; export class InterviewServiceError extends Error { diff --git a/frontend/src/services/organization.service.ts b/frontend/src/services/organization.service.ts index 0115cd4..a202235 100644 --- a/frontend/src/services/organization.service.ts +++ b/frontend/src/services/organization.service.ts @@ -4,7 +4,7 @@ * Mirrors app/schemas/organization.py and app/routers/organization.py */ -import axios, { AxiosError } from "axios"; +import { AxiosError } from "axios"; import { apiClient } from "./apiClient"; // ── Types (mirrors backend schemas) ───────────────────────────────────────── diff --git a/frontend/src/services/user.service.ts b/frontend/src/services/user.service.ts index 531d319..18429c0 100644 --- a/frontend/src/services/user.service.ts +++ b/frontend/src/services/user.service.ts @@ -4,7 +4,7 @@ * Mirrors backend schemas: app/schemas/user.py + app/schemas/interview.py */ -import axios, { AxiosError } from "axios"; +import { AxiosError } from "axios"; import { apiClient } from "./apiClient"; // ── Helpers ─────────────────────────────────────────────────────────────────── From bc6031ab0199fe21f6106ca48cfe30a0ff8cfa03 Mon Sep 17 00:00:00 2001 From: ranbeer06052009 Date: Thu, 28 May 2026 19:14:06 +0530 Subject: [PATCH 6/6] fix(frontend): address PR comments for apiClient handling --- frontend/src/App.tsx | 16 ++++++++++++++++ frontend/src/services/apiClient.ts | 16 ++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 67f49b9..95f73b7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -82,6 +82,22 @@ function App() { .finally(() => setHydrating(false)); }, []); + // Listen for global 401 errors from apiClient to reset state cleanly + useEffect(() => { + const handleUnauthorized = () => { + localStorage.removeItem("token"); + localStorage.removeItem("org_token"); + setAuth(null); + setActiveInterviewId(null); + setPage("login"); + }; + + window.addEventListener("auth:unauthorized", handleUnauthorized); + return () => { + window.removeEventListener("auth:unauthorized", handleUnauthorized); + }; + }, []); + const handleUserLoginSuccess = (data: TokenResponse) => { const hasProfile = Boolean( data.user.profile?.bio || data.user.profile?.github, diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts index 2aa5515..fff266f 100644 --- a/frontend/src/services/apiClient.ts +++ b/frontend/src/services/apiClient.ts @@ -4,9 +4,6 @@ const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; export const apiClient = axios.create({ baseURL: BASE_URL, - headers: { - "Content-Type": "application/json", - }, }); // Request Interceptor: Attach token from localStorage @@ -30,15 +27,18 @@ apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response && error.response.status === 401) { + const url = error.config?.url || ""; + // Do not trigger global redirect for login or signup endpoints + if (url.includes("/login") || url.includes("/signup")) { + return Promise.reject(error); + } + // Clear tokens localStorage.removeItem("token"); localStorage.removeItem("org_token"); - // Redirect to login page - // Using window.location to force a hard redirect and clear React state - if (!window.location.pathname.includes("/login")) { - window.location.href = "/login"; - } + // Dispatch custom event so App.tsx can cleanly update the page state + window.dispatchEvent(new CustomEvent("auth:unauthorized")); } return Promise.reject(error); },