create-arktos
Version:
🚀 A modern Node.js backend boilerplate with TypeScript, Express, JWT authentication, Prisma ORM, PostgreSQL, and Resend email service. Includes complete authentication flow, security middleware, and database management.
638 lines (559 loc) • 16.5 kB
text/typescript
/**
* ===================================
* ARKTOS BACKEND MIDDLEWARE INDEX
* ===================================
* Tüm middleware'lerin merkezi yönetimi
*/
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { ZodSchema, ZodError } from 'zod';
import { rateLimit } from 'express-rate-limit';
import helmet from 'helmet';
import cors from 'cors';
import { PrismaClientKnownRequestError, PrismaClientInitializationError } from '@prisma/client/runtime/library';
import { AuthenticatedRequest } from '../types';
import { createErrorResponse, createSuccessResponse } from '../utils/response';
import { ERROR_CODES } from '../constants/errorCodes';
import DatabaseService from '../services/database.service';
import logger from '../config/logger';
const dbService = DatabaseService.getInstance();
const prisma = dbService.getClient();
const isProduction = process.env.NODE_ENV === 'production';
// ===================================
// 🔐 AUTHENTICATION MIDDLEWARE
// ===================================
export const authMiddleware = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json(createErrorResponse(
'Access token required',
ERROR_CODES.AUTH_TOKEN_REQUIRED
));
return;
}
const token = authHeader.substring(7);
let decoded: any;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET!);
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
res.status(401).json(createErrorResponse(
'Access token expired',
ERROR_CODES.AUTH_TOKEN_EXPIRED
));
return;
}
res.status(401).json(createErrorResponse(
'Invalid access token',
ERROR_CODES.AUTH_TOKEN_INVALID
));
return;
}
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
username: true,
avatar: true,
role: true,
isEmailVerified: true,
isActive: true,
emailVerifiedAt: true,
lastLoginAt: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
res.status(401).json(createErrorResponse(
'User not found',
ERROR_CODES.AUTH_USER_NOT_FOUND
));
return;
}
if (!user.isActive) {
res.status(403).json(createErrorResponse(
'Account is deactivated',
ERROR_CODES.AUTH_ACCOUNT_DEACTIVATED
));
return;
}
req.user = user;
next();
} catch (error) {
logger.error('Auth middleware error:', error);
res.status(500).json(createErrorResponse(
'Authentication error',
ERROR_CODES.INTERNAL_ERROR
));
}
};
export const optionalAuthMiddleware = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
next();
return;
}
const token = authHeader.substring(7);
let decoded: any;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET!);
} catch (error) {
next();
return;
}
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
username: true,
avatar: true,
role: true,
isEmailVerified: true,
isActive: true,
emailVerifiedAt: true,
lastLoginAt: true,
createdAt: true,
updatedAt: true,
},
});
if (user && user.isActive) {
req.user = user;
}
next();
} catch (error) {
logger.error('Optional auth middleware error:', error);
next();
}
};
// Role-based access control
export const requireRole = (allowedRoles: string | string[]) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json(createErrorResponse(
'Authentication required',
ERROR_CODES.AUTH_TOKEN_REQUIRED
));
return;
}
const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
if (!roles.includes(req.user.role)) {
res.status(403).json(createErrorResponse(
'Insufficient permissions',
ERROR_CODES.AUTH_INSUFFICIENT_PERMISSIONS
));
return;
}
next();
};
};
// Email verification requirement
export const requireEmailVerification = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json(createErrorResponse(
'Authentication required',
ERROR_CODES.AUTH_TOKEN_REQUIRED
));
return;
}
if (!req.user.isEmailVerified) {
res.status(403).json(createErrorResponse(
'Email verification required',
ERROR_CODES.AUTH_EMAIL_NOT_VERIFIED
));
return;
}
next();
};
// ===================================
// ✅ VALIDATION MIDDLEWARE
// ===================================
export const validateRequest = (schema: ZodSchema, data: any): any => {
try {
return schema.parse(data);
} catch (error) {
if (error instanceof ZodError) {
const validationErrors = error.issues.map((err: any) => ({
field: err.path.join('.'),
message: err.message,
value: err.input,
}));
throw { validationErrors, isZodError: true };
}
throw error;
}
};
export const validationMiddleware = (schema: {
body?: ZodSchema;
query?: ZodSchema;
params?: ZodSchema;
}) => {
return (req: Request, res: Response, next: NextFunction): void => {
try {
if (schema.body) {
req.body = schema.body.parse(req.body);
}
if (schema.query) {
req.query = schema.query.parse(req.query) as any;
}
if (schema.params) {
req.params = schema.params.parse(req.params) as any;
}
next();
} catch (error) {
if (error instanceof ZodError) {
const validationErrors = error.issues.map((err: any) => ({
field: err.path.join('.'),
message: err.message,
}));
res.status(400).json(
createErrorResponse(
'Validation failed',
ERROR_CODES.VALIDATION_ERROR,
{ errors: validationErrors }
)
);
return;
}
next(error);
}
};
};
export const validateBody = (schema: ZodSchema) => validationMiddleware({ body: schema });
export const validateQuery = (schema: ZodSchema) => validationMiddleware({ query: schema });
export const validateParams = (schema: ZodSchema) => validationMiddleware({ params: schema });
// ===================================
// 🛡️ SECURITY MIDDLEWARE
// ===================================
// Helmet configuration
export const helmetConfig = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'https:', 'data:'],
connectSrc: ["'self'"],
mediaSrc: ["'self'"],
objectSrc: ["'none'"],
childSrc: ["'self'"],
frameSrc: ["'self'"],
workerSrc: ["'self'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
crossOriginEmbedderPolicy: false,
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'cross-origin' },
dnsPrefetchControl: { allow: false },
frameguard: { action: 'deny' },
hidePoweredBy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
ieNoOpen: true,
noSniff: true,
originAgentCluster: true,
permittedCrossDomainPolicies: false,
referrerPolicy: { policy: 'no-referrer' },
xssFilter: true,
});
// Additional security headers
export const securityHeaders = (req: Request, res: Response, next: NextFunction): void => {
res.removeHeader('X-Powered-By');
res.removeHeader('Server');
res.setHeader('X-API-Version', '1.0.0');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Download-Options', 'noopen');
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
if (req.path.includes('/auth/') || req.path.includes('/profile/')) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Surrogate-Control', 'no-store');
}
next();
};
// Request sanitization
export const sanitizeRequest = (req: Request, res: Response, next: NextFunction): void => {
if (req.body && typeof req.body === 'object') {
req.body = sanitizeObject(req.body);
}
if (req.query && typeof req.query === 'object') {
req.query = sanitizeObject(req.query);
}
next();
};
function sanitizeObject(obj: any): any {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
return obj
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
}
if (Array.isArray(obj)) {
return obj.map(sanitizeObject);
}
if (typeof obj === 'object') {
const sanitized: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
sanitized[key] = sanitizeObject(obj[key]);
}
}
return sanitized;
}
return obj;
}
// ===================================
// 🌐 CORS MIDDLEWARE
// ===================================
const allowedOrigins = [
'http://localhost:3000',
'http://127.0.0.1:3000',
'https://yourdomain.com',
];
const corsOptions: cors.CorsOptions = {
origin: function (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void
) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: Origin ${origin} not allowed`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-requested-with'],
optionsSuccessStatus: 200,
};
export const corsMiddleware = cors(corsOptions);
// ===================================
// 🚦 RATE LIMITING MIDDLEWARE
// ===================================
export const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: {
error: 'Too many requests from this IP, please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: isProduction ? undefined : () => 'development-key',
skip: () => !isProduction,
});
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: {
error: 'Too many authentication attempts, please try again later.',
},
skipSuccessfulRequests: true,
keyGenerator: isProduction ? undefined : () => 'development-auth-key',
skip: () => !isProduction,
});
export const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: {
error: 'Too many file uploads, please try again later.',
},
keyGenerator: isProduction ? undefined : () => 'development-upload-key',
skip: () => !isProduction,
});
export const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 30,
message: {
error: 'Too many API requests, please try again later.',
},
keyGenerator: isProduction ? undefined : () => 'development-api-key',
skip: () => !isProduction,
});
// ===================================
// ❌ ERROR HANDLING MIDDLEWARE
// ===================================
export interface AppError extends Error {
statusCode?: number;
code?: string;
isOperational?: boolean;
}
export const createError = (message: string, statusCode: number = 500, code?: string): AppError => {
const error: AppError = new Error(message);
error.statusCode = statusCode;
error.code = code;
error.isOperational = true;
return error;
};
export const errorHandler = (
error: Error | AppError | ZodError | PrismaClientKnownRequestError,
req: Request,
res: Response,
next: NextFunction
): void => {
logger.error('Error occurred:', {
error: error.message,
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
});
// Zod validation errors
if (error instanceof ZodError) {
const validationErrors = error.issues.map((err: any) => ({
field: err.path.join('.'),
message: err.message,
}));
res.status(400).json(createErrorResponse(
'Validation failed',
ERROR_CODES.VALIDATION_ERROR,
{ errors: validationErrors }
));
return;
}
// Prisma errors
if (error instanceof PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002':
res.status(409).json(createErrorResponse(
'A record with this value already exists',
ERROR_CODES.DATABASE_CONFLICT
));
return;
case 'P2025':
res.status(404).json(createErrorResponse(
'Record not found',
ERROR_CODES.DATABASE_NOT_FOUND
));
return;
case 'P2003':
res.status(400).json(createErrorResponse(
'Foreign key constraint failed',
ERROR_CODES.DATABASE_CONSTRAINT
));
return;
default:
res.status(500).json(createErrorResponse(
'Database error',
ERROR_CODES.DATABASE_ERROR
));
return;
}
}
// Prisma connection errors
if (error instanceof PrismaClientInitializationError) {
res.status(503).json(createErrorResponse(
'Database connection failed',
ERROR_CODES.DATABASE_CONNECTION
));
return;
}
// JWT errors
if (error.name === 'JsonWebTokenError') {
res.status(401).json(createErrorResponse(
'Invalid token',
ERROR_CODES.AUTH_INVALID_TOKEN
));
return;
}
if (error.name === 'TokenExpiredError') {
res.status(401).json(createErrorResponse(
'Token expired',
ERROR_CODES.AUTH_TOKEN_EXPIRED
));
return;
}
// App-specific errors
const appError = error as AppError;
if (appError.statusCode && appError.isOperational) {
res.status(appError.statusCode).json(createErrorResponse(
appError.message,
appError.code || ERROR_CODES.GENERIC_ERROR
));
return;
}
// Default server error
res.status(500).json(createErrorResponse(
process.env.NODE_ENV === 'production'
? 'Something went wrong!'
: error.message,
ERROR_CODES.INTERNAL_ERROR
));
};
export const notFoundHandler = (req: Request, res: Response): void => {
res.status(404).json(createErrorResponse(
`Route ${req.originalUrl} not found`,
ERROR_CODES.ROUTE_NOT_FOUND
));
};
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// ===================================
// 📦 COMBINED MIDDLEWARE EXPORTS
// ===================================
// Comprehensive security stack
export const securityMiddleware = [
helmetConfig,
corsMiddleware,
securityHeaders,
sanitizeRequest,
];
// Rate limiting stack
export const rateLimiter = {
general: generalLimiter,
auth: authLimiter,
upload: uploadLimiter,
api: apiLimiter,
};
// Auth stack
export const auth = {
required: authMiddleware,
optional: optionalAuthMiddleware,
requireRole,
requireEmailVerification,
requireAdmin: requireRole(['ADMIN']),
requireModerator: requireRole(['ADMIN', 'MODERATOR']),
};
// Validation stack
export const validation = {
middleware: validationMiddleware,
body: validateBody,
query: validateQuery,
params: validateParams,
request: validateRequest,
};
// Error handling stack
export const errorHandling = {
handler: errorHandler,
notFound: notFoundHandler,
asyncHandler,
createError,
};