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.
@@ -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;