create-arktos 1.0.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.
- package/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +533 -0
- package/bin/cli.js +318 -0
- package/eslint.config.js +79 -0
- package/package.json +108 -0
- package/prisma/schema.prisma +172 -0
- package/src/app.ts +101 -0
- package/src/config/env.validation.ts +15 -0
- package/src/config/logger.ts +76 -0
- package/src/constants/errorCodes.ts +59 -0
- package/src/constants/messages.ts +190 -0
- package/src/controllers/auth.controller.ts +546 -0
- package/src/middleware/index.ts +638 -0
- package/src/routes/auth.routes.ts +29 -0
- package/src/routes/index.ts +63 -0
- package/src/schemas/index.ts +139 -0
- package/src/services/database.service.ts +120 -0
- package/src/services/email.service.ts +244 -0
- package/src/services/jwt.service.ts +227 -0
- package/src/types/express.d.ts +19 -0
- package/src/types/index.ts +156 -0
- package/src/utils/response.ts +55 -0
- package/src/views/emails/notification.html +87 -0
- package/src/views/emails/resetPassword.html +118 -0
- package/src/views/emails/verification.html +107 -0
- package/src/views/emails/welcome.html +113 -0
- package/template.package.json +82 -0
- package/tsconfig.json +35 -0
- package/vercel.json +34 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ===================================
|
|
3
|
+
* ARKTOS BACKEND MIDDLEWARE INDEX
|
|
4
|
+
* ===================================
|
|
5
|
+
* Tüm middleware'lerin merkezi yönetimi
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Request, Response, NextFunction } from 'express';
|
|
9
|
+
import jwt from 'jsonwebtoken';
|
|
10
|
+
import { ZodSchema, ZodError } from 'zod';
|
|
11
|
+
import { rateLimit } from 'express-rate-limit';
|
|
12
|
+
import helmet from 'helmet';
|
|
13
|
+
import cors from 'cors';
|
|
14
|
+
import { PrismaClientKnownRequestError, PrismaClientInitializationError } from '@prisma/client/runtime/library';
|
|
15
|
+
|
|
16
|
+
import { AuthenticatedRequest } from '../types';
|
|
17
|
+
import { createErrorResponse, createSuccessResponse } from '../utils/response';
|
|
18
|
+
import { ERROR_CODES } from '../constants/errorCodes';
|
|
19
|
+
import DatabaseService from '../services/database.service';
|
|
20
|
+
import logger from '../config/logger';
|
|
21
|
+
|
|
22
|
+
const dbService = DatabaseService.getInstance();
|
|
23
|
+
const prisma = dbService.getClient();
|
|
24
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
25
|
+
|
|
26
|
+
// ===================================
|
|
27
|
+
// 🔐 AUTHENTICATION MIDDLEWARE
|
|
28
|
+
// ===================================
|
|
29
|
+
|
|
30
|
+
export const authMiddleware = async (
|
|
31
|
+
req: AuthenticatedRequest,
|
|
32
|
+
res: Response,
|
|
33
|
+
next: NextFunction
|
|
34
|
+
): Promise<void> => {
|
|
35
|
+
try {
|
|
36
|
+
const authHeader = req.headers.authorization;
|
|
37
|
+
|
|
38
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
39
|
+
res.status(401).json(createErrorResponse(
|
|
40
|
+
'Access token required',
|
|
41
|
+
ERROR_CODES.AUTH_TOKEN_REQUIRED
|
|
42
|
+
));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const token = authHeader.substring(7);
|
|
47
|
+
|
|
48
|
+
let decoded: any;
|
|
49
|
+
try {
|
|
50
|
+
decoded = jwt.verify(token, process.env.JWT_SECRET!);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
53
|
+
res.status(401).json(createErrorResponse(
|
|
54
|
+
'Access token expired',
|
|
55
|
+
ERROR_CODES.AUTH_TOKEN_EXPIRED
|
|
56
|
+
));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.status(401).json(createErrorResponse(
|
|
61
|
+
'Invalid access token',
|
|
62
|
+
ERROR_CODES.AUTH_TOKEN_INVALID
|
|
63
|
+
));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const user = await prisma.user.findUnique({
|
|
68
|
+
where: { id: decoded.userId },
|
|
69
|
+
select: {
|
|
70
|
+
id: true,
|
|
71
|
+
email: true,
|
|
72
|
+
firstName: true,
|
|
73
|
+
lastName: true,
|
|
74
|
+
username: true,
|
|
75
|
+
avatar: true,
|
|
76
|
+
role: true,
|
|
77
|
+
isEmailVerified: true,
|
|
78
|
+
isActive: true,
|
|
79
|
+
emailVerifiedAt: true,
|
|
80
|
+
lastLoginAt: true,
|
|
81
|
+
createdAt: true,
|
|
82
|
+
updatedAt: true,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!user) {
|
|
87
|
+
res.status(401).json(createErrorResponse(
|
|
88
|
+
'User not found',
|
|
89
|
+
ERROR_CODES.AUTH_USER_NOT_FOUND
|
|
90
|
+
));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!user.isActive) {
|
|
95
|
+
res.status(403).json(createErrorResponse(
|
|
96
|
+
'Account is deactivated',
|
|
97
|
+
ERROR_CODES.AUTH_ACCOUNT_DEACTIVATED
|
|
98
|
+
));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
req.user = user;
|
|
103
|
+
next();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error('Auth middleware error:', error);
|
|
106
|
+
res.status(500).json(createErrorResponse(
|
|
107
|
+
'Authentication error',
|
|
108
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const optionalAuthMiddleware = async (
|
|
114
|
+
req: AuthenticatedRequest,
|
|
115
|
+
res: Response,
|
|
116
|
+
next: NextFunction
|
|
117
|
+
): Promise<void> => {
|
|
118
|
+
try {
|
|
119
|
+
const authHeader = req.headers.authorization;
|
|
120
|
+
|
|
121
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
122
|
+
next();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const token = authHeader.substring(7);
|
|
127
|
+
|
|
128
|
+
let decoded: any;
|
|
129
|
+
try {
|
|
130
|
+
decoded = jwt.verify(token, process.env.JWT_SECRET!);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
next();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const user = await prisma.user.findUnique({
|
|
137
|
+
where: { id: decoded.userId },
|
|
138
|
+
select: {
|
|
139
|
+
id: true,
|
|
140
|
+
email: true,
|
|
141
|
+
firstName: true,
|
|
142
|
+
lastName: true,
|
|
143
|
+
username: true,
|
|
144
|
+
avatar: true,
|
|
145
|
+
role: true,
|
|
146
|
+
isEmailVerified: true,
|
|
147
|
+
isActive: true,
|
|
148
|
+
emailVerifiedAt: true,
|
|
149
|
+
lastLoginAt: true,
|
|
150
|
+
createdAt: true,
|
|
151
|
+
updatedAt: true,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (user && user.isActive) {
|
|
156
|
+
req.user = user;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
next();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('Optional auth middleware error:', error);
|
|
162
|
+
next();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Role-based access control
|
|
167
|
+
export const requireRole = (allowedRoles: string | string[]) => {
|
|
168
|
+
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
|
|
169
|
+
if (!req.user) {
|
|
170
|
+
res.status(401).json(createErrorResponse(
|
|
171
|
+
'Authentication required',
|
|
172
|
+
ERROR_CODES.AUTH_TOKEN_REQUIRED
|
|
173
|
+
));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
|
|
178
|
+
|
|
179
|
+
if (!roles.includes(req.user.role)) {
|
|
180
|
+
res.status(403).json(createErrorResponse(
|
|
181
|
+
'Insufficient permissions',
|
|
182
|
+
ERROR_CODES.AUTH_INSUFFICIENT_PERMISSIONS
|
|
183
|
+
));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
next();
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Email verification requirement
|
|
192
|
+
export const requireEmailVerification = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
|
|
193
|
+
if (!req.user) {
|
|
194
|
+
res.status(401).json(createErrorResponse(
|
|
195
|
+
'Authentication required',
|
|
196
|
+
ERROR_CODES.AUTH_TOKEN_REQUIRED
|
|
197
|
+
));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!req.user.isEmailVerified) {
|
|
202
|
+
res.status(403).json(createErrorResponse(
|
|
203
|
+
'Email verification required',
|
|
204
|
+
ERROR_CODES.AUTH_EMAIL_NOT_VERIFIED
|
|
205
|
+
));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
next();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ===================================
|
|
213
|
+
// ✅ VALIDATION MIDDLEWARE
|
|
214
|
+
// ===================================
|
|
215
|
+
|
|
216
|
+
export const validateRequest = (schema: ZodSchema, data: any): any => {
|
|
217
|
+
try {
|
|
218
|
+
return schema.parse(data);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof ZodError) {
|
|
221
|
+
const validationErrors = error.issues.map((err: any) => ({
|
|
222
|
+
field: err.path.join('.'),
|
|
223
|
+
message: err.message,
|
|
224
|
+
value: err.input,
|
|
225
|
+
}));
|
|
226
|
+
throw { validationErrors, isZodError: true };
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const validationMiddleware = (schema: {
|
|
233
|
+
body?: ZodSchema;
|
|
234
|
+
query?: ZodSchema;
|
|
235
|
+
params?: ZodSchema;
|
|
236
|
+
}) => {
|
|
237
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
238
|
+
try {
|
|
239
|
+
if (schema.body) {
|
|
240
|
+
req.body = schema.body.parse(req.body);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (schema.query) {
|
|
244
|
+
req.query = schema.query.parse(req.query) as any;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (schema.params) {
|
|
248
|
+
req.params = schema.params.parse(req.params) as any;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
next();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof ZodError) {
|
|
254
|
+
const validationErrors = error.issues.map((err: any) => ({
|
|
255
|
+
field: err.path.join('.'),
|
|
256
|
+
message: err.message,
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
res.status(400).json(
|
|
260
|
+
createErrorResponse(
|
|
261
|
+
'Validation failed',
|
|
262
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
263
|
+
{ errors: validationErrors }
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
next(error);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const validateBody = (schema: ZodSchema) => validationMiddleware({ body: schema });
|
|
275
|
+
export const validateQuery = (schema: ZodSchema) => validationMiddleware({ query: schema });
|
|
276
|
+
export const validateParams = (schema: ZodSchema) => validationMiddleware({ params: schema });
|
|
277
|
+
|
|
278
|
+
// ===================================
|
|
279
|
+
// 🛡️ SECURITY MIDDLEWARE
|
|
280
|
+
// ===================================
|
|
281
|
+
|
|
282
|
+
// Helmet configuration
|
|
283
|
+
export const helmetConfig = helmet({
|
|
284
|
+
contentSecurityPolicy: {
|
|
285
|
+
directives: {
|
|
286
|
+
defaultSrc: ["'self'"],
|
|
287
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
288
|
+
scriptSrc: ["'self'"],
|
|
289
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
290
|
+
fontSrc: ["'self'", 'https:', 'data:'],
|
|
291
|
+
connectSrc: ["'self'"],
|
|
292
|
+
mediaSrc: ["'self'"],
|
|
293
|
+
objectSrc: ["'none'"],
|
|
294
|
+
childSrc: ["'self'"],
|
|
295
|
+
frameSrc: ["'self'"],
|
|
296
|
+
workerSrc: ["'self'"],
|
|
297
|
+
frameAncestors: ["'none'"],
|
|
298
|
+
formAction: ["'self'"],
|
|
299
|
+
upgradeInsecureRequests: [],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
crossOriginEmbedderPolicy: false,
|
|
303
|
+
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
304
|
+
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
305
|
+
dnsPrefetchControl: { allow: false },
|
|
306
|
+
frameguard: { action: 'deny' },
|
|
307
|
+
hidePoweredBy: true,
|
|
308
|
+
hsts: {
|
|
309
|
+
maxAge: 31536000,
|
|
310
|
+
includeSubDomains: true,
|
|
311
|
+
preload: true,
|
|
312
|
+
},
|
|
313
|
+
ieNoOpen: true,
|
|
314
|
+
noSniff: true,
|
|
315
|
+
originAgentCluster: true,
|
|
316
|
+
permittedCrossDomainPolicies: false,
|
|
317
|
+
referrerPolicy: { policy: 'no-referrer' },
|
|
318
|
+
xssFilter: true,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Additional security headers
|
|
322
|
+
export const securityHeaders = (req: Request, res: Response, next: NextFunction): void => {
|
|
323
|
+
res.removeHeader('X-Powered-By');
|
|
324
|
+
res.removeHeader('Server');
|
|
325
|
+
|
|
326
|
+
res.setHeader('X-API-Version', '1.0.0');
|
|
327
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
328
|
+
res.setHeader('X-Download-Options', 'noopen');
|
|
329
|
+
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
|
330
|
+
|
|
331
|
+
if (req.path.includes('/auth/') || req.path.includes('/profile/')) {
|
|
332
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
333
|
+
res.setHeader('Pragma', 'no-cache');
|
|
334
|
+
res.setHeader('Expires', '0');
|
|
335
|
+
res.setHeader('Surrogate-Control', 'no-store');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
next();
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Request sanitization
|
|
342
|
+
export const sanitizeRequest = (req: Request, res: Response, next: NextFunction): void => {
|
|
343
|
+
if (req.body && typeof req.body === 'object') {
|
|
344
|
+
req.body = sanitizeObject(req.body);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (req.query && typeof req.query === 'object') {
|
|
348
|
+
req.query = sanitizeObject(req.query);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
next();
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
function sanitizeObject(obj: any): any {
|
|
355
|
+
if (obj === null || obj === undefined) {
|
|
356
|
+
return obj;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (typeof obj === 'string') {
|
|
360
|
+
return obj
|
|
361
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
362
|
+
.replace(/javascript:/gi, '')
|
|
363
|
+
.replace(/on\w+\s*=/gi, '');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (Array.isArray(obj)) {
|
|
367
|
+
return obj.map(sanitizeObject);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (typeof obj === 'object') {
|
|
371
|
+
const sanitized: any = {};
|
|
372
|
+
for (const key in obj) {
|
|
373
|
+
if (obj.hasOwnProperty(key)) {
|
|
374
|
+
sanitized[key] = sanitizeObject(obj[key]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return sanitized;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return obj;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ===================================
|
|
384
|
+
// 🌐 CORS MIDDLEWARE
|
|
385
|
+
// ===================================
|
|
386
|
+
|
|
387
|
+
const allowedOrigins = [
|
|
388
|
+
'http://localhost:3000',
|
|
389
|
+
'http://127.0.0.1:3000',
|
|
390
|
+
'https://yourdomain.com',
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
const corsOptions: cors.CorsOptions = {
|
|
394
|
+
origin: function (
|
|
395
|
+
origin: string | undefined,
|
|
396
|
+
callback: (err: Error | null, allow?: boolean) => void
|
|
397
|
+
) {
|
|
398
|
+
if (!origin || allowedOrigins.includes(origin)) {
|
|
399
|
+
callback(null, true);
|
|
400
|
+
} else {
|
|
401
|
+
callback(new Error(`CORS: Origin ${origin} not allowed`));
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
credentials: true,
|
|
405
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
406
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'x-requested-with'],
|
|
407
|
+
optionsSuccessStatus: 200,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const corsMiddleware = cors(corsOptions);
|
|
411
|
+
|
|
412
|
+
// ===================================
|
|
413
|
+
// 🚦 RATE LIMITING MIDDLEWARE
|
|
414
|
+
// ===================================
|
|
415
|
+
|
|
416
|
+
export const generalLimiter = rateLimit({
|
|
417
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
418
|
+
max: 100,
|
|
419
|
+
message: {
|
|
420
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
421
|
+
},
|
|
422
|
+
standardHeaders: true,
|
|
423
|
+
legacyHeaders: false,
|
|
424
|
+
keyGenerator: isProduction ? undefined : () => 'development-key',
|
|
425
|
+
skip: () => !isProduction,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
export const authLimiter = rateLimit({
|
|
429
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
430
|
+
max: 5,
|
|
431
|
+
message: {
|
|
432
|
+
error: 'Too many authentication attempts, please try again later.',
|
|
433
|
+
},
|
|
434
|
+
skipSuccessfulRequests: true,
|
|
435
|
+
keyGenerator: isProduction ? undefined : () => 'development-auth-key',
|
|
436
|
+
skip: () => !isProduction,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
export const uploadLimiter = rateLimit({
|
|
440
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
441
|
+
max: 10,
|
|
442
|
+
message: {
|
|
443
|
+
error: 'Too many file uploads, please try again later.',
|
|
444
|
+
},
|
|
445
|
+
keyGenerator: isProduction ? undefined : () => 'development-upload-key',
|
|
446
|
+
skip: () => !isProduction,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
export const apiLimiter = rateLimit({
|
|
450
|
+
windowMs: 1 * 60 * 1000, // 1 minute
|
|
451
|
+
max: 30,
|
|
452
|
+
message: {
|
|
453
|
+
error: 'Too many API requests, please try again later.',
|
|
454
|
+
},
|
|
455
|
+
keyGenerator: isProduction ? undefined : () => 'development-api-key',
|
|
456
|
+
skip: () => !isProduction,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ===================================
|
|
460
|
+
// ❌ ERROR HANDLING MIDDLEWARE
|
|
461
|
+
// ===================================
|
|
462
|
+
|
|
463
|
+
export interface AppError extends Error {
|
|
464
|
+
statusCode?: number;
|
|
465
|
+
code?: string;
|
|
466
|
+
isOperational?: boolean;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export const createError = (message: string, statusCode: number = 500, code?: string): AppError => {
|
|
470
|
+
const error: AppError = new Error(message);
|
|
471
|
+
error.statusCode = statusCode;
|
|
472
|
+
error.code = code;
|
|
473
|
+
error.isOperational = true;
|
|
474
|
+
return error;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
export const errorHandler = (
|
|
478
|
+
error: Error | AppError | ZodError | PrismaClientKnownRequestError,
|
|
479
|
+
req: Request,
|
|
480
|
+
res: Response,
|
|
481
|
+
next: NextFunction
|
|
482
|
+
): void => {
|
|
483
|
+
logger.error('Error occurred:', {
|
|
484
|
+
error: error.message,
|
|
485
|
+
stack: error.stack,
|
|
486
|
+
url: req.url,
|
|
487
|
+
method: req.method,
|
|
488
|
+
ip: req.ip,
|
|
489
|
+
userAgent: req.get('User-Agent'),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Zod validation errors
|
|
493
|
+
if (error instanceof ZodError) {
|
|
494
|
+
const validationErrors = error.issues.map((err: any) => ({
|
|
495
|
+
field: err.path.join('.'),
|
|
496
|
+
message: err.message,
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
res.status(400).json(createErrorResponse(
|
|
500
|
+
'Validation failed',
|
|
501
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
502
|
+
{ errors: validationErrors }
|
|
503
|
+
));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Prisma errors
|
|
508
|
+
if (error instanceof PrismaClientKnownRequestError) {
|
|
509
|
+
switch (error.code) {
|
|
510
|
+
case 'P2002':
|
|
511
|
+
res.status(409).json(createErrorResponse(
|
|
512
|
+
'A record with this value already exists',
|
|
513
|
+
ERROR_CODES.DATABASE_CONFLICT
|
|
514
|
+
));
|
|
515
|
+
return;
|
|
516
|
+
case 'P2025':
|
|
517
|
+
res.status(404).json(createErrorResponse(
|
|
518
|
+
'Record not found',
|
|
519
|
+
ERROR_CODES.DATABASE_NOT_FOUND
|
|
520
|
+
));
|
|
521
|
+
return;
|
|
522
|
+
case 'P2003':
|
|
523
|
+
res.status(400).json(createErrorResponse(
|
|
524
|
+
'Foreign key constraint failed',
|
|
525
|
+
ERROR_CODES.DATABASE_CONSTRAINT
|
|
526
|
+
));
|
|
527
|
+
return;
|
|
528
|
+
default:
|
|
529
|
+
res.status(500).json(createErrorResponse(
|
|
530
|
+
'Database error',
|
|
531
|
+
ERROR_CODES.DATABASE_ERROR
|
|
532
|
+
));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Prisma connection errors
|
|
538
|
+
if (error instanceof PrismaClientInitializationError) {
|
|
539
|
+
res.status(503).json(createErrorResponse(
|
|
540
|
+
'Database connection failed',
|
|
541
|
+
ERROR_CODES.DATABASE_CONNECTION
|
|
542
|
+
));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// JWT errors
|
|
547
|
+
if (error.name === 'JsonWebTokenError') {
|
|
548
|
+
res.status(401).json(createErrorResponse(
|
|
549
|
+
'Invalid token',
|
|
550
|
+
ERROR_CODES.AUTH_INVALID_TOKEN
|
|
551
|
+
));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (error.name === 'TokenExpiredError') {
|
|
556
|
+
res.status(401).json(createErrorResponse(
|
|
557
|
+
'Token expired',
|
|
558
|
+
ERROR_CODES.AUTH_TOKEN_EXPIRED
|
|
559
|
+
));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// App-specific errors
|
|
564
|
+
const appError = error as AppError;
|
|
565
|
+
if (appError.statusCode && appError.isOperational) {
|
|
566
|
+
res.status(appError.statusCode).json(createErrorResponse(
|
|
567
|
+
appError.message,
|
|
568
|
+
appError.code || ERROR_CODES.GENERIC_ERROR
|
|
569
|
+
));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Default server error
|
|
574
|
+
res.status(500).json(createErrorResponse(
|
|
575
|
+
process.env.NODE_ENV === 'production'
|
|
576
|
+
? 'Something went wrong!'
|
|
577
|
+
: error.message,
|
|
578
|
+
ERROR_CODES.INTERNAL_ERROR
|
|
579
|
+
));
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
export const notFoundHandler = (req: Request, res: Response): void => {
|
|
583
|
+
res.status(404).json(createErrorResponse(
|
|
584
|
+
`Route ${req.originalUrl} not found`,
|
|
585
|
+
ERROR_CODES.ROUTE_NOT_FOUND
|
|
586
|
+
));
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
|
|
590
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// ===================================
|
|
594
|
+
// 📦 COMBINED MIDDLEWARE EXPORTS
|
|
595
|
+
// ===================================
|
|
596
|
+
|
|
597
|
+
// Comprehensive security stack
|
|
598
|
+
export const securityMiddleware = [
|
|
599
|
+
helmetConfig,
|
|
600
|
+
corsMiddleware,
|
|
601
|
+
securityHeaders,
|
|
602
|
+
sanitizeRequest,
|
|
603
|
+
];
|
|
604
|
+
|
|
605
|
+
// Rate limiting stack
|
|
606
|
+
export const rateLimiter = {
|
|
607
|
+
general: generalLimiter,
|
|
608
|
+
auth: authLimiter,
|
|
609
|
+
upload: uploadLimiter,
|
|
610
|
+
api: apiLimiter,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Auth stack
|
|
614
|
+
export const auth = {
|
|
615
|
+
required: authMiddleware,
|
|
616
|
+
optional: optionalAuthMiddleware,
|
|
617
|
+
requireRole,
|
|
618
|
+
requireEmailVerification,
|
|
619
|
+
requireAdmin: requireRole(['ADMIN']),
|
|
620
|
+
requireModerator: requireRole(['ADMIN', 'MODERATOR']),
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// Validation stack
|
|
624
|
+
export const validation = {
|
|
625
|
+
middleware: validationMiddleware,
|
|
626
|
+
body: validateBody,
|
|
627
|
+
query: validateQuery,
|
|
628
|
+
params: validateParams,
|
|
629
|
+
request: validateRequest,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Error handling stack
|
|
633
|
+
export const errorHandling = {
|
|
634
|
+
handler: errorHandler,
|
|
635
|
+
notFound: notFoundHandler,
|
|
636
|
+
asyncHandler,
|
|
637
|
+
createError,
|
|
638
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import {
|
|
3
|
+
register,
|
|
4
|
+
login,
|
|
5
|
+
refreshToken,
|
|
6
|
+
logout,
|
|
7
|
+
verifyEmail,
|
|
8
|
+
getProfile,
|
|
9
|
+
updateProfile,
|
|
10
|
+
changePassword,
|
|
11
|
+
} from '../controllers/auth.controller';
|
|
12
|
+
import { auth, rateLimiter } from '../middleware';
|
|
13
|
+
|
|
14
|
+
const router = Router();
|
|
15
|
+
|
|
16
|
+
// Public routes (with auth rate limiting)
|
|
17
|
+
router.post('/register', rateLimiter.auth, register);
|
|
18
|
+
router.post('/login', rateLimiter.auth, login);
|
|
19
|
+
router.post('/refresh-token', rateLimiter.auth, refreshToken);
|
|
20
|
+
router.post('/logout', logout);
|
|
21
|
+
router.get('/verify-email/:token', verifyEmail);
|
|
22
|
+
|
|
23
|
+
// Protected routes
|
|
24
|
+
router.use(auth.required); // Apply auth middleware to all routes below
|
|
25
|
+
router.get('/profile', getProfile);
|
|
26
|
+
router.put('/profile', updateProfile);
|
|
27
|
+
router.put('/change-password', changePassword);
|
|
28
|
+
|
|
29
|
+
export default router;
|