mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-11-09 10:39:41 -06:00
135 lines
5.2 KiB
JavaScript
135 lines
5.2 KiB
JavaScript
import { SUPABASE_URL, NETLIFY_URL, getBasePath } from './config.js';
|
|
import { clearSession } from './state.js';
|
|
|
|
const LOGIN_PAGE = getBasePath() + '/index.html';
|
|
const SETUP_PAGE = getBasePath() + '/profile_setup.html';
|
|
const DASHBOARD_PAGE = getBasePath() + '/dashboard.html';
|
|
|
|
(async function guard() {
|
|
const path = window.location.pathname;
|
|
// Enhanced path checking for landing page
|
|
const isOnIndex = path.endsWith('/') || path.endsWith('index.html') || path === getBasePath() || path === getBasePath() + '/';
|
|
const isOnSetupPage = path.includes('profile_setup.html');
|
|
const isPublicPage = isOnIndex || path.includes('login') || path.includes('about') || path.includes('contact');
|
|
const isProtected = !isPublicPage;
|
|
|
|
const redirectToLogin = () => {
|
|
window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(path);
|
|
};
|
|
|
|
// 0. The app authenticates with `tinytorch_token` (set by email login and OAuth alike).
|
|
// Use it as the source of truth here too — older email logins never establish a
|
|
// Supabase client session, so gating on supabase.auth.getSession() locked them out.
|
|
let token = localStorage.getItem("tinytorch_token");
|
|
|
|
if (!token) {
|
|
if (isProtected) redirectToLogin();
|
|
return; // Public page with no token: nothing to guard.
|
|
}
|
|
|
|
// 1. Validate the token against the profile Edge Function, with one refresh retry.
|
|
let profile = null;
|
|
let retryCount = 0;
|
|
const MAX_RETRIES = 1;
|
|
|
|
do {
|
|
let res;
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
res = await fetch(`${SUPABASE_URL}/get-profile-details`, {
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
signal: controller.signal
|
|
});
|
|
clearTimeout(timeoutId);
|
|
} catch (e) {
|
|
// Network error / timeout: fail open rather than bounce the user around.
|
|
console.warn("Guard: profile fetch failed or timed out", e);
|
|
return;
|
|
}
|
|
|
|
// 401, or 400 with "Invalid Token", means the access token needs refreshing.
|
|
let needsRefresh = res.status === 401;
|
|
if (res.status === 400) {
|
|
try {
|
|
const errData = await res.clone().json();
|
|
if (errData?.error?.includes("Invalid Token")) needsRefresh = true;
|
|
} catch (e) { /* ignore parse error */ }
|
|
}
|
|
|
|
if (needsRefresh && retryCount === 0) {
|
|
const refreshTokenStr = localStorage.getItem("tinytorch_refresh_token");
|
|
if (refreshTokenStr) {
|
|
try {
|
|
const refreshRes = await fetch(`${NETLIFY_URL}/api/auth/refresh`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ refreshToken: refreshTokenStr })
|
|
});
|
|
if (refreshRes.ok) {
|
|
const refreshData = await refreshRes.json();
|
|
const session = refreshData.session || refreshData;
|
|
if (session?.access_token) {
|
|
token = session.access_token;
|
|
localStorage.setItem("tinytorch_token", token);
|
|
if (session.refresh_token) {
|
|
localStorage.setItem("tinytorch_refresh_token", session.refresh_token);
|
|
}
|
|
retryCount++;
|
|
continue; // Retry with refreshed token
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("Guard: token refresh failed", e);
|
|
}
|
|
}
|
|
// No refresh token, or refresh failed → the session is dead.
|
|
await clearSession();
|
|
if (isProtected) redirectToLogin();
|
|
return;
|
|
}
|
|
|
|
if (res.status === 404) {
|
|
// Account deleted server-side.
|
|
console.warn("Guard: account not found (404). Purging session...");
|
|
await clearSession();
|
|
if (isProtected) redirectToLogin();
|
|
return;
|
|
}
|
|
|
|
if (!res.ok) {
|
|
// Unknown server error: fail open rather than lock the user out.
|
|
console.warn("Guard: unexpected profile fetch status", res.status);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await res.json();
|
|
profile = data.profile;
|
|
} catch (e) {
|
|
console.warn("Guard: failed to parse profile response", e);
|
|
return;
|
|
}
|
|
break; // Success
|
|
} while (retryCount <= MAX_RETRIES);
|
|
|
|
// 2. Completeness rules.
|
|
const hasName = profile && profile.display_name;
|
|
const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution);
|
|
const hasLoc = profile && profile.location;
|
|
|
|
const isComplete = hasName && hasInst && hasLoc;
|
|
|
|
if (isComplete) {
|
|
if (isOnSetupPage) {
|
|
console.log("✅ Profile complete. Moving to dashboard...");
|
|
window.location.href = DASHBOARD_PAGE;
|
|
}
|
|
} else {
|
|
if (!isOnSetupPage && isProtected) {
|
|
console.log("🚧 Profile incomplete. Redirecting to setup...");
|
|
window.location.href = SETUP_PAGE;
|
|
}
|
|
}
|
|
})();
|