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.
- package/LICENSE +21 -0
- package/README.md +204 -259
- package/index.js +229 -113
- package/lib/cleanup.js +41 -129
- package/lib/features.js +239 -0
- package/lib/generator.js +286 -204
- package/lib/utils.js +43 -91
- package/package.json +81 -63
- package/templates/cicd/github-actions.yml.ejs +70 -0
- package/templates/config/database.mongo.js.ejs +29 -33
- package/templates/config/database.postgres.js.ejs +41 -40
- package/templates/config/database.prisma.js.ejs +26 -0
- package/templates/config/redis.js.ejs +28 -0
- package/templates/config/schema.prisma.ejs +20 -0
- package/templates/config/swagger.js.ejs +30 -0
- package/templates/config/websocket.js.ejs +62 -0
- package/templates/controllers/authController.js.ejs +152 -129
- package/templates/controllers/exampleController.js.ejs +92 -152
- package/templates/controllers/userController.js.ejs +52 -60
- package/templates/core/Dockerfile.ejs +41 -31
- package/templates/core/README.md.ejs +191 -179
- package/templates/core/app.js.ejs +114 -64
- package/templates/core/docker-compose.yml.ejs +59 -47
- package/templates/core/dockerignore.ejs +7 -0
- package/templates/core/env.ejs +25 -19
- package/templates/core/env.example.ejs +26 -0
- package/templates/core/eslintrc.json.ejs +50 -20
- package/templates/core/gitignore.ejs +51 -51
- package/templates/core/healthcheck.js.ejs +24 -24
- package/templates/core/jest.config.js.ejs +19 -22
- package/templates/core/package.json.ejs +70 -33
- package/templates/core/prettierrc.json.ejs +11 -11
- package/templates/core/server.js.ejs +64 -48
- package/templates/core/tsconfig.json.ejs +19 -0
- package/templates/middleware/auth.js.ejs +80 -66
- package/templates/middleware/cache.js.ejs +67 -0
- package/templates/middleware/errorHandler.js.ejs +50 -46
- package/templates/middleware/requestId.js.ejs +9 -0
- package/templates/middleware/validation.js.ejs +109 -47
- package/templates/migrations/create-users.js.ejs +50 -0
- package/templates/migrations/seed-users.js.ejs +34 -0
- package/templates/migrations/sequelizerc.ejs +8 -0
- package/templates/models/User.mongo.js.ejs +29 -29
- package/templates/models/User.postgres.js.ejs +40 -40
- package/templates/models/index.mongo.js.ejs +7 -7
- package/templates/models/index.postgres.js.ejs +11 -11
- package/templates/routes/authRoutes.js.ejs +222 -13
- package/templates/routes/exampleRoutes.js.ejs +100 -12
- package/templates/routes/index.js.ejs +34 -24
- package/templates/routes/userRoutes.js.ejs +78 -15
- package/templates/services/authService.js.ejs +111 -35
- package/templates/services/exampleService.js.ejs +112 -112
- package/templates/services/userService.mongodb.js.ejs +33 -33
- package/templates/services/userService.postgres.js.ejs +30 -30
- package/templates/services/userService.prisma.js.ejs +36 -0
- package/templates/tests/auth.test.js.ejs +83 -66
- package/templates/tests/example.test.js.ejs +109 -112
- package/templates/tests/setup.js.ejs +11 -11
- package/templates/tests/users.test.js.ejs +42 -42
- package/templates/utils/envValidator.js.ejs +23 -0
- package/templates/utils/errors.js.ejs +12 -12
- package/templates/utils/logger.js.ejs +37 -28
- package/templates/utils/response.js.ejs +28 -0
- package/templates/utils/validators.js.ejs +34 -34
- package/templates/config/swagger.json.ejs +0 -194
- package/templates/core/index.js.ejs +0 -24
|
@@ -1,49 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
error
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
error = new AppError(
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (err.name === '
|
|
31
|
-
const message = '
|
|
32
|
-
error = new AppError(message,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (err.name === '
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
@@ -1,48 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
return next(new AppError('
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
};
|