express-genix 1.1.4 → 2.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -259
  3. package/index.js +229 -113
  4. package/lib/cleanup.js +41 -129
  5. package/lib/features.js +239 -0
  6. package/lib/generator.js +286 -204
  7. package/lib/utils.js +43 -91
  8. package/package.json +81 -63
  9. package/templates/cicd/github-actions.yml.ejs +70 -0
  10. package/templates/config/database.mongo.js.ejs +29 -33
  11. package/templates/config/database.postgres.js.ejs +41 -40
  12. package/templates/config/database.prisma.js.ejs +26 -0
  13. package/templates/config/redis.js.ejs +28 -0
  14. package/templates/config/schema.prisma.ejs +20 -0
  15. package/templates/config/swagger.js.ejs +30 -0
  16. package/templates/config/websocket.js.ejs +62 -0
  17. package/templates/controllers/authController.js.ejs +152 -129
  18. package/templates/controllers/exampleController.js.ejs +92 -152
  19. package/templates/controllers/userController.js.ejs +52 -60
  20. package/templates/core/Dockerfile.ejs +41 -31
  21. package/templates/core/README.md.ejs +191 -179
  22. package/templates/core/app.js.ejs +114 -64
  23. package/templates/core/docker-compose.yml.ejs +59 -47
  24. package/templates/core/dockerignore.ejs +7 -0
  25. package/templates/core/env.ejs +25 -19
  26. package/templates/core/env.example.ejs +26 -0
  27. package/templates/core/eslintrc.json.ejs +50 -20
  28. package/templates/core/gitignore.ejs +51 -51
  29. package/templates/core/healthcheck.js.ejs +24 -24
  30. package/templates/core/jest.config.js.ejs +19 -22
  31. package/templates/core/package.json.ejs +70 -33
  32. package/templates/core/prettierrc.json.ejs +11 -11
  33. package/templates/core/server.js.ejs +64 -48
  34. package/templates/core/tsconfig.json.ejs +19 -0
  35. package/templates/middleware/auth.js.ejs +80 -66
  36. package/templates/middleware/cache.js.ejs +67 -0
  37. package/templates/middleware/errorHandler.js.ejs +50 -46
  38. package/templates/middleware/requestId.js.ejs +9 -0
  39. package/templates/middleware/validation.js.ejs +109 -47
  40. package/templates/migrations/create-users.js.ejs +50 -0
  41. package/templates/migrations/seed-users.js.ejs +34 -0
  42. package/templates/migrations/sequelizerc.ejs +8 -0
  43. package/templates/models/User.mongo.js.ejs +29 -29
  44. package/templates/models/User.postgres.js.ejs +40 -40
  45. package/templates/models/index.mongo.js.ejs +7 -7
  46. package/templates/models/index.postgres.js.ejs +11 -11
  47. package/templates/routes/authRoutes.js.ejs +222 -13
  48. package/templates/routes/exampleRoutes.js.ejs +100 -12
  49. package/templates/routes/index.js.ejs +34 -24
  50. package/templates/routes/userRoutes.js.ejs +78 -15
  51. package/templates/services/authService.js.ejs +111 -35
  52. package/templates/services/exampleService.js.ejs +112 -112
  53. package/templates/services/userService.mongodb.js.ejs +33 -33
  54. package/templates/services/userService.postgres.js.ejs +30 -30
  55. package/templates/services/userService.prisma.js.ejs +36 -0
  56. package/templates/tests/auth.test.js.ejs +83 -66
  57. package/templates/tests/example.test.js.ejs +109 -112
  58. package/templates/tests/setup.js.ejs +11 -11
  59. package/templates/tests/users.test.js.ejs +42 -42
  60. package/templates/utils/envValidator.js.ejs +23 -0
  61. package/templates/utils/errors.js.ejs +12 -12
  62. package/templates/utils/logger.js.ejs +37 -28
  63. package/templates/utils/response.js.ejs +28 -0
  64. package/templates/utils/validators.js.ejs +34 -34
  65. package/templates/config/swagger.json.ejs +0 -194
  66. package/templates/core/index.js.ejs +0 -24
@@ -1,49 +1,65 @@
1
- const cluster = require('cluster');
2
- const os = require('os');
3
- const app = require('./app');<% if (hasDatabase) { %>
4
- const db = require('./config/database');<% } %>
5
-
6
- const port = process.env.PORT || 3000;
7
-
8
- const startServer = async () => {<% if (hasDatabase) { %>
9
- await db.connect();<% } %>
10
-
11
- if (cluster.isMaster) {
12
- const numCPUs = os.cpus().length;
13
- console.log(`🚀 Master ${process.pid} is running`);
14
- console.log(`📊 Forking ${numCPUs} workers...`);
15
-
16
- // Fork workers
17
- for (let i = 0; i < numCPUs; i++) {
18
- cluster.fork();
19
- }
20
-
21
- cluster.on('exit', (worker, code, signal) => {
22
- console.log(`💥 Worker ${worker.process.pid} died with code ${code} and signal ${signal}`);
23
- console.log('🔄 Starting a new worker...');
24
- cluster.fork();
25
- });
26
- } else {
27
- app.listen(port, () => {
28
- console.log(`🌟 Worker ${process.pid} running on http://localhost:${port}`);
29
- console.log(`📚 API Documentation: http://localhost:${port}/api-docs`);
30
- console.log(`❤️ Health Check: http://localhost:${port}/health`);
31
- console.log(`🛠️ Environment: ${process.env.NODE_ENV || 'development'}`);
32
- });
33
- }
34
- };
35
-
36
- startServer().catch(console.error);
37
-
38
- // Graceful Shutdown
39
- process.on('SIGTERM', async () => {
40
- console.log('🛑 SIGTERM signal received: closing HTTP server');<% if (hasDatabase) { %>
41
- await db.disconnect();<% } %>
42
- process.exit(0);
43
- });
44
-
45
- process.on('SIGINT', async () => {
46
- console.log('🛑 SIGINT signal received: closing HTTP server');<% if (hasDatabase) { %>
47
- await db.disconnect();<% } %>
48
- process.exit(0);
1
+ require('dotenv').config();
2
+
3
+ const cluster = require('cluster');
4
+ const os = require('os');
5
+ <% if (hasWebsocket) { %>const http = require('http');<% } %>
6
+ const app = require('./app');<% if (hasDatabase) { %>
7
+ const db = require('./config/database');<% } %><% if (hasRedis) { %>
8
+ const { connectRedis } = require('./config/redis');<% } %><% if (hasWebsocket) { %>
9
+ const { setupWebSocket } = require('./config/websocket');<% } %>
10
+ const { createLogger } = require('./utils/logger');
11
+
12
+ const logger = createLogger('Server');
13
+ const port = process.env.PORT || 3000;
14
+ const isPrimary = cluster.isPrimary ?? cluster.isMaster;
15
+
16
+ const startServer = async () => {
17
+ if (process.env.NODE_ENV === 'production' && isPrimary) {
18
+ const numCPUs = os.cpus().length;
19
+ logger.info(`Master ${process.pid} is running, forking ${numCPUs} workers`);
20
+
21
+ for (let i = 0; i < numCPUs; i++) {
22
+ cluster.fork();
23
+ }
24
+
25
+ cluster.on('exit', (worker, code, signal) => {
26
+ logger.warn(`Worker ${worker.process.pid} died (code: ${code}, signal: ${signal}). Restarting...`);
27
+ cluster.fork();
28
+ });
29
+ } else {<% if (hasDatabase) { %>
30
+ await db.connect();<% } %><% if (hasRedis) { %>
31
+ await connectRedis();<% } %>
32
+
33
+ <% if (hasWebsocket) { %> const httpServer = http.createServer(app);
34
+ const io = setupWebSocket(httpServer);
35
+ app.set('io', io);
36
+
37
+ const server = httpServer.listen(port, () => {<% } else { %> const server = app.listen(port, () => {<% } %>
38
+ logger.info(`Worker ${process.pid} running on http://localhost:${port}`);
39
+ logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
40
+ });
41
+
42
+ // Graceful shutdown
43
+ const shutdown = async (signal) => {
44
+ logger.info(`${signal} received — shutting down gracefully`);
45
+ server.close(async () => {<% if (hasDatabase) { %>
46
+ await db.disconnect();<% } %>
47
+ process.exit(0);
48
+ });
49
+
50
+ // Force exit after 10s
51
+ setTimeout(() => {
52
+ logger.error('Forced shutdown after timeout');
53
+ process.exit(1);
54
+ }, 10000);
55
+ };
56
+
57
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
58
+ process.on('SIGINT', () => shutdown('SIGINT'));
59
+ }
60
+ };
61
+
62
+ startServer().catch((err) => {
63
+ logger.error('Failed to start server', { error: err.message });
64
+ process.exit(1);
49
65
  });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "tests"]
19
+ }
@@ -1,66 +1,80 @@
1
- const jwt = require('jsonwebtoken');
2
- const { AppError } = require('../utils/errors');
3
-
4
- const jwtSecret = process.env.JWT_SECRET || 'your-secret-key';
5
-
6
- /**
7
- * Middleware to authenticate JWT tokens
8
- */
9
- const authenticateToken = (req, res, next) => {
10
- const authHeader = req.headers['authorization'];
11
- const token = authHeader && authHeader.split(' ')[1];
12
-
13
- if (!token) {
14
- return next(new AppError('Access token is required', 401));
15
- }
16
-
17
- jwt.verify(token, jwtSecret, (err, decoded) => {
18
- if (err) {
19
- return next(new AppError('Invalid or expired token', 403));
20
- }
21
-
22
- req.user = decoded;
23
- next();
24
- });
25
- };
26
-
27
- /**
28
- * Optional authentication middleware - doesn't require token
29
- */
30
- const optionalAuth = (req, res, next) => {
31
- const authHeader = req.headers['authorization'];
32
- const token = authHeader && authHeader.split(' ')[1];
33
-
34
- if (token) {
35
- jwt.verify(token, jwtSecret, (err, decoded) => {
36
- if (!err) {
37
- req.user = decoded;
38
- }
39
- });
40
- }
41
-
42
- next();
43
- };
44
-
45
- /**
46
- * Middleware to check if user has specific role
47
- */
48
- const requireRole = (role) => {
49
- return (req, res, next) => {
50
- if (!req.user) {
51
- return next(new AppError('Authentication required', 401));
52
- }
53
-
54
- if (req.user.role !== role) {
55
- return next(new AppError('Insufficient permissions', 403));
56
- }
57
-
58
- next();
59
- };
60
- };
61
-
62
- module.exports = {
63
- authenticateToken,
64
- optionalAuth,
65
- requireRole,
66
- };
1
+ const jwt = require('jsonwebtoken');
2
+ const { AppError } = require('../utils/errors');
3
+ const authService = require('../services/authService');
4
+
5
+ const authenticateToken = <% if (hasRedis) { %>async <% } %>(req, res, next) => {
6
+ const authHeader = req.headers['authorization'];
7
+ const token = authHeader && authHeader.split(' ')[1];
8
+
9
+ if (!token) {
10
+ return next(new AppError('Access token is required', 401));
11
+ }
12
+
13
+ <% if (hasRedis) { %>
14
+ try {
15
+ const blacklisted = await authService.isTokenBlacklisted(token);
16
+ if (blacklisted) {
17
+ return next(new AppError('Token has been revoked', 401));
18
+ }
19
+ } catch {
20
+ return next(new AppError('Token validation failed', 500));
21
+ }
22
+ <% } else { %>
23
+ if (authService.isTokenBlacklisted(token)) {
24
+ return next(new AppError('Token has been revoked', 401));
25
+ }
26
+ <% } %>
27
+
28
+ jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
29
+ if (err) {
30
+ return next(new AppError('Invalid or expired token', 403));
31
+ }
32
+ req.user = decoded;
33
+ next();
34
+ });
35
+ };
36
+
37
+ const optionalAuth = <% if (hasRedis) { %>async <% } %>(req, res, next) => {
38
+ const authHeader = req.headers['authorization'];
39
+ const token = authHeader && authHeader.split(' ')[1];
40
+
41
+ <% if (hasRedis) { %>
42
+ if (token) {
43
+ try {
44
+ const blacklisted = await authService.isTokenBlacklisted(token);
45
+ if (!blacklisted) {
46
+ jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
47
+ if (!err) {
48
+ req.user = decoded;
49
+ }
50
+ });
51
+ }
52
+ } catch {
53
+ // Silently skip auth on Redis failure for optional auth
54
+ }
55
+ }
56
+ <% } else { %>
57
+ if (token && !authService.isTokenBlacklisted(token)) {
58
+ jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
59
+ if (!err) {
60
+ req.user = decoded;
61
+ }
62
+ });
63
+ }
64
+ <% } %>
65
+ next();
66
+ };
67
+
68
+ const requireRole = (role) => {
69
+ return (req, res, next) => {
70
+ if (!req.user) {
71
+ return next(new AppError('Authentication required', 401));
72
+ }
73
+ if (req.user.role !== role) {
74
+ return next(new AppError('Insufficient permissions', 403));
75
+ }
76
+ next();
77
+ };
78
+ };
79
+
80
+ module.exports = { authenticateToken, optionalAuth, requireRole };
@@ -0,0 +1,67 @@
1
+ const { redis } = require('../config/redis');
2
+ const { createLogger } = require('../utils/logger');
3
+
4
+ const logger = createLogger('Cache');
5
+
6
+ /**
7
+ * Express middleware that caches JSON responses in Redis.
8
+ *
9
+ * Usage:
10
+ * router.get('/items', cache(300), controller.list); // 5-minute cache
11
+ *
12
+ * Cache key is derived from the request method + original URL.
13
+ * Only successful (2xx) JSON responses are cached.
14
+ * Set res.locals.skipCache = true inside a handler to bypass.
15
+ */
16
+ const cache = (ttlSeconds = 60) => {
17
+ return async (req, res, next) => {
18
+ if (req.method !== 'GET') {
19
+ return next();
20
+ }
21
+
22
+ const key = `cache:${req.originalUrl}`;
23
+
24
+ try {
25
+ const cached = await redis.get(key);
26
+ if (cached) {
27
+ logger.debug(`Cache hit: ${key}`);
28
+ return res.status(200).json(JSON.parse(cached));
29
+ }
30
+ } catch (err) {
31
+ logger.warn('Cache read failed, skipping', { error: err.message });
32
+ }
33
+
34
+ // Intercept res.json to cache the response
35
+ const originalJson = res.json.bind(res);
36
+ res.json = (body) => {
37
+ if (res.statusCode >= 200 && res.statusCode < 300 && !res.locals.skipCache) {
38
+ redis
39
+ .set(key, JSON.stringify(body), 'EX', ttlSeconds)
40
+ .catch((err) => logger.warn('Cache write failed', { error: err.message }));
41
+ }
42
+ return originalJson(body);
43
+ };
44
+
45
+ next();
46
+ };
47
+ };
48
+
49
+ /**
50
+ * Invalidate cache entries matching a pattern.
51
+ *
52
+ * Usage:
53
+ * await invalidateCache('/api/items*');
54
+ */
55
+ const invalidateCache = async (pattern) => {
56
+ try {
57
+ const keys = await redis.keys(`cache:${pattern}`);
58
+ if (keys.length > 0) {
59
+ await redis.del(...keys);
60
+ logger.debug(`Invalidated ${keys.length} cache keys matching ${pattern}`);
61
+ }
62
+ } catch (err) {
63
+ logger.warn('Cache invalidation failed', { error: err.message });
64
+ }
65
+ };
66
+
67
+ module.exports = { cache, invalidateCache };
@@ -1,47 +1,51 @@
1
-
2
- // templates/middleware/errorHandler.js.ejs
3
- const { AppError } = require('../utils/errors');
4
-
5
- const errorHandler = (err, req, res, next) => {
6
- let error = { ...err };
7
- error.message = err.message;
8
-
9
- console.error(err);
10
-
11
- <% if (db === 'mongodb') { %> // Mongoose bad ObjectId
12
- if (err.name === 'CastError') {
13
- const message = 'Resource not found';
14
- error = new AppError(message, 404);
15
- }
16
-
17
- // Mongoose duplicate key
18
- if (err.code === 11000) {
19
- const message = 'Duplicate field value entered';
20
- error = new AppError(message, 400);
21
- }
22
-
23
- // Mongoose validation error
24
- if (err.name === 'ValidationError') {
25
- const message = Object.values(err.errors).map(val => val.message).join(', ');
26
- error = new AppError(message, 400);
27
- }<% } %>
28
-
29
- <% if (hasDatabase) { %> // JWT errors
30
- if (err.name === 'JsonWebTokenError') {
31
- const message = 'Invalid token';
32
- error = new AppError(message, 401);
33
- }
34
-
35
- if (err.name === 'TokenExpiredError') {
36
- const message = 'Token expired';
37
- error = new AppError(message, 401);
38
- }<% } %>
39
-
40
- res.status(error.statusCode || 500).json({
41
- success: false,
42
- error: error.message || 'Server Error',
43
- ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
44
- });
45
- };
46
-
1
+ const { AppError } = require('../utils/errors');
2
+ const { createLogger } = require('../utils/logger');
3
+
4
+ const logger = createLogger('ErrorHandler');
5
+
6
+ const errorHandler = (err, req, res, next) => {
7
+ let error = { ...err };
8
+ error.message = err.message;
9
+
10
+ logger.error(err.message, { stack: err.stack });
11
+
12
+ <% if (db === 'mongodb') { %>
13
+ if (err.name === 'CastError') {
14
+ error = new AppError('Resource not found', 404);
15
+ }
16
+
17
+ if (err.code === 11000) {
18
+ error = new AppError('Duplicate field value entered', 400);
19
+ }
20
+
21
+ if (err.name === 'ValidationError') {
22
+ const message = Object.values(err.errors).map((val) => val.message).join(', ');
23
+ error = new AppError(message, 400);
24
+ }
25
+ <% } %><% if (db === 'postgresql') { %>
26
+ if (err.name === 'SequelizeUniqueConstraintError') {
27
+ error = new AppError('Duplicate field value entered', 400);
28
+ }
29
+
30
+ if (err.name === 'SequelizeValidationError') {
31
+ const message = err.errors.map((e) => e.message).join(', ');
32
+ error = new AppError(message, 400);
33
+ }
34
+ <% } %><% if (hasAuth) { %>
35
+ if (err.name === 'JsonWebTokenError') {
36
+ error = new AppError('Invalid token', 401);
37
+ }
38
+
39
+ if (err.name === 'TokenExpiredError') {
40
+ error = new AppError('Token expired', 401);
41
+ }
42
+ <% } %>
43
+
44
+ res.status(error.statusCode || 500).json({
45
+ success: false,
46
+ error: error.message || 'Internal Server Error',
47
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
48
+ });
49
+ };
50
+
47
51
  module.exports = errorHandler;
@@ -0,0 +1,9 @@
1
+ const { v4: uuidv4 } = require('uuid');
2
+
3
+ const requestId = (req, res, next) => {
4
+ req.id = req.headers['x-request-id'] || uuidv4();
5
+ res.setHeader('X-Request-ID', req.id);
6
+ next();
7
+ };
8
+
9
+ module.exports = { requestId };
@@ -1,48 +1,110 @@
1
- // templates/middleware/validation.js.ejs
2
- const { AppError } = require('../utils/errors');
3
-
4
- const validateEmail = (email) => {
5
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
- return emailRegex.test(email);
7
- };
8
-
9
- const validateLogin = (req, res, next) => {
10
- const { email, password } = req.body;
11
-
12
- if (!email || !password) {
13
- return next(new AppError('Email and password are required', 400));
14
- }
15
-
16
- if (!validateEmail(email)) {
17
- return next(new AppError('Please provide a valid email address', 400));
18
- }
19
-
20
- next();
21
- };
22
-
23
- const validateRegister = (req, res, next) => {
24
- const { username, email, password } = req.body;
25
-
26
- if (!username || !email || !password) {
27
- return next(new AppError('Username, email, and password are required', 400));
28
- }
29
-
30
- if (username.length < 3 || username.length > 50) {
31
- return next(new AppError('Username must be between 3 and 50 characters', 400));
32
- }
33
-
34
- if (!validateEmail(email)) {
35
- return next(new AppError('Please provide a valid email address', 400));
36
- }
37
-
38
- if (password.length < 6) {
39
- return next(new AppError('Password must be at least 6 characters long', 400));
40
- }
41
-
42
- next();
43
- };
44
-
45
- module.exports = {
46
- validateLogin,
47
- validateRegister,
1
+ const { z } = require('zod');
2
+ const validator = require('validator');
3
+ const { AppError } = require('../utils/errors');
4
+
5
+ /**
6
+ * Generic Zod validation middleware.
7
+ * Pass a Zod schema for body, query, and/or params.
8
+ *
9
+ * Usage:
10
+ * router.post('/users', validate({ body: createUserSchema }), controller.create);
11
+ */
12
+ const validate = (schemas) => (req, res, next) => {
13
+ const errors = [];
14
+
15
+ for (const [source, schema] of Object.entries(schemas)) {
16
+ const result = schema.safeParse(req[source]);
17
+ if (!result.success) {
18
+ result.error.issues.forEach((issue) => {
19
+ errors.push(`${source}.${issue.path.join('.')}: ${issue.message}`);
20
+ });
21
+ } else {
22
+ req[source] = result.data; // replace with parsed/transformed data
23
+ }
24
+ }
25
+
26
+ if (errors.length > 0) {
27
+ return next(new AppError(errors.join('; '), 400));
28
+ }
29
+
30
+ next();
31
+ };
32
+
33
+ // ─── Auth Schemas ───────────────────────────────────
34
+
35
+ const registerSchema = z.object({
36
+ username: z
37
+ .string({ required_error: 'Username is required' })
38
+ .min(3, 'Username must be at least 3 characters')
39
+ .max(50, 'Username must be at most 50 characters')
40
+ .transform((val) => validator.escape(val.trim())),
41
+ email: z
42
+ .string({ required_error: 'Email is required' })
43
+ .email('Please provide a valid email address')
44
+ .transform((val) => validator.normalizeEmail(val) || val),
45
+ password: z
46
+ .string({ required_error: 'Password is required' })
47
+ .min(8, 'Password must be at least 8 characters')
48
+ .refine(
49
+ (val) =>
50
+ validator.isStrongPassword(val, {
51
+ minLength: 8,
52
+ minLowercase: 1,
53
+ minUppercase: 1,
54
+ minNumbers: 1,
55
+ minSymbols: 0,
56
+ }),
57
+ { message: 'Password must contain uppercase, lowercase, and a number' }
58
+ ),
59
+ });
60
+
61
+ const loginSchema = z.object({
62
+ email: z
63
+ .string({ required_error: 'Email is required' })
64
+ .email('Please provide a valid email address')
65
+ .transform((val) => validator.normalizeEmail(val) || val),
66
+ password: z.string({ required_error: 'Password is required' }).min(1),
67
+ });
68
+
69
+ const refreshSchema = z.object({
70
+ refreshToken: z.string({ required_error: 'Refresh token is required' }).min(1),
71
+ });
72
+
73
+ const forgotPasswordSchema = z.object({
74
+ email: z
75
+ .string({ required_error: 'Email is required' })
76
+ .email('Please provide a valid email address'),
77
+ });
78
+
79
+ const resetPasswordSchema = z.object({
80
+ token: z.string({ required_error: 'Reset token is required' }).min(1),
81
+ password: z
82
+ .string({ required_error: 'Password is required' })
83
+ .min(8, 'Password must be at least 8 characters')
84
+ .refine(
85
+ (val) =>
86
+ validator.isStrongPassword(val, {
87
+ minLength: 8,
88
+ minLowercase: 1,
89
+ minUppercase: 1,
90
+ minNumbers: 1,
91
+ minSymbols: 0,
92
+ }),
93
+ { message: 'Password must contain uppercase, lowercase, and a number' }
94
+ ),
95
+ });
96
+
97
+ // Backward-compatible named validators
98
+ const validateRegister = validate({ body: registerSchema });
99
+ const validateLogin = validate({ body: loginSchema });
100
+
101
+ module.exports = {
102
+ validate,
103
+ registerSchema,
104
+ loginSchema,
105
+ refreshSchema,
106
+ forgotPasswordSchema,
107
+ resetPasswordSchema,
108
+ validateRegister,
109
+ validateLogin,
48
110
  };