20 lines
2.7 KiB
TypeScript
20 lines
2.7 KiB
TypeScript
import type { NextFunction, Request, Response } from 'express';
|
|
import { resolveSession, verifyCsrf, type SessionPrincipalRow } from '../db/sessions';
|
|
import type { AuthenticatedPrincipal } from '../lib/authProvider';
|
|
|
|
declare global { namespace Express { interface Request { principal?:AuthenticatedPrincipal; sessionToken?:string; sessionRow?:SessionPrincipalRow } } }
|
|
function cookie(req:Request,name:string):string|undefined { const raw=req.headers.cookie??''; for(const part of raw.split(';')){const [key,...rest]=part.trim().split('=');if(key===name)return decodeURIComponent(rest.join('='));} return undefined; }
|
|
export function optionalAuth(req:Request,_res:Response,next:NextFunction):void {
|
|
const token=cookie(req,'conductor_session'); if(!token){next();return;} const row=resolveSession(token);
|
|
if(!row||row.revoked_at||row.enabled!==1||Date.parse(row.expires_at)<=Date.now()){next();return;}
|
|
req.sessionToken=token;req.sessionRow=row;req.principal={userId:row.user_id,username:row.username,displayName:row.display_name,email:row.email,role:row.role,provider:'local'};next();
|
|
}
|
|
export function requireAuth(req:Request,res:Response,next:NextFunction):void { optionalAuth(req,res,()=>{if(!req.principal){res.status(401).json({code:'AUTH_REQUIRED',error:'Authentication is required.'});return;}next();}); }
|
|
export function requireAdmin(req:Request,res:Response,next:NextFunction):void { requireAuth(req,res,()=>{if(req.principal?.role!=='admin'){res.status(403).json({code:'ADMIN_REQUIRED',error:'Administrator access is required.'});return;}next();}); }
|
|
export function requireCsrf(req:Request,res:Response,next:NextFunction):void { if(!req.sessionRow||!verifyCsrf(req.sessionRow,req.header('x-csrf-token'))){res.status(403).json({code:'CSRF_INVALID',error:'The security token is missing or invalid.'});return;}next(); }
|
|
export function requireMutationCsrf(req:Request,res:Response,next:NextFunction):void { if(req.method==='GET'||req.method==='HEAD'||req.method==='OPTIONS'){next();return;}requireCsrf(req,res,next); }
|
|
export const sessionCookie = (token:string,expiresAt:string) => `conductor_session=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Expires=${new Date(expiresAt).toUTCString()}${process.env.NODE_ENV==='production'?'; Secure':''}`;
|
|
export const csrfCookie = (token:string,expiresAt:string) => `conductor_csrf=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Expires=${new Date(expiresAt).toUTCString()}${process.env.NODE_ENV==='production'?'; Secure':''}`;
|
|
export const clearSessionCookie = () => `conductor_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${process.env.NODE_ENV==='production'?'; Secure':''}`;
|
|
export const clearCsrfCookie = () => `conductor_csrf=; Path=/; SameSite=Lax; Max-Age=0${process.env.NODE_ENV==='production'?'; Secure':''}`;
|