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
package/README.md
CHANGED
|
@@ -15,16 +15,31 @@ A production-grade CLI tool that generates Express.js applications with best-in-
|
|
|
15
15
|
|
|
16
16
|
**Security & Auth**
|
|
17
17
|
- JWT access + refresh tokens with token blacklist logout
|
|
18
|
+
- Role-Based Access Control (RBAC) — admin, moderator, user roles with permission system
|
|
19
|
+
- Admin panel routes for user management (list, view, update roles, delete)
|
|
20
|
+
- Password reset flow (forgot-password / reset-password with crypto tokens)
|
|
18
21
|
- Optional Redis-backed blacklist for production multi-instance deployments
|
|
22
|
+
- Zod request validation with pre-built schemas (register, login, reset, etc.)
|
|
19
23
|
- bcrypt password hashing, input sanitization (`validator`)
|
|
20
24
|
- Helmet, CORS, environment validation on startup
|
|
21
25
|
- Auto-generated cryptographically secure JWT secrets
|
|
26
|
+
- Soft deletes with restore capability
|
|
22
27
|
|
|
23
28
|
**API & Documentation**
|
|
24
|
-
- Swagger UI + swagger-jsdoc annotation-based docs
|
|
29
|
+
- Swagger UI + swagger-jsdoc annotation-based docs with example request/response bodies
|
|
30
|
+
- GraphQL (Apollo Server) with type definitions and resolvers
|
|
25
31
|
- Consistent `{ success, data, meta }` response envelope
|
|
32
|
+
- API versioning (`/api/v1/` prefix)
|
|
26
33
|
- Paginated list endpoints
|
|
27
34
|
- Request ID / correlation tracking
|
|
35
|
+
- Response caching middleware (Redis-backed, configurable TTL)
|
|
36
|
+
|
|
37
|
+
**Services & Infrastructure**
|
|
38
|
+
- Email service (Nodemailer) with welcome and password reset emails
|
|
39
|
+
- File uploads (Multer) with type filtering and size limits
|
|
40
|
+
- Background jobs (BullMQ) with Redis-backed queues and workers
|
|
41
|
+
- Prometheus metrics (`/metrics` endpoint with prom-client)
|
|
42
|
+
- Audit logging middleware with request tracking and sensitive field redaction
|
|
28
43
|
|
|
29
44
|
**Developer Experience**
|
|
30
45
|
- Interactive prompts — pick language, database, features via checkbox
|
|
@@ -68,7 +83,7 @@ You'll be prompted for:
|
|
|
68
83
|
1. **Project name**
|
|
69
84
|
2. **Language** — JavaScript or TypeScript
|
|
70
85
|
3. **Database** — MongoDB, PostgreSQL (Sequelize), PostgreSQL (Prisma), or None
|
|
71
|
-
4. **Features** — Auth, Rate Limiting, Swagger, Redis
|
|
86
|
+
4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL
|
|
72
87
|
5. **Logger** — Winston or Pino
|
|
73
88
|
|
|
74
89
|
The CLI generates your project, installs dependencies, formats code, and creates an initial git commit.
|
|
@@ -130,6 +145,8 @@ my-express-app/
|
|
|
130
145
|
| POST | `/api/auth/login` | Login (returns access + refresh tokens) |
|
|
131
146
|
| POST | `/api/auth/refresh` | Refresh access token |
|
|
132
147
|
| POST | `/api/auth/logout` | Logout (blacklists token) |
|
|
148
|
+
| POST | `/api/auth/forgot-password` | Request password reset email |
|
|
149
|
+
| POST | `/api/auth/reset-password` | Reset password with token |
|
|
133
150
|
|
|
134
151
|
### Users (protected)
|
|
135
152
|
| Method | Endpoint | Description |
|
|
@@ -141,7 +158,7 @@ my-express-app/
|
|
|
141
158
|
### Health
|
|
142
159
|
| Method | Endpoint | Description |
|
|
143
160
|
|--------|----------|-------------|
|
|
144
|
-
| GET | `/health` | Health check with uptime
|
|
161
|
+
| GET | `/health` | Health check with uptime, DB status, Redis status, memory usage |
|
|
145
162
|
|
|
146
163
|
## Available Scripts
|
|
147
164
|
|
package/index.js
CHANGED
|
@@ -75,6 +75,14 @@ async function main() {
|
|
|
75
75
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
76
76
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
77
77
|
{ name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
|
|
78
|
+
{ name: 'Email Service (Nodemailer)', value: 'email' },
|
|
79
|
+
{ name: 'File Uploads (Multer)', value: 'fileUpload' },
|
|
80
|
+
{ name: 'Soft Deletes', value: 'softDelete' },
|
|
81
|
+
{ name: 'Audit Logging', value: 'auditLog' },
|
|
82
|
+
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
83
|
+
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
84
|
+
{ name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
|
|
85
|
+
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
78
86
|
],
|
|
79
87
|
when: (ans) => ans.db !== 'none',
|
|
80
88
|
},
|
|
@@ -89,6 +97,11 @@ async function main() {
|
|
|
89
97
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
90
98
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
91
99
|
{ name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
|
|
100
|
+
{ name: 'File Uploads (Multer)', value: 'fileUpload' },
|
|
101
|
+
{ name: 'Audit Logging', value: 'auditLog' },
|
|
102
|
+
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
103
|
+
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
104
|
+
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
92
105
|
],
|
|
93
106
|
when: (ans) => ans.db === 'none',
|
|
94
107
|
},
|
|
@@ -121,6 +134,14 @@ async function main() {
|
|
|
121
134
|
hasWebsocket: features.includes('websocket'),
|
|
122
135
|
hasRequestId: features.includes('requestId'),
|
|
123
136
|
hasRedis: features.includes('redis') && features.includes('auth') && answers.db !== 'none',
|
|
137
|
+
hasEmail: features.includes('email') && answers.db !== 'none',
|
|
138
|
+
hasFileUpload: features.includes('fileUpload'),
|
|
139
|
+
hasSoftDelete: features.includes('softDelete') && answers.db !== 'none',
|
|
140
|
+
hasAuditLog: features.includes('auditLog'),
|
|
141
|
+
hasMetrics: features.includes('metrics'),
|
|
142
|
+
hasApiVersioning: features.includes('apiVersioning'),
|
|
143
|
+
hasBackgroundJobs: features.includes('backgroundJobs'),
|
|
144
|
+
hasGraphQL: features.includes('graphql'),
|
|
124
145
|
jwtSecret: generateSecret(),
|
|
125
146
|
jwtRefreshSecret: generateSecret(),
|
|
126
147
|
};
|
|
@@ -163,7 +184,7 @@ async function main() {
|
|
|
163
184
|
To get started:
|
|
164
185
|
cd ${config.projectName}
|
|
165
186
|
npm run dev
|
|
166
|
-
${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}
|
|
187
|
+
${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}
|
|
167
188
|
Health Check: http://localhost:3000/health
|
|
168
189
|
|
|
169
190
|
Available scripts:
|
package/lib/features.js
CHANGED
|
@@ -232,6 +232,14 @@ const inferConfig = (projectDir, pkg) => {
|
|
|
232
232
|
hasWebsocket: !!(pkg.dependencies && pkg.dependencies['socket.io']),
|
|
233
233
|
hasRedis: !!(pkg.dependencies && pkg.dependencies.ioredis),
|
|
234
234
|
hasRequestId: !!(pkg.dependencies && pkg.dependencies.uuid),
|
|
235
|
+
hasEmail: !!(pkg.dependencies && pkg.dependencies.nodemailer),
|
|
236
|
+
hasFileUpload: !!(pkg.dependencies && pkg.dependencies.multer),
|
|
237
|
+
hasSoftDelete: false,
|
|
238
|
+
hasAuditLog: false,
|
|
239
|
+
hasMetrics: !!(pkg.dependencies && pkg.dependencies['prom-client']),
|
|
240
|
+
hasApiVersioning: false,
|
|
241
|
+
hasBackgroundJobs: !!(pkg.dependencies && pkg.dependencies.bullmq),
|
|
242
|
+
hasGraphQL: !!(pkg.dependencies && pkg.dependencies['@apollo/server']),
|
|
235
243
|
logger: pkg.dependencies && pkg.dependencies.pino ? 'pino' : 'winston',
|
|
236
244
|
};
|
|
237
245
|
};
|
package/lib/generator.js
CHANGED
|
@@ -49,6 +49,18 @@ const createDirectoryStructure = (projectDir, config) => {
|
|
|
49
49
|
dirs.push('.github', '.github/workflows');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (config.hasFileUpload) {
|
|
53
|
+
dirs.push('uploads');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.hasBackgroundJobs) {
|
|
57
|
+
dirs.push('src/jobs');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.hasGraphQL) {
|
|
61
|
+
dirs.push('src/graphql');
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
fs.mkdirSync(projectDir);
|
|
53
65
|
dirs.forEach((dir) => {
|
|
54
66
|
fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
|
|
@@ -128,6 +140,10 @@ const generateFiles = async (config, projectDir) => {
|
|
|
128
140
|
configFiles.push({ template: 'config/redis.js.ejs', output: `src/config/redis.${ext}` });
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
if (config.hasBackgroundJobs) {
|
|
144
|
+
configFiles.push({ template: 'config/queue.js.ejs', output: `src/config/queue.${ext}` });
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
// Route files
|
|
132
148
|
const routeFiles = [
|
|
133
149
|
{ template: 'routes/index.js.ejs', output: `src/routes/index.${ext}` },
|
|
@@ -141,11 +157,13 @@ const generateFiles = async (config, projectDir) => {
|
|
|
141
157
|
if (config.hasAuth) {
|
|
142
158
|
controllerFiles.push(
|
|
143
159
|
{ template: 'controllers/authController.js.ejs', output: `src/controllers/authController.${ext}` },
|
|
144
|
-
{ template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` }
|
|
160
|
+
{ template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` },
|
|
161
|
+
{ template: 'controllers/adminController.js.ejs', output: `src/controllers/adminController.${ext}` }
|
|
145
162
|
);
|
|
146
163
|
routeFiles.push(
|
|
147
164
|
{ template: 'routes/authRoutes.js.ejs', output: `src/routes/authRoutes.${ext}` },
|
|
148
|
-
{ template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` }
|
|
165
|
+
{ template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` },
|
|
166
|
+
{ template: 'routes/adminRoutes.js.ejs', output: `src/routes/adminRoutes.${ext}` }
|
|
149
167
|
);
|
|
150
168
|
serviceFiles.push(
|
|
151
169
|
{ template: 'services/authService.js.ejs', output: `src/services/authService.${ext}` }
|
|
@@ -178,6 +196,24 @@ const generateFiles = async (config, projectDir) => {
|
|
|
178
196
|
);
|
|
179
197
|
}
|
|
180
198
|
|
|
199
|
+
if (config.hasEmail) {
|
|
200
|
+
serviceFiles.push(
|
|
201
|
+
{ template: 'services/emailService.js.ejs', output: `src/services/emailService.${ext}` }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (config.hasFileUpload) {
|
|
206
|
+
routeFiles.push(
|
|
207
|
+
{ template: 'routes/uploadRoutes.js.ejs', output: `src/routes/uploadRoutes.${ext}` }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (config.hasBackgroundJobs) {
|
|
212
|
+
routeFiles.push(
|
|
213
|
+
{ template: 'routes/jobRoutes.js.ejs', output: `src/routes/jobRoutes.${ext}` }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
181
217
|
// Middleware files
|
|
182
218
|
const middlewareFiles = [
|
|
183
219
|
{ template: 'middleware/errorHandler.js.ejs', output: `src/middleware/errorHandler.${ext}` },
|
|
@@ -196,6 +232,30 @@ const generateFiles = async (config, projectDir) => {
|
|
|
196
232
|
);
|
|
197
233
|
}
|
|
198
234
|
|
|
235
|
+
if (config.hasAuth) {
|
|
236
|
+
middlewareFiles.push(
|
|
237
|
+
{ template: 'middleware/rbac.js.ejs', output: `src/middleware/rbac.${ext}` }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (config.hasFileUpload) {
|
|
242
|
+
middlewareFiles.push(
|
|
243
|
+
{ template: 'middleware/upload.js.ejs', output: `src/middleware/upload.${ext}` }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (config.hasAuditLog) {
|
|
248
|
+
middlewareFiles.push(
|
|
249
|
+
{ template: 'middleware/auditLog.js.ejs', output: `src/middleware/auditLog.${ext}` }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (config.hasMetrics) {
|
|
254
|
+
middlewareFiles.push(
|
|
255
|
+
{ template: 'middleware/metrics.js.ejs', output: `src/middleware/metrics.${ext}` }
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
199
259
|
if (config.hasRedis) {
|
|
200
260
|
middlewareFiles.push(
|
|
201
261
|
{ template: 'middleware/cache.js.ejs', output: `src/middleware/cache.${ext}` }
|
|
@@ -245,6 +305,21 @@ const generateFiles = async (config, projectDir) => {
|
|
|
245
305
|
...testFiles,
|
|
246
306
|
];
|
|
247
307
|
|
|
308
|
+
// GraphQL files
|
|
309
|
+
if (config.hasGraphQL) {
|
|
310
|
+
allFiles.push(
|
|
311
|
+
{ template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
|
|
312
|
+
{ template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Background job worker
|
|
317
|
+
if (config.hasBackgroundJobs) {
|
|
318
|
+
allFiles.push(
|
|
319
|
+
{ template: 'jobs/worker.js.ejs', output: `src/jobs/worker.${ext}` }
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
248
323
|
const errors = [];
|
|
249
324
|
|
|
250
325
|
for (const file of allFiles) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-genix",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Production-grade CLI to generate Express apps with JWT, TypeScript, Prisma, MongoDB, PostgreSQL,
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"express-genix": "
|
|
7
|
+
"express-genix": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node index.js",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { Queue } = require('bullmq');
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
const redisConnection = {
|
|
5
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
6
|
+
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Define your queues here
|
|
10
|
+
const emailQueue = new Queue('email', { connection: redisConnection });
|
|
11
|
+
const defaultQueue = new Queue('default', { connection: redisConnection });
|
|
12
|
+
|
|
13
|
+
const addJob = async (queueName, jobName, data, options = {}) => {
|
|
14
|
+
const queues = { email: emailQueue, default: defaultQueue };
|
|
15
|
+
const queue = queues[queueName] || defaultQueue;
|
|
16
|
+
|
|
17
|
+
const job = await queue.add(jobName, data, {
|
|
18
|
+
attempts: 3,
|
|
19
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
20
|
+
removeOnComplete: { count: 100 },
|
|
21
|
+
removeOnFail: { count: 500 },
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
logger.info(`Job added: ${jobName} (${job.id}) to queue ${queueName}`);
|
|
26
|
+
return job;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = { emailQueue, defaultQueue, addJob, redisConnection };
|
|
@@ -12,8 +12,10 @@ model User {
|
|
|
12
12
|
username String @unique @db.VarChar(50)
|
|
13
13
|
email String @unique
|
|
14
14
|
password String
|
|
15
|
+
role String @default("user")
|
|
15
16
|
createdAt DateTime @default(now()) @map("created_at")
|
|
16
17
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
18
|
+
<% if (hasSoftDelete) { %> deletedAt DateTime? @map("deleted_at")<% } %>
|
|
17
19
|
|
|
18
20
|
@@map("users")
|
|
19
21
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const userService = require('../services/userService');
|
|
2
|
+
const { AppError } = require('../utils/errors');
|
|
3
|
+
const { success } = require('../utils/response');
|
|
4
|
+
|
|
5
|
+
const listUsers = async (req, res, next) => {
|
|
6
|
+
try {
|
|
7
|
+
const page = parseInt(req.query.page, 10) || 1;
|
|
8
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
9
|
+
const users = await userService.findAll({ page, limit });
|
|
10
|
+
|
|
11
|
+
return success(res, users);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
next(error);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getUserById = async (req, res, next) => {
|
|
18
|
+
try {
|
|
19
|
+
const user = await userService.findById(req.params.id);
|
|
20
|
+
if (!user) {
|
|
21
|
+
throw new AppError('User not found', 404);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return success(res, {
|
|
25
|
+
user: {
|
|
26
|
+
id: user.id,
|
|
27
|
+
username: user.username,
|
|
28
|
+
email: user.email,
|
|
29
|
+
role: user.role,
|
|
30
|
+
createdAt: user.createdAt,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
next(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const updateUserRole = async (req, res, next) => {
|
|
39
|
+
try {
|
|
40
|
+
const { role } = req.body;
|
|
41
|
+
const validRoles = ['user', 'moderator', 'admin'];
|
|
42
|
+
|
|
43
|
+
if (!validRoles.includes(role)) {
|
|
44
|
+
throw new AppError(`Invalid role. Must be one of: ${validRoles.join(', ')}`, 400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const user = await userService.findById(req.params.id);
|
|
48
|
+
if (!user) {
|
|
49
|
+
throw new AppError('User not found', 404);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updatedUser = await userService.updateById(req.params.id, { role });
|
|
53
|
+
|
|
54
|
+
return success(res, {
|
|
55
|
+
message: 'User role updated successfully',
|
|
56
|
+
user: {
|
|
57
|
+
id: updatedUser.id,
|
|
58
|
+
username: updatedUser.username,
|
|
59
|
+
email: updatedUser.email,
|
|
60
|
+
role: updatedUser.role,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
next(error);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const deleteUser = async (req, res, next) => {
|
|
69
|
+
try {
|
|
70
|
+
if (req.params.id === req.user.userId) {
|
|
71
|
+
throw new AppError('Cannot delete your own admin account', 400);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const user = await userService.findById(req.params.id);
|
|
75
|
+
if (!user) {
|
|
76
|
+
throw new AppError('User not found', 404);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await userService.deleteById(req.params.id);
|
|
80
|
+
return success(res, { message: 'User deleted successfully' });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
next(error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
<% if (hasSoftDelete) { %>
|
|
86
|
+
const restoreUser = async (req, res, next) => {
|
|
87
|
+
try {
|
|
88
|
+
const user = await userService.restoreById(req.params.id);
|
|
89
|
+
if (!user) {
|
|
90
|
+
throw new AppError('User not found or not deleted', 404);
|
|
91
|
+
}
|
|
92
|
+
return success(res, { message: 'User restored successfully' });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
next(error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const listDeletedUsers = async (req, res, next) => {
|
|
99
|
+
try {
|
|
100
|
+
const page = parseInt(req.query.page, 10) || 1;
|
|
101
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
102
|
+
const users = await userService.findDeleted({ page, limit });
|
|
103
|
+
return success(res, users);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
next(error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
<% } %>
|
|
109
|
+
module.exports = { listUsers, getUserById, updateUserRole, deleteUser<% if (hasSoftDelete) { %>, restoreUser, listDeletedUsers<% } %> };
|
|
@@ -3,6 +3,7 @@ const authService = require('../services/authService');
|
|
|
3
3
|
const userService = require('../services/userService');
|
|
4
4
|
const { AppError } = require('../utils/errors');
|
|
5
5
|
const { success, created } = require('../utils/response');
|
|
6
|
+
<% if (hasEmail) { %>const emailService = require('../services/emailService');<% } %>
|
|
6
7
|
|
|
7
8
|
const register = async (req, res, next) => {
|
|
8
9
|
try {
|
|
@@ -16,7 +17,10 @@ const register = async (req, res, next) => {
|
|
|
16
17
|
const hashedPassword = await bcrypt.hash(password, 12);
|
|
17
18
|
const user = await userService.create({ username, email, password: hashedPassword });
|
|
18
19
|
const tokens = authService.generateTokens(user);
|
|
19
|
-
|
|
20
|
+
<% if (hasEmail) { %>
|
|
21
|
+
// Send welcome email (non-blocking)
|
|
22
|
+
emailService.sendWelcomeEmail(email, username).catch(() => {});
|
|
23
|
+
<% } %>
|
|
20
24
|
return created(res, {
|
|
21
25
|
message: 'User registered successfully',
|
|
22
26
|
user: { id: user.id, username: user.username, email: user.email },
|
|
@@ -113,12 +117,13 @@ const forgotPassword = async (req, res, next) => {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
const resetToken = await authService.generateResetToken(email);
|
|
116
|
-
|
|
120
|
+
<% if (hasEmail) { %>
|
|
121
|
+
await emailService.sendPasswordResetEmail(email, resetToken);
|
|
122
|
+
<% } else { %>
|
|
117
123
|
// TODO: Send email with reset link
|
|
118
|
-
// await emailService.sendResetEmail(email, resetToken);
|
|
119
|
-
//
|
|
120
124
|
// For development, log the token:
|
|
121
125
|
console.log(`Password reset token for ${email}: ${resetToken}`);
|
|
126
|
+
<% } %>
|
|
122
127
|
|
|
123
128
|
return success(res, { message: 'If an account with that email exists, a reset link has been sent.' });
|
|
124
129
|
} catch (error) {
|
|
@@ -6,6 +6,13 @@ const morgan = require('morgan');
|
|
|
6
6
|
<% if (hasSwagger) { %>const swaggerUi = require('swagger-ui-express');
|
|
7
7
|
const swaggerSpec = require('./config/swagger');<% } %>
|
|
8
8
|
<% if (hasRequestId) { %>const { requestId } = require('./middleware/requestId');<% } %>
|
|
9
|
+
<% if (hasMetrics) { %>const { metricsMiddleware, metricsEndpoint } = require('./middleware/metrics');<% } %>
|
|
10
|
+
<% if (hasAuditLog) { %>const { auditLog } = require('./middleware/auditLog');<% } %>
|
|
11
|
+
<% if (hasGraphQL) { %>const { ApolloServer } = require('@apollo/server');
|
|
12
|
+
const { expressMiddleware } = require('@apollo/server/express4');
|
|
13
|
+
const typeDefs = require('./graphql/typeDefs');
|
|
14
|
+
const resolvers = require('./graphql/resolvers');<% if (hasAuth) { %>
|
|
15
|
+
const authService = require('./services/authService');<% } %><% } %>
|
|
9
16
|
const routes = require('./routes');
|
|
10
17
|
const errorHandler = require('./middleware/errorHandler');
|
|
11
18
|
<% if (hasDatabase || hasAuth) { %>const { validateEnv } = require('./utils/envValidator');
|
|
@@ -31,6 +38,18 @@ app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
|
|
31
38
|
// Body parsing
|
|
32
39
|
app.use(express.json({ limit: '10mb' }));
|
|
33
40
|
app.use(express.urlencoded({ extended: true }));
|
|
41
|
+
<% if (hasFileUpload) { %>
|
|
42
|
+
// Serve uploaded files
|
|
43
|
+
const path = require('path');
|
|
44
|
+
app.use('/uploads', express.static(path.join(process.cwd(), process.env.UPLOAD_DIR || 'uploads')));
|
|
45
|
+
<% } %>
|
|
46
|
+
<% if (hasMetrics) { %>
|
|
47
|
+
// Prometheus metrics
|
|
48
|
+
app.use(metricsMiddleware);
|
|
49
|
+
app.get('/metrics', metricsEndpoint);<% } %>
|
|
50
|
+
<% if (hasAuditLog) { %>
|
|
51
|
+
// Audit logging
|
|
52
|
+
app.use(auditLog());<% } %>
|
|
34
53
|
<% if (hasRateLimit) { %>
|
|
35
54
|
// Rate limiting
|
|
36
55
|
const limiter = rateLimit({
|
|
@@ -96,9 +115,39 @@ app.get('/health', async (req, res) => {
|
|
|
96
115
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
|
97
116
|
customCss: '.swagger-ui .topbar { display: none }',
|
|
98
117
|
}));<% } %>
|
|
118
|
+
<% if (hasGraphQL) { %>
|
|
119
|
+
// GraphQL setup (async — call initGraphQL() before starting server)
|
|
120
|
+
let apolloServer;
|
|
121
|
+
|
|
122
|
+
const initGraphQL = async () => {
|
|
123
|
+
apolloServer = new ApolloServer({ typeDefs, resolvers });
|
|
124
|
+
await apolloServer.start();
|
|
125
|
+
|
|
126
|
+
app.use('/graphql', expressMiddleware(apolloServer, {
|
|
127
|
+
context: async ({ req }) => {
|
|
128
|
+
<% if (hasAuth) { %> // Extract user from JWT if present
|
|
129
|
+
const authHeader = req.headers.authorization;
|
|
130
|
+
if (authHeader) {
|
|
131
|
+
const token = authHeader.split(' ')[1];
|
|
132
|
+
try {
|
|
133
|
+
const user = authService.verifyToken(token);
|
|
134
|
+
return { user };
|
|
135
|
+
} catch {
|
|
136
|
+
return { user: null };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
<% } %> return { user: null };
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
};
|
|
99
143
|
|
|
144
|
+
app.initGraphQL = initGraphQL;
|
|
145
|
+
<% } %>
|
|
100
146
|
// API routes
|
|
101
|
-
app.use('/api', routes);
|
|
147
|
+
<% if (hasApiVersioning) { %>app.use('/api/v1', routes);
|
|
148
|
+
// Future versions: app.use('/api/v2', v2Routes);
|
|
149
|
+
<% } else { %>app.use('/api', routes);
|
|
150
|
+
<% } %>
|
|
102
151
|
|
|
103
152
|
// 404 handler
|
|
104
153
|
app.use('*', (req, res) => {
|
package/templates/core/env.ejs
CHANGED
|
@@ -11,6 +11,23 @@ JWT_REFRESH_EXPIRE=7d
|
|
|
11
11
|
<% } %><% if (hasRedis) { %>
|
|
12
12
|
# Redis (token blacklist store)
|
|
13
13
|
REDIS_URL=redis://localhost:6379
|
|
14
|
+
<% } %><% if (hasBackgroundJobs && !hasRedis) { %>
|
|
15
|
+
# Redis (required for BullMQ)
|
|
16
|
+
REDIS_HOST=localhost
|
|
17
|
+
REDIS_PORT=6379
|
|
18
|
+
<% } %><% if (hasEmail) { %>
|
|
19
|
+
# Email (SMTP)
|
|
20
|
+
SMTP_HOST=smtp.ethereal.email
|
|
21
|
+
SMTP_PORT=587
|
|
22
|
+
SMTP_SECURE=false
|
|
23
|
+
SMTP_USER=
|
|
24
|
+
SMTP_PASS=
|
|
25
|
+
SMTP_FROM="App" <noreply@example.com>
|
|
26
|
+
FRONTEND_URL=http://localhost:3000
|
|
27
|
+
<% } %><% if (hasFileUpload) { %>
|
|
28
|
+
# File Uploads
|
|
29
|
+
UPLOAD_DIR=uploads
|
|
30
|
+
UPLOAD_MAX_SIZE=5242880
|
|
14
31
|
<% } %><% if (db === 'mongodb') { %>
|
|
15
32
|
# Database
|
|
16
33
|
MONGO_URI=mongodb://localhost:27017/<%= projectName %>
|
|
@@ -11,6 +11,23 @@ JWT_REFRESH_EXPIRE=7d
|
|
|
11
11
|
<% } %><% if (hasRedis) { %>
|
|
12
12
|
# Redis (token blacklist store)
|
|
13
13
|
REDIS_URL=redis://localhost:6379
|
|
14
|
+
<% } %><% if (hasBackgroundJobs && !hasRedis) { %>
|
|
15
|
+
# Redis (required for BullMQ)
|
|
16
|
+
REDIS_HOST=localhost
|
|
17
|
+
REDIS_PORT=6379
|
|
18
|
+
<% } %><% if (hasEmail) { %>
|
|
19
|
+
# Email (SMTP)
|
|
20
|
+
SMTP_HOST=smtp.ethereal.email
|
|
21
|
+
SMTP_PORT=587
|
|
22
|
+
SMTP_SECURE=false
|
|
23
|
+
SMTP_USER=
|
|
24
|
+
SMTP_PASS=
|
|
25
|
+
SMTP_FROM="App" <noreply@example.com>
|
|
26
|
+
FRONTEND_URL=http://localhost:3000
|
|
27
|
+
<% } %><% if (hasFileUpload) { %>
|
|
28
|
+
# File Uploads
|
|
29
|
+
UPLOAD_DIR=uploads
|
|
30
|
+
UPLOAD_MAX_SIZE=5242880
|
|
14
31
|
<% } %><% if (db === 'mongodb') { %>
|
|
15
32
|
# Database
|
|
16
33
|
MONGO_URI=mongodb://localhost:27017/<%= projectName %>
|
|
@@ -39,7 +39,14 @@
|
|
|
39
39
|
"@prisma/client": "^5.22.0"<% } %><% if (hasWebsocket) { %>,
|
|
40
40
|
"socket.io": "^4.8.0"<% } %><% if (hasRedis) { %>,
|
|
41
41
|
"ioredis": "^5.4.0"<% } %><% if (hasRequestId) { %>,
|
|
42
|
-
"uuid": "^10.0.0"<% } %><% if (
|
|
42
|
+
"uuid": "^10.0.0"<% } %><% if (hasEmail) { %>,
|
|
43
|
+
"nodemailer": "^6.9.0"<% } %><% if (hasFileUpload) { %>,
|
|
44
|
+
"multer": "^1.4.5-lts.1"<% } %><% if (hasMetrics) { %>,
|
|
45
|
+
"prom-client": "^15.1.0"<% } %><% if (hasGraphQL) { %>,
|
|
46
|
+
"@apollo/server": "^4.11.0",
|
|
47
|
+
"graphql": "^16.9.0",
|
|
48
|
+
"graphql-tag": "^2.12.6"<% } %><% if (hasBackgroundJobs) { %>,
|
|
49
|
+
"bullmq": "^5.12.0"<% } %><% if (logger === 'winston') { %>,
|
|
43
50
|
"winston": "^3.15.0"<% } %><% if (logger === 'pino') { %>,
|
|
44
51
|
"pino": "^9.5.0",
|
|
45
52
|
"pino-pretty": "^13.0.0"<% } %>
|
|
@@ -6,7 +6,8 @@ const os = require('os');
|
|
|
6
6
|
const app = require('./app');<% if (hasDatabase) { %>
|
|
7
7
|
const db = require('./config/database');<% } %><% if (hasRedis) { %>
|
|
8
8
|
const { connectRedis } = require('./config/redis');<% } %><% if (hasWebsocket) { %>
|
|
9
|
-
const { setupWebSocket } = require('./config/websocket');<% } %>
|
|
9
|
+
const { setupWebSocket } = require('./config/websocket');<% } %><% if (hasBackgroundJobs) { %>
|
|
10
|
+
const { startWorkers } = require('./jobs/worker');<% } %>
|
|
10
11
|
const { createLogger } = require('./utils/logger');
|
|
11
12
|
|
|
12
13
|
const logger = createLogger('Server');
|
|
@@ -28,7 +29,9 @@ const startServer = async () => {
|
|
|
28
29
|
});
|
|
29
30
|
} else {<% if (hasDatabase) { %>
|
|
30
31
|
await db.connect();<% } %><% if (hasRedis) { %>
|
|
31
|
-
await connectRedis();<% } %>
|
|
32
|
+
await connectRedis();<% } %><% if (hasGraphQL) { %>
|
|
33
|
+
await app.initGraphQL();<% } %><% if (hasBackgroundJobs) { %>
|
|
34
|
+
startWorkers();<% } %>
|
|
32
35
|
|
|
33
36
|
<% if (hasWebsocket) { %> const httpServer = http.createServer(app);
|
|
34
37
|
const io = setupWebSocket(httpServer);
|