66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
|
|
import { defineMiddleware } from 'astro:middleware';
|
||
|
|
import { getSessionManager } from './lib/auth/session-manager.js';
|
||
|
|
|
||
|
|
// Public routes — must be reachable WITHOUT a valid session so the auth
|
||
|
|
// flow itself can happen. Everything else requires authentication.
|
||
|
|
// The dashboard at `/` is intentionally NOT public: it renders real
|
||
|
|
// machine + session data. Add explicit landing/marketing paths here if
|
||
|
|
// one is added later.
|
||
|
|
const PUBLIC_API_PREFIX = '/api/auth/';
|
||
|
|
const PUBLIC_PAGE_PATHS = new Set<string>([
|
||
|
|
'/login',
|
||
|
|
'/login/',
|
||
|
|
]);
|
||
|
|
const PUBLIC_ASSET_PREFIXES = ['/assets/', '/_astro/'];
|
||
|
|
const PUBLIC_ASSET_FILES = new Set<string>([
|
||
|
|
'/favicon.svg',
|
||
|
|
'/favicon.ico',
|
||
|
|
'/robots.txt',
|
||
|
|
]);
|
||
|
|
|
||
|
|
function isPublicPath(pathname: string): boolean {
|
||
|
|
if (pathname.startsWith(PUBLIC_API_PREFIX)) return true;
|
||
|
|
if (PUBLIC_PAGE_PATHS.has(pathname)) return true;
|
||
|
|
if (PUBLIC_ASSET_FILES.has(pathname)) return true;
|
||
|
|
for (const prefix of PUBLIC_ASSET_PREFIXES) {
|
||
|
|
if (pathname.startsWith(prefix)) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const onRequest = defineMiddleware(async (context, next) => {
|
||
|
|
const { pathname } = context.url;
|
||
|
|
|
||
|
|
if (isPublicPath(pathname)) {
|
||
|
|
return next();
|
||
|
|
}
|
||
|
|
|
||
|
|
const sessionMgr = getSessionManager();
|
||
|
|
const result = await sessionMgr.validateSession(context);
|
||
|
|
|
||
|
|
if (result.valid && result.user) {
|
||
|
|
context.locals.user = result.user;
|
||
|
|
return next();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Unauthenticated: JSON for /api/*, redirect for pages.
|
||
|
|
if (pathname.startsWith('/api/')) {
|
||
|
|
return new Response(
|
||
|
|
JSON.stringify({
|
||
|
|
authenticated: false,
|
||
|
|
error: 'authentication_required',
|
||
|
|
reason: result.reason ?? 'missing_cookie',
|
||
|
|
login_url: '/api/auth/login',
|
||
|
|
}),
|
||
|
|
{
|
||
|
|
status: 401,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const loginUrl = new URL('/api/auth/login', context.url);
|
||
|
|
loginUrl.searchParams.set('return_to', pathname + context.url.search);
|
||
|
|
return context.redirect(loginUrl.pathname + loginUrl.search, 302);
|
||
|
|
});
|