= ({
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..322ccd3 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;
}
@@ -89,12 +88,7 @@ type ConsoleState =
}
| { kind: "error"; message: string };
-export default function DsaPanel({
- sessionId,
- token,
- 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 ?? "",
@@ -125,11 +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 },
- token,
- );
+ const res = await dsaRun(sessionId, {
+ source_code: source,
+ language,
+ stdin,
+ });
setConsoleState({ kind: "run-result", result: res });
} catch (e) {
setConsoleState({
@@ -143,11 +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 },
- token,
- );
+ const res = await dsaTest(sessionId, { source_code: source, language });
setConsoleState({ kind: "test-result", result: res });
} catch (e) {
setConsoleState({
@@ -162,11 +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 },
- token,
- );
+ const res = await dsaSubmit(sessionId, { source_code: source, language });
setConsoleState({
kind: "submit-result",
results: res.case_results,
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..0eff5e3 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 (
= ({
);
-};
+}
const BgBlobs = () => (
<>
diff --git a/frontend/src/features/user/hooks/useDashboard.ts b/frontend/src/features/user/hooks/useDashboard.ts
index 13a42a9..c85cfc7 100644
--- a/frontend/src/features/user/hooks/useDashboard.ts
+++ b/frontend/src/features/user/hooks/useDashboard.ts
@@ -71,14 +71,13 @@ const initialState: DashboardState = {
tick: 0,
};
-export function useDashboard(token: string): UseDashboardReturn {
+export function useDashboard(): UseDashboardReturn {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
- 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..9d73c2b 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({
@@ -56,18 +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,
},
- token,
- );
+ });
onComplete(updated);
} catch (err) {
if (err instanceof UserServiceError) {
@@ -84,7 +79,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..fff266f
--- /dev/null
+++ b/frontend/src/services/apiClient.ts
@@ -0,0 +1,45 @@
+import axios from "axios";
+
+const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
+
+export const apiClient = axios.create({
+ baseURL: BASE_URL,
+});
+
+// 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) {
+ 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");
+
+ // Dispatch custom event so App.tsx can cleanly update the page state
+ window.dispatchEvent(new CustomEvent("auth:unauthorized"));
+ }
+ return Promise.reject(error);
+ },
+);
diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts
index 28873ed..5145b82 100644
--- a/frontend/src/services/auth.service.ts
+++ b/frontend/src/services/auth.service.ts
@@ -4,6 +4,9 @@
* Mirrors the backend schemas in app/schemas/user.py
*/
+import { AxiosError } from "axios";
+import { apiClient } from "./apiClient";
+
const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
// ── Request / Response types (mirrors backend schemas) ──────────────────────
@@ -47,21 +50,20 @@ 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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
throw new AuthServiceError(
- response.status,
- data?.detail ?? "Login failed. Please check your credentials.",
+ axiosError.response?.status ?? 500,
+ axiosError.response?.data?.detail ??
+ "Login failed. Please check your credentials.",
);
}
-
- return response.json() as Promise;
}
// ── Google OIDC ───────────────────────────────────────────────────────────────
@@ -79,18 +81,15 @@ 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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
throw new AuthServiceError(
- response.status,
- data?.detail ?? "Could not load your account.",
+ axiosError.response?.status ?? 500,
+ axiosError.response?.data?.detail ?? "Could not load your account.",
);
}
-
- return response.json() as Promise;
}
diff --git a/frontend/src/services/interview.service.ts b/frontend/src/services/interview.service.ts
index c0f2818..3025b43 100644
--- a/frontend/src/services/interview.service.ts
+++ b/frontend/src/services/interview.service.ts
@@ -1,4 +1,5 @@
-const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
+import { AxiosError } from "axios";
+import { apiClient } from "./apiClient";
export class InterviewServiceError extends Error {
public readonly statusCode: number;
@@ -87,94 +88,112 @@ 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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new InterviewServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new InterviewServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new InterviewServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new InterviewServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new InterviewServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new InterviewServiceError(
+ 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 fb444cd..a202235 100644
--- a/frontend/src/services/organization.service.ts
+++ b/frontend/src/services/organization.service.ts
@@ -4,7 +4,8 @@
* Mirrors app/schemas/organization.py and app/routers/organization.py
*/
-const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
+import { AxiosError } from "axios";
+import { apiClient } from "./apiClient";
// ── Types (mirrors backend schemas) ─────────────────────────────────────────
@@ -41,31 +42,25 @@ 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 ────────────────────────────────────────────────────────────────
/** POST /organizations/signup */
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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new OrgServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.response?.data?.detail ?? "Request failed. Please try again.",
+ );
+ }
}
/**
@@ -76,11 +71,18 @@ 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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new OrgServiceError(
+ 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 794ff3b..18429c0 100644
--- a/frontend/src/services/user.service.ts
+++ b/frontend/src/services/user.service.ts
@@ -4,7 +4,8 @@
* Mirrors backend schemas: app/schemas/user.py + app/schemas/interview.py
*/
-const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
+import { AxiosError } from "axios";
+import { apiClient } from "./apiClient";
// ── Helpers ───────────────────────────────────────────────────────────────────
export class UserServiceError extends Error {
@@ -16,21 +17,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) ───────────────────────────────────────────
export interface UserProfileUpdate {
@@ -83,32 +69,43 @@ 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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new UserServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new UserServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.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) {
+ const axiosError = error as AxiosError<{ detail: string }>;
+ throw new UserServiceError(
+ axiosError.response?.status ?? 500,
+ axiosError.response?.data?.detail ?? "Request failed.",
+ );
+ }
}