express-genix 2.0.1 → 3.0.1

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 (33) hide show
  1. package/README.md +61 -11
  2. package/index.js +22 -1
  3. package/lib/features.js +8 -0
  4. package/lib/generator.js +77 -2
  5. package/package.json +2 -2
  6. package/templates/config/queue.js.ejs +29 -0
  7. package/templates/config/schema.prisma.ejs +2 -0
  8. package/templates/controllers/adminController.js.ejs +109 -0
  9. package/templates/controllers/authController.js.ejs +9 -4
  10. package/templates/controllers/userController.js.ejs +1 -0
  11. package/templates/core/app.js.ejs +50 -1
  12. package/templates/core/env.ejs +17 -0
  13. package/templates/core/env.example.ejs +17 -0
  14. package/templates/core/package.json.ejs +8 -1
  15. package/templates/core/server.js.ejs +5 -2
  16. package/templates/graphql/resolvers.js.ejs +61 -0
  17. package/templates/graphql/typeDefs.js.ejs +53 -0
  18. package/templates/jobs/worker.js.ejs +60 -0
  19. package/templates/middleware/auditLog.js.ejs +62 -0
  20. package/templates/middleware/metrics.js.ejs +65 -0
  21. package/templates/middleware/rbac.js.ejs +86 -0
  22. package/templates/middleware/upload.js.ejs +50 -0
  23. package/templates/models/User.mongo.js.ejs +29 -0
  24. package/templates/models/User.postgres.js.ejs +7 -1
  25. package/templates/routes/adminRoutes.js.ejs +150 -0
  26. package/templates/routes/index.js.ejs +6 -0
  27. package/templates/routes/jobRoutes.js.ejs +85 -0
  28. package/templates/routes/uploadRoutes.js.ejs +100 -0
  29. package/templates/services/authService.js.ejs +1 -0
  30. package/templates/services/emailService.js.ejs +88 -0
  31. package/templates/services/userService.mongodb.js.ejs +32 -2
  32. package/templates/services/userService.postgres.js.ejs +33 -1
  33. package/templates/services/userService.prisma.js.ejs +50 -6
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  A production-grade CLI tool that generates Express.js applications with best-in-class defaults. Scaffold a complete REST API in seconds — with TypeScript, authentication, Prisma/Mongoose/Sequelize, Docker, CI/CD, and more.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/express-genix.svg)](https://www.npmjs.com/package/express-genix)
6
+ [![npm downloads](https://img.shields.io/npm/dm/express-genix.svg)](https://www.npmjs.com/package/express-genix)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
8
9
 
@@ -15,20 +16,32 @@ A production-grade CLI tool that generates Express.js applications with best-in-
15
16
 
16
17
  **Security & Auth**
17
18
  - JWT access + refresh tokens with token blacklist logout
19
+ - Role-Based Access Control (RBAC) — admin, moderator, user roles with permission system
20
+ - Admin panel routes for user management (list, view, update roles, delete)
18
21
  - Password reset flow (forgot-password / reset-password with crypto tokens)
19
22
  - Optional Redis-backed blacklist for production multi-instance deployments
20
23
  - Zod request validation with pre-built schemas (register, login, reset, etc.)
21
24
  - bcrypt password hashing, input sanitization (`validator`)
22
25
  - Helmet, CORS, environment validation on startup
23
26
  - Auto-generated cryptographically secure JWT secrets
27
+ - Soft deletes with restore capability
24
28
 
25
29
  **API & Documentation**
26
30
  - Swagger UI + swagger-jsdoc annotation-based docs with example request/response bodies
31
+ - GraphQL (Apollo Server) with type definitions and resolvers
27
32
  - Consistent `{ success, data, meta }` response envelope
33
+ - API versioning (`/api/v1/` prefix)
28
34
  - Paginated list endpoints
29
35
  - Request ID / correlation tracking
30
36
  - Response caching middleware (Redis-backed, configurable TTL)
31
37
 
38
+ **Services & Infrastructure**
39
+ - Email service (Nodemailer) with welcome and password reset emails
40
+ - File uploads (Multer) with type filtering and size limits
41
+ - Background jobs (BullMQ) with Redis-backed queues and workers
42
+ - Prometheus metrics (`/metrics` endpoint with prom-client)
43
+ - Audit logging middleware with request tracking and sensitive field redaction
44
+
32
45
  **Developer Experience**
33
46
  - Interactive prompts — pick language, database, features via checkbox
34
47
  - Winston or Pino logger (you choose)
@@ -71,7 +84,7 @@ You'll be prompted for:
71
84
  1. **Project name**
72
85
  2. **Language** — JavaScript or TypeScript
73
86
  3. **Database** — MongoDB, PostgreSQL (Sequelize), PostgreSQL (Prisma), or None
74
- 4. **Features** — Auth, Rate Limiting, Swagger, Redis Token Blacklist, Docker, CI/CD, WebSocket, Request ID
87
+ 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
75
88
  5. **Logger** — Winston or Pino
76
89
 
77
90
  The CLI generates your project, installs dependencies, formats code, and creates an initial git commit.
@@ -103,19 +116,22 @@ express-genix add prisma # Adds Prisma schema, client config, migrations
103
116
  ```
104
117
  my-express-app/
105
118
  ├── src/
106
- │ ├── config/ # Database, Swagger, WebSocket configuration
107
- │ ├── controllers/ # Route handlers
108
- │ ├── middleware/ # Auth, validation, error handling, request ID
109
- │ ├── models/ # Database models (Mongoose/Sequelize)
119
+ │ ├── config/ # Database, Swagger, WebSocket, queue configuration
120
+ │ ├── controllers/ # Route handlers (auth, user, admin, example)
121
+ │ ├── graphql/ # GraphQL type definitions & resolvers (if selected)
122
+ │ ├── jobs/ # BullMQ background workers (if selected)
123
+ │ ├── middleware/ # Auth, RBAC, validation, error handling, uploads, metrics, audit log
124
+ │ ├── models/ # Database models (Mongoose/Sequelize/Prisma)
110
125
  │ ├── routes/ # API route definitions with Swagger annotations
111
- │ ├── services/ # Business logic layer
126
+ │ ├── services/ # Business logic layer (auth, user, email)
112
127
  │ ├── utils/ # Logger, errors, response helpers, validators
113
128
  │ ├── app.js|ts # Express app setup
114
- │ └── server.js|ts # Server + clustering + WebSocket
129
+ │ └── server.js|ts # Server + clustering + WebSocket + workers
115
130
  ├── tests/ # Jest + Supertest suites
116
131
  ├── prisma/ # Prisma schema (if selected)
117
132
  ├── migrations/ # Sequelize migrations (if PostgreSQL + Sequelize)
118
133
  ├── seeders/ # Sequelize seeders (if PostgreSQL + Sequelize)
134
+ ├── uploads/ # File upload directory (if selected)
119
135
  ├── .github/workflows/ # CI/CD pipeline (if selected)
120
136
  ├── .env # Auto-generated environment config
121
137
  ├── .env.example # Template for team sharing
@@ -126,6 +142,8 @@ my-express-app/
126
142
 
127
143
  ## API Endpoints (with database + auth)
128
144
 
145
+ > When API versioning is enabled, all `/api/*` routes become `/api/v1/*`.
146
+
129
147
  ### Authentication
130
148
  | Method | Endpoint | Description |
131
149
  |--------|----------|-------------|
@@ -141,12 +159,35 @@ my-express-app/
141
159
  |--------|----------|-------------|
142
160
  | GET | `/api/users/profile` | Get current user |
143
161
  | PUT | `/api/users/profile` | Update profile |
144
- | DELETE | `/api/users/profile` | Delete account |
162
+ | DELETE | `/api/users/profile` | Delete account (soft delete if enabled) |
163
+
164
+ ### Admin (RBAC — admin role required)
165
+ | Method | Endpoint | Description |
166
+ |--------|----------|-------------|
167
+ | GET | `/api/admin/users` | List all users (paginated) |
168
+ | GET | `/api/admin/users/:id` | Get user by ID |
169
+ | PUT | `/api/admin/users/:id/role` | Update user role |
170
+ | DELETE | `/api/admin/users/:id` | Delete user |
171
+ | POST | `/api/admin/users/:id/restore` | Restore soft-deleted user |
172
+
173
+ ### File Uploads
174
+ | Method | Endpoint | Description |
175
+ |--------|----------|-------------|
176
+ | POST | `/api/uploads/single` | Upload a single file |
177
+ | POST | `/api/uploads/multiple` | Upload multiple files (max 10) |
178
+
179
+ ### Background Jobs (BullMQ)
180
+ | Method | Endpoint | Description |
181
+ |--------|----------|-------------|
182
+ | POST | `/api/jobs` | Dispatch a background job |
183
+ | GET | `/api/jobs/:id` | Get job status |
145
184
 
146
- ### Health
185
+ ### Observability & Health
147
186
  | Method | Endpoint | Description |
148
187
  |--------|----------|-------------|
149
- | GET | `/health` | Health check with uptime, DB status, Redis status, memory usage |
188
+ | GET | `/health` | Health check (uptime, DB, Redis, memory) |
189
+ | GET | `/metrics` | Prometheus metrics (prom-client) |
190
+ | GET | `/graphql` | GraphQL Playground (Apollo Server) |
150
191
 
151
192
  ## Available Scripts
152
193
 
@@ -185,10 +226,17 @@ Generated `.env` includes auto-generated JWT secrets:
185
226
  | `JWT_SECRET` | Access token secret (auto-generated) | — |
186
227
  | `JWT_REFRESH_SECRET` | Refresh token secret (auto-generated) | — |
187
228
  | `MONGO_URI` / `DATABASE_URL` | Database connection string | — |
188
- | `REDIS_URL` | Redis URL (when Redis blacklist enabled) | `redis://localhost:6379` |
229
+ | `REDIS_URL` | Redis URL (token blacklist, caching, BullMQ) | `redis://localhost:6379` |
189
230
  | `RATE_LIMIT_WINDOW_MS` | Rate limit window (ms) | `900000` |
190
231
  | `RATE_LIMIT_MAX` | Max requests per window | `100` |
191
232
  | `LOG_LEVEL` | Logging level | `info` |
233
+ | `SMTP_HOST` | SMTP server host (email service) | — |
234
+ | `SMTP_PORT` | SMTP server port | `587` |
235
+ | `SMTP_USER` | SMTP username | — |
236
+ | `SMTP_PASS` | SMTP password | — |
237
+ | `EMAIL_FROM` | Default sender address | — |
238
+ | `UPLOAD_MAX_SIZE` | Max file upload size in bytes | `5242880` (5 MB) |
239
+ | `UPLOAD_DIR` | Upload destination directory | `uploads` |
192
240
 
193
241
  ## Docker
194
242
 
@@ -196,6 +244,8 @@ Generated `.env` includes auto-generated JWT secrets:
196
244
  docker-compose up --build
197
245
  ```
198
246
 
247
+ The generated `docker-compose.yml` includes services for your app and its dependencies (MongoDB/PostgreSQL, Redis) with health checks, volumes, and a shared network. The `Dockerfile` uses multi-stage builds, runs as a non-root user, and only copies production dependencies.
248
+
199
249
  ## Contributing
200
250
 
201
251
  Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
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,7 +1,7 @@
1
1
  {
2
2
  "name": "express-genix",
3
- "version": "2.0.1",
4
- "description": "Production-grade CLI to generate Express apps with JWT, TypeScript, Prisma, MongoDB, PostgreSQL, rate-limiting, Swagger, CI/CD, and more",
3
+ "version": "3.0.1",
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
7
  "express-genix": "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) {
@@ -14,6 +14,7 @@ const getProfile = async (req, res, next) => {
14
14
  id: user.id,
15
15
  username: user.username,
16
16
  email: user.email,
17
+ role: user.role,
17
18
  createdAt: user.createdAt,
18
19
  },
19
20
  });
@@ -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) => {
@@ -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 %>