Skip to content

Commit d674e79

Browse files
fix: fetchWithAuth now always gets fresh Supabase token (prevents Signature has expired); improve 401 handling in grading retry loop
1 parent c07288c commit d674e79

2 files changed

Lines changed: 57 additions & 10 deletions

File tree

src/app/context/AuthContext.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,40 @@ export function AuthProvider({ children }: { children: ReactNode }) {
402402
setRefreshToken(null);
403403
};
404404

405-
const fetchWithAuth = async (url: string, options: RequestInit = {}) => {
406-
const headers = new Headers(options.headers || {});
407-
// Always load token from active Supabase session
408-
const activeToken = token;
409-
if (activeToken) headers.set("Authorization", `Bearer ${activeToken}`);
410-
return safeFetch(url, { ...options, headers });
405+
const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
406+
// Always get the freshest token from Supabase SDK (auto-refreshes if expired)
407+
let activeToken = token;
408+
if (process.env.NEXT_PUBLIC_SUPABASE_URL) {
409+
try {
410+
const { data: { session } } = await supabase.auth.getSession();
411+
if (session?.access_token) {
412+
activeToken = session.access_token;
413+
// Keep React state in sync
414+
if (session.access_token !== token) setToken(session.access_token);
415+
}
416+
} catch { /* fall back to cached token */ }
417+
}
418+
419+
const makeRequest = async (bearerToken: string | null): Promise<Response> => {
420+
const headers = new Headers(options.headers || {});
421+
if (bearerToken) headers.set("Authorization", `Bearer ${bearerToken}`);
422+
return safeFetch(url, { ...options, headers });
423+
};
424+
425+
const res = await makeRequest(activeToken);
426+
427+
// On 401 (token expired), force a Supabase token refresh and retry once
428+
if (res.status === 401 && process.env.NEXT_PUBLIC_SUPABASE_URL) {
429+
try {
430+
const { data: { session } } = await supabase.auth.refreshSession();
431+
if (session?.access_token) {
432+
setToken(session.access_token);
433+
return makeRequest(session.access_token);
434+
}
435+
} catch { /* ignore refresh error, return original 401 */ }
436+
}
437+
438+
return res;
411439
};
412440

413441
const setGeminiKey = async (key: string) => {

src/app/dashboard/exams/page.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,18 +571,23 @@ export default function ExamsPage() {
571571
headers: { "Content-Type": "application/json" },
572572
body: JSON.stringify(gradePayload),
573573
});
574+
// 401 = session expired — stop retrying, prompt user
575+
if (res.status === 401) {
576+
alert("Your session has expired. Please refresh the page and try again — your answer sheets were uploaded successfully.");
577+
return;
578+
}
574579
const json = await res.json();
575580
if (res.ok && json.data && json.data.run_id) {
576581
setRunId(json.data.run_id);
577582
return; // Success — stop retrying
578583
}
579-
// If still parsing, retry after delay
584+
// If still parsing (400), retry after delay
580585
const detail: string = json.detail || "";
581586
const stillParsing = detail.includes("still being parsed") || detail.includes("No parsed submissions");
582587
if (stillParsing && attempt < MAX_RETRIES) {
583588
setTimeout(tryStartGrading, 10000); // retry in 10s
584-
} else if (!stillParsing) {
585-
alert(`Grading error: ${detail}`);
589+
} else if (!stillParsing && detail) {
590+
alert(`Could not start grading: ${detail}`);
586591
}
587592
// else exhausted retries silently — user can click "Start Grading Manually"
588593
} catch (e: any) {
@@ -612,8 +617,22 @@ export default function ExamsPage() {
612617
headers: { "Content-Type": "application/json" },
613618
body: JSON.stringify(gradePayload),
614619
});
620+
if (res.status === 401) {
621+
alert("Your session has expired. Please refresh the page and log back in to start grading.");
622+
return;
623+
}
615624
const json = await res.json();
616-
if (!res.ok) throw new Error(json.detail || "Grading failed to start. Submissions might still be parsing or rubric is not approved.");
625+
if (!res.ok) {
626+
const detail = json.detail || "";
627+
if (detail.includes("still being parsed")) {
628+
alert("Submissions are still being parsed. Please wait 1–2 minutes and try again.");
629+
} else if (detail.includes("No parsed submissions")) {
630+
alert("No parsed submissions found yet. If you just uploaded files, wait 1–2 minutes for Celery to process them.");
631+
} else {
632+
throw new Error(detail || "Grading failed to start. Rubric may not be approved yet.");
633+
}
634+
return;
635+
}
617636
if (json.data && json.data.run_id) {
618637
setRunId(json.data.run_id);
619638
}

0 commit comments

Comments
 (0)