express-genix 2.0.0 → 3.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/README.md +20 -3
- package/index.js +22 -1
- package/lib/features.js +8 -0
- package/lib/generator.js +77 -2
- package/package.json +3 -3
- package/templates/config/queue.js.ejs +29 -0
- package/templates/config/schema.prisma.ejs +2 -0
- package/templates/controllers/adminController.js.ejs +109 -0
- package/templates/controllers/authController.js.ejs +9 -4
- package/templates/controllers/userController.js.ejs +1 -0
- package/templates/core/app.js.ejs +50 -1
- package/templates/core/env.ejs +17 -0
- package/templates/core/env.example.ejs +17 -0
- package/templates/core/package.json.ejs +8 -1
- package/templates/core/server.js.ejs +5 -2
- package/templates/graphql/resolvers.js.ejs +61 -0
- package/templates/graphql/typeDefs.js.ejs +53 -0
- package/templates/jobs/worker.js.ejs +60 -0
- package/templates/middleware/auditLog.js.ejs +62 -0
- package/templates/middleware/metrics.js.ejs +65 -0
- package/templates/middleware/rbac.js.ejs +86 -0
- package/templates/middleware/upload.js.ejs +50 -0
- package/templates/models/User.mongo.js.ejs +29 -0
- package/templates/models/User.postgres.js.ejs +7 -1
- package/templates/routes/adminRoutes.js.ejs +150 -0
- package/templates/routes/index.js.ejs +6 -0
- package/templates/routes/jobRoutes.js.ejs +85 -0
- package/templates/routes/uploadRoutes.js.ejs +100 -0
- package/templates/services/authService.js.ejs +1 -0
- package/templates/services/emailService.js.ejs +88 -0
- package/templates/services/userService.mongodb.js.ejs +32 -2
- package/templates/services/userService.postgres.js.ejs +33 -1
- package/templates/services/userService.prisma.js.ejs +50 -6
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<% if (hasAuth) { %>const bcrypt = require('bcryptjs');
|
|
2
|
+
const authService = require('../services/authService');
|
|
3
|
+
const userService = require('../services/userService');
|
|
4
|
+
<% } %>
|
|
5
|
+
const resolvers = {
|
|
6
|
+
Query: {
|
|
7
|
+
hello: () => 'Hello from GraphQL!',
|
|
8
|
+
<% if (hasAuth) { %>
|
|
9
|
+
me: async (_, __, { user }) => {
|
|
10
|
+
if (!user) throw new Error('Authentication required');
|
|
11
|
+
return userService.findById(user.userId);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
users: async (_, { page = 1, limit = 20 }, { user }) => {
|
|
15
|
+
if (!user || user.role !== 'admin') throw new Error('Admin access required');
|
|
16
|
+
return userService.findAll({ page, limit });
|
|
17
|
+
},
|
|
18
|
+
<% } %> },
|
|
19
|
+
<% if (hasAuth) { %>
|
|
20
|
+
Mutation: {
|
|
21
|
+
register: async (_, { input }) => {
|
|
22
|
+
const { username, email, password } = input;
|
|
23
|
+
|
|
24
|
+
const existing = await userService.findByEmail(email);
|
|
25
|
+
if (existing) throw new Error('User already exists with this email');
|
|
26
|
+
|
|
27
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
28
|
+
const user = await userService.create({ username, email, password: hashedPassword });
|
|
29
|
+
const tokens = authService.generateTokens(user);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
user: { id: user.id, username: user.username, email: user.email, role: user.role || 'user', createdAt: user.createdAt },
|
|
33
|
+
...tokens,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
login: async (_, { input }) => {
|
|
38
|
+
const { email, password } = input;
|
|
39
|
+
|
|
40
|
+
const user = await userService.findByEmail(email);
|
|
41
|
+
if (!user) throw new Error('Invalid credentials');
|
|
42
|
+
|
|
43
|
+
const valid = await bcrypt.compare(password, user.password);
|
|
44
|
+
if (!valid) throw new Error('Invalid credentials');
|
|
45
|
+
|
|
46
|
+
const tokens = authService.generateTokens(user);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
user: { id: user.id, username: user.username, email: user.email, role: user.role || 'user', createdAt: user.createdAt },
|
|
50
|
+
...tokens,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
updateProfile: async (_, { input }, { user }) => {
|
|
55
|
+
if (!user) throw new Error('Authentication required');
|
|
56
|
+
return userService.updateById(user.userId, input);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
<% } %>};
|
|
60
|
+
|
|
61
|
+
module.exports = resolvers;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const gql = require('graphql-tag');
|
|
2
|
+
|
|
3
|
+
const typeDefs = gql`
|
|
4
|
+
type Query {
|
|
5
|
+
hello: String
|
|
6
|
+
<% if (hasAuth) { %> me: User
|
|
7
|
+
users(page: Int, limit: Int): UserList
|
|
8
|
+
<% } %> }
|
|
9
|
+
|
|
10
|
+
<% if (hasAuth) { %> type Mutation {
|
|
11
|
+
register(input: RegisterInput!): AuthPayload
|
|
12
|
+
login(input: LoginInput!): AuthPayload
|
|
13
|
+
updateProfile(input: UpdateProfileInput!): User
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type User {
|
|
17
|
+
id: ID!
|
|
18
|
+
username: String!
|
|
19
|
+
email: String!
|
|
20
|
+
role: String!
|
|
21
|
+
createdAt: String!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type UserList {
|
|
25
|
+
users: [User!]!
|
|
26
|
+
total: Int!
|
|
27
|
+
page: Int!
|
|
28
|
+
totalPages: Int!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AuthPayload {
|
|
32
|
+
user: User!
|
|
33
|
+
accessToken: String!
|
|
34
|
+
refreshToken: String!
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
input RegisterInput {
|
|
38
|
+
username: String!
|
|
39
|
+
email: String!
|
|
40
|
+
password: String!
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
input LoginInput {
|
|
44
|
+
email: String!
|
|
45
|
+
password: String!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
input UpdateProfileInput {
|
|
49
|
+
username: String
|
|
50
|
+
}
|
|
51
|
+
<% } %>`;
|
|
52
|
+
|
|
53
|
+
module.exports = typeDefs;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { Worker } = require('bullmq');
|
|
2
|
+
const { redisConnection } = require('../config/queue');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
|
|
5
|
+
// Email job processor
|
|
6
|
+
const emailProcessor = async (job) => {
|
|
7
|
+
logger.info(`Processing email job: ${job.name} (${job.id})`);
|
|
8
|
+
|
|
9
|
+
switch (job.name) {
|
|
10
|
+
case 'send-welcome':
|
|
11
|
+
// TODO: Integrate with your email service
|
|
12
|
+
logger.info(`Sending welcome email to ${job.data.email}`);
|
|
13
|
+
break;
|
|
14
|
+
case 'send-reset':
|
|
15
|
+
logger.info(`Sending password reset email to ${job.data.email}`);
|
|
16
|
+
break;
|
|
17
|
+
default:
|
|
18
|
+
logger.warn(`Unknown email job: ${job.name}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Default job processor
|
|
23
|
+
const defaultProcessor = async (job) => {
|
|
24
|
+
logger.info(`Processing job: ${job.name} (${job.id})`, job.data);
|
|
25
|
+
|
|
26
|
+
switch (job.name) {
|
|
27
|
+
case 'cleanup':
|
|
28
|
+
logger.info('Running cleanup task...');
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
logger.warn(`Unknown job: ${job.name}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const startWorkers = () => {
|
|
36
|
+
const emailWorker = new Worker('email', emailProcessor, {
|
|
37
|
+
connection: redisConnection,
|
|
38
|
+
concurrency: 5,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const defaultWorker = new Worker('default', defaultProcessor, {
|
|
42
|
+
connection: redisConnection,
|
|
43
|
+
concurrency: 3,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
[emailWorker, defaultWorker].forEach((worker) => {
|
|
47
|
+
worker.on('completed', (job) => {
|
|
48
|
+
logger.info(`Job completed: ${job.name} (${job.id})`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
worker.on('failed', (job, err) => {
|
|
52
|
+
logger.error(`Job failed: ${job.name} (${job.id}) — ${err.message}`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
logger.info('Background workers started');
|
|
57
|
+
return { emailWorker, defaultWorker };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = { startWorkers };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Audit Logging Middleware
|
|
5
|
+
*
|
|
6
|
+
* Tracks API requests with user context, action, and resource details.
|
|
7
|
+
* Logs are written via the configured logger (Winston/Pino).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* router.post('/users', authenticateToken, auditLog('CREATE_USER'), handler);
|
|
11
|
+
* app.use(auditLog()); // log all requests
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const auditLog = (action) => (req, res, next) => {
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
|
|
17
|
+
// Capture response on finish
|
|
18
|
+
const originalEnd = res.end;
|
|
19
|
+
res.end = function (...args) {
|
|
20
|
+
const duration = Date.now() - startTime;
|
|
21
|
+
|
|
22
|
+
const auditEntry = {
|
|
23
|
+
type: 'AUDIT',
|
|
24
|
+
action: action || `${req.method} ${req.route ? req.route.path : req.path}`,
|
|
25
|
+
method: req.method,
|
|
26
|
+
path: req.originalUrl,
|
|
27
|
+
statusCode: res.statusCode,
|
|
28
|
+
duration: `${duration}ms`,
|
|
29
|
+
ip: req.ip,
|
|
30
|
+
userAgent: req.get('user-agent'),
|
|
31
|
+
userId: req.user ? req.user.userId : 'anonymous',
|
|
32
|
+
username: req.user ? req.user.username : 'anonymous',
|
|
33
|
+
requestId: req.id || req.headers['x-request-id'] || null,
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Don't log request/response bodies in production for security
|
|
38
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
39
|
+
if (req.body && Object.keys(req.body).length > 0) {
|
|
40
|
+
// Redact sensitive fields
|
|
41
|
+
const safeBody = { ...req.body };
|
|
42
|
+
const sensitiveFields = ['password', 'token', 'refreshToken', 'secret', 'creditCard'];
|
|
43
|
+
sensitiveFields.forEach((field) => {
|
|
44
|
+
if (safeBody[field]) safeBody[field] = '[REDACTED]';
|
|
45
|
+
});
|
|
46
|
+
auditEntry.requestBody = safeBody;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (res.statusCode >= 400) {
|
|
51
|
+
logger.warn(auditEntry);
|
|
52
|
+
} else {
|
|
53
|
+
logger.info(auditEntry);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
originalEnd.apply(res, args);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
next();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
module.exports = { auditLog };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const client = require('prom-client');
|
|
2
|
+
|
|
3
|
+
// Create a Registry
|
|
4
|
+
const register = new client.Registry();
|
|
5
|
+
|
|
6
|
+
// Add default metrics (event loop lag, heap size, etc.)
|
|
7
|
+
client.collectDefaultMetrics({ register });
|
|
8
|
+
|
|
9
|
+
// Custom metrics
|
|
10
|
+
const httpRequestDuration = new client.Histogram({
|
|
11
|
+
name: 'http_request_duration_seconds',
|
|
12
|
+
help: 'Duration of HTTP requests in seconds',
|
|
13
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
14
|
+
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
15
|
+
});
|
|
16
|
+
register.registerMetric(httpRequestDuration);
|
|
17
|
+
|
|
18
|
+
const httpRequestTotal = new client.Counter({
|
|
19
|
+
name: 'http_requests_total',
|
|
20
|
+
help: 'Total number of HTTP requests',
|
|
21
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
22
|
+
});
|
|
23
|
+
register.registerMetric(httpRequestTotal);
|
|
24
|
+
|
|
25
|
+
const activeConnections = new client.Gauge({
|
|
26
|
+
name: 'http_active_connections',
|
|
27
|
+
help: 'Number of active HTTP connections',
|
|
28
|
+
});
|
|
29
|
+
register.registerMetric(activeConnections);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Metrics middleware — tracks request duration, count, and active connections.
|
|
33
|
+
*/
|
|
34
|
+
const metricsMiddleware = (req, res, next) => {
|
|
35
|
+
// Skip metrics endpoint itself
|
|
36
|
+
if (req.path === '/metrics') return next();
|
|
37
|
+
|
|
38
|
+
activeConnections.inc();
|
|
39
|
+
const end = httpRequestDuration.startTimer();
|
|
40
|
+
|
|
41
|
+
res.on('finish', () => {
|
|
42
|
+
const route = req.route ? req.route.path : req.path;
|
|
43
|
+
const labels = {
|
|
44
|
+
method: req.method,
|
|
45
|
+
route,
|
|
46
|
+
status_code: res.statusCode,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
end(labels);
|
|
50
|
+
httpRequestTotal.inc(labels);
|
|
51
|
+
activeConnections.dec();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
next();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* GET /metrics handler — returns Prometheus-formatted metrics.
|
|
59
|
+
*/
|
|
60
|
+
const metricsEndpoint = async (req, res) => {
|
|
61
|
+
res.set('Content-Type', register.contentType);
|
|
62
|
+
res.end(await register.metrics());
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports = { metricsMiddleware, metricsEndpoint, register };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { AppError } = require('../utils/errors');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Role-Based Access Control (RBAC) Middleware
|
|
5
|
+
*
|
|
6
|
+
* Roles hierarchy: admin > moderator > user
|
|
7
|
+
* Usage:
|
|
8
|
+
* router.get('/admin', authenticateToken, requireRole('admin'), handler);
|
|
9
|
+
* router.put('/posts/:id', authenticateToken, requirePermission('posts:write'), handler);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ROLES = {
|
|
13
|
+
admin: { level: 3, permissions: ['*'] },
|
|
14
|
+
moderator: { level: 2, permissions: [
|
|
15
|
+
'users:read', 'users:list',
|
|
16
|
+
'posts:read', 'posts:write', 'posts:delete',
|
|
17
|
+
'comments:read', 'comments:write', 'comments:delete',
|
|
18
|
+
]},
|
|
19
|
+
user: { level: 1, permissions: [
|
|
20
|
+
'users:read',
|
|
21
|
+
'posts:read', 'posts:write',
|
|
22
|
+
'comments:read', 'comments:write',
|
|
23
|
+
]},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const hasPermission = (role, permission) => {
|
|
27
|
+
const config = ROLES[role];
|
|
28
|
+
if (!config) return false;
|
|
29
|
+
if (config.permissions.includes('*')) return true;
|
|
30
|
+
return config.permissions.includes(permission);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Require a minimum role level.
|
|
35
|
+
* @param {...string} roles - Allowed roles (e.g., 'admin', 'moderator')
|
|
36
|
+
*/
|
|
37
|
+
const requireRole = (...roles) => (req, res, next) => {
|
|
38
|
+
if (!req.user) {
|
|
39
|
+
return next(new AppError('Authentication required', 401));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const userRole = req.user.role || 'user';
|
|
43
|
+
if (!roles.includes(userRole)) {
|
|
44
|
+
return next(new AppError(`Role '${userRole}' is not authorized. Required: ${roles.join(' or ')}`, 403));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
next();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Require a specific permission.
|
|
52
|
+
* @param {string} permission - e.g., 'users:write', 'posts:delete'
|
|
53
|
+
*/
|
|
54
|
+
const requirePermission = (permission) => (req, res, next) => {
|
|
55
|
+
if (!req.user) {
|
|
56
|
+
return next(new AppError('Authentication required', 401));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const userRole = req.user.role || 'user';
|
|
60
|
+
if (!hasPermission(userRole, permission)) {
|
|
61
|
+
return next(new AppError(`Permission '${permission}' denied for role '${userRole}'`, 403));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
next();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Require ownership OR a specific role.
|
|
69
|
+
* Checks req.params[paramName] === req.user.userId, or falls back to role check.
|
|
70
|
+
*/
|
|
71
|
+
const requireOwnerOrRole = (paramName = 'id', ...roles) => (req, res, next) => {
|
|
72
|
+
if (!req.user) {
|
|
73
|
+
return next(new AppError('Authentication required', 401));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isOwner = req.params[paramName] === req.user.userId;
|
|
77
|
+
const userRole = req.user.role || 'user';
|
|
78
|
+
|
|
79
|
+
if (isOwner || roles.includes(userRole)) {
|
|
80
|
+
return next();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return next(new AppError('Not authorized to access this resource', 403));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = { requireRole, requirePermission, requireOwnerOrRole, hasPermission, ROLES };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const multer = require('multer');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { AppError } = require('../utils/errors');
|
|
5
|
+
|
|
6
|
+
// Allowed MIME types
|
|
7
|
+
const ALLOWED_TYPES = {
|
|
8
|
+
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
9
|
+
document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|
10
|
+
any: null, // allow all
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Storage configuration
|
|
14
|
+
const storage = multer.diskStorage({
|
|
15
|
+
destination: (req, file, cb) => {
|
|
16
|
+
cb(null, path.join(process.cwd(), process.env.UPLOAD_DIR || 'uploads'));
|
|
17
|
+
},
|
|
18
|
+
filename: (req, file, cb) => {
|
|
19
|
+
const uniqueSuffix = crypto.randomBytes(16).toString('hex');
|
|
20
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
21
|
+
cb(null, `${uniqueSuffix}${ext}`);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const createUpload = (options = {}) => {
|
|
26
|
+
const {
|
|
27
|
+
maxSize = parseInt(process.env.UPLOAD_MAX_SIZE, 10) || 5 * 1024 * 1024, // 5MB
|
|
28
|
+
allowedTypes = 'image',
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
const allowedMimes = ALLOWED_TYPES[allowedTypes];
|
|
32
|
+
|
|
33
|
+
return multer({
|
|
34
|
+
storage,
|
|
35
|
+
limits: { fileSize: maxSize },
|
|
36
|
+
fileFilter: (req, file, cb) => {
|
|
37
|
+
if (allowedMimes && !allowedMimes.includes(file.mimetype)) {
|
|
38
|
+
return cb(new AppError(`File type '${file.mimetype}' not allowed. Allowed: ${allowedMimes.join(', ')}`, 400));
|
|
39
|
+
}
|
|
40
|
+
cb(null, true);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Pre-configured upload instances
|
|
46
|
+
const uploadImage = createUpload({ allowedTypes: 'image' });
|
|
47
|
+
const uploadDocument = createUpload({ allowedTypes: 'document', maxSize: 10 * 1024 * 1024 });
|
|
48
|
+
const uploadAny = createUpload({ allowedTypes: 'any', maxSize: 10 * 1024 * 1024 });
|
|
49
|
+
|
|
50
|
+
module.exports = { createUpload, uploadImage, uploadDocument, uploadAny };
|
|
@@ -22,9 +22,38 @@ const userSchema = new mongoose.Schema({
|
|
|
22
22
|
required: [true, 'Password is required'],
|
|
23
23
|
minlength: [8, 'Password must be at least 8 characters long'],
|
|
24
24
|
},
|
|
25
|
+
role: {
|
|
26
|
+
type: String,
|
|
27
|
+
enum: ['user', 'moderator', 'admin'],
|
|
28
|
+
default: 'user',
|
|
29
|
+
},<% if (hasSoftDelete) { %>
|
|
30
|
+
deletedAt: {
|
|
31
|
+
type: Date,
|
|
32
|
+
default: null,
|
|
33
|
+
index: true,
|
|
34
|
+
},<% } %>
|
|
25
35
|
}, {
|
|
26
36
|
timestamps: true,
|
|
27
37
|
});
|
|
38
|
+
<% if (hasSoftDelete) { %>
|
|
39
|
+
// Soft delete: exclude deleted documents by default
|
|
40
|
+
userSchema.pre(/^find/, function (next) {
|
|
41
|
+
if (!this.getQuery().includeDeleted) {
|
|
42
|
+
this.where({ deletedAt: null });
|
|
43
|
+
} else {
|
|
44
|
+
delete this.getQuery().includeDeleted;
|
|
45
|
+
}
|
|
46
|
+
next();
|
|
47
|
+
});
|
|
28
48
|
|
|
49
|
+
userSchema.methods.softDelete = function () {
|
|
50
|
+
this.deletedAt = new Date();
|
|
51
|
+
return this.save();
|
|
52
|
+
};
|
|
29
53
|
|
|
54
|
+
userSchema.methods.restore = function () {
|
|
55
|
+
this.deletedAt = null;
|
|
56
|
+
return this.save();
|
|
57
|
+
};
|
|
58
|
+
<% } %>
|
|
30
59
|
module.exports = mongoose.model('User', userSchema);
|
|
@@ -33,8 +33,14 @@ const User = sequelize.define('User', {
|
|
|
33
33
|
notEmpty: true,
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
+
role: {
|
|
37
|
+
type: DataTypes.ENUM('user', 'moderator', 'admin'),
|
|
38
|
+
defaultValue: 'user',
|
|
39
|
+
allowNull: false,
|
|
40
|
+
},
|
|
36
41
|
}, {
|
|
37
|
-
timestamps: true
|
|
42
|
+
timestamps: true,<% if (hasSoftDelete) { %>
|
|
43
|
+
paranoid: true, // Enables soft deletes (adds deletedAt column)<% } %>
|
|
38
44
|
tableName: 'users',
|
|
39
45
|
});
|
|
40
46
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const adminController = require('../controllers/adminController');
|
|
3
|
+
const { authenticateToken } = require('../middleware/auth');
|
|
4
|
+
const { requireRole } = require('../middleware/rbac');
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @swagger
|
|
10
|
+
* /admin/users:
|
|
11
|
+
* get:
|
|
12
|
+
* summary: List all users (admin only)
|
|
13
|
+
* tags: [Admin]
|
|
14
|
+
* security:
|
|
15
|
+
* - bearerAuth: []
|
|
16
|
+
* parameters:
|
|
17
|
+
* - in: query
|
|
18
|
+
* name: page
|
|
19
|
+
* schema:
|
|
20
|
+
* type: integer
|
|
21
|
+
* default: 1
|
|
22
|
+
* - in: query
|
|
23
|
+
* name: limit
|
|
24
|
+
* schema:
|
|
25
|
+
* type: integer
|
|
26
|
+
* default: 20
|
|
27
|
+
* responses:
|
|
28
|
+
* 200:
|
|
29
|
+
* description: List of users
|
|
30
|
+
* 403:
|
|
31
|
+
* description: Forbidden — admin role required
|
|
32
|
+
*/
|
|
33
|
+
router.get('/users', authenticateToken, requireRole('admin'), adminController.listUsers);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @swagger
|
|
37
|
+
* /admin/users/{id}:
|
|
38
|
+
* get:
|
|
39
|
+
* summary: Get user by ID (admin only)
|
|
40
|
+
* tags: [Admin]
|
|
41
|
+
* security:
|
|
42
|
+
* - bearerAuth: []
|
|
43
|
+
* parameters:
|
|
44
|
+
* - in: path
|
|
45
|
+
* name: id
|
|
46
|
+
* required: true
|
|
47
|
+
* schema:
|
|
48
|
+
* type: string
|
|
49
|
+
* responses:
|
|
50
|
+
* 200:
|
|
51
|
+
* description: User details
|
|
52
|
+
* 404:
|
|
53
|
+
* description: User not found
|
|
54
|
+
*/
|
|
55
|
+
router.get('/users/:id', authenticateToken, requireRole('admin', 'moderator'), adminController.getUserById);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @swagger
|
|
59
|
+
* /admin/users/{id}/role:
|
|
60
|
+
* patch:
|
|
61
|
+
* summary: Update user role (admin only)
|
|
62
|
+
* tags: [Admin]
|
|
63
|
+
* security:
|
|
64
|
+
* - bearerAuth: []
|
|
65
|
+
* parameters:
|
|
66
|
+
* - in: path
|
|
67
|
+
* name: id
|
|
68
|
+
* required: true
|
|
69
|
+
* schema:
|
|
70
|
+
* type: string
|
|
71
|
+
* requestBody:
|
|
72
|
+
* required: true
|
|
73
|
+
* content:
|
|
74
|
+
* application/json:
|
|
75
|
+
* schema:
|
|
76
|
+
* type: object
|
|
77
|
+
* required:
|
|
78
|
+
* - role
|
|
79
|
+
* properties:
|
|
80
|
+
* role:
|
|
81
|
+
* type: string
|
|
82
|
+
* enum: [user, moderator, admin]
|
|
83
|
+
* example: moderator
|
|
84
|
+
* responses:
|
|
85
|
+
* 200:
|
|
86
|
+
* description: Role updated
|
|
87
|
+
* 400:
|
|
88
|
+
* description: Invalid role
|
|
89
|
+
*/
|
|
90
|
+
router.patch('/users/:id/role', authenticateToken, requireRole('admin'), adminController.updateUserRole);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @swagger
|
|
94
|
+
* /admin/users/{id}:
|
|
95
|
+
* delete:
|
|
96
|
+
* summary: Delete a user (admin only)
|
|
97
|
+
* tags: [Admin]
|
|
98
|
+
* security:
|
|
99
|
+
* - bearerAuth: []
|
|
100
|
+
* parameters:
|
|
101
|
+
* - in: path
|
|
102
|
+
* name: id
|
|
103
|
+
* required: true
|
|
104
|
+
* schema:
|
|
105
|
+
* type: string
|
|
106
|
+
* responses:
|
|
107
|
+
* 200:
|
|
108
|
+
* description: User deleted
|
|
109
|
+
* 400:
|
|
110
|
+
* description: Cannot delete own account
|
|
111
|
+
*/
|
|
112
|
+
router.delete('/users/:id', authenticateToken, requireRole('admin'), adminController.deleteUser);
|
|
113
|
+
<% if (hasSoftDelete) { %>
|
|
114
|
+
/**
|
|
115
|
+
* @swagger
|
|
116
|
+
* /admin/users/{id}/restore:
|
|
117
|
+
* post:
|
|
118
|
+
* summary: Restore a soft-deleted user (admin only)
|
|
119
|
+
* tags: [Admin]
|
|
120
|
+
* security:
|
|
121
|
+
* - bearerAuth: []
|
|
122
|
+
* parameters:
|
|
123
|
+
* - in: path
|
|
124
|
+
* name: id
|
|
125
|
+
* required: true
|
|
126
|
+
* schema:
|
|
127
|
+
* type: string
|
|
128
|
+
* responses:
|
|
129
|
+
* 200:
|
|
130
|
+
* description: User restored
|
|
131
|
+
* 404:
|
|
132
|
+
* description: User not found or not deleted
|
|
133
|
+
*/
|
|
134
|
+
router.post('/users/:id/restore', authenticateToken, requireRole('admin'), adminController.restoreUser);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @swagger
|
|
138
|
+
* /admin/users/deleted:
|
|
139
|
+
* get:
|
|
140
|
+
* summary: List soft-deleted users (admin only)
|
|
141
|
+
* tags: [Admin]
|
|
142
|
+
* security:
|
|
143
|
+
* - bearerAuth: []
|
|
144
|
+
* responses:
|
|
145
|
+
* 200:
|
|
146
|
+
* description: List of deleted users
|
|
147
|
+
*/
|
|
148
|
+
router.get('/users/deleted', authenticateToken, requireRole('admin'), adminController.listDeletedUsers);
|
|
149
|
+
<% } %>
|
|
150
|
+
module.exports = router;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
<% if (hasAuth) { %>const authRoutes = require('./authRoutes');
|
|
3
3
|
const userRoutes = require('./userRoutes');
|
|
4
|
+
const adminRoutes = require('./adminRoutes');
|
|
4
5
|
<% } else { %>const exampleRoutes = require('./exampleRoutes');
|
|
6
|
+
<% } %><% if (hasFileUpload) { %>const uploadRoutes = require('./uploadRoutes');
|
|
7
|
+
<% } %><% if (hasBackgroundJobs) { %>const jobRoutes = require('./jobRoutes');
|
|
5
8
|
<% } %>
|
|
6
9
|
|
|
7
10
|
const router = express.Router();
|
|
8
11
|
|
|
9
12
|
<% if (hasAuth) { %>router.use('/auth', authRoutes);
|
|
10
13
|
router.use('/users', userRoutes);
|
|
14
|
+
router.use('/admin', adminRoutes);
|
|
11
15
|
<% } else { %>router.use('/examples', exampleRoutes);
|
|
16
|
+
<% } %><% if (hasFileUpload) { %>router.use('/uploads', uploadRoutes);
|
|
17
|
+
<% } %><% if (hasBackgroundJobs) { %>router.use('/jobs', jobRoutes);
|
|
12
18
|
<% } %>
|
|
13
19
|
|
|
14
20
|
/**
|