From 2b8164575ce6f0f27d165c1cc1c5629fcbc7fb1e Mon Sep 17 00:00:00 2001 From: samad13 Date: Mon, 29 Jun 2026 20:52:48 +0100 Subject: [PATCH 1/4] fix --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e12effc..2ba6041 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,8 @@ pnpm test --- +linked PR 5 + ## 📄 License This project is licensed under the ISC License. From 1ce0a13a56cbc2b29acaaa213dd0d0a3e5736f14 Mon Sep 17 00:00:00 2001 From: samad13 Date: Mon, 29 Jun 2026 20:54:23 +0100 Subject: [PATCH 2/4] fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ba6041..9e554e0 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ pnpm test --- -linked PR 5 +linked PR 5,4 ## 📄 License From 978a9c4cebee3f2b0cc0a97e27681950b04fb7e1 Mon Sep 17 00:00:00 2001 From: samad13 Date: Mon, 29 Jun 2026 20:55:36 +0100 Subject: [PATCH 3/4] fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e554e0..27cd678 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ pnpm test --- -linked PR 5,4 +linked PR 5,4,6 ## 📄 License From 273719b3c8312c3857858f59bdfe7dfff650e337 Mon Sep 17 00:00:00 2001 From: samad13 Date: Wed, 1 Jul 2026 11:52:21 +0100 Subject: [PATCH 4/4] fix features --- src/app.ts | 16 +++++++-------- src/config/env.ts | 35 +++++++++++++++++++++++++++++++++ src/config/logger.ts | 25 ++++++++++++----------- src/middleware/requestLogger.ts | 29 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 src/config/env.ts create mode 100644 src/middleware/requestLogger.ts diff --git a/src/app.ts b/src/app.ts index ac1ab58..eec24f6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,9 +4,10 @@ import helmet from 'helmet'; import compression from 'compression'; import rateLimit from 'express-rate-limit'; import { connectDatabase } from './config/database'; -import logger from './config/logger'; import errorHandler from './middleware/errorHandler'; +import requestLogger from './middleware/requestLogger'; import routes from './routes'; +import env from './config/env'; const app = express(); @@ -16,15 +17,15 @@ app.use(helmet()); // CORS configuration app.use( cors({ - origin: process.env.CORS_ORIGIN || '*', + origin: env.CORS_ORIGIN, credentials: true, }), ); // Rate limiting const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs + windowMs: env.RATE_LIMIT_WINDOW_MS, + max: env.RATE_LIMIT_MAX_REQUESTS, standardHeaders: true, legacyHeaders: false, }); @@ -37,11 +38,8 @@ app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Compression app.use(compression()); -// Logging middleware -app.use((req, res, next) => { - logger.info(`${req.method} ${req.url}`); - next(); -}); +// Request logging middleware +app.use(requestLogger); // Health check endpoint app.get('/health', (req, res) => { diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..f45e519 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + PORT: z.coerce.number().int().min(1).max(65535).default(3000), + MONGODB_URI: z.string().url(), + JWT_SECRET: z.string().min(16), + JWT_EXPIRES_IN: z.string().default('7d'), + BCRYPT_ROUNDS: z.coerce.number().int().min(8).max(31).default(10), + LOG_LEVEL: z.enum(['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']).default('info'), + CORS_ORIGIN: z.string().url(), + RATE_LIMIT_WINDOW_MS: z.coerce.number().int().min(1000).default(900000), + RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().min(1).default(100), +}); + +let env: z.infer; + +try { + env = envSchema.parse(process.env); +} catch (error) { + if (error instanceof z.ZodError) { + console.error('❌ Invalid environment variables:'); + error.issues.forEach((issue) => { + console.error(` - ${issue.path.join('.')}: ${issue.message}`); + }); + } else { + console.error('❌ Failed to parse environment variables:', error); + } + process.exit(1); +} + +export default env; diff --git a/src/config/logger.ts b/src/config/logger.ts index f4d3070..4f078f8 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -1,4 +1,5 @@ import winston from 'winston'; +import env from './env'; const levels = { error: 0, @@ -8,12 +9,6 @@ const levels = { debug: 4, }; -const level = (): string => { - const env = process.env.NODE_ENV || 'development'; - const isDevelopment = env === 'development'; - return process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'warn'); -}; - const colors = { error: 'red', warn: 'yellow', @@ -24,25 +19,33 @@ const colors = { winston.addColors(colors); -const format = winston.format.combine( +const devFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.colorize({ all: true }), winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), ); +const prodFormat = winston.format.combine(winston.format.timestamp(), winston.format.json()); + const transports = [ - new winston.transports.Console(), + new winston.transports.Console({ + format: env.NODE_ENV === 'development' ? devFormat : prodFormat, + }), new winston.transports.File({ filename: 'logs/error.log', level: 'error', + format: prodFormat, + }), + new winston.transports.File({ + filename: 'logs/all.log', + format: prodFormat, }), - new winston.transports.File({ filename: 'logs/all.log' }), ]; const logger = winston.createLogger({ - level: level(), + level: env.LOG_LEVEL, levels, - format, + format: env.NODE_ENV === 'development' ? devFormat : prodFormat, transports, }); diff --git a/src/middleware/requestLogger.ts b/src/middleware/requestLogger.ts new file mode 100644 index 0000000..9eb0e8c --- /dev/null +++ b/src/middleware/requestLogger.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from 'express'; +import logger from '../config/logger'; + +const requestLogger = (req: Request, res: Response, next: NextFunction): void => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + const logMessage = `${req.method} ${req.originalUrl} - ${res.statusCode} (${duration}ms)`; + const logData = { + method: req.method, + url: req.originalUrl, + statusCode: res.statusCode, + duration, + ip: req.ip, + userAgent: req.get('user-agent'), + }; + + if (res.statusCode >= 400) { + logger.warn(logMessage, logData); + } else { + logger.info(logMessage, logData); + } + }); + + next(); +}; + +export default requestLogger;