misc
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
// Short-term failure cache for API auth timeouts
|
||||
let lastAuthFailureTime = 0;
|
||||
const AUTH_FAILURE_CACHE_MS = 30000; // 30 seconds
|
||||
/**
|
||||
* API route authentication helper
|
||||
* Validates user session for protected API endpoints
|
||||
@@ -10,6 +13,17 @@ import { API_URL } from '@/config';
|
||||
* @returns {Promise<{user: object|null, error: Response|null, setCookieHeader: string|null}>}
|
||||
*/
|
||||
export async function requireApiAuth(request) {
|
||||
// If we recently failed due to API timeout, immediately fail closed
|
||||
if (Date.now() - lastAuthFailureTime < AUTH_FAILURE_CACHE_MS) {
|
||||
return {
|
||||
user: null,
|
||||
error: new Response(JSON.stringify({ error: 'Authentication error', message: 'API unreachable or timed out (cached)' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
setCookieHeader: null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const cookieHeader = request.headers.get('cookie') ?? '';
|
||||
|
||||
@@ -25,20 +39,57 @@ export async function requireApiAuth(request) {
|
||||
}
|
||||
|
||||
// Try to get user identity
|
||||
let res = await fetch(`${API_URL}/auth/id`, {
|
||||
headers: { Cookie: cookieHeader },
|
||||
credentials: 'include',
|
||||
});
|
||||
// Add timeout to fetch
|
||||
let controller = new AbortController();
|
||||
let timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout
|
||||
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();
|
||||
return {
|
||||
user: null,
|
||||
error: new Response(JSON.stringify({ error: 'Authentication error', message: 'API unreachable or timed out' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
setCookieHeader: null,
|
||||
};
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
|
||||
let newSetCookieHeader = null;
|
||||
|
||||
// If unauthorized, try to refresh the token
|
||||
if (res.status === 401) {
|
||||
const refreshRes = await fetch(`${API_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { Cookie: cookieHeader },
|
||||
credentials: 'include',
|
||||
});
|
||||
// Add timeout to refresh fetch
|
||||
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);
|
||||
return {
|
||||
user: null,
|
||||
error: new Response(JSON.stringify({ error: 'Authentication error', message: 'API unreachable or timed out' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
setCookieHeader: null,
|
||||
};
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!refreshRes.ok) {
|
||||
return {
|
||||
@@ -51,13 +102,19 @@ export async function requireApiAuth(request) {
|
||||
};
|
||||
}
|
||||
|
||||
// Capture the Set-Cookie header from the refresh response to forward to client
|
||||
newSetCookieHeader = refreshRes.headers.get('set-cookie');
|
||||
|
||||
// Capture the Set-Cookie headers from the refresh response to forward to client
|
||||
if (typeof refreshRes.headers.getSetCookie === 'function') {
|
||||
newSetCookieHeader = refreshRes.headers.getSetCookie();
|
||||
} else {
|
||||
const sc = refreshRes.headers.get('set-cookie');
|
||||
if (sc) newSetCookieHeader = sc.split(/,(?=\s*\w+=)/).map(s => s.trim());
|
||||
}
|
||||
|
||||
let newCookieHeader = cookieHeader;
|
||||
|
||||
if (newSetCookieHeader) {
|
||||
const cookiesArray = newSetCookieHeader.split(/,(?=\s*\w+=)/);
|
||||
if (newSetCookieHeader && newSetCookieHeader.length > 0) {
|
||||
// newSetCookieHeader is an array of cookie strings; build a cookie header for the retry
|
||||
const cookiesArray = Array.isArray(newSetCookieHeader) ? newSetCookieHeader : [newSetCookieHeader];
|
||||
newCookieHeader = cookiesArray.map(c => c.split(';')[0]).join('; ');
|
||||
} else {
|
||||
return {
|
||||
@@ -70,10 +127,28 @@ export async function requireApiAuth(request) {
|
||||
};
|
||||
}
|
||||
|
||||
res = await fetch(`${API_URL}/auth/id`, {
|
||||
headers: { Cookie: newCookieHeader },
|
||||
credentials: 'include',
|
||||
});
|
||||
// Add timeout to retry fetch
|
||||
let retryController = new AbortController();
|
||||
let retryTimeout = setTimeout(() => retryController.abort(), 3000);
|
||||
try {
|
||||
res = await fetch(`${API_URL}/auth/id`, {
|
||||
headers: { Cookie: newCookieHeader },
|
||||
credentials: 'include',
|
||||
signal: retryController.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
clearTimeout(retryTimeout);
|
||||
lastAuthFailureTime = Date.now();
|
||||
return {
|
||||
user: null,
|
||||
error: new Response(JSON.stringify({ error: 'Authentication error', message: 'API unreachable or timed out' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
setCookieHeader: null,
|
||||
};
|
||||
}
|
||||
clearTimeout(retryTimeout);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -109,9 +184,12 @@ export async function requireApiAuth(request) {
|
||||
* @param {string|null} setCookieHeader - Set-Cookie header from auth refresh
|
||||
*/
|
||||
export function createApiResponse(data, status = 200, setCookieHeader = null) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const headers = new Headers({ 'Content-Type': 'application/json' });
|
||||
if (setCookieHeader) {
|
||||
headers['Set-Cookie'] = setCookieHeader;
|
||||
const cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||||
for (const c of cookies) {
|
||||
if (c) headers.append('Set-Cookie', c.trim());
|
||||
}
|
||||
}
|
||||
return new Response(JSON.stringify(data), { status, headers });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user