2025-08-28 11:15:17 -04:00
|
|
|
// requireAuthHook.js
|
|
|
|
|
import { API_URL } from "@/config";
|
2025-08-21 15:07:10 -04:00
|
|
|
|
2025-12-17 13:33:31 -05:00
|
|
|
// Short-term failure cache for auth timeouts/errors
|
|
|
|
|
let lastAuthFailureTime = 0;
|
|
|
|
|
const AUTH_FAILURE_CACHE_MS = 30000; // 30 seconds
|
|
|
|
|
|
2025-12-05 14:21:52 -05:00
|
|
|
// WeakMap to cache auth promises per Astro context (request)
|
|
|
|
|
const authCache = new WeakMap();
|
|
|
|
|
|
2025-08-28 11:15:17 -04:00
|
|
|
export const requireAuthHook = async (Astro) => {
|
2025-12-17 13:33:31 -05:00
|
|
|
// If we recently failed due to API timeout/unreachability, fail closed quickly
|
|
|
|
|
if (Date.now() - lastAuthFailureTime < AUTH_FAILURE_CACHE_MS) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-12-05 14:21:52 -05:00
|
|
|
// Check if we already have a cached promise for this request
|
|
|
|
|
if (authCache.has(Astro)) {
|
|
|
|
|
return authCache.get(Astro);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a promise and cache it immediately to prevent race conditions
|
|
|
|
|
const authPromise = performAuth(Astro);
|
|
|
|
|
authCache.set(Astro, authPromise);
|
|
|
|
|
|
|
|
|
|
// Return the promise - all callers will await the same promise
|
|
|
|
|
return authPromise;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function performAuth(Astro) {
|
2025-08-21 15:07:10 -04:00
|
|
|
try {
|
2025-08-28 11:15:17 -04:00
|
|
|
const cookieHeader = Astro.request.headers.get("cookie") ?? "";
|
2025-12-17 13:33:31 -05:00
|
|
|
// Add timeout to avoid hanging SSR render
|
|
|
|
|
let controller = new AbortController();
|
|
|
|
|
let timeout = setTimeout(() => controller.abort(), 3000);
|
|
|
|
|
let res;
|
|
|
|
|
try {
|
|
|
|
|
res = await fetch(`${API_URL}/auth/id`, {
|
2025-08-28 11:15:17 -04:00
|
|
|
headers: { Cookie: cookieHeader },
|
|
|
|
|
credentials: "include",
|
2025-12-17 13:33:31 -05:00
|
|
|
signal: controller.signal,
|
2025-08-28 11:15:17 -04:00
|
|
|
});
|
2025-12-17 13:33:31 -05:00
|
|
|
} catch (err) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
lastAuthFailureTime = Date.now();
|
|
|
|
|
console.error("[SSR] auth/id failed or timed out", err);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
|
|
|
|
if (res.status === 401) {
|
2025-12-19 10:26:22 -05:00
|
|
|
// Check if we even have a refresh token before attempting refresh
|
|
|
|
|
if (!cookieHeader.includes('refresh_token=')) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 13:33:31 -05:00
|
|
|
// Try refresh with timeout
|
|
|
|
|
controller = new AbortController();
|
|
|
|
|
timeout = setTimeout(() => controller.abort(), 3000);
|
|
|
|
|
let refreshRes;
|
|
|
|
|
try {
|
|
|
|
|
refreshRes = await fetch(`${API_URL}/auth/refresh`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { Cookie: cookieHeader },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
lastAuthFailureTime = Date.now();
|
|
|
|
|
console.error("[SSR] auth/refresh failed or timed out", err);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
clearTimeout(timeout);
|
2025-08-28 11:15:17 -04:00
|
|
|
|
|
|
|
|
if (!refreshRes.ok) {
|
2025-12-19 10:26:22 -05:00
|
|
|
let errorDetail = '';
|
|
|
|
|
try {
|
|
|
|
|
const errorBody = await refreshRes.text();
|
|
|
|
|
errorDetail = ` - ${errorBody}`;
|
|
|
|
|
} catch {}
|
|
|
|
|
console.error(`[SSR] Token refresh failed ${refreshRes.status}${errorDetail}`);
|
2025-08-28 11:15:17 -04:00
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-21 15:07:10 -04:00
|
|
|
|
2025-12-05 14:21:52 -05:00
|
|
|
// Get all Set-Cookie headers (getSetCookie returns an array)
|
|
|
|
|
let setCookies = [];
|
|
|
|
|
if (typeof refreshRes.headers.getSetCookie === 'function') {
|
|
|
|
|
setCookies = refreshRes.headers.getSetCookie();
|
2025-09-22 11:15:24 -04:00
|
|
|
} else {
|
2025-12-05 14:21:52 -05:00
|
|
|
// Fallback for older Node versions
|
|
|
|
|
const setCookieHeader = refreshRes.headers.get("set-cookie");
|
|
|
|
|
if (setCookieHeader) {
|
|
|
|
|
// Split on comma followed by a cookie name (word=), avoiding splitting on Expires dates
|
|
|
|
|
setCookies = setCookieHeader.split(/,(?=\s*[a-zA-Z_][a-zA-Z0-9_]*=)/);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (setCookies.length === 0) {
|
2025-09-22 11:15:24 -04:00
|
|
|
console.error("No set-cookie header found in refresh response");
|
|
|
|
|
return null;
|
2025-08-21 15:07:10 -04:00
|
|
|
}
|
2025-08-28 11:15:17 -04:00
|
|
|
|
2025-12-05 14:21:52 -05:00
|
|
|
// Forward cookies to client
|
|
|
|
|
setCookies.forEach((c) => Astro.response.headers.append("set-cookie", c.trim()));
|
|
|
|
|
|
|
|
|
|
// Build new cookie header for the retry request
|
|
|
|
|
const newCookieHeader = setCookies.map(c => c.split(";")[0].trim()).join("; ");
|
|
|
|
|
|
2025-12-17 13:33:31 -05:00
|
|
|
controller = new AbortController();
|
|
|
|
|
timeout = setTimeout(() => controller.abort(), 3000);
|
|
|
|
|
try {
|
|
|
|
|
res = await fetch(`${API_URL}/auth/id`, {
|
|
|
|
|
headers: { Cookie: newCookieHeader },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
lastAuthFailureTime = Date.now();
|
|
|
|
|
console.error("[SSR] auth/id retry failed or timed out", err);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
clearTimeout(timeout);
|
2025-08-21 15:07:10 -04:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 11:15:17 -04:00
|
|
|
if (!res.ok) {
|
2025-09-22 11:15:24 -04:00
|
|
|
console.error("Failed to fetch user ID after token refresh", res.status);
|
2025-08-28 11:15:17 -04:00
|
|
|
return null;
|
|
|
|
|
}
|
2025-09-22 11:15:24 -04:00
|
|
|
|
2025-08-28 11:15:17 -04:00
|
|
|
const user = await res.json();
|
|
|
|
|
return user;
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("[SSR] requireAuthHook error:", err);
|
|
|
|
|
return null;
|
2025-08-21 15:07:10 -04:00
|
|
|
}
|
2025-12-05 14:21:52 -05:00
|
|
|
}
|