servcraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.dockerignore +45 -0
  2. package/.env.example +46 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +11 -0
  7. package/Dockerfile +76 -0
  8. package/Dockerfile.dev +31 -0
  9. package/README.md +232 -0
  10. package/commitlint.config.js +24 -0
  11. package/dist/cli/index.cjs +3968 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +3945 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/index.cjs +2458 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +828 -0
  20. package/dist/index.d.ts +828 -0
  21. package/dist/index.js +2332 -0
  22. package/dist/index.js.map +1 -0
  23. package/docker-compose.prod.yml +118 -0
  24. package/docker-compose.yml +147 -0
  25. package/eslint.config.js +27 -0
  26. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  27. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  28. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  29. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  30. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
  31. package/npm-cache/_update-notifier-last-checked +0 -0
  32. package/package.json +112 -0
  33. package/prisma/schema.prisma +157 -0
  34. package/src/cli/commands/add-module.ts +422 -0
  35. package/src/cli/commands/db.ts +137 -0
  36. package/src/cli/commands/docs.ts +16 -0
  37. package/src/cli/commands/generate.ts +459 -0
  38. package/src/cli/commands/init.ts +640 -0
  39. package/src/cli/index.ts +32 -0
  40. package/src/cli/templates/controller.ts +67 -0
  41. package/src/cli/templates/dynamic-prisma.ts +89 -0
  42. package/src/cli/templates/dynamic-schemas.ts +232 -0
  43. package/src/cli/templates/dynamic-types.ts +60 -0
  44. package/src/cli/templates/module-index.ts +33 -0
  45. package/src/cli/templates/prisma-model.ts +17 -0
  46. package/src/cli/templates/repository.ts +104 -0
  47. package/src/cli/templates/routes.ts +70 -0
  48. package/src/cli/templates/schemas.ts +26 -0
  49. package/src/cli/templates/service.ts +58 -0
  50. package/src/cli/templates/types.ts +27 -0
  51. package/src/cli/utils/docs-generator.ts +47 -0
  52. package/src/cli/utils/field-parser.ts +315 -0
  53. package/src/cli/utils/helpers.ts +89 -0
  54. package/src/config/env.ts +80 -0
  55. package/src/config/index.ts +97 -0
  56. package/src/core/index.ts +5 -0
  57. package/src/core/logger.ts +43 -0
  58. package/src/core/server.ts +132 -0
  59. package/src/database/index.ts +7 -0
  60. package/src/database/prisma.ts +54 -0
  61. package/src/database/seed.ts +59 -0
  62. package/src/index.ts +63 -0
  63. package/src/middleware/error-handler.ts +73 -0
  64. package/src/middleware/index.ts +3 -0
  65. package/src/middleware/security.ts +116 -0
  66. package/src/modules/audit/audit.service.ts +192 -0
  67. package/src/modules/audit/index.ts +2 -0
  68. package/src/modules/audit/types.ts +37 -0
  69. package/src/modules/auth/auth.controller.ts +182 -0
  70. package/src/modules/auth/auth.middleware.ts +87 -0
  71. package/src/modules/auth/auth.routes.ts +123 -0
  72. package/src/modules/auth/auth.service.ts +142 -0
  73. package/src/modules/auth/index.ts +49 -0
  74. package/src/modules/auth/schemas.ts +52 -0
  75. package/src/modules/auth/types.ts +69 -0
  76. package/src/modules/email/email.service.ts +212 -0
  77. package/src/modules/email/index.ts +10 -0
  78. package/src/modules/email/templates.ts +213 -0
  79. package/src/modules/email/types.ts +57 -0
  80. package/src/modules/swagger/index.ts +3 -0
  81. package/src/modules/swagger/schema-builder.ts +263 -0
  82. package/src/modules/swagger/swagger.service.ts +169 -0
  83. package/src/modules/swagger/types.ts +68 -0
  84. package/src/modules/user/index.ts +30 -0
  85. package/src/modules/user/schemas.ts +49 -0
  86. package/src/modules/user/types.ts +78 -0
  87. package/src/modules/user/user.controller.ts +139 -0
  88. package/src/modules/user/user.repository.ts +156 -0
  89. package/src/modules/user/user.routes.ts +199 -0
  90. package/src/modules/user/user.service.ts +145 -0
  91. package/src/modules/validation/index.ts +18 -0
  92. package/src/modules/validation/validator.ts +104 -0
  93. package/src/types/common.ts +61 -0
  94. package/src/types/index.ts +10 -0
  95. package/src/utils/errors.ts +66 -0
  96. package/src/utils/index.ts +33 -0
  97. package/src/utils/pagination.ts +38 -0
  98. package/src/utils/response.ts +63 -0
  99. package/tests/integration/auth.test.ts +59 -0
  100. package/tests/setup.ts +17 -0
  101. package/tests/unit/modules/validation.test.ts +88 -0
  102. package/tests/unit/utils/errors.test.ts +113 -0
  103. package/tests/unit/utils/pagination.test.ts +82 -0
  104. package/tsconfig.json +33 -0
  105. package/tsup.config.ts +14 -0
  106. package/vitest.config.ts +34 -0
@@ -0,0 +1,97 @@
1
+ import { env, isDevelopment, isProduction, isTest, isStaging } from './env.js';
2
+ import type { Env } from './env.js';
3
+
4
+ export interface AppConfig {
5
+ env: Env;
6
+ server: {
7
+ port: number;
8
+ host: string;
9
+ };
10
+ jwt: {
11
+ secret: string;
12
+ accessExpiresIn: string;
13
+ refreshExpiresIn: string;
14
+ };
15
+ security: {
16
+ corsOrigin: string | string[];
17
+ rateLimit: {
18
+ max: number;
19
+ windowMs: number;
20
+ };
21
+ };
22
+ email: {
23
+ host?: string;
24
+ port?: number;
25
+ user?: string;
26
+ pass?: string;
27
+ from?: string;
28
+ };
29
+ database: {
30
+ url?: string;
31
+ };
32
+ redis: {
33
+ url?: string;
34
+ };
35
+ swagger: {
36
+ enabled: boolean;
37
+ route: string;
38
+ title: string;
39
+ description: string;
40
+ version: string;
41
+ };
42
+ }
43
+
44
+ function parseCorsOrigin(origin: string): string | string[] {
45
+ if (origin === '*') return '*';
46
+ if (origin.includes(',')) {
47
+ return origin.split(',').map((o) => o.trim());
48
+ }
49
+ return origin;
50
+ }
51
+
52
+ export function createConfig(): AppConfig {
53
+ return {
54
+ env,
55
+ server: {
56
+ port: env.PORT,
57
+ host: env.HOST,
58
+ },
59
+ jwt: {
60
+ secret: env.JWT_SECRET || 'change-me-in-production-please-32chars',
61
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
62
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
63
+ },
64
+ security: {
65
+ corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
66
+ rateLimit: {
67
+ max: env.RATE_LIMIT_MAX,
68
+ windowMs: env.RATE_LIMIT_WINDOW_MS,
69
+ },
70
+ },
71
+ email: {
72
+ host: env.SMTP_HOST,
73
+ port: env.SMTP_PORT,
74
+ user: env.SMTP_USER,
75
+ pass: env.SMTP_PASS,
76
+ from: env.SMTP_FROM,
77
+ },
78
+ database: {
79
+ url: env.DATABASE_URL,
80
+ },
81
+ redis: {
82
+ url: env.REDIS_URL,
83
+ },
84
+ swagger: {
85
+ enabled: env.SWAGGER_ENABLED,
86
+ route: env.SWAGGER_ROUTE,
87
+ title: env.SWAGGER_TITLE,
88
+ description: env.SWAGGER_DESCRIPTION,
89
+ version: env.SWAGGER_VERSION,
90
+ },
91
+ };
92
+ }
93
+
94
+ export const config = createConfig();
95
+
96
+ export { env, isDevelopment, isProduction, isTest, isStaging };
97
+ export type { Env };
@@ -0,0 +1,5 @@
1
+ export { Server, createServer } from './server.js';
2
+ export type { ServerConfig } from './server.js';
3
+
4
+ export { logger, createLogger } from './logger.js';
5
+ export type { Logger, LoggerConfig } from './logger.js';
@@ -0,0 +1,43 @@
1
+ import pino from 'pino';
2
+ import type { Logger } from 'pino';
3
+
4
+ export interface LoggerConfig {
5
+ level: string;
6
+ pretty: boolean;
7
+ name?: string;
8
+ }
9
+
10
+ const defaultConfig: LoggerConfig = {
11
+ level: process.env.LOG_LEVEL || 'info',
12
+ pretty: process.env.NODE_ENV !== 'production',
13
+ name: 'servcraft',
14
+ };
15
+
16
+ export function createLogger(config: Partial<LoggerConfig> = {}): Logger {
17
+ const mergedConfig = { ...defaultConfig, ...config };
18
+
19
+ const transport = mergedConfig.pretty
20
+ ? {
21
+ target: 'pino-pretty',
22
+ options: {
23
+ colorize: true,
24
+ translateTime: 'SYS:standard',
25
+ ignore: 'pid,hostname',
26
+ },
27
+ }
28
+ : undefined;
29
+
30
+ return pino({
31
+ name: mergedConfig.name,
32
+ level: mergedConfig.level,
33
+ transport,
34
+ formatters: {
35
+ level: (label) => ({ level: label }),
36
+ },
37
+ timestamp: pino.stdTimeFunctions.isoTime,
38
+ });
39
+ }
40
+
41
+ export const logger = createLogger();
42
+
43
+ export type { Logger };
@@ -0,0 +1,132 @@
1
+ import Fastify from 'fastify';
2
+ import type { FastifyInstance, FastifyServerOptions } from 'fastify';
3
+ import { logger, createLogger } from './logger.js';
4
+ import type { Logger } from './logger.js';
5
+
6
+ export interface ServerConfig {
7
+ port: number;
8
+ host: string;
9
+ logger?: Logger;
10
+ trustProxy?: boolean;
11
+ bodyLimit?: number;
12
+ requestTimeout?: number;
13
+ }
14
+
15
+ const defaultConfig: ServerConfig = {
16
+ port: parseInt(process.env.PORT || '3000', 10),
17
+ host: process.env.HOST || '0.0.0.0',
18
+ trustProxy: true,
19
+ bodyLimit: 1048576, // 1MB
20
+ requestTimeout: 30000, // 30s
21
+ };
22
+
23
+ export class Server {
24
+ private app: FastifyInstance;
25
+ private config: ServerConfig;
26
+ private logger: Logger;
27
+ private isShuttingDown = false;
28
+
29
+ constructor(config: Partial<ServerConfig> = {}) {
30
+ this.config = { ...defaultConfig, ...config };
31
+ this.logger = this.config.logger || logger;
32
+
33
+ const fastifyOptions: FastifyServerOptions = {
34
+ logger: this.logger,
35
+ trustProxy: this.config.trustProxy,
36
+ bodyLimit: this.config.bodyLimit,
37
+ requestTimeout: this.config.requestTimeout,
38
+ };
39
+
40
+ this.app = Fastify(fastifyOptions);
41
+ this.setupHealthCheck();
42
+ this.setupGracefulShutdown();
43
+ }
44
+
45
+ get instance(): FastifyInstance {
46
+ return this.app;
47
+ }
48
+
49
+ private setupHealthCheck(): void {
50
+ this.app.get('/health', async (_request, reply) => {
51
+ const healthcheck = {
52
+ status: 'ok',
53
+ timestamp: new Date().toISOString(),
54
+ uptime: process.uptime(),
55
+ memory: process.memoryUsage(),
56
+ version: process.env.npm_package_version || '0.1.0',
57
+ };
58
+
59
+ return reply.status(200).send(healthcheck);
60
+ });
61
+
62
+ this.app.get('/ready', async (_request, reply) => {
63
+ if (this.isShuttingDown) {
64
+ return reply.status(503).send({ status: 'shutting_down' });
65
+ }
66
+ return reply.status(200).send({ status: 'ready' });
67
+ });
68
+ }
69
+
70
+ private setupGracefulShutdown(): void {
71
+ const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
72
+
73
+ signals.forEach((signal) => {
74
+ process.on(signal, async () => {
75
+ this.logger.info(`Received ${signal}, starting graceful shutdown...`);
76
+ await this.shutdown();
77
+ });
78
+ });
79
+
80
+ process.on('uncaughtException', async (error) => {
81
+ this.logger.error({ err: error }, 'Uncaught exception');
82
+ await this.shutdown(1);
83
+ });
84
+
85
+ process.on('unhandledRejection', async (reason) => {
86
+ this.logger.error({ err: reason }, 'Unhandled rejection');
87
+ await this.shutdown(1);
88
+ });
89
+ }
90
+
91
+ async shutdown(exitCode = 0): Promise<void> {
92
+ if (this.isShuttingDown) {
93
+ return;
94
+ }
95
+
96
+ this.isShuttingDown = true;
97
+ this.logger.info('Graceful shutdown initiated...');
98
+
99
+ const shutdownTimeout = setTimeout(() => {
100
+ this.logger.error('Graceful shutdown timeout, forcing exit');
101
+ process.exit(1);
102
+ }, 30000);
103
+
104
+ try {
105
+ await this.app.close();
106
+ this.logger.info('Server closed successfully');
107
+ clearTimeout(shutdownTimeout);
108
+ process.exit(exitCode);
109
+ } catch (error) {
110
+ this.logger.error({ err: error }, 'Error during shutdown');
111
+ clearTimeout(shutdownTimeout);
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ async start(): Promise<void> {
117
+ try {
118
+ await this.app.listen({
119
+ port: this.config.port,
120
+ host: this.config.host,
121
+ });
122
+ this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
123
+ } catch (error) {
124
+ this.logger.error({ err: error }, 'Failed to start server');
125
+ throw error;
126
+ }
127
+ }
128
+ }
129
+
130
+ export function createServer(config: Partial<ServerConfig> = {}): Server {
131
+ return new Server(config);
132
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ prisma,
3
+ connectDatabase,
4
+ disconnectDatabase,
5
+ checkDatabaseHealth,
6
+ PrismaClient,
7
+ } from './prisma.js';
@@ -0,0 +1,54 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { logger } from '../core/logger.js';
3
+ import { isProduction } from '../config/index.js';
4
+
5
+ declare global {
6
+ // eslint-disable-next-line no-var
7
+ var __prisma: PrismaClient | undefined;
8
+ }
9
+
10
+ const prismaClientSingleton = (): PrismaClient => {
11
+ return new PrismaClient({
12
+ log: isProduction()
13
+ ? ['error']
14
+ : ['query', 'info', 'warn', 'error'],
15
+ errorFormat: isProduction() ? 'minimal' : 'pretty',
16
+ });
17
+ };
18
+
19
+ // Use singleton pattern to prevent multiple instances in development
20
+ export const prisma = globalThis.__prisma ?? prismaClientSingleton();
21
+
22
+ if (!isProduction()) {
23
+ globalThis.__prisma = prisma;
24
+ }
25
+
26
+ export async function connectDatabase(): Promise<void> {
27
+ try {
28
+ await prisma.$connect();
29
+ logger.info('Database connected successfully');
30
+ } catch (error) {
31
+ logger.error({ err: error }, 'Failed to connect to database');
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ export async function disconnectDatabase(): Promise<void> {
37
+ try {
38
+ await prisma.$disconnect();
39
+ logger.info('Database disconnected');
40
+ } catch (error) {
41
+ logger.error({ err: error }, 'Error disconnecting from database');
42
+ }
43
+ }
44
+
45
+ export async function checkDatabaseHealth(): Promise<boolean> {
46
+ try {
47
+ await prisma.$queryRaw`SELECT 1`;
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ export { PrismaClient };
@@ -0,0 +1,59 @@
1
+ import { prisma } from './prisma.js';
2
+ import bcrypt from 'bcryptjs';
3
+ import { logger } from '../core/logger.js';
4
+
5
+ async function main(): Promise<void> {
6
+ logger.info('Starting database seed...');
7
+
8
+ // Create default admin user
9
+ const hashedPassword = await bcrypt.hash('Admin@123456', 12);
10
+
11
+ const admin = await prisma.user.upsert({
12
+ where: { email: 'admin@servcraft.local' },
13
+ update: {},
14
+ create: {
15
+ email: 'admin@servcraft.local',
16
+ password: hashedPassword,
17
+ name: 'Administrator',
18
+ role: 'SUPER_ADMIN',
19
+ status: 'ACTIVE',
20
+ emailVerified: true,
21
+ },
22
+ });
23
+
24
+ logger.info({ userId: admin.id }, 'Admin user created/updated');
25
+
26
+ // Create default settings
27
+ const defaultSettings = [
28
+ { key: 'app.name', value: 'Servcraft', type: 'string', group: 'general' },
29
+ { key: 'app.description', value: 'A modular Node.js backend framework', type: 'string', group: 'general' },
30
+ { key: 'auth.registration_enabled', value: true, type: 'boolean', group: 'auth' },
31
+ { key: 'auth.email_verification_required', value: false, type: 'boolean', group: 'auth' },
32
+ { key: 'email.enabled', value: false, type: 'boolean', group: 'email' },
33
+ ];
34
+
35
+ for (const setting of defaultSettings) {
36
+ await prisma.setting.upsert({
37
+ where: { key: setting.key },
38
+ update: { value: setting.value },
39
+ create: {
40
+ key: setting.key,
41
+ value: setting.value,
42
+ type: setting.type,
43
+ group: setting.group,
44
+ },
45
+ });
46
+ }
47
+
48
+ logger.info('Default settings created');
49
+ logger.info('Seed completed successfully');
50
+ }
51
+
52
+ main()
53
+ .catch((e) => {
54
+ logger.error({ err: e }, 'Seed failed');
55
+ process.exit(1);
56
+ })
57
+ .finally(async () => {
58
+ await prisma.$disconnect();
59
+ });
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { Server, createServer } from './core/server.js';
2
+ import { logger } from './core/logger.js';
3
+ import { config } from './config/index.js';
4
+ import { registerSecurity, registerErrorHandler } from './middleware/index.js';
5
+ import { registerAuthModule } from './modules/auth/index.js';
6
+ import { registerUserModule } from './modules/user/index.js';
7
+ import { registerSwagger } from './modules/swagger/index.js';
8
+
9
+ async function bootstrap(): Promise<void> {
10
+ // Create server instance
11
+ const server = createServer({
12
+ port: config.server.port,
13
+ host: config.server.host,
14
+ });
15
+
16
+ const app = server.instance;
17
+
18
+ // Register error handler
19
+ registerErrorHandler(app);
20
+
21
+ // Register security middleware
22
+ await registerSecurity(app);
23
+
24
+ // Register Swagger documentation (auto-updates as routes are added)
25
+ await registerSwagger(app, {
26
+ enabled: config.swagger.enabled,
27
+ route: config.swagger.route,
28
+ title: config.swagger.title,
29
+ description: config.swagger.description,
30
+ version: config.swagger.version,
31
+ });
32
+
33
+ // Register auth module
34
+ const authService = await registerAuthModule(app);
35
+
36
+ // Register user module (depends on auth for guard middleware)
37
+ await registerUserModule(app, authService);
38
+
39
+ // Start server
40
+ await server.start();
41
+
42
+ logger.info({
43
+ env: config.env.NODE_ENV,
44
+ port: config.server.port,
45
+ }, 'Servcraft server started');
46
+ }
47
+
48
+ bootstrap().catch((err) => {
49
+ logger.error({ err }, 'Failed to start server');
50
+ process.exit(1);
51
+ });
52
+
53
+ // Export for library usage
54
+ export * from './core/index.js';
55
+ export * from './config/index.js';
56
+ export * from './middleware/index.js';
57
+ export * from './utils/index.js';
58
+ export * from './types/index.js';
59
+ export * from './modules/auth/index.js';
60
+ export * from './modules/user/index.js';
61
+ export * from './modules/email/index.js';
62
+ export * from './modules/validation/index.js';
63
+ export * from './modules/audit/index.js';
@@ -0,0 +1,73 @@
1
+ import type { FastifyInstance, FastifyError, FastifyRequest, FastifyReply } from 'fastify';
2
+ import { isAppError } from '../utils/errors.js';
3
+ import { logger } from '../core/logger.js';
4
+ import { isProduction } from '../config/index.js';
5
+
6
+ export function registerErrorHandler(app: FastifyInstance): void {
7
+ app.setErrorHandler(
8
+ (error: FastifyError | Error, request: FastifyRequest, reply: FastifyReply) => {
9
+ // Log the error
10
+ logger.error(
11
+ {
12
+ err: error,
13
+ requestId: request.id,
14
+ method: request.method,
15
+ url: request.url,
16
+ },
17
+ 'Request error'
18
+ );
19
+
20
+ // Handle AppError (our custom errors)
21
+ if (isAppError(error)) {
22
+ return reply.status(error.statusCode).send({
23
+ success: false,
24
+ message: error.message,
25
+ errors: error.errors,
26
+ ...(isProduction() ? {} : { stack: error.stack }),
27
+ });
28
+ }
29
+
30
+ // Handle Fastify validation errors
31
+ if ('validation' in error && error.validation) {
32
+ const errors: Record<string, string[]> = {};
33
+ for (const err of error.validation) {
34
+ const field = err.instancePath?.replace('/', '') || 'body';
35
+ if (!errors[field]) {
36
+ errors[field] = [];
37
+ }
38
+ errors[field].push(err.message || 'Invalid value');
39
+ }
40
+
41
+ return reply.status(400).send({
42
+ success: false,
43
+ message: 'Validation failed',
44
+ errors,
45
+ });
46
+ }
47
+
48
+ // Handle Fastify errors with statusCode
49
+ if ('statusCode' in error && typeof error.statusCode === 'number') {
50
+ return reply.status(error.statusCode).send({
51
+ success: false,
52
+ message: error.message,
53
+ ...(isProduction() ? {} : { stack: error.stack }),
54
+ });
55
+ }
56
+
57
+ // Handle unknown errors
58
+ return reply.status(500).send({
59
+ success: false,
60
+ message: isProduction() ? 'Internal server error' : error.message,
61
+ ...(isProduction() ? {} : { stack: error.stack }),
62
+ });
63
+ }
64
+ );
65
+
66
+ // Handle 404
67
+ app.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {
68
+ return reply.status(404).send({
69
+ success: false,
70
+ message: `Route ${request.method} ${request.url} not found`,
71
+ });
72
+ });
73
+ }
@@ -0,0 +1,3 @@
1
+ export { registerErrorHandler } from './error-handler.js';
2
+ export { registerSecurity, registerBruteForceProtection } from './security.js';
3
+ export type { SecurityOptions } from './security.js';
@@ -0,0 +1,116 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import helmet from '@fastify/helmet';
3
+ import cors from '@fastify/cors';
4
+ import rateLimit from '@fastify/rate-limit';
5
+ import { config } from '../config/index.js';
6
+ import { logger } from '../core/logger.js';
7
+
8
+ export interface SecurityOptions {
9
+ helmet?: boolean;
10
+ cors?: boolean;
11
+ rateLimit?: boolean;
12
+ }
13
+
14
+ const defaultOptions: SecurityOptions = {
15
+ helmet: true,
16
+ cors: true,
17
+ rateLimit: true,
18
+ };
19
+
20
+ export async function registerSecurity(
21
+ app: FastifyInstance,
22
+ options: SecurityOptions = {}
23
+ ): Promise<void> {
24
+ const opts = { ...defaultOptions, ...options };
25
+
26
+ // Helmet - Security headers
27
+ if (opts.helmet) {
28
+ await app.register(helmet, {
29
+ contentSecurityPolicy: {
30
+ directives: {
31
+ defaultSrc: ["'self'"],
32
+ styleSrc: ["'self'", "'unsafe-inline'"],
33
+ scriptSrc: ["'self'"],
34
+ imgSrc: ["'self'", 'data:', 'https:'],
35
+ },
36
+ },
37
+ crossOriginEmbedderPolicy: false,
38
+ });
39
+ logger.debug('Helmet security headers enabled');
40
+ }
41
+
42
+ // CORS
43
+ if (opts.cors) {
44
+ await app.register(cors, {
45
+ origin: config.security.corsOrigin,
46
+ credentials: true,
47
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
48
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
49
+ exposedHeaders: ['X-Total-Count', 'X-Page', 'X-Limit'],
50
+ maxAge: 86400, // 24 hours
51
+ });
52
+ logger.debug({ origin: config.security.corsOrigin }, 'CORS enabled');
53
+ }
54
+
55
+ // Rate Limiting
56
+ if (opts.rateLimit) {
57
+ await app.register(rateLimit, {
58
+ max: config.security.rateLimit.max,
59
+ timeWindow: config.security.rateLimit.windowMs,
60
+ errorResponseBuilder: (_request, context) => ({
61
+ success: false,
62
+ message: 'Too many requests, please try again later',
63
+ retryAfter: context.after,
64
+ }),
65
+ keyGenerator: (request) => {
66
+ // Use X-Forwarded-For if behind proxy, otherwise use IP
67
+ return (
68
+ request.headers['x-forwarded-for']?.toString().split(',')[0] ||
69
+ request.ip ||
70
+ 'unknown'
71
+ );
72
+ },
73
+ });
74
+ logger.debug(
75
+ {
76
+ max: config.security.rateLimit.max,
77
+ windowMs: config.security.rateLimit.windowMs,
78
+ },
79
+ 'Rate limiting enabled'
80
+ );
81
+ }
82
+ }
83
+
84
+ // Brute force protection for specific routes (login, etc.)
85
+ export async function registerBruteForceProtection(
86
+ app: FastifyInstance,
87
+ routePrefix: string,
88
+ options: { max?: number; timeWindow?: number } = {}
89
+ ): Promise<void> {
90
+ const { max = 5, timeWindow = 300000 } = options; // 5 attempts per 5 minutes
91
+
92
+ await app.register(rateLimit, {
93
+ max,
94
+ timeWindow,
95
+ keyGenerator: (request) => {
96
+ const ip =
97
+ request.headers['x-forwarded-for']?.toString().split(',')[0] ||
98
+ request.ip ||
99
+ 'unknown';
100
+ return `brute:${routePrefix}:${ip}`;
101
+ },
102
+ errorResponseBuilder: () => ({
103
+ success: false,
104
+ message: 'Too many attempts. Please try again later.',
105
+ }),
106
+ onExceeded: (request) => {
107
+ logger.warn(
108
+ {
109
+ ip: request.ip,
110
+ route: routePrefix,
111
+ },
112
+ 'Brute force protection triggered'
113
+ );
114
+ },
115
+ });
116
+ }