begin js(x) to ts(x)

This commit is contained in:
2025-12-19 11:59:00 -05:00
parent 564bfefa4a
commit 823c8b52b3
51 changed files with 1342 additions and 584 deletions

View File

@@ -0,0 +1,146 @@
// requireAuthHook.ts
import { API_URL } from "@/config";
import type { AstroGlobal } from 'astro';
export interface AuthUser {
id?: string;
username?: string;
roles?: string[];
[key: string]: unknown;
}
// Short-term failure cache for auth timeouts/errors
let lastAuthFailureTime: number = 0;
const AUTH_FAILURE_CACHE_MS: number = 30000; // 30 seconds
// WeakMap to cache auth promises per Astro context (request)
const authCache = new WeakMap<AstroGlobal, Promise<AuthUser | null>>();
export const requireAuthHook = async (Astro: AstroGlobal): Promise<AuthUser | null> => {
// If we recently failed due to API timeout/unreachability, fail closed quickly
if (Date.now() - lastAuthFailureTime < AUTH_FAILURE_CACHE_MS) {
return null;
}
// Check if we already have a cached promise for this request
if (authCache.has(Astro)) {
return authCache.get(Astro) ?? null;
}
// 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: AstroGlobal): Promise<AuthUser | null> {
try {
const cookieHeader = Astro.request.headers.get("cookie") ?? "";
// 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`, {
headers: { Cookie: cookieHeader },
credentials: "include",
signal: controller.signal,
});
} 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) {
// Check if we even have a refresh token before attempting refresh
if (!cookieHeader.includes('refresh_token=')) {
return null;
}
// 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);
if (!refreshRes.ok) {
let errorDetail = '';
try {
const errorBody = await refreshRes.text();
errorDetail = ` - ${errorBody}`;
} catch {}
console.error(`[SSR] Token refresh failed ${refreshRes.status}${errorDetail}`);
return null;
}
// Get all Set-Cookie headers (getSetCookie returns an array)
let setCookies: string[] = [];
if (typeof refreshRes.headers.getSetCookie === 'function') {
setCookies = refreshRes.headers.getSetCookie();
} else {
// 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) {
console.error("No set-cookie header found in refresh response");
return null;
}
// 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("; ");
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);
}
if (!res.ok) {
console.error("Failed to fetch user ID after token refresh", res.status);
return null;
}
const user = await res.json();
return user;
} catch (err) {
console.error("[SSR] requireAuthHook error:", err);
return null;
}
}