AI Implementation feature(820): Project Scaffold and Platform Foundations (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import * as crypto from 'crypto';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiError } from '../errors/api-error';
|
||||
import { ERROR_CODES } from '../errors/error-codes';
|
||||
|
||||
const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
const COOKIE_NAME = 'csrf';
|
||||
const HEADER_NAME = 'x-csrf-token';
|
||||
const SKIP_PATH_PREFIXES = ['/api/v1/auth/register-first-admin', '/api/v1/auth/login'];
|
||||
|
||||
function isSkipped(path: string): boolean {
|
||||
return SKIP_PATH_PREFIXES.some((p) => path === p || path.startsWith(p + '/'));
|
||||
}
|
||||
|
||||
function readCookie(req: Request): string | null {
|
||||
const header = req.headers.cookie;
|
||||
if (!header) return null;
|
||||
const parts = header.split(';');
|
||||
for (const part of parts) {
|
||||
const [k, ...v] = part.trim().split('=');
|
||||
if (k === COOKIE_NAME) return v.join('=');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the request's effective path. In NestJS mounted middleware,
|
||||
* `req.path` may already be stripped to '/' when the controller is reached
|
||||
* through a nested router; use the original URL as the authoritative source.
|
||||
*/
|
||||
function requestPath(req: Request): string {
|
||||
const orig = req.originalUrl || req.url || req.path || '/';
|
||||
const qIdx = orig.indexOf('?');
|
||||
return qIdx >= 0 ? orig.slice(0, qIdx) : orig;
|
||||
}
|
||||
|
||||
function safeEqual(a: string, b: string): boolean {
|
||||
const bufA = Buffer.from(a);
|
||||
const bufB = Buffer.from(b);
|
||||
if (bufA.length !== bufB.length) return false;
|
||||
return crypto.timingSafeEqual(bufA, bufB);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CsrfMiddleware implements NestMiddleware {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
/**
|
||||
* Public helper: ensure a `csrf` cookie exists on the response and return its value.
|
||||
* If a token already exists in the request cookies, that one is returned and re-set.
|
||||
* Otherwise a new one is minted, set on the response, and returned.
|
||||
*/
|
||||
setOrGetCsrfToken(req: Request, res: Response): string {
|
||||
const existing = readCookie(req);
|
||||
if (existing) {
|
||||
this.writeCookie(res, existing);
|
||||
return existing;
|
||||
}
|
||||
const token = crypto.randomBytes(24).toString('base64url');
|
||||
this.writeCookie(res, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction): void {
|
||||
const path = requestPath(req);
|
||||
if (isSkipped(path)) {
|
||||
this.setOrGetCsrfToken(req, res);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!UNSAFE_METHODS.has(req.method)) {
|
||||
this.setOrGetCsrfToken(req, res);
|
||||
return next();
|
||||
}
|
||||
|
||||
this.setOrGetCsrfToken(req, res);
|
||||
|
||||
const cookieToken = readCookie(req);
|
||||
const headerToken = (req.headers[HEADER_NAME] as string | undefined) ?? '';
|
||||
if (!cookieToken || !headerToken || !safeEqual(cookieToken, headerToken)) {
|
||||
throw new ApiError(ERROR_CODES.CSRF_INVALID, 'CSRF token missing or invalid', 403);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
private writeCookie(res: Response, token: string): void {
|
||||
const tls = this.config.get<boolean>('TLS_ENABLED', false);
|
||||
res.cookie(COOKIE_NAME, token, {
|
||||
httpOnly: false,
|
||||
sameSite: tls ? 'strict' : 'lax',
|
||||
secure: tls,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user