118 lines
3.4 KiB
JavaScript
118 lines
3.4 KiB
JavaScript
|
|
/**
|
||
|
|
* API route authentication helper
|
||
|
|
* Validates user session for protected API endpoints
|
||
|
|
*/
|
||
|
|
import { API_URL } from '@/config';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if the request has a valid authentication session
|
||
|
|
* @param {Request} request - The incoming request
|
||
|
|
* @returns {Promise<{user: object|null, error: Response|null, setCookieHeader: string|null}>}
|
||
|
|
*/
|
||
|
|
export async function requireApiAuth(request) {
|
||
|
|
try {
|
||
|
|
const cookieHeader = request.headers.get('cookie') ?? '';
|
||
|
|
|
||
|
|
if (!cookieHeader) {
|
||
|
|
return {
|
||
|
|
user: null,
|
||
|
|
error: new Response(JSON.stringify({ error: 'Authentication required' }), {
|
||
|
|
status: 401,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}),
|
||
|
|
setCookieHeader: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to get user identity
|
||
|
|
let res = await fetch(`${API_URL}/auth/id`, {
|
||
|
|
headers: { Cookie: cookieHeader },
|
||
|
|
credentials: 'include',
|
||
|
|
});
|
||
|
|
|
||
|
|
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',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!refreshRes.ok) {
|
||
|
|
return {
|
||
|
|
user: null,
|
||
|
|
error: new Response(JSON.stringify({ error: 'Session expired' }), {
|
||
|
|
status: 401,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}),
|
||
|
|
setCookieHeader: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Capture the Set-Cookie header from the refresh response to forward to client
|
||
|
|
newSetCookieHeader = refreshRes.headers.get('set-cookie');
|
||
|
|
|
||
|
|
let newCookieHeader = cookieHeader;
|
||
|
|
|
||
|
|
if (newSetCookieHeader) {
|
||
|
|
const cookiesArray = newSetCookieHeader.split(/,(?=\s*\w+=)/);
|
||
|
|
newCookieHeader = cookiesArray.map(c => c.split(';')[0]).join('; ');
|
||
|
|
} else {
|
||
|
|
return {
|
||
|
|
user: null,
|
||
|
|
error: new Response(JSON.stringify({ error: 'Session refresh failed' }), {
|
||
|
|
status: 401,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}),
|
||
|
|
setCookieHeader: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
res = await fetch(`${API_URL}/auth/id`, {
|
||
|
|
headers: { Cookie: newCookieHeader },
|
||
|
|
credentials: 'include',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!res.ok) {
|
||
|
|
return {
|
||
|
|
user: null,
|
||
|
|
error: new Response(JSON.stringify({ error: 'Authentication failed' }), {
|
||
|
|
status: 401,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}),
|
||
|
|
setCookieHeader: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = await res.json();
|
||
|
|
return { user, error: null, setCookieHeader: newSetCookieHeader };
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API auth error:', err);
|
||
|
|
return {
|
||
|
|
user: null,
|
||
|
|
error: new Response(JSON.stringify({ error: 'Authentication error' }), {
|
||
|
|
status: 500,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}),
|
||
|
|
setCookieHeader: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Helper to create a response with optional Set-Cookie header forwarding
|
||
|
|
* @param {any} data - Response data
|
||
|
|
* @param {number} status - HTTP status code
|
||
|
|
* @param {string|null} setCookieHeader - Set-Cookie header from auth refresh
|
||
|
|
*/
|
||
|
|
export function createApiResponse(data, status = 200, setCookieHeader = null) {
|
||
|
|
const headers = { 'Content-Type': 'application/json' };
|
||
|
|
if (setCookieHeader) {
|
||
|
|
headers['Set-Cookie'] = setCookieHeader;
|
||
|
|
}
|
||
|
|
return new Response(JSON.stringify(data), { status, headers });
|
||
|
|
}
|