feat: security and auditing

This commit is contained in:
2026-02-04 16:48:08 -08:00
parent 11be34cd21
commit 0a654f423a
42 changed files with 2195 additions and 160 deletions
+23 -5
View File
@@ -23,13 +23,34 @@ declare module "@fastify/jwt" {
}
}
const getJwtSecret = (): string => {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("JWT_SECRET environment variable is required");
}
return secret;
};
const authPlugin: FastifyPluginAsync = async (app) => {
const jwtSecret = getJwtSecret();
// Register cookie plugin with signing secret
app.register(fastifyCookie, {
secret: jwtSecret,
});
// Register JWT plugin
app.register(fastifyJwt, {
secret: process.env.JWT_SECRET || "your-secret-key",
secret: jwtSecret,
sign: {
algorithm: "HS256",
},
verify: {
algorithms: ["HS256"],
},
cookie: {
cookieName: "auth-token",
signed: false,
signed: true,
},
formatUser: (payload: { sub: string; email?: string; username: string; isAdmin: boolean }) => {
return {
@@ -41,9 +62,6 @@ const authPlugin: FastifyPluginAsync = async (app) => {
},
});
// Register cookie plugin
app.register(fastifyCookie);
// Register Discord OAuth2
app.register(fastifyOauth2, {
name: "oauth2Discord",
+26
View File
@@ -0,0 +1,26 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import fastifyPlugin from "fastify-plugin";
import fastifyCors from "@fastify/cors";
const corsPlugin: FastifyPluginAsync = async (app) => {
const baseUrl = process.env.BASE_URL;
if (!baseUrl) {
throw new Error("BASE_URL environment variable is required");
}
await app.register(fastifyCors, {
origin: baseUrl,
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
});
};
export default fastifyPlugin(corsPlugin);
+26
View File
@@ -0,0 +1,26 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync, FastifyRequest } from "fastify";
import fastifyPlugin from "fastify-plugin";
import fastifyCsrf from "@fastify/csrf-protection";
const csrfPlugin: FastifyPluginAsync = async (app) => {
await app.register(fastifyCsrf, {
sessionPlugin: "@fastify/cookie",
cookieOpts: {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
},
getToken: (request: FastifyRequest) => {
return request.headers["x-csrf-token"] as string;
},
});
};
export default fastifyPlugin(csrfPlugin);
+27
View File
@@ -0,0 +1,27 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import fastifyPlugin from "fastify-plugin";
import fastifyHelmet from "@fastify/helmet";
const helmetPlugin: FastifyPluginAsync = async (app) => {
await app.register(fastifyHelmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", process.env.FRONTEND_URL ?? "http://localhost:4200"],
},
},
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: { policy: "cross-origin" },
});
};
export default fastifyPlugin(helmetPlugin);
+37
View File
@@ -0,0 +1,37 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import fastifyPlugin from "fastify-plugin";
import fastifyRateLimit from "@fastify/rate-limit";
import { AuditService } from "../services/audit.service";
import { AuditAction, AuditCategory } from "@library/shared-types";
const rateLimitPlugin: FastifyPluginAsync = async (app) => {
await app.register(fastifyRateLimit, {
max: 100,
timeWindow: "1 minute",
errorResponseBuilder: (request) => {
// Log rate limit exceeded event
AuditService.log({
action: AuditAction.RATE_LIMIT_EXCEEDED,
category: AuditCategory.SECURITY,
details: `Rate limit exceeded for URL: ${request.url}`,
success: false,
}, request).catch(() => {
// Ignore logging errors to avoid blocking the response
});
return {
statusCode: 429,
error: "Too Many Requests",
message: "You have exceeded the rate limit. Please try again later.",
};
},
});
};
export default fastifyPlugin(rateLimitPlugin);