generated from nhcarrigan/template
feat: security and auditing
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user