create-efc-app 0.3.6 → 0.4.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/dist/index.js +855 -329
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,6 @@ async function scaffold(opts) {
|
|
|
19
19
|
await writeGitignore(dest);
|
|
20
20
|
await writeEnvFiles(dest, opts);
|
|
21
21
|
await writeExampleRoute(dest, opts);
|
|
22
|
-
if (opts.rbac) await writeRequireRoleMiddleware(dest, opts);
|
|
23
22
|
if (opts.userPortal) await writeUserModel(dest, opts);
|
|
24
23
|
if (opts.adminPortal) await writeAdminModel(dest, opts);
|
|
25
24
|
await writeAuthRoutes(dest, opts);
|
|
@@ -149,6 +148,7 @@ async function writeEnvFiles(dest, opts) {
|
|
|
149
148
|
const resolvedPort = isGmail ? "465" : opts.smtpPort ?? "587";
|
|
150
149
|
const passComment = isGmail ? " # Gmail App Password (16 chars) \u2014 NOT your regular Gmail password. Generate at: Google Account > Security > 2-Step Verification > App passwords" : "";
|
|
151
150
|
const smtpVars = opts.mailer ? `
|
|
151
|
+
APP_URL=http://localhost:3000
|
|
152
152
|
SMTP_HOST=${resolvedHost}
|
|
153
153
|
SMTP_PORT=${resolvedPort}
|
|
154
154
|
SMTP_USER=${opts.smtpUser ?? ""}
|
|
@@ -156,6 +156,7 @@ SMTP_PASS=${opts.smtpPass ?? ""}${passComment}
|
|
|
156
156
|
SMTP_FROM=${opts.smtpUser ?? "noreply@example.com"}
|
|
157
157
|
` : "";
|
|
158
158
|
const smtpExample = opts.mailer ? `
|
|
159
|
+
APP_URL=http://localhost:3000
|
|
159
160
|
SMTP_HOST=${resolvedHost}
|
|
160
161
|
SMTP_PORT=${resolvedPort}
|
|
161
162
|
SMTP_USER=your@email.com
|
|
@@ -182,14 +183,18 @@ async function writeExampleRoute(dest, opts) {
|
|
|
182
183
|
const metaTs = opts.routeDocs ? `import type { RouteMeta } from 'express-file-cluster';
|
|
183
184
|
|
|
184
185
|
export const meta: RouteMeta = {
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
GET: {
|
|
187
|
+
description: 'Health check \u2014 returns server status and current timestamp.',
|
|
188
|
+
response: { status: 200, body: { status: 'OK', timestamp: '2024-01-01T00:00:00.000Z' } },
|
|
189
|
+
},
|
|
187
190
|
};
|
|
188
191
|
|
|
189
192
|
` : "";
|
|
190
193
|
const metaJs = opts.routeDocs ? `export const meta = {
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
GET: {
|
|
195
|
+
description: 'Health check \u2014 returns server status and current timestamp.',
|
|
196
|
+
response: { status: 200, body: { status: 'OK', timestamp: '2024-01-01T00:00:00.000Z' } },
|
|
197
|
+
},
|
|
193
198
|
};
|
|
194
199
|
|
|
195
200
|
` : "";
|
|
@@ -291,27 +296,42 @@ export interface UserDocument {
|
|
|
291
296
|
avatar?: string;
|
|
292
297
|
isVerified: boolean;
|
|
293
298
|
isActive: boolean;
|
|
299
|
+
verifyToken?: string;
|
|
300
|
+
resetToken?: string;
|
|
301
|
+
resetTokenExpiry?: Date;
|
|
302
|
+
refreshToken?: string;
|
|
303
|
+
refreshTokenExpiry?: Date;
|
|
294
304
|
}
|
|
295
305
|
|
|
296
306
|
export const User = defineModel<UserDocument>('User', {
|
|
297
|
-
name:
|
|
298
|
-
email:
|
|
299
|
-
password:
|
|
300
|
-
role:
|
|
301
|
-
avatar:
|
|
302
|
-
isVerified:
|
|
303
|
-
isActive:
|
|
307
|
+
name: { type: 'string', required: true },
|
|
308
|
+
email: { type: 'string', required: true, unique: true },
|
|
309
|
+
password: { type: 'string', required: true },
|
|
310
|
+
role: { type: 'string', required: true, default: 'user' },
|
|
311
|
+
avatar: { type: 'string' },
|
|
312
|
+
isVerified: { type: 'boolean', default: false },
|
|
313
|
+
isActive: { type: 'boolean', default: true },
|
|
314
|
+
verifyToken: { type: 'string' },
|
|
315
|
+
resetToken: { type: 'string' },
|
|
316
|
+
resetTokenExpiry: { type: 'date' },
|
|
317
|
+
refreshToken: { type: 'string' },
|
|
318
|
+
refreshTokenExpiry: { type: 'date' },
|
|
304
319
|
});
|
|
305
320
|
` : `import { defineModel } from 'express-file-cluster';
|
|
306
321
|
|
|
307
322
|
export const User = defineModel('User', {
|
|
308
|
-
name:
|
|
309
|
-
email:
|
|
310
|
-
password:
|
|
311
|
-
role:
|
|
312
|
-
avatar:
|
|
313
|
-
isVerified:
|
|
314
|
-
isActive:
|
|
323
|
+
name: { type: 'string', required: true },
|
|
324
|
+
email: { type: 'string', required: true, unique: true },
|
|
325
|
+
password: { type: 'string', required: true },
|
|
326
|
+
role: { type: 'string', required: true, default: 'user' },
|
|
327
|
+
avatar: { type: 'string' },
|
|
328
|
+
isVerified: { type: 'boolean', default: false },
|
|
329
|
+
isActive: { type: 'boolean', default: true },
|
|
330
|
+
verifyToken: { type: 'string' },
|
|
331
|
+
resetToken: { type: 'string' },
|
|
332
|
+
resetTokenExpiry: { type: 'date' },
|
|
333
|
+
refreshToken: { type: 'string' },
|
|
334
|
+
refreshTokenExpiry: { type: 'date' },
|
|
315
335
|
});
|
|
316
336
|
` : ts ? `// TODO: define your Drizzle schema for User
|
|
317
337
|
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
@@ -346,25 +366,37 @@ export interface AdminDocument {
|
|
|
346
366
|
role: string;
|
|
347
367
|
permissions: string[];
|
|
348
368
|
isActive: boolean;
|
|
369
|
+
resetToken?: string;
|
|
370
|
+
resetTokenExpiry?: Date;
|
|
371
|
+
refreshToken?: string;
|
|
372
|
+
refreshTokenExpiry?: Date;
|
|
349
373
|
}
|
|
350
374
|
|
|
351
375
|
export const Admin = defineModel<AdminDocument>('Admin', {
|
|
352
|
-
name:
|
|
353
|
-
email:
|
|
354
|
-
password:
|
|
355
|
-
role:
|
|
356
|
-
permissions:
|
|
357
|
-
isActive:
|
|
376
|
+
name: { type: 'string', required: true },
|
|
377
|
+
email: { type: 'string', required: true, unique: true },
|
|
378
|
+
password: { type: 'string', required: true },
|
|
379
|
+
role: { type: 'string', required: true, default: 'admin' },
|
|
380
|
+
permissions: { type: 'array', default: [] },
|
|
381
|
+
isActive: { type: 'boolean', default: true },
|
|
382
|
+
resetToken: { type: 'string' },
|
|
383
|
+
resetTokenExpiry: { type: 'date' },
|
|
384
|
+
refreshToken: { type: 'string' },
|
|
385
|
+
refreshTokenExpiry: { type: 'date' },
|
|
358
386
|
});
|
|
359
387
|
` : `import { defineModel } from 'express-file-cluster';
|
|
360
388
|
|
|
361
389
|
export const Admin = defineModel('Admin', {
|
|
362
|
-
name:
|
|
363
|
-
email:
|
|
364
|
-
password:
|
|
365
|
-
role:
|
|
366
|
-
permissions:
|
|
367
|
-
isActive:
|
|
390
|
+
name: { type: 'string', required: true },
|
|
391
|
+
email: { type: 'string', required: true, unique: true },
|
|
392
|
+
password: { type: 'string', required: true },
|
|
393
|
+
role: { type: 'string', required: true, default: 'admin' },
|
|
394
|
+
permissions: { type: 'array', default: [] },
|
|
395
|
+
isActive: { type: 'boolean', default: true },
|
|
396
|
+
resetToken: { type: 'string' },
|
|
397
|
+
resetTokenExpiry: { type: 'date' },
|
|
398
|
+
refreshToken: { type: 'string' },
|
|
399
|
+
refreshTokenExpiry: { type: 'date' },
|
|
368
400
|
});
|
|
369
401
|
` : ts ? `// TODO: define your Drizzle schema for Admin
|
|
370
402
|
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
@@ -392,28 +424,51 @@ async function writeAuthRoutes(dest, opts) {
|
|
|
392
424
|
const loginMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
393
425
|
|
|
394
426
|
export const meta: RouteMeta = {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
427
|
+
POST: {
|
|
428
|
+
description: 'Authenticate a user or admin and issue a JWT.',
|
|
429
|
+
request: { body: { email: 'user@example.com', password: 'user' } },
|
|
430
|
+
response: { status: 200, body: { message: 'Logged in as user' } },
|
|
431
|
+
},
|
|
398
432
|
};
|
|
399
433
|
|
|
400
434
|
` : `export const meta = {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
POST: {
|
|
436
|
+
description: 'Authenticate a user or admin and issue a JWT.',
|
|
437
|
+
request: { body: { email: 'user@example.com', password: 'user' } },
|
|
438
|
+
response: { status: 200, body: { message: 'Logged in as user' } },
|
|
439
|
+
},
|
|
404
440
|
};
|
|
405
441
|
|
|
406
442
|
` : "";
|
|
407
443
|
const loginDbImports = opts.database === "mongodb" ? ts ? `import bcrypt from 'bcrypt';
|
|
444
|
+
import crypto from 'node:crypto';
|
|
408
445
|
import { User } from '../../model/User.js';
|
|
409
446
|
import { Admin } from '../../model/Admin.js';
|
|
410
447
|
` : `import bcrypt from 'bcrypt';
|
|
448
|
+
import crypto from 'node:crypto';
|
|
411
449
|
import { User } from '../../model/User.js';
|
|
412
450
|
import { Admin } from '../../model/Admin.js';
|
|
413
451
|
` : "";
|
|
414
452
|
const loginContent = opts.database === "mongodb" ? ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
415
453
|
import type { Request, Response } from 'express';
|
|
416
|
-
${loginDbImports}${loginMeta}
|
|
454
|
+
${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
455
|
+
|
|
456
|
+
async function issueRefreshToken(
|
|
457
|
+
res: Response,
|
|
458
|
+
model: { update: (id: string, data: Record<string, unknown>) => Promise<unknown> },
|
|
459
|
+
id: string,
|
|
460
|
+
): Promise<void> {
|
|
461
|
+
const refreshToken = crypto.randomBytes(40).toString('hex');
|
|
462
|
+
await model.update(id, { refreshToken, refreshTokenExpiry: new Date(Date.now() + REFRESH_TOKEN_TTL_MS) });
|
|
463
|
+
res.cookie('efc_refresh_token', refreshToken, {
|
|
464
|
+
httpOnly: true,
|
|
465
|
+
secure: process.env.NODE_ENV === 'production',
|
|
466
|
+
sameSite: 'strict',
|
|
467
|
+
maxAge: REFRESH_TOKEN_TTL_MS,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export const POST = async (req: Request, res: Response) => {
|
|
417
472
|
const { email, password } = req.body;
|
|
418
473
|
if (!email || !password) return res.status(400).json({ error: 'email and password are required' });
|
|
419
474
|
|
|
@@ -423,6 +478,7 @@ ${loginDbImports}${loginMeta}export const POST = async (req: Request, res: Respo
|
|
|
423
478
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
424
479
|
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
425
480
|
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
481
|
+
await issueRefreshToken(res, Admin, admin.id);
|
|
426
482
|
return res.json({ message: 'Logged in as admin' });
|
|
427
483
|
}
|
|
428
484
|
|
|
@@ -432,10 +488,24 @@ ${loginDbImports}${loginMeta}export const POST = async (req: Request, res: Respo
|
|
|
432
488
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
433
489
|
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
434
490
|
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
491
|
+
await issueRefreshToken(res, User, user.id);
|
|
435
492
|
res.json({ message: 'Logged in' });
|
|
436
493
|
};
|
|
437
494
|
` : `import { issueToken } from 'express-file-cluster/auth';
|
|
438
|
-
${loginDbImports}${loginMeta}
|
|
495
|
+
${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
496
|
+
|
|
497
|
+
async function issueRefreshToken(res, model, id) {
|
|
498
|
+
const refreshToken = crypto.randomBytes(40).toString('hex');
|
|
499
|
+
await model.update(id, { refreshToken, refreshTokenExpiry: new Date(Date.now() + REFRESH_TOKEN_TTL_MS) });
|
|
500
|
+
res.cookie('efc_refresh_token', refreshToken, {
|
|
501
|
+
httpOnly: true,
|
|
502
|
+
secure: process.env.NODE_ENV === 'production',
|
|
503
|
+
sameSite: 'strict',
|
|
504
|
+
maxAge: REFRESH_TOKEN_TTL_MS,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export const POST = async (req, res) => {
|
|
439
509
|
const { email, password } = req.body;
|
|
440
510
|
if (!email || !password) return res.status(400).json({ error: 'email and password are required' });
|
|
441
511
|
|
|
@@ -445,6 +515,7 @@ ${loginDbImports}${loginMeta}export const POST = async (req, res) => {
|
|
|
445
515
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
446
516
|
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
447
517
|
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
518
|
+
await issueRefreshToken(res, Admin, admin.id);
|
|
448
519
|
return res.json({ message: 'Logged in as admin' });
|
|
449
520
|
}
|
|
450
521
|
|
|
@@ -454,6 +525,7 @@ ${loginDbImports}${loginMeta}export const POST = async (req, res) => {
|
|
|
454
525
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
455
526
|
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
456
527
|
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
528
|
+
await issueRefreshToken(res, User, user.id);
|
|
457
529
|
res.json({ message: 'Logged in' });
|
|
458
530
|
};
|
|
459
531
|
` : ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
@@ -473,13 +545,17 @@ ${loginMeta}export const POST = async (req, res) => {
|
|
|
473
545
|
const logoutMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
474
546
|
|
|
475
547
|
export const meta: RouteMeta = {
|
|
476
|
-
|
|
477
|
-
|
|
548
|
+
POST: {
|
|
549
|
+
description: 'Clear the auth cookie and log the user out.',
|
|
550
|
+
response: { status: 200, body: { message: 'Logged out successfully' } },
|
|
551
|
+
},
|
|
478
552
|
};
|
|
479
553
|
|
|
480
554
|
` : `export const meta = {
|
|
481
|
-
|
|
482
|
-
|
|
555
|
+
POST: {
|
|
556
|
+
description: 'Clear the auth cookie and log the user out.',
|
|
557
|
+
response: { status: 200, body: { message: 'Logged out successfully' } },
|
|
558
|
+
},
|
|
483
559
|
};
|
|
484
560
|
|
|
485
561
|
` : "";
|
|
@@ -487,11 +563,13 @@ export const meta: RouteMeta = {
|
|
|
487
563
|
import type { Request, Response } from 'express';
|
|
488
564
|
${logoutMeta}export const POST = async (_req: Request, res: Response) => {
|
|
489
565
|
revokeToken(res);
|
|
566
|
+
res.clearCookie('efc_refresh_token');
|
|
490
567
|
res.json({ message: 'Logged out successfully' });
|
|
491
568
|
};
|
|
492
569
|
` : `import { revokeToken } from 'express-file-cluster/auth';
|
|
493
570
|
${logoutMeta}export const POST = async (_req, res) => {
|
|
494
571
|
revokeToken(res);
|
|
572
|
+
res.clearCookie('efc_refresh_token');
|
|
495
573
|
res.json({ message: 'Logged out successfully' });
|
|
496
574
|
};
|
|
497
575
|
`;
|
|
@@ -501,23 +579,36 @@ ${logoutMeta}export const POST = async (_req, res) => {
|
|
|
501
579
|
const registerMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
502
580
|
|
|
503
581
|
export const meta: RouteMeta = {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
582
|
+
POST: {
|
|
583
|
+
description: 'Register a new user account.',
|
|
584
|
+
request: { body: { name: 'Jane Doe', email: 'jane@example.com', password: 'secret' } },
|
|
585
|
+
response: { status: 201, body: { message: 'Account created successfully' } },
|
|
586
|
+
},
|
|
507
587
|
};
|
|
508
588
|
|
|
509
589
|
` : `export const meta = {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
590
|
+
POST: {
|
|
591
|
+
description: 'Register a new user account.',
|
|
592
|
+
request: { body: { name: 'Jane Doe', email: 'jane@example.com', password: 'secret' } },
|
|
593
|
+
response: { status: 201, body: { message: 'Account created successfully' } },
|
|
594
|
+
},
|
|
513
595
|
};
|
|
514
596
|
|
|
515
597
|
` : "";
|
|
516
|
-
const registerDbImports = opts.database === "mongodb" ?
|
|
517
|
-
import
|
|
518
|
-
|
|
519
|
-
import { User } from '../../model/User.js';
|
|
598
|
+
const registerDbImports = opts.database === "mongodb" ? `import bcrypt from 'bcrypt';
|
|
599
|
+
import crypto from 'node:crypto';
|
|
600
|
+
${opts.mailer ? "import { enqueue } from 'express-file-cluster/tasks';\n" : ""}import { User } from '../../model/User.js';
|
|
520
601
|
` : "";
|
|
602
|
+
const sendVerifyEmail = opts.mailer ? `
|
|
603
|
+
var appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
604
|
+
await enqueue('SendEmail', {
|
|
605
|
+
to: email,
|
|
606
|
+
subject: 'Verify your email address',
|
|
607
|
+
body: 'Welcome, ' + name + '! Verify your email: ' + appUrl + '/auth/verify-email?token=' + verifyToken,
|
|
608
|
+
});
|
|
609
|
+
` : `
|
|
610
|
+
// TODO: email this token to the user \u2014 enable the Mailer feature to auto-wire SendEmail
|
|
611
|
+
`;
|
|
521
612
|
const registerContent = opts.database === "mongodb" ? ts ? `import type { Request, Response } from 'express';
|
|
522
613
|
${registerDbImports}${registerMeta}export const POST = async (req: Request, res: Response) => {
|
|
523
614
|
const { name, email, password } = req.body;
|
|
@@ -527,8 +618,10 @@ ${registerDbImports}${registerMeta}export const POST = async (req: Request, res:
|
|
|
527
618
|
const existing = await User.findOne({ email });
|
|
528
619
|
if (existing) return res.status(409).json({ error: 'Email already in use' });
|
|
529
620
|
const hashed = await bcrypt.hash(password, 10);
|
|
530
|
-
const
|
|
531
|
-
const { password:
|
|
621
|
+
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
622
|
+
const user = await User.create({ name, email, password: hashed, verifyToken });
|
|
623
|
+
${sendVerifyEmail}
|
|
624
|
+
const { password: _, verifyToken: __, ...safe } = user;
|
|
532
625
|
res.status(201).json({ message: 'Account created successfully', user: safe });
|
|
533
626
|
};
|
|
534
627
|
` : `${registerDbImports}${registerMeta}export const POST = async (req, res) => {
|
|
@@ -539,8 +632,10 @@ ${registerDbImports}${registerMeta}export const POST = async (req: Request, res:
|
|
|
539
632
|
const existing = await User.findOne({ email });
|
|
540
633
|
if (existing) return res.status(409).json({ error: 'Email already in use' });
|
|
541
634
|
const hashed = await bcrypt.hash(password, 10);
|
|
542
|
-
const
|
|
543
|
-
const { password:
|
|
635
|
+
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
636
|
+
const user = await User.create({ name, email, password: hashed, verifyToken });
|
|
637
|
+
${sendVerifyEmail}
|
|
638
|
+
const { password: _, verifyToken: __, ...safe } = user;
|
|
544
639
|
res.status(201).json({ message: 'Account created successfully', user: safe });
|
|
545
640
|
};
|
|
546
641
|
` : ts ? `import type { Request, Response } from 'express';
|
|
@@ -564,68 +659,42 @@ ${registerMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
564
659
|
const meMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
565
660
|
|
|
566
661
|
export const meta: RouteMeta = {
|
|
567
|
-
|
|
568
|
-
|
|
662
|
+
GET: {
|
|
663
|
+
description: 'Return the currently authenticated user.',
|
|
664
|
+
response: { status: 200, body: { user: { id: '1', role: 'user', email: 'user@example.com' } } },
|
|
665
|
+
},
|
|
569
666
|
};
|
|
570
667
|
|
|
571
668
|
` : `export const meta = {
|
|
572
|
-
|
|
573
|
-
|
|
669
|
+
GET: {
|
|
670
|
+
description: 'Return the currently authenticated user.',
|
|
671
|
+
response: { status: 200, body: { user: { id: '1', role: 'user', email: 'user@example.com' } } },
|
|
672
|
+
},
|
|
574
673
|
};
|
|
575
674
|
|
|
576
675
|
` : "";
|
|
577
|
-
const
|
|
578
|
-
` : `import { requireRole } from '../../middleware/requireRole.js';
|
|
579
|
-
` : "";
|
|
580
|
-
const meMiddlewares = opts.rbac ? `export const middlewares = [requireAuth, requireRole('user', 'admin')];
|
|
676
|
+
const meMiddlewares = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
581
677
|
|
|
582
678
|
` : `export const middlewares = [requireAuth];
|
|
583
679
|
|
|
584
680
|
`;
|
|
585
681
|
const meContent = ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
586
|
-
|
|
682
|
+
import type { Request, Response } from 'express';
|
|
587
683
|
${meMeta}${meMiddlewares}export const GET = async (req: Request, res: Response) => {
|
|
588
684
|
res.json({ user: (req as any).user });
|
|
589
685
|
};
|
|
590
686
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
591
|
-
${
|
|
687
|
+
${meMeta}${meMiddlewares}export const GET = async (req, res) => {
|
|
592
688
|
res.json({ user: req.user });
|
|
593
689
|
};
|
|
594
690
|
`;
|
|
595
691
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `register.${ext}`), registerContent);
|
|
596
692
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `me.${ext}`), meContent);
|
|
597
693
|
}
|
|
598
|
-
async function writeRequireRoleMiddleware(dest, opts) {
|
|
599
|
-
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
600
|
-
const content = opts.language === "typescript" ? `import type { Request, Response, NextFunction } from 'express';
|
|
601
|
-
|
|
602
|
-
export function requireRole(...roles: string[]) {
|
|
603
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
604
|
-
const user = (req as any).user;
|
|
605
|
-
if (!user) return res.status(401).json({ error: 'Unauthorized' });
|
|
606
|
-
if (!roles.includes(user.role)) return res.status(403).json({ error: 'Forbidden' });
|
|
607
|
-
next();
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
` : `export function requireRole(...roles) {
|
|
611
|
-
return (req, res, next) => {
|
|
612
|
-
const user = req.user;
|
|
613
|
-
if (!user) return res.status(401).json({ error: 'Unauthorized' });
|
|
614
|
-
if (!roles.includes(user.role)) return res.status(403).json({ error: 'Forbidden' });
|
|
615
|
-
next();
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
`;
|
|
619
|
-
await fs.outputFile(path.join(dest, "src", "middleware", `requireRole.${ext}`), content);
|
|
620
|
-
}
|
|
621
694
|
async function writeAdminRoutes(dest, opts) {
|
|
622
695
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
623
696
|
const ts = opts.language === "typescript";
|
|
624
|
-
const
|
|
625
|
-
` : "";
|
|
626
|
-
const usersRequireRoleImport = opts.rbac ? `import { requireRole } from '../../../middleware/requireRole.js';
|
|
627
|
-
` : "";
|
|
628
|
-
const middlewares = opts.rbac ? `export const middlewares = [requireAuth, requireRole('admin')];
|
|
697
|
+
const middlewares = opts.rbac ? `export const middlewares = [requireAuth('admin')];
|
|
629
698
|
` : `export const middlewares = [requireAuth];
|
|
630
699
|
`;
|
|
631
700
|
const roleGuard = opts.rbac ? "" : ts ? ` const user = (req as any).user;
|
|
@@ -642,20 +711,24 @@ async function writeAdminRoutes(dest, opts) {
|
|
|
642
711
|
const dashboardMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
643
712
|
|
|
644
713
|
export const meta: RouteMeta = {
|
|
645
|
-
|
|
646
|
-
|
|
714
|
+
GET: {
|
|
715
|
+
description: 'Admin dashboard stats. Requires admin role.',
|
|
716
|
+
response: { status: 200, body: { stats: { totalUsers: 120, activeUsers: 98, verifiedUsers: 84 } } },
|
|
717
|
+
},
|
|
647
718
|
};
|
|
648
719
|
|
|
649
720
|
` : `export const meta = {
|
|
650
|
-
|
|
651
|
-
|
|
721
|
+
GET: {
|
|
722
|
+
description: 'Admin dashboard stats. Requires admin role.',
|
|
723
|
+
response: { status: 200, body: { stats: { totalUsers: 120, activeUsers: 98, verifiedUsers: 84 } } },
|
|
724
|
+
},
|
|
652
725
|
};
|
|
653
726
|
|
|
654
727
|
` : "";
|
|
655
728
|
const dashboardDbImport = opts.database === "mongodb" ? `import { User } from '../../model/User.js';
|
|
656
729
|
` : "";
|
|
657
730
|
const dashboardContent = opts.database === "mongodb" ? ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
658
|
-
|
|
731
|
+
import type { Request, Response } from 'express';
|
|
659
732
|
${dashboardDbImport}${dashboardMeta}${middlewares}
|
|
660
733
|
export const GET = async (_req: Request, res: Response) => {
|
|
661
734
|
${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all([
|
|
@@ -666,7 +739,7 @@ ${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all
|
|
|
666
739
|
res.json({ stats: { totalUsers, activeUsers, verifiedUsers } });
|
|
667
740
|
};
|
|
668
741
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
669
|
-
${
|
|
742
|
+
${dashboardDbImport}${dashboardMeta}${middlewares}
|
|
670
743
|
export const GET = async (_req, res) => {
|
|
671
744
|
${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all([
|
|
672
745
|
User.count({}),
|
|
@@ -676,14 +749,14 @@ ${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all
|
|
|
676
749
|
res.json({ stats: { totalUsers, activeUsers, verifiedUsers } });
|
|
677
750
|
};
|
|
678
751
|
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
679
|
-
|
|
752
|
+
import type { Request, Response } from 'express';
|
|
680
753
|
${dashboardMeta}${middlewares}
|
|
681
754
|
export const GET = async (_req: Request, res: Response) => {
|
|
682
755
|
${roleGuard} // TODO: aggregate stats from DB
|
|
683
756
|
res.json({ stats: { users: 0 } });
|
|
684
757
|
};
|
|
685
758
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
686
|
-
${
|
|
759
|
+
${dashboardMeta}${middlewares}
|
|
687
760
|
export const GET = async (_req, res) => {
|
|
688
761
|
${roleGuard} // TODO: aggregate stats from DB
|
|
689
762
|
res.json({ stats: { users: 0 } });
|
|
@@ -693,13 +766,29 @@ ${roleGuard} // TODO: aggregate stats from DB
|
|
|
693
766
|
const usersListMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
694
767
|
|
|
695
768
|
export const meta: RouteMeta = {
|
|
696
|
-
|
|
697
|
-
|
|
769
|
+
GET: {
|
|
770
|
+
description: 'List all users, paginated (admin only).',
|
|
771
|
+
request: { query: { page: '1', limit: '20' } },
|
|
772
|
+
response: { status: 200, body: { users: [], total: 0, page: 1, limit: 20 } },
|
|
773
|
+
},
|
|
774
|
+
POST: {
|
|
775
|
+
description: 'Create a new user account (admin only).',
|
|
776
|
+
request: { body: { name: 'Jane Doe', email: 'jane@example.com', password: 'secret', role: 'user' } },
|
|
777
|
+
response: { status: 201, body: { message: 'User created', user: { id: 'new-id', name: 'Jane Doe', email: 'jane@example.com', role: 'user' } } },
|
|
778
|
+
},
|
|
698
779
|
};
|
|
699
780
|
|
|
700
781
|
` : `export const meta = {
|
|
701
|
-
|
|
702
|
-
|
|
782
|
+
GET: {
|
|
783
|
+
description: 'List all users, paginated (admin only).',
|
|
784
|
+
request: { query: { page: '1', limit: '20' } },
|
|
785
|
+
response: { status: 200, body: { users: [], total: 0, page: 1, limit: 20 } },
|
|
786
|
+
},
|
|
787
|
+
POST: {
|
|
788
|
+
description: 'Create a new user account (admin only).',
|
|
789
|
+
request: { body: { name: 'Jane Doe', email: 'jane@example.com', password: 'secret', role: 'user' } },
|
|
790
|
+
response: { status: 201, body: { message: 'User created', user: { id: 'new-id', name: 'Jane Doe', email: 'jane@example.com', role: 'user' } } },
|
|
791
|
+
},
|
|
703
792
|
};
|
|
704
793
|
|
|
705
794
|
` : "";
|
|
@@ -709,7 +798,7 @@ import { User } from '../../../model/User.js';
|
|
|
709
798
|
import { User } from '../../../model/User.js';
|
|
710
799
|
` : "";
|
|
711
800
|
const usersListContent = opts.database === "mongodb" ? ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
712
|
-
|
|
801
|
+
import type { Request, Response } from 'express';
|
|
713
802
|
${adminUsersDbImport}${usersListMeta}${middlewares}
|
|
714
803
|
export const GET = async (req: Request, res: Response) => {
|
|
715
804
|
${roleGuard} const page = Math.max(1, Number(req.query.page) || 1);
|
|
@@ -730,7 +819,7 @@ ${roleGuard} const { name, email, password, role } = req.body;
|
|
|
730
819
|
res.status(201).json({ message: 'User created', user: safe });
|
|
731
820
|
};
|
|
732
821
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
733
|
-
${
|
|
822
|
+
${adminUsersDbImport}${usersListMeta}${middlewares}
|
|
734
823
|
export const GET = async (req, res) => {
|
|
735
824
|
${roleGuard} const page = Math.max(1, Number(req.query.page) || 1);
|
|
736
825
|
const limit = Math.min(100, Number(req.query.limit) || 20);
|
|
@@ -750,7 +839,7 @@ ${roleGuard} const { name, email, password, role } = req.body;
|
|
|
750
839
|
res.status(201).json({ message: 'User created', user: safe });
|
|
751
840
|
};
|
|
752
841
|
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
753
|
-
|
|
842
|
+
import type { Request, Response } from 'express';
|
|
754
843
|
${usersListMeta}${middlewares}
|
|
755
844
|
export const GET = async (_req: Request, res: Response) => {
|
|
756
845
|
${roleGuard} // TODO: fetch users from DB with pagination
|
|
@@ -764,7 +853,7 @@ ${roleGuard} const { name, email, role } = req.body;
|
|
|
764
853
|
res.status(201).json({ message: 'User created', user: { id: 'new-id', name, email, role: role ?? 'user' } });
|
|
765
854
|
};
|
|
766
855
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
767
|
-
${
|
|
856
|
+
${usersListMeta}${middlewares}
|
|
768
857
|
export const GET = async (_req, res) => {
|
|
769
858
|
${roleGuard} // TODO: fetch users from DB with pagination
|
|
770
859
|
res.json({ users: [], total: 0 });
|
|
@@ -781,18 +870,46 @@ ${roleGuard} const { name, email, role } = req.body;
|
|
|
781
870
|
const userByIdMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
782
871
|
|
|
783
872
|
export const meta: RouteMeta = {
|
|
784
|
-
|
|
873
|
+
GET: {
|
|
874
|
+
description: 'Fetch a single user by ID (admin only).',
|
|
875
|
+
request: { params: { id: 'usr_01HXZ' } },
|
|
876
|
+
response: { status: 200, body: { user: { id: 'usr_01HXZ', name: 'Jane Doe', email: 'jane@example.com', role: 'user' } } },
|
|
877
|
+
},
|
|
878
|
+
PUT: {
|
|
879
|
+
description: 'Update a user by ID (admin only).',
|
|
880
|
+
request: { params: { id: 'usr_01HXZ' }, body: { name: 'Jane Doe', email: 'jane@example.com', role: 'user', isActive: true } },
|
|
881
|
+
response: { status: 200, body: { message: 'User updated', user: { id: 'usr_01HXZ', name: 'Jane Doe', email: 'jane@example.com', role: 'user' } } },
|
|
882
|
+
},
|
|
883
|
+
DELETE: {
|
|
884
|
+
description: 'Delete a user by ID (admin only).',
|
|
885
|
+
request: { params: { id: 'usr_01HXZ' } },
|
|
886
|
+
response: { status: 200, body: { message: 'User usr_01HXZ deleted' } },
|
|
887
|
+
},
|
|
785
888
|
};
|
|
786
889
|
|
|
787
890
|
` : `export const meta = {
|
|
788
|
-
|
|
891
|
+
GET: {
|
|
892
|
+
description: 'Fetch a single user by ID (admin only).',
|
|
893
|
+
request: { params: { id: 'usr_01HXZ' } },
|
|
894
|
+
response: { status: 200, body: { user: { id: 'usr_01HXZ', name: 'Jane Doe', email: 'jane@example.com', role: 'user' } } },
|
|
895
|
+
},
|
|
896
|
+
PUT: {
|
|
897
|
+
description: 'Update a user by ID (admin only).',
|
|
898
|
+
request: { params: { id: 'usr_01HXZ' }, body: { name: 'Jane Doe', email: 'jane@example.com', role: 'user', isActive: true } },
|
|
899
|
+
response: { status: 200, body: { message: 'User updated', user: { id: 'usr_01HXZ', name: 'Jane Doe', email: 'jane@example.com', role: 'user' } } },
|
|
900
|
+
},
|
|
901
|
+
DELETE: {
|
|
902
|
+
description: 'Delete a user by ID (admin only).',
|
|
903
|
+
request: { params: { id: 'usr_01HXZ' } },
|
|
904
|
+
response: { status: 200, body: { message: 'User usr_01HXZ deleted' } },
|
|
905
|
+
},
|
|
789
906
|
};
|
|
790
907
|
|
|
791
908
|
` : "";
|
|
792
909
|
const adminUserByIdDbImport = opts.database === "mongodb" ? `import { User } from '../../../model/User.js';
|
|
793
910
|
` : "";
|
|
794
911
|
const userByIdContent = opts.database === "mongodb" ? ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
795
|
-
|
|
912
|
+
import type { Request, Response } from 'express';
|
|
796
913
|
${adminUserByIdDbImport}${userByIdMeta}${middlewares}
|
|
797
914
|
export const GET = async (req: Request, res: Response) => {
|
|
798
915
|
${roleGuard} const { id } = req.params;
|
|
@@ -819,7 +936,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
819
936
|
res.json({ message: \`User \${id} deleted\` });
|
|
820
937
|
};
|
|
821
938
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
822
|
-
${
|
|
939
|
+
${adminUserByIdDbImport}${userByIdMeta}${middlewares}
|
|
823
940
|
export const GET = async (req, res) => {
|
|
824
941
|
${roleGuard} const { id } = req.params;
|
|
825
942
|
const user = await User.findById(id);
|
|
@@ -845,7 +962,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
845
962
|
res.json({ message: \`User \${id} deleted\` });
|
|
846
963
|
};
|
|
847
964
|
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
848
|
-
|
|
965
|
+
import type { Request, Response } from 'express';
|
|
849
966
|
${userByIdMeta}${middlewares}
|
|
850
967
|
export const GET = async (req: Request, res: Response) => {
|
|
851
968
|
${roleGuard} const { id } = req.params;
|
|
@@ -865,7 +982,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
865
982
|
res.json({ message: \`User \${id} deleted\` });
|
|
866
983
|
};
|
|
867
984
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
868
|
-
${
|
|
985
|
+
${userByIdMeta}${middlewares}
|
|
869
986
|
export const GET = async (req, res) => {
|
|
870
987
|
${roleGuard} const { id } = req.params;
|
|
871
988
|
// TODO: fetch user from DB
|
|
@@ -889,28 +1006,40 @@ ${roleGuard} const { id } = req.params;
|
|
|
889
1006
|
async function writeUserRoutes(dest, opts) {
|
|
890
1007
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
891
1008
|
const ts = opts.language === "typescript";
|
|
892
|
-
const
|
|
893
|
-
` : "";
|
|
894
|
-
const middlewares = opts.rbac ? `export const middlewares = [requireAuth, requireRole('user', 'admin')];
|
|
1009
|
+
const middlewares = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
895
1010
|
` : `export const middlewares = [requireAuth];
|
|
896
1011
|
`;
|
|
897
1012
|
const profileMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
898
1013
|
|
|
899
1014
|
export const meta: RouteMeta = {
|
|
900
|
-
|
|
901
|
-
|
|
1015
|
+
GET: {
|
|
1016
|
+
description: "Fetch the authenticated user's profile.",
|
|
1017
|
+
response: { status: 200, body: { user: { id: '1', role: 'user', email: 'user@example.com' } } },
|
|
1018
|
+
},
|
|
1019
|
+
PUT: {
|
|
1020
|
+
description: "Update the authenticated user's profile.",
|
|
1021
|
+
request: { body: { name: 'Jane Doe', email: 'jane@example.com' } },
|
|
1022
|
+
response: { status: 200, body: { message: 'Profile updated', user: { id: '1', role: 'user', email: 'jane@example.com' } } },
|
|
1023
|
+
},
|
|
902
1024
|
};
|
|
903
1025
|
|
|
904
1026
|
` : `export const meta = {
|
|
905
|
-
|
|
906
|
-
|
|
1027
|
+
GET: {
|
|
1028
|
+
description: "Fetch the authenticated user's profile.",
|
|
1029
|
+
response: { status: 200, body: { user: { id: '1', role: 'user', email: 'user@example.com' } } },
|
|
1030
|
+
},
|
|
1031
|
+
PUT: {
|
|
1032
|
+
description: "Update the authenticated user's profile.",
|
|
1033
|
+
request: { body: { name: 'Jane Doe', email: 'jane@example.com' } },
|
|
1034
|
+
response: { status: 200, body: { message: 'Profile updated', user: { id: '1', role: 'user', email: 'jane@example.com' } } },
|
|
1035
|
+
},
|
|
907
1036
|
};
|
|
908
1037
|
|
|
909
1038
|
` : "";
|
|
910
1039
|
const profileDbImport = opts.database === "mongodb" ? `import { User } from '../../model/User.js';
|
|
911
1040
|
` : "";
|
|
912
1041
|
const profileContent = opts.database === "mongodb" ? ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
913
|
-
|
|
1042
|
+
import type { Request, Response } from 'express';
|
|
914
1043
|
${profileDbImport}${profileMeta}${middlewares}
|
|
915
1044
|
export const GET = async (req: Request, res: Response) => {
|
|
916
1045
|
const { id } = (req as any).user;
|
|
@@ -929,7 +1058,7 @@ export const PUT = async (req: Request, res: Response) => {
|
|
|
929
1058
|
res.json({ message: 'Profile updated', user: safe });
|
|
930
1059
|
};
|
|
931
1060
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
932
|
-
${
|
|
1061
|
+
${profileDbImport}${profileMeta}${middlewares}
|
|
933
1062
|
export const GET = async (req, res) => {
|
|
934
1063
|
const { id } = req.user;
|
|
935
1064
|
const user = await User.findById(id);
|
|
@@ -947,7 +1076,7 @@ export const PUT = async (req, res) => {
|
|
|
947
1076
|
res.json({ message: 'Profile updated', user: safe });
|
|
948
1077
|
};
|
|
949
1078
|
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
950
|
-
|
|
1079
|
+
import type { Request, Response } from 'express';
|
|
951
1080
|
${profileMeta}${middlewares}
|
|
952
1081
|
export const GET = async (req: Request, res: Response) => {
|
|
953
1082
|
res.json({ user: (req as any).user });
|
|
@@ -959,7 +1088,7 @@ export const PUT = async (req: Request, res: Response) => {
|
|
|
959
1088
|
res.json({ message: 'Profile updated', user: { ...(req as any).user, name, email } });
|
|
960
1089
|
};
|
|
961
1090
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
962
|
-
${
|
|
1091
|
+
${profileMeta}${middlewares}
|
|
963
1092
|
export const GET = async (req, res) => {
|
|
964
1093
|
res.json({ user: req.user });
|
|
965
1094
|
};
|
|
@@ -972,20 +1101,32 @@ export const PUT = async (req, res) => {
|
|
|
972
1101
|
`;
|
|
973
1102
|
await fs.outputFile(path.join(dest, "src", "api", "user", `profile.${ext}`), profileContent);
|
|
974
1103
|
}
|
|
975
|
-
function mkMeta(opts,
|
|
1104
|
+
function mkMeta(opts, methods) {
|
|
976
1105
|
if (!opts.routeDocs) return "";
|
|
977
|
-
const
|
|
1106
|
+
const blocks = Object.entries(methods).map(([method, m]) => {
|
|
1107
|
+
const reqParts = [];
|
|
1108
|
+
if (m.params) reqParts.push(`params: ${m.params}`);
|
|
1109
|
+
if (m.query) reqParts.push(`query: ${m.query}`);
|
|
1110
|
+
if (m.request) reqParts.push(`body: ${m.request}`);
|
|
1111
|
+
const reqLine = reqParts.length ? ` request: { ${reqParts.join(", ")} },
|
|
978
1112
|
` : "";
|
|
1113
|
+
const status = m.status ?? 200;
|
|
1114
|
+
const respLine = m.response !== void 0 ? ` response: { status: ${status}, body: ${m.response} },
|
|
1115
|
+
` : ` response: { status: ${status} },
|
|
1116
|
+
`;
|
|
1117
|
+
const description = m.description.replace(/'/g, "\\'");
|
|
1118
|
+
return ` ${method}: {
|
|
1119
|
+
description: '${description}',
|
|
1120
|
+
${reqLine}${respLine} },`;
|
|
1121
|
+
}).join("\n");
|
|
979
1122
|
return opts.language === "typescript" ? `import type { RouteMeta } from 'express-file-cluster';
|
|
980
1123
|
|
|
981
1124
|
export const meta: RouteMeta = {
|
|
982
|
-
|
|
983
|
-
${reqLine} response: { status: 200, body: ${body} },
|
|
1125
|
+
${blocks}
|
|
984
1126
|
};
|
|
985
1127
|
|
|
986
1128
|
` : `export const meta = {
|
|
987
|
-
|
|
988
|
-
${reqLine} response: { status: 200, body: ${body} },
|
|
1129
|
+
${blocks}
|
|
989
1130
|
};
|
|
990
1131
|
|
|
991
1132
|
`;
|
|
@@ -1700,11 +1841,7 @@ export {};
|
|
|
1700
1841
|
async function writeAuthExtendedRoutes(dest, opts) {
|
|
1701
1842
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1702
1843
|
const ts = opts.language === "typescript";
|
|
1703
|
-
const
|
|
1704
|
-
` : "";
|
|
1705
|
-
const rr3 = opts.rbac ? `import { requireRole } from '../../../middleware/requireRole.js';
|
|
1706
|
-
` : "";
|
|
1707
|
-
const mwUser2 = opts.rbac ? `export const middlewares = [requireAuth, requireRole('user', 'admin')];
|
|
1844
|
+
const mwUser2 = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
1708
1845
|
` : `export const middlewares = [requireAuth];
|
|
1709
1846
|
`;
|
|
1710
1847
|
const mwUser3 = mwUser2;
|
|
@@ -1712,8 +1849,96 @@ async function writeAuthExtendedRoutes(dest, opts) {
|
|
|
1712
1849
|
` : "";
|
|
1713
1850
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
1714
1851
|
`;
|
|
1715
|
-
const
|
|
1716
|
-
const
|
|
1852
|
+
const mongo = opts.database === "mongodb";
|
|
1853
|
+
const sendResetEmail = opts.mailer ? ` var appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
1854
|
+
await enqueue('SendEmail', {
|
|
1855
|
+
to: email,
|
|
1856
|
+
subject: 'Reset your password',
|
|
1857
|
+
body: 'Reset your password: ' + appUrl + '/auth/reset-password?token=' + resetToken,
|
|
1858
|
+
});
|
|
1859
|
+
` : ` // TODO: email this token to the user \u2014 enable the Mailer feature to auto-wire SendEmail
|
|
1860
|
+
`;
|
|
1861
|
+
const sendVerifyEmailResend = opts.mailer ? ` var appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
1862
|
+
await enqueue('SendEmail', {
|
|
1863
|
+
to: email,
|
|
1864
|
+
subject: 'Verify your email address',
|
|
1865
|
+
body: 'Verify your email: ' + appUrl + '/auth/verify-email?token=' + verifyToken,
|
|
1866
|
+
});
|
|
1867
|
+
` : ` // TODO: email this token to the user \u2014 enable the Mailer feature to auto-wire SendEmail
|
|
1868
|
+
`;
|
|
1869
|
+
const mailerTaskImport = opts.mailer ? `import { enqueue } from 'express-file-cluster/tasks';
|
|
1870
|
+
` : "";
|
|
1871
|
+
const refreshMeta = mkMeta(opts, {
|
|
1872
|
+
POST: { description: "Refresh the JWT using the refresh-token cookie and issue a new access token.", response: `{ message: 'Token refreshed' }` }
|
|
1873
|
+
});
|
|
1874
|
+
const refreshContent = mongo ? ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
1875
|
+
import type { Request, Response } from 'express';
|
|
1876
|
+
import crypto from 'node:crypto';
|
|
1877
|
+
import { User } from '../../model/User.js';
|
|
1878
|
+
import { Admin } from '../../model/Admin.js';
|
|
1879
|
+
${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
1880
|
+
|
|
1881
|
+
export const POST = async (req: Request, res: Response) => {
|
|
1882
|
+
const token = req.cookies?.['efc_refresh_token'] || req.body?.refreshToken;
|
|
1883
|
+
if (!token) return res.status(401).json({ error: 'Refresh token required' });
|
|
1884
|
+
|
|
1885
|
+
const user = await User.findOne({ refreshToken: token });
|
|
1886
|
+
const admin = user ? null : await Admin.findOne({ refreshToken: token });
|
|
1887
|
+
const account = user || admin;
|
|
1888
|
+
|
|
1889
|
+
if (!account || !account.refreshTokenExpiry || new Date(account.refreshTokenExpiry) < new Date()) {
|
|
1890
|
+
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const newRefreshToken = crypto.randomBytes(40).toString('hex');
|
|
1894
|
+
const refreshTokenExpiry = new Date(Date.now() + REFRESH_TOKEN_TTL_MS);
|
|
1895
|
+
if (user) await User.update(user.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1896
|
+
else if (admin) await Admin.update(admin.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1897
|
+
|
|
1898
|
+
res.cookie('efc_refresh_token', newRefreshToken, {
|
|
1899
|
+
httpOnly: true,
|
|
1900
|
+
secure: process.env.NODE_ENV === 'production',
|
|
1901
|
+
sameSite: 'strict',
|
|
1902
|
+
maxAge: REFRESH_TOKEN_TTL_MS,
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
await issueToken(res, { id: account.id, role: account.role, email: account.email });
|
|
1906
|
+
res.json({ message: 'Token refreshed' });
|
|
1907
|
+
};
|
|
1908
|
+
` : `import { issueToken } from 'express-file-cluster/auth';
|
|
1909
|
+
import crypto from 'node:crypto';
|
|
1910
|
+
import { User } from '../../model/User.js';
|
|
1911
|
+
import { Admin } from '../../model/Admin.js';
|
|
1912
|
+
${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
1913
|
+
|
|
1914
|
+
export const POST = async (req, res) => {
|
|
1915
|
+
const token = req.cookies?.efc_refresh_token || req.body?.refreshToken;
|
|
1916
|
+
if (!token) return res.status(401).json({ error: 'Refresh token required' });
|
|
1917
|
+
|
|
1918
|
+
const user = await User.findOne({ refreshToken: token });
|
|
1919
|
+
const admin = user ? null : await Admin.findOne({ refreshToken: token });
|
|
1920
|
+
const account = user || admin;
|
|
1921
|
+
|
|
1922
|
+
if (!account || !account.refreshTokenExpiry || new Date(account.refreshTokenExpiry) < new Date()) {
|
|
1923
|
+
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const newRefreshToken = crypto.randomBytes(40).toString('hex');
|
|
1927
|
+
const refreshTokenExpiry = new Date(Date.now() + REFRESH_TOKEN_TTL_MS);
|
|
1928
|
+
if (user) await User.update(user.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1929
|
+
else if (admin) await Admin.update(admin.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1930
|
+
|
|
1931
|
+
res.cookie('efc_refresh_token', newRefreshToken, {
|
|
1932
|
+
httpOnly: true,
|
|
1933
|
+
secure: process.env.NODE_ENV === 'production',
|
|
1934
|
+
sameSite: 'strict',
|
|
1935
|
+
maxAge: REFRESH_TOKEN_TTL_MS,
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
await issueToken(res, { id: account.id, role: account.role, email: account.email });
|
|
1939
|
+
res.json({ message: 'Token refreshed' });
|
|
1940
|
+
};
|
|
1941
|
+
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
1717
1942
|
${refreshMeta}export const POST = async (_req: Request, res: Response) => {
|
|
1718
1943
|
// TODO: validate refresh token, issue new JWT
|
|
1719
1944
|
res.json({ message: 'Token refreshed' });
|
|
@@ -1724,8 +1949,62 @@ ${refreshMeta}export const POST = async (_req: Request, res: Response) => {
|
|
|
1724
1949
|
};
|
|
1725
1950
|
`;
|
|
1726
1951
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `refresh.${ext}`), refreshContent);
|
|
1727
|
-
const veMeta = mkMeta(opts,
|
|
1728
|
-
|
|
1952
|
+
const veMeta = mkMeta(opts, {
|
|
1953
|
+
GET: { description: "Verify an email address using the token from the verification link.", query: `{ token: 'a1b2c3d4' }`, response: `{ message: 'Email verified' }` },
|
|
1954
|
+
POST: { description: "Resend the verification email to a given address.", request: `{ email: 'user@example.com' }`, response: `{ message: 'Verification email sent' }` }
|
|
1955
|
+
});
|
|
1956
|
+
const veContent = mongo ? ts ? `import type { Request, Response } from 'express';
|
|
1957
|
+
import crypto from 'node:crypto';
|
|
1958
|
+
${mailerTaskImport}import { User } from '../../model/User.js';
|
|
1959
|
+
${veMeta}export const GET = async (req: Request, res: Response) => {
|
|
1960
|
+
const { token } = req.query;
|
|
1961
|
+
if (!token || typeof token !== 'string') return res.status(400).json({ error: 'token is required' });
|
|
1962
|
+
|
|
1963
|
+
const user = await User.findOne({ verifyToken: token });
|
|
1964
|
+
if (!user) return res.status(400).json({ error: 'Invalid or expired verification token' });
|
|
1965
|
+
|
|
1966
|
+
await User.update(user.id, { isVerified: true, verifyToken: '' });
|
|
1967
|
+
res.json({ message: 'Email verified' });
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
export const POST = async (req: Request, res: Response) => {
|
|
1971
|
+
const { email } = req.body;
|
|
1972
|
+
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
1973
|
+
|
|
1974
|
+
const user = await User.findOne({ email });
|
|
1975
|
+
if (user && !user.isVerified) {
|
|
1976
|
+
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
1977
|
+
await User.update(user.id, { verifyToken });
|
|
1978
|
+
${sendVerifyEmailResend} }
|
|
1979
|
+
|
|
1980
|
+
res.json({ message: 'Verification email sent' });
|
|
1981
|
+
};
|
|
1982
|
+
` : `import crypto from 'node:crypto';
|
|
1983
|
+
${mailerTaskImport}import { User } from '../../model/User.js';
|
|
1984
|
+
${veMeta}export const GET = async (req, res) => {
|
|
1985
|
+
const { token } = req.query;
|
|
1986
|
+
if (!token || typeof token !== 'string') return res.status(400).json({ error: 'token is required' });
|
|
1987
|
+
|
|
1988
|
+
const user = await User.findOne({ verifyToken: token });
|
|
1989
|
+
if (!user) return res.status(400).json({ error: 'Invalid or expired verification token' });
|
|
1990
|
+
|
|
1991
|
+
await User.update(user.id, { isVerified: true, verifyToken: '' });
|
|
1992
|
+
res.json({ message: 'Email verified' });
|
|
1993
|
+
};
|
|
1994
|
+
|
|
1995
|
+
export const POST = async (req, res) => {
|
|
1996
|
+
const { email } = req.body;
|
|
1997
|
+
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
1998
|
+
|
|
1999
|
+
const user = await User.findOne({ email });
|
|
2000
|
+
if (user && !user.isVerified) {
|
|
2001
|
+
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
2002
|
+
await User.update(user.id, { verifyToken });
|
|
2003
|
+
${sendVerifyEmailResend} }
|
|
2004
|
+
|
|
2005
|
+
res.json({ message: 'Verification email sent' });
|
|
2006
|
+
};
|
|
2007
|
+
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
1729
2008
|
${veMeta}export const GET = async (req: Request, res: Response) => {
|
|
1730
2009
|
const { token } = req.query;
|
|
1731
2010
|
// TODO: verify token and mark user as verified
|
|
@@ -1748,8 +2027,57 @@ export const POST = async (_req, res) => {
|
|
|
1748
2027
|
};
|
|
1749
2028
|
`;
|
|
1750
2029
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `verify-email.${ext}`), veContent);
|
|
1751
|
-
const fpMeta = mkMeta(opts,
|
|
1752
|
-
|
|
2030
|
+
const fpMeta = mkMeta(opts, {
|
|
2031
|
+
POST: { description: "Send a password reset email to the given address.", request: `{ email: 'user@example.com' }`, response: `{ message: 'Reset email sent' }` }
|
|
2032
|
+
});
|
|
2033
|
+
const fpContent = mongo ? ts ? `import type { Request, Response } from 'express';
|
|
2034
|
+
import crypto from 'node:crypto';
|
|
2035
|
+
${mailerTaskImport}import { User } from '../../model/User.js';
|
|
2036
|
+
import { Admin } from '../../model/Admin.js';
|
|
2037
|
+
${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2038
|
+
|
|
2039
|
+
export const POST = async (req: Request, res: Response) => {
|
|
2040
|
+
const { email } = req.body;
|
|
2041
|
+
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
2042
|
+
|
|
2043
|
+
const user = await User.findOne({ email });
|
|
2044
|
+
const admin = user ? null : await Admin.findOne({ email });
|
|
2045
|
+
|
|
2046
|
+
// Always respond the same way whether or not the account exists, so this
|
|
2047
|
+
// endpoint can't be used to enumerate registered emails.
|
|
2048
|
+
if (user || admin) {
|
|
2049
|
+
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
2050
|
+
const resetTokenExpiry = new Date(Date.now() + RESET_TOKEN_TTL_MS);
|
|
2051
|
+
if (user) await User.update(user.id, { resetToken, resetTokenExpiry });
|
|
2052
|
+
else if (admin) await Admin.update(admin.id, { resetToken, resetTokenExpiry });
|
|
2053
|
+
${sendResetEmail} }
|
|
2054
|
+
|
|
2055
|
+
res.json({ message: 'Reset email sent' });
|
|
2056
|
+
};
|
|
2057
|
+
` : `import crypto from 'node:crypto';
|
|
2058
|
+
${mailerTaskImport}import { User } from '../../model/User.js';
|
|
2059
|
+
import { Admin } from '../../model/Admin.js';
|
|
2060
|
+
${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2061
|
+
|
|
2062
|
+
export const POST = async (req, res) => {
|
|
2063
|
+
const { email } = req.body;
|
|
2064
|
+
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
2065
|
+
|
|
2066
|
+
const user = await User.findOne({ email });
|
|
2067
|
+
const admin = user ? null : await Admin.findOne({ email });
|
|
2068
|
+
|
|
2069
|
+
// Always respond the same way whether or not the account exists, so this
|
|
2070
|
+
// endpoint can't be used to enumerate registered emails.
|
|
2071
|
+
if (user || admin) {
|
|
2072
|
+
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
2073
|
+
const resetTokenExpiry = new Date(Date.now() + RESET_TOKEN_TTL_MS);
|
|
2074
|
+
if (user) await User.update(user.id, { resetToken, resetTokenExpiry });
|
|
2075
|
+
else if (admin) await Admin.update(admin.id, { resetToken, resetTokenExpiry });
|
|
2076
|
+
${sendResetEmail} }
|
|
2077
|
+
|
|
2078
|
+
res.json({ message: 'Reset email sent' });
|
|
2079
|
+
};
|
|
2080
|
+
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
1753
2081
|
${fpMeta}export const POST = async (req: Request, res: Response) => {
|
|
1754
2082
|
const { email } = req.body;
|
|
1755
2083
|
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
@@ -1764,8 +2092,53 @@ ${fpMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
1764
2092
|
};
|
|
1765
2093
|
`;
|
|
1766
2094
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `forgot-password.${ext}`), fpContent);
|
|
1767
|
-
const rpMeta = mkMeta(opts,
|
|
1768
|
-
|
|
2095
|
+
const rpMeta = mkMeta(opts, {
|
|
2096
|
+
POST: { description: "Reset password using a valid reset token.", request: `{ token: 'reset-token', password: 'newpassword' }`, response: `{ message: 'Password reset successfully' }` }
|
|
2097
|
+
});
|
|
2098
|
+
const rpContent = mongo ? ts ? `import type { Request, Response } from 'express';
|
|
2099
|
+
import bcrypt from 'bcrypt';
|
|
2100
|
+
import { User } from '../../model/User.js';
|
|
2101
|
+
import { Admin } from '../../model/Admin.js';
|
|
2102
|
+
${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
2103
|
+
const { token, password } = req.body;
|
|
2104
|
+
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
2105
|
+
|
|
2106
|
+
const user = await User.findOne({ resetToken: token });
|
|
2107
|
+
const admin = user ? null : await Admin.findOne({ resetToken: token });
|
|
2108
|
+
const account = user || admin;
|
|
2109
|
+
|
|
2110
|
+
if (!account || !account.resetTokenExpiry || new Date(account.resetTokenExpiry) < new Date()) {
|
|
2111
|
+
return res.status(400).json({ error: 'Invalid or expired reset token' });
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const hashed = await bcrypt.hash(password, 10);
|
|
2115
|
+
if (user) await User.update(user.id, { password: hashed, resetToken: '' });
|
|
2116
|
+
else if (admin) await Admin.update(admin.id, { password: hashed, resetToken: '' });
|
|
2117
|
+
|
|
2118
|
+
res.json({ message: 'Password reset successfully' });
|
|
2119
|
+
};
|
|
2120
|
+
` : `import bcrypt from 'bcrypt';
|
|
2121
|
+
import { User } from '../../model/User.js';
|
|
2122
|
+
import { Admin } from '../../model/Admin.js';
|
|
2123
|
+
${rpMeta}export const POST = async (req, res) => {
|
|
2124
|
+
const { token, password } = req.body;
|
|
2125
|
+
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
2126
|
+
|
|
2127
|
+
const user = await User.findOne({ resetToken: token });
|
|
2128
|
+
const admin = user ? null : await Admin.findOne({ resetToken: token });
|
|
2129
|
+
const account = user || admin;
|
|
2130
|
+
|
|
2131
|
+
if (!account || !account.resetTokenExpiry || new Date(account.resetTokenExpiry) < new Date()) {
|
|
2132
|
+
return res.status(400).json({ error: 'Invalid or expired reset token' });
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
const hashed = await bcrypt.hash(password, 10);
|
|
2136
|
+
if (user) await User.update(user.id, { password: hashed, resetToken: '' });
|
|
2137
|
+
else if (admin) await Admin.update(admin.id, { password: hashed, resetToken: '' });
|
|
2138
|
+
|
|
2139
|
+
res.json({ message: 'Password reset successfully' });
|
|
2140
|
+
};
|
|
2141
|
+
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
1769
2142
|
${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
1770
2143
|
const { token, password } = req.body;
|
|
1771
2144
|
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
@@ -1780,8 +2153,10 @@ ${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
1780
2153
|
};
|
|
1781
2154
|
`;
|
|
1782
2155
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `reset-password.${ext}`), rpContent);
|
|
1783
|
-
const cpMeta = mkMeta(opts,
|
|
1784
|
-
|
|
2156
|
+
const cpMeta = mkMeta(opts, {
|
|
2157
|
+
POST: { description: "Change password for the authenticated user.", request: `{ oldPassword: 'current', newPassword: 'newpassword' }`, response: `{ message: 'Password changed successfully' }` }
|
|
2158
|
+
});
|
|
2159
|
+
const cpContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1785
2160
|
${cpMeta}${mwUser2}
|
|
1786
2161
|
export const POST = async (req: Request, res: Response) => {
|
|
1787
2162
|
const { oldPassword, newPassword } = req.body;
|
|
@@ -1789,7 +2164,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
1789
2164
|
// TODO: verify old password, hash and update new password
|
|
1790
2165
|
res.json({ message: 'Password changed successfully' });
|
|
1791
2166
|
};
|
|
1792
|
-
` : `${RA}${
|
|
2167
|
+
` : `${RA}${cpMeta}${mwUser2}
|
|
1793
2168
|
export const POST = async (req, res) => {
|
|
1794
2169
|
const { oldPassword, newPassword } = req.body;
|
|
1795
2170
|
if (!oldPassword || !newPassword) return res.status(400).json({ error: 'oldPassword and newPassword are required' });
|
|
@@ -1798,8 +2173,11 @@ export const POST = async (req, res) => {
|
|
|
1798
2173
|
};
|
|
1799
2174
|
`;
|
|
1800
2175
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `change-password.${ext}`), cpContent);
|
|
1801
|
-
const tfaSetupMeta = mkMeta(opts,
|
|
1802
|
-
|
|
2176
|
+
const tfaSetupMeta = mkMeta(opts, {
|
|
2177
|
+
GET: { description: "Generate a TOTP secret and QR code to set up 2FA.", response: `{ qrCode: 'otpauth://totp/...', secret: 'BASE32SECRET' }` },
|
|
2178
|
+
POST: { description: "Confirm a TOTP code to enable 2FA for the authenticated user.", request: `{ code: '123456' }`, response: `{ message: '2FA enabled' }` }
|
|
2179
|
+
});
|
|
2180
|
+
const tfaSetupContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1803
2181
|
${tfaSetupMeta}${mwUser3}
|
|
1804
2182
|
export const GET = async (req: Request, res: Response) => {
|
|
1805
2183
|
// TODO: generate TOTP secret and return QR code URL
|
|
@@ -1812,7 +2190,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
1812
2190
|
// TODO: verify TOTP code and enable 2FA
|
|
1813
2191
|
res.json({ message: '2FA enabled' });
|
|
1814
2192
|
};
|
|
1815
|
-
` : `${RA}${
|
|
2193
|
+
` : `${RA}${tfaSetupMeta}${mwUser3}
|
|
1816
2194
|
export const GET = async (req, res) => {
|
|
1817
2195
|
// TODO: generate TOTP secret and return QR code URL
|
|
1818
2196
|
res.json({ qrCode: 'otpauth://totp/...', secret: 'BASE32SECRET' });
|
|
@@ -1826,7 +2204,9 @@ export const POST = async (req, res) => {
|
|
|
1826
2204
|
};
|
|
1827
2205
|
`;
|
|
1828
2206
|
await fs.outputFile(path.join(dest, "src", "api", "auth", "2fa", `setup.${ext}`), tfaSetupContent);
|
|
1829
|
-
const tfaVerifyMeta = mkMeta(opts,
|
|
2207
|
+
const tfaVerifyMeta = mkMeta(opts, {
|
|
2208
|
+
POST: { description: "Verify a TOTP code during login.", request: `{ code: '123456' }`, response: `{ message: '2FA verified' }` }
|
|
2209
|
+
});
|
|
1830
2210
|
const tfaVerifyContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1831
2211
|
${tfaVerifyMeta}export const POST = async (req: Request, res: Response) => {
|
|
1832
2212
|
const { code } = req.body;
|
|
@@ -1842,8 +2222,10 @@ ${tfaVerifyMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
1842
2222
|
};
|
|
1843
2223
|
`;
|
|
1844
2224
|
await fs.outputFile(path.join(dest, "src", "api", "auth", "2fa", `verify.${ext}`), tfaVerifyContent);
|
|
1845
|
-
const tfaDisableMeta = mkMeta(opts,
|
|
1846
|
-
|
|
2225
|
+
const tfaDisableMeta = mkMeta(opts, {
|
|
2226
|
+
POST: { description: "Disable 2FA for the authenticated user.", request: `{ code: '123456' }`, response: `{ message: '2FA disabled' }` }
|
|
2227
|
+
});
|
|
2228
|
+
const tfaDisableContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1847
2229
|
${tfaDisableMeta}${mwUser3}
|
|
1848
2230
|
export const POST = async (req: Request, res: Response) => {
|
|
1849
2231
|
const { code } = req.body;
|
|
@@ -1851,7 +2233,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
1851
2233
|
// TODO: verify code and disable 2FA
|
|
1852
2234
|
res.json({ message: '2FA disabled' });
|
|
1853
2235
|
};
|
|
1854
|
-
` : `${RA}${
|
|
2236
|
+
` : `${RA}${tfaDisableMeta}${mwUser3}
|
|
1855
2237
|
export const POST = async (req, res) => {
|
|
1856
2238
|
const { code } = req.body;
|
|
1857
2239
|
if (!code) return res.status(400).json({ error: 'code is required' });
|
|
@@ -1860,28 +2242,33 @@ export const POST = async (req, res) => {
|
|
|
1860
2242
|
};
|
|
1861
2243
|
`;
|
|
1862
2244
|
await fs.outputFile(path.join(dest, "src", "api", "auth", "2fa", `disable.${ext}`), tfaDisableContent);
|
|
1863
|
-
const sessListMeta = mkMeta(opts,
|
|
1864
|
-
|
|
2245
|
+
const sessListMeta = mkMeta(opts, {
|
|
2246
|
+
GET: { description: "List all active sessions for the authenticated user.", response: `{ sessions: [] }` }
|
|
2247
|
+
});
|
|
2248
|
+
const sessListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1865
2249
|
${sessListMeta}${mwUser3}
|
|
1866
2250
|
export const GET = async (req: Request, res: Response) => {
|
|
1867
2251
|
// TODO: fetch sessions for req.user.id
|
|
1868
2252
|
res.json({ sessions: [] });
|
|
1869
2253
|
};
|
|
1870
|
-
` : `${RA}${
|
|
2254
|
+
` : `${RA}${sessListMeta}${mwUser3}
|
|
1871
2255
|
export const GET = async (req, res) => {
|
|
1872
2256
|
// TODO: fetch sessions for req.user.id
|
|
1873
2257
|
res.json({ sessions: [] });
|
|
1874
2258
|
};
|
|
1875
2259
|
`;
|
|
1876
2260
|
await fs.outputFile(path.join(dest, "src", "api", "auth", "sessions", `index.${ext}`), sessListContent);
|
|
1877
|
-
const
|
|
1878
|
-
|
|
2261
|
+
const sessRevokeMeta = mkMeta(opts, {
|
|
2262
|
+
DELETE: { description: "Revoke a single active session by ID.", params: `{ id: 'sess_01HXZ' }`, response: `{ message: 'Session sess_01HXZ revoked' }` }
|
|
2263
|
+
});
|
|
2264
|
+
const sessRevokeContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2265
|
+
${sessRevokeMeta}${mwUser3}
|
|
1879
2266
|
export const DELETE = async (req: Request, res: Response) => {
|
|
1880
2267
|
const { id } = req.params;
|
|
1881
2268
|
// TODO: revoke session by id
|
|
1882
2269
|
res.json({ message: \`Session \${id} revoked\` });
|
|
1883
2270
|
};
|
|
1884
|
-
` : `${RA}${
|
|
2271
|
+
` : `${RA}${sessRevokeMeta}${mwUser3}
|
|
1885
2272
|
export const DELETE = async (req, res) => {
|
|
1886
2273
|
const { id } = req.params;
|
|
1887
2274
|
// TODO: revoke session by id
|
|
@@ -1893,19 +2280,18 @@ export const DELETE = async (req, res) => {
|
|
|
1893
2280
|
async function writeUserExtendedRoutes(dest, opts) {
|
|
1894
2281
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1895
2282
|
const ts = opts.language === "typescript";
|
|
1896
|
-
const
|
|
1897
|
-
` : "";
|
|
1898
|
-
const rr3 = opts.rbac ? `import { requireRole } from '../../../middleware/requireRole.js';
|
|
1899
|
-
` : "";
|
|
1900
|
-
const mw2 = opts.rbac ? `export const middlewares = [requireAuth, requireRole('user', 'admin')];
|
|
2283
|
+
const mw2 = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
1901
2284
|
` : `export const middlewares = [requireAuth];
|
|
1902
2285
|
`;
|
|
1903
2286
|
const mw3 = mw2;
|
|
1904
2287
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
1905
2288
|
`;
|
|
1906
2289
|
const user = ts ? `(req as any).user` : `req.user`;
|
|
1907
|
-
const avatarMeta = mkMeta(opts,
|
|
1908
|
-
|
|
2290
|
+
const avatarMeta = mkMeta(opts, {
|
|
2291
|
+
POST: { description: "Upload a new avatar image for the authenticated user.", response: `{ message: 'Avatar updated', url: 'https://example.com/avatar.jpg' }` },
|
|
2292
|
+
DELETE: { description: "Remove the authenticated user's avatar.", response: `{ message: 'Avatar removed' }` }
|
|
2293
|
+
});
|
|
2294
|
+
const avatarContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1909
2295
|
${avatarMeta}${mw2}
|
|
1910
2296
|
export const POST = async (req: Request, res: Response) => {
|
|
1911
2297
|
// TODO: handle multipart upload, store file, update user.avatar
|
|
@@ -1916,7 +2302,7 @@ export const DELETE = async (req: Request, res: Response) => {
|
|
|
1916
2302
|
// TODO: remove avatar and clear user.avatar
|
|
1917
2303
|
res.json({ message: 'Avatar removed' });
|
|
1918
2304
|
};
|
|
1919
|
-
` : `${RA}${
|
|
2305
|
+
` : `${RA}${avatarMeta}${mw2}
|
|
1920
2306
|
export const POST = async (req, res) => {
|
|
1921
2307
|
// TODO: handle multipart upload, store file, update user.avatar
|
|
1922
2308
|
res.json({ message: 'Avatar updated', url: 'https://example.com/avatar.jpg' });
|
|
@@ -1928,8 +2314,11 @@ export const DELETE = async (req, res) => {
|
|
|
1928
2314
|
};
|
|
1929
2315
|
`;
|
|
1930
2316
|
await fs.outputFile(path.join(dest, "src", "api", "user", `avatar.${ext}`), avatarContent);
|
|
1931
|
-
const settingsMeta = mkMeta(opts,
|
|
1932
|
-
|
|
2317
|
+
const settingsMeta = mkMeta(opts, {
|
|
2318
|
+
GET: { description: "Get account settings (notifications, language, theme, privacy) for the authenticated user.", response: `{ settings: { notifications: true, language: 'en', theme: 'system', privacy: 'public' } }` },
|
|
2319
|
+
PUT: { description: "Update account settings for the authenticated user.", request: `{ notifications: true, language: 'en', theme: 'system', privacy: 'public' }`, response: `{ message: 'Settings updated', settings: { notifications: true, language: 'en', theme: 'system', privacy: 'public' } }` }
|
|
2320
|
+
});
|
|
2321
|
+
const settingsContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1933
2322
|
${settingsMeta}${mw2}
|
|
1934
2323
|
export const GET = async (req: Request, res: Response) => {
|
|
1935
2324
|
// TODO: fetch user settings from DB
|
|
@@ -1941,7 +2330,7 @@ export const PUT = async (req: Request, res: Response) => {
|
|
|
1941
2330
|
// TODO: update user settings in DB
|
|
1942
2331
|
res.json({ message: 'Settings updated', settings: { notifications, language, theme, privacy } });
|
|
1943
2332
|
};
|
|
1944
|
-
` : `${RA}${
|
|
2333
|
+
` : `${RA}${settingsMeta}${mw2}
|
|
1945
2334
|
export const GET = async (req, res) => {
|
|
1946
2335
|
// TODO: fetch user settings from DB
|
|
1947
2336
|
res.json({ settings: { notifications: true, language: 'en', theme: 'system', privacy: 'public' } });
|
|
@@ -1954,8 +2343,11 @@ export const PUT = async (req, res) => {
|
|
|
1954
2343
|
};
|
|
1955
2344
|
`;
|
|
1956
2345
|
await fs.outputFile(path.join(dest, "src", "api", "user", `settings.${ext}`), settingsContent);
|
|
1957
|
-
const accountMeta = mkMeta(opts,
|
|
1958
|
-
|
|
2346
|
+
const accountMeta = mkMeta(opts, {
|
|
2347
|
+
GET: { description: "Download a personal data export for the authenticated user.", response: `{ data: { user: { id: '1', role: 'user', email: 'user@example.com' }, exportedAt: '2026-01-01T00:00:00.000Z' } }` },
|
|
2348
|
+
DELETE: { description: "Schedule the authenticated account for deletion (requires password confirmation).", request: `{ password: 'current' }`, response: `{ message: 'Account scheduled for deletion' }` }
|
|
2349
|
+
});
|
|
2350
|
+
const accountContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1959
2351
|
${accountMeta}${mw2}
|
|
1960
2352
|
export const GET = async (req: Request, res: Response) => {
|
|
1961
2353
|
// TODO: compile and return personal data export
|
|
@@ -1968,7 +2360,7 @@ export const DELETE = async (req: Request, res: Response) => {
|
|
|
1968
2360
|
// TODO: verify password, schedule account deletion
|
|
1969
2361
|
res.json({ message: 'Account scheduled for deletion' });
|
|
1970
2362
|
};
|
|
1971
|
-
` : `${RA}${
|
|
2363
|
+
` : `${RA}${accountMeta}${mw2}
|
|
1972
2364
|
export const GET = async (req, res) => {
|
|
1973
2365
|
// TODO: compile and return personal data export
|
|
1974
2366
|
res.json({ data: { user: ${user}, exportedAt: new Date().toISOString() } });
|
|
@@ -1982,29 +2374,33 @@ export const DELETE = async (req, res) => {
|
|
|
1982
2374
|
};
|
|
1983
2375
|
`;
|
|
1984
2376
|
await fs.outputFile(path.join(dest, "src", "api", "user", `account.${ext}`), accountContent);
|
|
1985
|
-
const dashMeta = mkMeta(opts,
|
|
1986
|
-
|
|
2377
|
+
const dashMeta = mkMeta(opts, {
|
|
2378
|
+
GET: { description: "Personal dashboard: stats, recent activity, and quick actions.", response: `{ stats: {}, recentActivity: [], quickActions: [] }` }
|
|
2379
|
+
});
|
|
2380
|
+
const dashContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
1987
2381
|
${dashMeta}${mw2}
|
|
1988
2382
|
export const GET = async (req: Request, res: Response) => {
|
|
1989
2383
|
// TODO: aggregate personal stats and activity
|
|
1990
2384
|
res.json({ stats: {}, recentActivity: [], quickActions: [] });
|
|
1991
2385
|
};
|
|
1992
|
-
` : `${RA}${
|
|
2386
|
+
` : `${RA}${dashMeta}${mw2}
|
|
1993
2387
|
export const GET = async (req, res) => {
|
|
1994
2388
|
// TODO: aggregate personal stats and activity
|
|
1995
2389
|
res.json({ stats: {}, recentActivity: [], quickActions: [] });
|
|
1996
2390
|
};
|
|
1997
2391
|
`;
|
|
1998
2392
|
await fs.outputFile(path.join(dest, "src", "api", "user", `dashboard.${ext}`), dashContent);
|
|
1999
|
-
const actMeta = mkMeta(opts,
|
|
2000
|
-
|
|
2393
|
+
const actMeta = mkMeta(opts, {
|
|
2394
|
+
GET: { description: "Paginated activity history for the authenticated user.", query: `{ page: '1', limit: '20' }`, response: `{ activities: [], total: 0, page: 1, limit: 20 }` }
|
|
2395
|
+
});
|
|
2396
|
+
const actContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2001
2397
|
${actMeta}${mw2}
|
|
2002
2398
|
export const GET = async (req: Request, res: Response) => {
|
|
2003
2399
|
const { page = 1, limit = 20 } = req.query;
|
|
2004
2400
|
// TODO: fetch paginated activity log
|
|
2005
2401
|
res.json({ activities: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
2006
2402
|
};
|
|
2007
|
-
` : `${RA}${
|
|
2403
|
+
` : `${RA}${actMeta}${mw2}
|
|
2008
2404
|
export const GET = async (req, res) => {
|
|
2009
2405
|
const { page = 1, limit = 20 } = req.query;
|
|
2010
2406
|
// TODO: fetch paginated activity log
|
|
@@ -2012,8 +2408,11 @@ export const GET = async (req, res) => {
|
|
|
2012
2408
|
};
|
|
2013
2409
|
`;
|
|
2014
2410
|
await fs.outputFile(path.join(dest, "src", "api", "user", `activity.${ext}`), actContent);
|
|
2015
|
-
const notifListMeta = mkMeta(opts,
|
|
2016
|
-
|
|
2411
|
+
const notifListMeta = mkMeta(opts, {
|
|
2412
|
+
GET: { description: "List notifications for the authenticated user.", response: `{ notifications: [], total: 0, unread: 0 }` },
|
|
2413
|
+
POST: { description: "Mark all notifications as read for the authenticated user.", response: `{ message: 'All notifications marked as read' }` }
|
|
2414
|
+
});
|
|
2415
|
+
const notifListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2017
2416
|
${notifListMeta}${mw3}
|
|
2018
2417
|
export const GET = async (req: Request, res: Response) => {
|
|
2019
2418
|
// TODO: fetch notifications for user
|
|
@@ -2024,7 +2423,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2024
2423
|
// TODO: mark all notifications as read
|
|
2025
2424
|
res.json({ message: 'All notifications marked as read' });
|
|
2026
2425
|
};
|
|
2027
|
-
` : `${RA}${
|
|
2426
|
+
` : `${RA}${notifListMeta}${mw3}
|
|
2028
2427
|
export const GET = async (req, res) => {
|
|
2029
2428
|
// TODO: fetch notifications for user
|
|
2030
2429
|
res.json({ notifications: [], total: 0, unread: 0 });
|
|
@@ -2036,8 +2435,13 @@ export const POST = async (req, res) => {
|
|
|
2036
2435
|
};
|
|
2037
2436
|
`;
|
|
2038
2437
|
await fs.outputFile(path.join(dest, "src", "api", "user", "notifications", `index.${ext}`), notifListContent);
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2438
|
+
const notifByIdMeta = mkMeta(opts, {
|
|
2439
|
+
GET: { description: "Fetch a single notification by ID.", params: `{ id: 'notif_01HXZ' }`, response: `{ notification: { id: 'notif_01HXZ' } }` },
|
|
2440
|
+
PATCH: { description: "Mark a single notification as read.", params: `{ id: 'notif_01HXZ' }`, response: `{ message: 'Notification marked as read', id: 'notif_01HXZ' }` },
|
|
2441
|
+
DELETE: { description: "Delete a single notification.", params: `{ id: 'notif_01HXZ' }`, response: `{ message: 'Notification notif_01HXZ deleted' }` }
|
|
2442
|
+
});
|
|
2443
|
+
const notifByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2444
|
+
${notifByIdMeta}${mw3}
|
|
2041
2445
|
export const GET = async (req: Request, res: Response) => {
|
|
2042
2446
|
const { id } = req.params;
|
|
2043
2447
|
// TODO: fetch notification by id
|
|
@@ -2055,7 +2459,7 @@ export const DELETE = async (req: Request, res: Response) => {
|
|
|
2055
2459
|
// TODO: delete notification
|
|
2056
2460
|
res.json({ message: \`Notification \${id} deleted\` });
|
|
2057
2461
|
};
|
|
2058
|
-
` : `${RA}${
|
|
2462
|
+
` : `${RA}${notifByIdMeta}${mw3}
|
|
2059
2463
|
export const GET = async (req, res) => {
|
|
2060
2464
|
const { id } = req.params;
|
|
2061
2465
|
// TODO: fetch notification by id
|
|
@@ -2075,8 +2479,11 @@ export const DELETE = async (req, res) => {
|
|
|
2075
2479
|
};
|
|
2076
2480
|
`;
|
|
2077
2481
|
await fs.outputFile(path.join(dest, "src", "api", "user", "notifications", `[id].${ext}`), notifByIdContent);
|
|
2078
|
-
const filesListMeta = mkMeta(opts,
|
|
2079
|
-
|
|
2482
|
+
const filesListMeta = mkMeta(opts, {
|
|
2483
|
+
GET: { description: "List uploaded files with storage usage for the authenticated user.", response: `{ files: [], total: 0, storageUsed: 0 }` },
|
|
2484
|
+
POST: { description: "Upload a new file for the authenticated user.", response: `{ message: 'File uploaded', file: { id: 'new-id' } }`, status: 201 }
|
|
2485
|
+
});
|
|
2486
|
+
const filesListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2080
2487
|
${filesListMeta}${mw3}
|
|
2081
2488
|
export const GET = async (req: Request, res: Response) => {
|
|
2082
2489
|
// TODO: fetch user files and compute storage usage
|
|
@@ -2087,7 +2494,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2087
2494
|
// TODO: handle multipart upload, persist file record
|
|
2088
2495
|
res.status(201).json({ message: 'File uploaded', file: { id: 'new-id' } });
|
|
2089
2496
|
};
|
|
2090
|
-
` : `${RA}${
|
|
2497
|
+
` : `${RA}${filesListMeta}${mw3}
|
|
2091
2498
|
export const GET = async (req, res) => {
|
|
2092
2499
|
// TODO: fetch user files and compute storage usage
|
|
2093
2500
|
res.json({ files: [], total: 0, storageUsed: 0 });
|
|
@@ -2099,8 +2506,12 @@ export const POST = async (req, res) => {
|
|
|
2099
2506
|
};
|
|
2100
2507
|
`;
|
|
2101
2508
|
await fs.outputFile(path.join(dest, "src", "api", "user", "files", `index.${ext}`), filesListContent);
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2509
|
+
const fileByIdMeta = mkMeta(opts, {
|
|
2510
|
+
GET: { description: "Get a download/preview URL for a single file by ID.", params: `{ id: 'file_01HXZ' }`, response: `{ file: { id: 'file_01HXZ', url: 'https://example.com/files/file_01HXZ' } }` },
|
|
2511
|
+
DELETE: { description: "Delete a file from storage.", params: `{ id: 'file_01HXZ' }`, response: `{ message: 'File file_01HXZ deleted' }` }
|
|
2512
|
+
});
|
|
2513
|
+
const fileByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2514
|
+
${fileByIdMeta}${mw3}
|
|
2104
2515
|
export const GET = async (req: Request, res: Response) => {
|
|
2105
2516
|
const { id } = req.params;
|
|
2106
2517
|
// TODO: stream or redirect to file download/preview URL
|
|
@@ -2112,7 +2523,7 @@ export const DELETE = async (req: Request, res: Response) => {
|
|
|
2112
2523
|
// TODO: delete file from storage and DB
|
|
2113
2524
|
res.json({ message: \`File \${id} deleted\` });
|
|
2114
2525
|
};
|
|
2115
|
-
` : `${RA}${
|
|
2526
|
+
` : `${RA}${fileByIdMeta}${mw3}
|
|
2116
2527
|
export const GET = async (req, res) => {
|
|
2117
2528
|
const { id } = req.params;
|
|
2118
2529
|
// TODO: stream or redirect to file download/preview URL
|
|
@@ -2126,8 +2537,12 @@ export const DELETE = async (req, res) => {
|
|
|
2126
2537
|
};
|
|
2127
2538
|
`;
|
|
2128
2539
|
await fs.outputFile(path.join(dest, "src", "api", "user", "files", `[id].${ext}`), fileByIdContent);
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2540
|
+
const favListMeta = mkMeta(opts, {
|
|
2541
|
+
GET: { description: "List favorites for the authenticated user.", response: `{ favorites: [] }` },
|
|
2542
|
+
POST: { description: "Add an entity to the authenticated user's favorites.", request: `{ entityId: 'post_01HXZ', entityType: 'post' }`, response: `{ message: 'Added to favorites' }`, status: 201 }
|
|
2543
|
+
});
|
|
2544
|
+
const favListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2545
|
+
${favListMeta}${mw3}
|
|
2131
2546
|
export const GET = async (req: Request, res: Response) => {
|
|
2132
2547
|
// TODO: fetch user favorites
|
|
2133
2548
|
res.json({ favorites: [] });
|
|
@@ -2139,7 +2554,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2139
2554
|
// TODO: add to favorites
|
|
2140
2555
|
res.status(201).json({ message: 'Added to favorites' });
|
|
2141
2556
|
};
|
|
2142
|
-
` : `${RA}${
|
|
2557
|
+
` : `${RA}${favListMeta}${mw3}
|
|
2143
2558
|
export const GET = async (req, res) => {
|
|
2144
2559
|
// TODO: fetch user favorites
|
|
2145
2560
|
res.json({ favorites: [] });
|
|
@@ -2153,14 +2568,17 @@ export const POST = async (req, res) => {
|
|
|
2153
2568
|
};
|
|
2154
2569
|
`;
|
|
2155
2570
|
await fs.outputFile(path.join(dest, "src", "api", "user", "favorites", `index.${ext}`), favListContent);
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2571
|
+
const favByIdMeta = mkMeta(opts, {
|
|
2572
|
+
DELETE: { description: "Remove an entity from the authenticated user favorites.", params: `{ id: 'fav_01HXZ' }`, response: `{ message: 'Removed favorite fav_01HXZ' }` }
|
|
2573
|
+
});
|
|
2574
|
+
const favByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2575
|
+
${favByIdMeta}${mw3}
|
|
2158
2576
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2159
2577
|
const { id } = req.params;
|
|
2160
2578
|
// TODO: remove from favorites
|
|
2161
2579
|
res.json({ message: \`Removed favorite \${id}\` });
|
|
2162
2580
|
};
|
|
2163
|
-
` : `${RA}${
|
|
2581
|
+
` : `${RA}${favByIdMeta}${mw3}
|
|
2164
2582
|
export const DELETE = async (req, res) => {
|
|
2165
2583
|
const { id } = req.params;
|
|
2166
2584
|
// TODO: remove from favorites
|
|
@@ -2168,8 +2586,12 @@ export const DELETE = async (req, res) => {
|
|
|
2168
2586
|
};
|
|
2169
2587
|
`;
|
|
2170
2588
|
await fs.outputFile(path.join(dest, "src", "api", "user", "favorites", `[id].${ext}`), favByIdContent);
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2589
|
+
const bkListMeta = mkMeta(opts, {
|
|
2590
|
+
GET: { description: "List bookmarks for the authenticated user.", response: `{ bookmarks: [] }` },
|
|
2591
|
+
POST: { description: "Save a new bookmark for the authenticated user.", request: `{ url: 'https://example.com', title: 'Example' }`, response: `{ message: 'Bookmark saved' }`, status: 201 }
|
|
2592
|
+
});
|
|
2593
|
+
const bkListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2594
|
+
${bkListMeta}${mw3}
|
|
2173
2595
|
export const GET = async (req: Request, res: Response) => {
|
|
2174
2596
|
// TODO: fetch user bookmarks
|
|
2175
2597
|
res.json({ bookmarks: [] });
|
|
@@ -2181,7 +2603,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2181
2603
|
// TODO: save bookmark
|
|
2182
2604
|
res.status(201).json({ message: 'Bookmark saved' });
|
|
2183
2605
|
};
|
|
2184
|
-
` : `${RA}${
|
|
2606
|
+
` : `${RA}${bkListMeta}${mw3}
|
|
2185
2607
|
export const GET = async (req, res) => {
|
|
2186
2608
|
// TODO: fetch user bookmarks
|
|
2187
2609
|
res.json({ bookmarks: [] });
|
|
@@ -2195,14 +2617,17 @@ export const POST = async (req, res) => {
|
|
|
2195
2617
|
};
|
|
2196
2618
|
`;
|
|
2197
2619
|
await fs.outputFile(path.join(dest, "src", "api", "user", "bookmarks", `index.${ext}`), bkListContent);
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2620
|
+
const bkByIdMeta = mkMeta(opts, {
|
|
2621
|
+
DELETE: { description: "Delete a bookmark by ID.", params: `{ id: 'bm_01HXZ' }`, response: `{ message: 'Bookmark bm_01HXZ removed' }` }
|
|
2622
|
+
});
|
|
2623
|
+
const bkByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2624
|
+
${bkByIdMeta}${mw3}
|
|
2200
2625
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2201
2626
|
const { id } = req.params;
|
|
2202
2627
|
// TODO: delete bookmark
|
|
2203
2628
|
res.json({ message: \`Bookmark \${id} removed\` });
|
|
2204
2629
|
};
|
|
2205
|
-
` : `${RA}${
|
|
2630
|
+
` : `${RA}${bkByIdMeta}${mw3}
|
|
2206
2631
|
export const DELETE = async (req, res) => {
|
|
2207
2632
|
const { id } = req.params;
|
|
2208
2633
|
// TODO: delete bookmark
|
|
@@ -2210,15 +2635,17 @@ export const DELETE = async (req, res) => {
|
|
|
2210
2635
|
};
|
|
2211
2636
|
`;
|
|
2212
2637
|
await fs.outputFile(path.join(dest, "src", "api", "user", "bookmarks", `[id].${ext}`), bkByIdContent);
|
|
2213
|
-
const searchMeta = mkMeta(opts,
|
|
2214
|
-
|
|
2638
|
+
const searchMeta = mkMeta(opts, {
|
|
2639
|
+
GET: { description: "Search with filters, sort, and pagination.", query: `{ q: 'query', filter: 'active', sort: 'newest', page: '1', limit: '20' }`, response: `{ results: [], total: 0, page: 1, limit: 20 }` }
|
|
2640
|
+
});
|
|
2641
|
+
const searchContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2215
2642
|
${searchMeta}${mw2}
|
|
2216
2643
|
export const GET = async (req: Request, res: Response) => {
|
|
2217
2644
|
const { q, filter, sort, page = 1, limit = 20 } = req.query;
|
|
2218
2645
|
// TODO: implement search
|
|
2219
2646
|
res.json({ results: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
2220
2647
|
};
|
|
2221
|
-
` : `${RA}${
|
|
2648
|
+
` : `${RA}${searchMeta}${mw2}
|
|
2222
2649
|
export const GET = async (req, res) => {
|
|
2223
2650
|
const { q, filter, sort, page = 1, limit = 20 } = req.query;
|
|
2224
2651
|
// TODO: implement search
|
|
@@ -2226,8 +2653,11 @@ export const GET = async (req, res) => {
|
|
|
2226
2653
|
};
|
|
2227
2654
|
`;
|
|
2228
2655
|
await fs.outputFile(path.join(dest, "src", "api", "user", `search.${ext}`), searchContent);
|
|
2229
|
-
const akListMeta = mkMeta(opts,
|
|
2230
|
-
|
|
2656
|
+
const akListMeta = mkMeta(opts, {
|
|
2657
|
+
GET: { description: "List API keys for the authenticated user.", response: `{ apiKeys: [] }` },
|
|
2658
|
+
POST: { description: "Generate a new API key for the authenticated user.", request: `{ name: 'CI key' }`, response: `{ message: 'API key created', key: 'efc_...' }`, status: 201 }
|
|
2659
|
+
});
|
|
2660
|
+
const akListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2231
2661
|
${akListMeta}${mw3}
|
|
2232
2662
|
export const GET = async (req: Request, res: Response) => {
|
|
2233
2663
|
// TODO: fetch api keys for user
|
|
@@ -2240,7 +2670,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2240
2670
|
// TODO: generate and store API key
|
|
2241
2671
|
res.status(201).json({ message: 'API key created', key: 'efc_...' });
|
|
2242
2672
|
};
|
|
2243
|
-
` : `${RA}${
|
|
2673
|
+
` : `${RA}${akListMeta}${mw3}
|
|
2244
2674
|
export const GET = async (req, res) => {
|
|
2245
2675
|
// TODO: fetch api keys for user
|
|
2246
2676
|
res.json({ apiKeys: [] });
|
|
@@ -2254,14 +2684,17 @@ export const POST = async (req, res) => {
|
|
|
2254
2684
|
};
|
|
2255
2685
|
`;
|
|
2256
2686
|
await fs.outputFile(path.join(dest, "src", "api", "user", "api-keys", `index.${ext}`), akListContent);
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2687
|
+
const akByIdMeta = mkMeta(opts, {
|
|
2688
|
+
DELETE: { description: "Revoke and delete an API key by ID.", params: `{ id: 'key_01HXZ' }`, response: `{ message: 'API key key_01HXZ revoked' }` }
|
|
2689
|
+
});
|
|
2690
|
+
const akByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2691
|
+
${akByIdMeta}${mw3}
|
|
2259
2692
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2260
2693
|
const { id } = req.params;
|
|
2261
2694
|
// TODO: revoke and delete API key
|
|
2262
2695
|
res.json({ message: \`API key \${id} revoked\` });
|
|
2263
2696
|
};
|
|
2264
|
-
` : `${RA}${
|
|
2697
|
+
` : `${RA}${akByIdMeta}${mw3}
|
|
2265
2698
|
export const DELETE = async (req, res) => {
|
|
2266
2699
|
const { id } = req.params;
|
|
2267
2700
|
// TODO: revoke and delete API key
|
|
@@ -2273,32 +2706,34 @@ export const DELETE = async (req, res) => {
|
|
|
2273
2706
|
async function writeUserBillingRoutes(dest, opts) {
|
|
2274
2707
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
2275
2708
|
const ts = opts.language === "typescript";
|
|
2276
|
-
const
|
|
2277
|
-
` : "";
|
|
2278
|
-
const rr4 = opts.rbac ? `import { requireRole } from '../../../../middleware/requireRole.js';
|
|
2279
|
-
` : "";
|
|
2280
|
-
const mw3 = opts.rbac ? `export const middlewares = [requireAuth, requireRole('user', 'admin')];
|
|
2709
|
+
const mw3 = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
2281
2710
|
` : `export const middlewares = [requireAuth];
|
|
2282
2711
|
`;
|
|
2283
2712
|
const mw4 = mw3;
|
|
2284
2713
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
2285
2714
|
`;
|
|
2286
|
-
const plansMeta = mkMeta(opts,
|
|
2287
|
-
|
|
2715
|
+
const plansMeta = mkMeta(opts, {
|
|
2716
|
+
GET: { description: "List all available subscription plans.", response: `{ plans: [] }` }
|
|
2717
|
+
});
|
|
2718
|
+
const plansContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2288
2719
|
${plansMeta}${mw3}
|
|
2289
2720
|
export const GET = async (_req: Request, res: Response) => {
|
|
2290
2721
|
// TODO: fetch active plans from DB
|
|
2291
2722
|
res.json({ plans: [] });
|
|
2292
2723
|
};
|
|
2293
|
-
` : `${RA}${
|
|
2724
|
+
` : `${RA}${plansMeta}${mw3}
|
|
2294
2725
|
export const GET = async (_req, res) => {
|
|
2295
2726
|
// TODO: fetch active plans from DB
|
|
2296
2727
|
res.json({ plans: [] });
|
|
2297
2728
|
};
|
|
2298
2729
|
`;
|
|
2299
2730
|
await fs.outputFile(path.join(dest, "src", "api", "user", "billing", `plans.${ext}`), plansContent);
|
|
2300
|
-
const subMeta = mkMeta(opts,
|
|
2301
|
-
|
|
2731
|
+
const subMeta = mkMeta(opts, {
|
|
2732
|
+
GET: { description: "Get the current subscription for the authenticated user.", response: `{ subscription: null }` },
|
|
2733
|
+
POST: { description: "Subscribe the authenticated user to a plan.", request: `{ planId: 'plan_01HXZ' }`, response: `{ message: 'Subscribed', subscription: { planId: 'plan_01HXZ' } }`, status: 201 },
|
|
2734
|
+
DELETE: { description: "Cancel the current subscription at period end.", response: `{ message: 'Subscription cancelled' }` }
|
|
2735
|
+
});
|
|
2736
|
+
const subContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2302
2737
|
${subMeta}${mw3}
|
|
2303
2738
|
export const GET = async (req: Request, res: Response) => {
|
|
2304
2739
|
// TODO: fetch current subscription for user
|
|
@@ -2316,7 +2751,7 @@ export const DELETE = async (req: Request, res: Response) => {
|
|
|
2316
2751
|
// TODO: cancel subscription at period end
|
|
2317
2752
|
res.json({ message: 'Subscription cancelled' });
|
|
2318
2753
|
};
|
|
2319
|
-
` : `${RA}${
|
|
2754
|
+
` : `${RA}${subMeta}${mw3}
|
|
2320
2755
|
export const GET = async (req, res) => {
|
|
2321
2756
|
// TODO: fetch current subscription for user
|
|
2322
2757
|
res.json({ subscription: null });
|
|
@@ -2335,8 +2770,12 @@ export const DELETE = async (req, res) => {
|
|
|
2335
2770
|
};
|
|
2336
2771
|
`;
|
|
2337
2772
|
await fs.outputFile(path.join(dest, "src", "api", "user", "billing", `subscription.${ext}`), subContent);
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2773
|
+
const pmListMeta = mkMeta(opts, {
|
|
2774
|
+
GET: { description: "List payment methods for the authenticated user.", response: `{ paymentMethods: [] }` },
|
|
2775
|
+
POST: { description: "Attach a new payment method via the payment gateway.", request: `{ token: 'tok_...' }`, response: `{ message: 'Payment method added' }`, status: 201 }
|
|
2776
|
+
});
|
|
2777
|
+
const pmListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2778
|
+
${pmListMeta}${mw4}
|
|
2340
2779
|
export const GET = async (req: Request, res: Response) => {
|
|
2341
2780
|
// TODO: fetch payment methods for user
|
|
2342
2781
|
res.json({ paymentMethods: [] });
|
|
@@ -2348,7 +2787,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2348
2787
|
// TODO: attach payment method via payment gateway
|
|
2349
2788
|
res.status(201).json({ message: 'Payment method added' });
|
|
2350
2789
|
};
|
|
2351
|
-
` : `${RA}${
|
|
2790
|
+
` : `${RA}${pmListMeta}${mw4}
|
|
2352
2791
|
export const GET = async (req, res) => {
|
|
2353
2792
|
// TODO: fetch payment methods for user
|
|
2354
2793
|
res.json({ paymentMethods: [] });
|
|
@@ -2362,14 +2801,17 @@ export const POST = async (req, res) => {
|
|
|
2362
2801
|
};
|
|
2363
2802
|
`;
|
|
2364
2803
|
await fs.outputFile(path.join(dest, "src", "api", "user", "billing", "payment-methods", `index.${ext}`), pmListContent);
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2804
|
+
const pmByIdMeta = mkMeta(opts, {
|
|
2805
|
+
DELETE: { description: "Detach a payment method from the payment gateway.", params: `{ id: 'pm_01HXZ' }`, response: `{ message: 'Payment method pm_01HXZ removed' }` }
|
|
2806
|
+
});
|
|
2807
|
+
const pmByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2808
|
+
${pmByIdMeta}${mw4}
|
|
2367
2809
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2368
2810
|
const { id } = req.params;
|
|
2369
2811
|
// TODO: detach payment method from payment gateway
|
|
2370
2812
|
res.json({ message: \`Payment method \${id} removed\` });
|
|
2371
2813
|
};
|
|
2372
|
-
` : `${RA}${
|
|
2814
|
+
` : `${RA}${pmByIdMeta}${mw4}
|
|
2373
2815
|
export const DELETE = async (req, res) => {
|
|
2374
2816
|
const { id } = req.params;
|
|
2375
2817
|
// TODO: detach payment method from payment gateway
|
|
@@ -2377,28 +2819,33 @@ export const DELETE = async (req, res) => {
|
|
|
2377
2819
|
};
|
|
2378
2820
|
`;
|
|
2379
2821
|
await fs.outputFile(path.join(dest, "src", "api", "user", "billing", "payment-methods", `[id].${ext}`), pmByIdContent);
|
|
2380
|
-
const invListMeta = mkMeta(opts,
|
|
2381
|
-
|
|
2822
|
+
const invListMeta = mkMeta(opts, {
|
|
2823
|
+
GET: { description: "List all invoices for the authenticated user.", response: `{ invoices: [], total: 0 }` }
|
|
2824
|
+
});
|
|
2825
|
+
const invListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2382
2826
|
${invListMeta}${mw4}
|
|
2383
2827
|
export const GET = async (req: Request, res: Response) => {
|
|
2384
2828
|
// TODO: fetch invoices for user
|
|
2385
2829
|
res.json({ invoices: [], total: 0 });
|
|
2386
2830
|
};
|
|
2387
|
-
` : `${RA}${
|
|
2831
|
+
` : `${RA}${invListMeta}${mw4}
|
|
2388
2832
|
export const GET = async (req, res) => {
|
|
2389
2833
|
// TODO: fetch invoices for user
|
|
2390
2834
|
res.json({ invoices: [], total: 0 });
|
|
2391
2835
|
};
|
|
2392
2836
|
`;
|
|
2393
2837
|
await fs.outputFile(path.join(dest, "src", "api", "user", "billing", "invoices", `index.${ext}`), invListContent);
|
|
2394
|
-
const
|
|
2395
|
-
|
|
2838
|
+
const invByIdMeta = mkMeta(opts, {
|
|
2839
|
+
GET: { description: "Get the download URL for a single invoice by ID.", params: `{ id: 'inv_01HXZ' }`, response: `{ invoice: { id: 'inv_01HXZ', downloadUrl: 'https://example.com/invoices/inv_01HXZ.pdf' } }` }
|
|
2840
|
+
});
|
|
2841
|
+
const invByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2842
|
+
${invByIdMeta}${mw4}
|
|
2396
2843
|
export const GET = async (req: Request, res: Response) => {
|
|
2397
2844
|
const { id } = req.params;
|
|
2398
2845
|
// TODO: return invoice PDF URL or inline data
|
|
2399
2846
|
res.json({ invoice: { id, downloadUrl: 'https://...' } });
|
|
2400
2847
|
};
|
|
2401
|
-
` : `${RA}${
|
|
2848
|
+
` : `${RA}${invByIdMeta}${mw4}
|
|
2402
2849
|
export const GET = async (req, res) => {
|
|
2403
2850
|
const { id } = req.params;
|
|
2404
2851
|
// TODO: return invoice PDF URL or inline data
|
|
@@ -2410,15 +2857,16 @@ export const GET = async (req, res) => {
|
|
|
2410
2857
|
async function writeSupportRoutes(dest, opts) {
|
|
2411
2858
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
2412
2859
|
const ts = opts.language === "typescript";
|
|
2413
|
-
const
|
|
2414
|
-
` : "";
|
|
2415
|
-
const mw3 = opts.rbac ? `export const middlewares = [requireAuth, requireRole('user', 'admin')];
|
|
2860
|
+
const mw3 = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
2416
2861
|
` : `export const middlewares = [requireAuth];
|
|
2417
2862
|
`;
|
|
2418
2863
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
2419
2864
|
`;
|
|
2420
|
-
const ticketListMeta = mkMeta(opts,
|
|
2421
|
-
|
|
2865
|
+
const ticketListMeta = mkMeta(opts, {
|
|
2866
|
+
GET: { description: "List the authenticated user's own support tickets.", response: `{ tickets: [], total: 0 }` },
|
|
2867
|
+
POST: { description: "Create a new support ticket.", request: `{ subject: 'Issue with login', message: 'I cannot log in', priority: 'normal' }`, response: `{ message: 'Ticket created', ticket: { id: 'new-id', subject: 'Issue with login', status: 'open' } }`, status: 201 }
|
|
2868
|
+
});
|
|
2869
|
+
const ticketListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2422
2870
|
${ticketListMeta}${mw3}
|
|
2423
2871
|
export const GET = async (req: Request, res: Response) => {
|
|
2424
2872
|
// TODO: fetch tickets for current user
|
|
@@ -2431,7 +2879,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2431
2879
|
// TODO: create support ticket in DB
|
|
2432
2880
|
res.status(201).json({ message: 'Ticket created', ticket: { id: 'new-id', subject, status: 'open' } });
|
|
2433
2881
|
};
|
|
2434
|
-
` : `${RA}${
|
|
2882
|
+
` : `${RA}${ticketListMeta}${mw3}
|
|
2435
2883
|
export const GET = async (req, res) => {
|
|
2436
2884
|
// TODO: fetch tickets for current user
|
|
2437
2885
|
res.json({ tickets: [], total: 0 });
|
|
@@ -2445,8 +2893,11 @@ export const POST = async (req, res) => {
|
|
|
2445
2893
|
};
|
|
2446
2894
|
`;
|
|
2447
2895
|
await fs.outputFile(path.join(dest, "src", "api", "support", "tickets", `index.${ext}`), ticketListContent);
|
|
2448
|
-
const ticketByIdMeta = mkMeta(opts,
|
|
2449
|
-
|
|
2896
|
+
const ticketByIdMeta = mkMeta(opts, {
|
|
2897
|
+
GET: { description: "Fetch a single support ticket by ID.", params: `{ id: 'tk_01HXZ' }`, response: `{ ticket: { id: 'tk_01HXZ', subject: 'Issue', status: 'open', replies: [] } }` },
|
|
2898
|
+
PUT: { description: "Add a reply to a support ticket or update its status.", params: `{ id: 'tk_01HXZ' }`, request: `{ reply: 'Thanks, resolved.', status: 'closed' }`, response: `{ message: 'Ticket updated', ticket: { id: 'tk_01HXZ', status: 'closed' } }` }
|
|
2899
|
+
});
|
|
2900
|
+
const ticketByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2450
2901
|
${ticketByIdMeta}${mw3}
|
|
2451
2902
|
export const GET = async (req: Request, res: Response) => {
|
|
2452
2903
|
const { id } = req.params;
|
|
@@ -2460,7 +2911,7 @@ export const PUT = async (req: Request, res: Response) => {
|
|
|
2460
2911
|
// TODO: add reply or update status
|
|
2461
2912
|
res.json({ message: 'Ticket updated', ticket: { id, status: status ?? 'open' } });
|
|
2462
2913
|
};
|
|
2463
|
-
` : `${RA}${
|
|
2914
|
+
` : `${RA}${ticketByIdMeta}${mw3}
|
|
2464
2915
|
export const GET = async (req, res) => {
|
|
2465
2916
|
const { id } = req.params;
|
|
2466
2917
|
// TODO: fetch ticket by id, verify ownership
|
|
@@ -2479,11 +2930,7 @@ export const PUT = async (req, res) => {
|
|
|
2479
2930
|
async function writeAdminExtendedRoutes(dest, opts) {
|
|
2480
2931
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
2481
2932
|
const ts = opts.language === "typescript";
|
|
2482
|
-
const
|
|
2483
|
-
` : "";
|
|
2484
|
-
const rr4 = opts.rbac ? `import { requireRole } from '../../../../middleware/requireRole.js';
|
|
2485
|
-
` : "";
|
|
2486
|
-
const mwAdmin3 = opts.rbac ? `export const middlewares = [requireAuth, requireRole('admin')];
|
|
2933
|
+
const mwAdmin3 = opts.rbac ? `export const middlewares = [requireAuth('admin')];
|
|
2487
2934
|
` : `export const middlewares = [requireAuth];
|
|
2488
2935
|
`;
|
|
2489
2936
|
const mwAdmin4 = mwAdmin3;
|
|
@@ -2494,29 +2941,33 @@ async function writeAdminExtendedRoutes(dest, opts) {
|
|
|
2494
2941
|
`;
|
|
2495
2942
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
2496
2943
|
`;
|
|
2497
|
-
const analyticsOverviewMeta = mkMeta(opts,
|
|
2498
|
-
|
|
2944
|
+
const analyticsOverviewMeta = mkMeta(opts, {
|
|
2945
|
+
GET: { description: "Analytics overview: users, revenue, traffic (admin only).", response: `{ users: {}, revenue: {}, traffic: {} }` }
|
|
2946
|
+
});
|
|
2947
|
+
const analyticsOverviewContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2499
2948
|
${analyticsOverviewMeta}${mwAdmin3}
|
|
2500
2949
|
export const GET = async (req: Request, res: Response) => {
|
|
2501
2950
|
${roleGuard} // TODO: aggregate analytics overview
|
|
2502
2951
|
res.json({ users: {}, revenue: {}, traffic: {} });
|
|
2503
2952
|
};
|
|
2504
|
-
` : `${RA}${
|
|
2953
|
+
` : `${RA}${analyticsOverviewMeta}${mwAdmin3}
|
|
2505
2954
|
export const GET = async (req, res) => {
|
|
2506
2955
|
${roleGuard} // TODO: aggregate analytics overview
|
|
2507
2956
|
res.json({ users: {}, revenue: {}, traffic: {} });
|
|
2508
2957
|
};
|
|
2509
2958
|
`;
|
|
2510
2959
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "analytics", `index.${ext}`), analyticsOverviewContent);
|
|
2511
|
-
const analyticsUsersMeta = mkMeta(opts,
|
|
2512
|
-
|
|
2960
|
+
const analyticsUsersMeta = mkMeta(opts, {
|
|
2961
|
+
GET: { description: "User analytics: registrations, active users, churn (admin only).", query: `{ period: '30d' }`, response: `{ registrations: [], activeUsers: 0, churn: 0, period: '30d' }` }
|
|
2962
|
+
});
|
|
2963
|
+
const analyticsUsersContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2513
2964
|
${analyticsUsersMeta}${mwAdmin3}
|
|
2514
2965
|
export const GET = async (req: Request, res: Response) => {
|
|
2515
2966
|
${roleGuard} const { period = '30d' } = req.query;
|
|
2516
2967
|
// TODO: fetch user analytics for period
|
|
2517
2968
|
res.json({ registrations: [], activeUsers: 0, churn: 0, period });
|
|
2518
2969
|
};
|
|
2519
|
-
` : `${RA}${
|
|
2970
|
+
` : `${RA}${analyticsUsersMeta}${mwAdmin3}
|
|
2520
2971
|
export const GET = async (req, res) => {
|
|
2521
2972
|
${roleGuard} const { period = '30d' } = req.query;
|
|
2522
2973
|
// TODO: fetch user analytics for period
|
|
@@ -2524,15 +2975,17 @@ ${roleGuard} const { period = '30d' } = req.query;
|
|
|
2524
2975
|
};
|
|
2525
2976
|
`;
|
|
2526
2977
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "analytics", `users.${ext}`), analyticsUsersContent);
|
|
2527
|
-
const analyticsRevenueMeta = mkMeta(opts,
|
|
2528
|
-
|
|
2978
|
+
const analyticsRevenueMeta = mkMeta(opts, {
|
|
2979
|
+
GET: { description: "Revenue analytics: MRR, ARR, payment history (admin only).", query: `{ period: '30d' }`, response: `{ mrr: 0, arr: 0, history: [], period: '30d' }` }
|
|
2980
|
+
});
|
|
2981
|
+
const analyticsRevenueContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2529
2982
|
${analyticsRevenueMeta}${mwAdmin3}
|
|
2530
2983
|
export const GET = async (req: Request, res: Response) => {
|
|
2531
2984
|
${roleGuard} const { period = '30d' } = req.query;
|
|
2532
2985
|
// TODO: fetch revenue analytics for period
|
|
2533
2986
|
res.json({ mrr: 0, arr: 0, history: [], period });
|
|
2534
2987
|
};
|
|
2535
|
-
` : `${RA}${
|
|
2988
|
+
` : `${RA}${analyticsRevenueMeta}${mwAdmin3}
|
|
2536
2989
|
export const GET = async (req, res) => {
|
|
2537
2990
|
${roleGuard} const { period = '30d' } = req.query;
|
|
2538
2991
|
// TODO: fetch revenue analytics for period
|
|
@@ -2540,15 +2993,17 @@ ${roleGuard} const { period = '30d' } = req.query;
|
|
|
2540
2993
|
};
|
|
2541
2994
|
`;
|
|
2542
2995
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "analytics", `revenue.${ext}`), analyticsRevenueContent);
|
|
2543
|
-
const analyticsTrafficMeta = mkMeta(opts,
|
|
2544
|
-
|
|
2996
|
+
const analyticsTrafficMeta = mkMeta(opts, {
|
|
2997
|
+
GET: { description: "Traffic analytics: page views, devices, countries (admin only).", query: `{ period: '30d' }`, response: `{ pageViews: 0, devices: {}, countries: {}, period: '30d' }` }
|
|
2998
|
+
});
|
|
2999
|
+
const analyticsTrafficContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2545
3000
|
${analyticsTrafficMeta}${mwAdmin3}
|
|
2546
3001
|
export const GET = async (req: Request, res: Response) => {
|
|
2547
3002
|
${roleGuard} const { period = '30d' } = req.query;
|
|
2548
3003
|
// TODO: fetch traffic analytics for period
|
|
2549
3004
|
res.json({ pageViews: 0, devices: {}, countries: {}, period });
|
|
2550
3005
|
};
|
|
2551
|
-
` : `${RA}${
|
|
3006
|
+
` : `${RA}${analyticsTrafficMeta}${mwAdmin3}
|
|
2552
3007
|
export const GET = async (req, res) => {
|
|
2553
3008
|
${roleGuard} const { period = '30d' } = req.query;
|
|
2554
3009
|
// TODO: fetch traffic analytics for period
|
|
@@ -2556,15 +3011,18 @@ ${roleGuard} const { period = '30d' } = req.query;
|
|
|
2556
3011
|
};
|
|
2557
3012
|
`;
|
|
2558
3013
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "analytics", `traffic.${ext}`), analyticsTrafficContent);
|
|
2559
|
-
const
|
|
2560
|
-
|
|
3014
|
+
const suspendMeta = mkMeta(opts, {
|
|
3015
|
+
POST: { description: "Suspend a user account (admin only).", params: `{ id: 'usr_01HXZ' }`, request: `{ reason: 'Terms of service violation' }`, response: `{ message: 'User usr_01HXZ suspended', reason: 'Terms of service violation' }` }
|
|
3016
|
+
});
|
|
3017
|
+
const suspendContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3018
|
+
${suspendMeta}${mwAdmin4}
|
|
2561
3019
|
export const POST = async (req: Request, res: Response) => {
|
|
2562
3020
|
${roleGuard} const { id } = req.params;
|
|
2563
3021
|
const { reason } = req.body;
|
|
2564
3022
|
// TODO: set user.isActive = false, log audit event
|
|
2565
3023
|
res.json({ message: \`User \${id} suspended\`, reason });
|
|
2566
3024
|
};
|
|
2567
|
-
` : `${RA}${
|
|
3025
|
+
` : `${RA}${suspendMeta}${mwAdmin4}
|
|
2568
3026
|
export const POST = async (req, res) => {
|
|
2569
3027
|
${roleGuard} const { id } = req.params;
|
|
2570
3028
|
const { reason } = req.body;
|
|
@@ -2573,14 +3031,17 @@ ${roleGuard} const { id } = req.params;
|
|
|
2573
3031
|
};
|
|
2574
3032
|
`;
|
|
2575
3033
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "users", "[id]", `suspend.${ext}`), suspendContent);
|
|
2576
|
-
const
|
|
2577
|
-
|
|
3034
|
+
const activateMeta = mkMeta(opts, {
|
|
3035
|
+
POST: { description: "Reactivate a suspended user account (admin only).", params: `{ id: 'usr_01HXZ' }`, response: `{ message: 'User usr_01HXZ activated' }` }
|
|
3036
|
+
});
|
|
3037
|
+
const activateContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3038
|
+
${activateMeta}${mwAdmin4}
|
|
2578
3039
|
export const POST = async (req: Request, res: Response) => {
|
|
2579
3040
|
${roleGuard} const { id } = req.params;
|
|
2580
3041
|
// TODO: set user.isActive = true, log audit event
|
|
2581
3042
|
res.json({ message: \`User \${id} activated\` });
|
|
2582
3043
|
};
|
|
2583
|
-
` : `${RA}${
|
|
3044
|
+
` : `${RA}${activateMeta}${mwAdmin4}
|
|
2584
3045
|
export const POST = async (req, res) => {
|
|
2585
3046
|
${roleGuard} const { id } = req.params;
|
|
2586
3047
|
// TODO: set user.isActive = true, log audit event
|
|
@@ -2588,14 +3049,17 @@ ${roleGuard} const { id } = req.params;
|
|
|
2588
3049
|
};
|
|
2589
3050
|
`;
|
|
2590
3051
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "users", "[id]", `activate.${ext}`), activateContent);
|
|
2591
|
-
const
|
|
2592
|
-
|
|
3052
|
+
const verifyUserMeta = mkMeta(opts, {
|
|
3053
|
+
POST: { description: "Mark a user's email as verified (admin only).", params: `{ id: 'usr_01HXZ' }`, response: `{ message: 'User usr_01HXZ verified' }` }
|
|
3054
|
+
});
|
|
3055
|
+
const verifyUserContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3056
|
+
${verifyUserMeta}${mwAdmin4}
|
|
2593
3057
|
export const POST = async (req: Request, res: Response) => {
|
|
2594
3058
|
${roleGuard} const { id } = req.params;
|
|
2595
3059
|
// TODO: set user.isVerified = true
|
|
2596
3060
|
res.json({ message: \`User \${id} verified\` });
|
|
2597
3061
|
};
|
|
2598
|
-
` : `${RA}${
|
|
3062
|
+
` : `${RA}${verifyUserMeta}${mwAdmin4}
|
|
2599
3063
|
export const POST = async (req, res) => {
|
|
2600
3064
|
${roleGuard} const { id } = req.params;
|
|
2601
3065
|
// TODO: set user.isVerified = true
|
|
@@ -2603,8 +3067,10 @@ ${roleGuard} const { id } = req.params;
|
|
|
2603
3067
|
};
|
|
2604
3068
|
`;
|
|
2605
3069
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "users", "[id]", `verify.${ext}`), verifyUserContent);
|
|
2606
|
-
const exportMeta = mkMeta(opts,
|
|
2607
|
-
|
|
3070
|
+
const exportMeta = mkMeta(opts, {
|
|
3071
|
+
GET: { description: "Export all users as a CSV download (admin only).", response: `'id,name,email,role,createdAt\\n'` }
|
|
3072
|
+
});
|
|
3073
|
+
const exportContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2608
3074
|
${exportMeta}${mwAdmin3}
|
|
2609
3075
|
export const GET = async (_req: Request, res: Response) => {
|
|
2610
3076
|
${roleGuard} // TODO: generate CSV of all users and stream response
|
|
@@ -2612,7 +3078,7 @@ ${roleGuard} // TODO: generate CSV of all users and stream response
|
|
|
2612
3078
|
res.setHeader('Content-Disposition', 'attachment; filename="users.csv"');
|
|
2613
3079
|
res.send('id,name,email,role,createdAt\\n');
|
|
2614
3080
|
};
|
|
2615
|
-
` : `${RA}${
|
|
3081
|
+
` : `${RA}${exportMeta}${mwAdmin3}
|
|
2616
3082
|
export const GET = async (_req, res) => {
|
|
2617
3083
|
${roleGuard} // TODO: generate CSV of all users and stream response
|
|
2618
3084
|
res.setHeader('Content-Type', 'text/csv');
|
|
@@ -2621,8 +3087,11 @@ ${roleGuard} // TODO: generate CSV of all users and stream response
|
|
|
2621
3087
|
};
|
|
2622
3088
|
`;
|
|
2623
3089
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "users", `export.${ext}`), exportContent);
|
|
2624
|
-
const adminsListMeta = mkMeta(opts,
|
|
2625
|
-
|
|
3090
|
+
const adminsListMeta = mkMeta(opts, {
|
|
3091
|
+
GET: { description: "List all admin accounts (admin only).", response: `{ admins: [], total: 0 }` },
|
|
3092
|
+
POST: { description: "Create a new admin account (admin only).", request: `{ name: 'Jane Doe', email: 'jane@example.com', role: 'admin' }`, response: `{ message: 'Admin created', admin: { id: 'new-id', name: 'Jane Doe', email: 'jane@example.com', role: 'admin' } }`, status: 201 }
|
|
3093
|
+
});
|
|
3094
|
+
const adminsListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2626
3095
|
${adminsListMeta}${mwAdmin3}
|
|
2627
3096
|
export const GET = async (_req: Request, res: Response) => {
|
|
2628
3097
|
${roleGuard} // TODO: fetch admins from DB
|
|
@@ -2635,7 +3104,7 @@ ${roleGuard} const { name, email, role } = req.body;
|
|
|
2635
3104
|
// TODO: create admin account
|
|
2636
3105
|
res.status(201).json({ message: 'Admin created', admin: { id: 'new-id', name, email, role: role ?? 'admin' } });
|
|
2637
3106
|
};
|
|
2638
|
-
` : `${RA}${
|
|
3107
|
+
` : `${RA}${adminsListMeta}${mwAdmin3}
|
|
2639
3108
|
export const GET = async (_req, res) => {
|
|
2640
3109
|
${roleGuard} // TODO: fetch admins from DB
|
|
2641
3110
|
res.json({ admins: [], total: 0 });
|
|
@@ -2649,8 +3118,13 @@ ${roleGuard} const { name, email, role } = req.body;
|
|
|
2649
3118
|
};
|
|
2650
3119
|
`;
|
|
2651
3120
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "admins", `index.${ext}`), adminsListContent);
|
|
2652
|
-
const
|
|
2653
|
-
|
|
3121
|
+
const adminByIdMeta = mkMeta(opts, {
|
|
3122
|
+
GET: { description: "Fetch a single admin account by ID (admin only).", params: `{ id: 'adm_01HXZ' }`, response: `{ admin: { id: 'adm_01HXZ' } }` },
|
|
3123
|
+
PUT: { description: "Update an admin account by ID (admin only).", params: `{ id: 'adm_01HXZ' }`, request: `{ name: 'Jane Doe', role: 'admin' }`, response: `{ message: 'Admin updated', admin: { id: 'adm_01HXZ', name: 'Jane Doe', role: 'admin' } }` },
|
|
3124
|
+
DELETE: { description: "Delete an admin account by ID (admin only).", params: `{ id: 'adm_01HXZ' }`, response: `{ message: 'Admin adm_01HXZ deleted' }` }
|
|
3125
|
+
});
|
|
3126
|
+
const adminByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3127
|
+
${adminByIdMeta}${mwAdmin3}
|
|
2654
3128
|
export const GET = async (req: Request, res: Response) => {
|
|
2655
3129
|
${roleGuard} const { id } = req.params;
|
|
2656
3130
|
// TODO: fetch admin by id
|
|
@@ -2668,7 +3142,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
2668
3142
|
// TODO: delete admin
|
|
2669
3143
|
res.json({ message: \`Admin \${id} deleted\` });
|
|
2670
3144
|
};
|
|
2671
|
-
` : `${RA}${
|
|
3145
|
+
` : `${RA}${adminByIdMeta}${mwAdmin3}
|
|
2672
3146
|
export const GET = async (req, res) => {
|
|
2673
3147
|
${roleGuard} const { id } = req.params;
|
|
2674
3148
|
// TODO: fetch admin by id
|
|
@@ -2689,8 +3163,12 @@ ${roleGuard} const { id } = req.params;
|
|
|
2689
3163
|
`;
|
|
2690
3164
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "admins", `[id].${ext}`), adminByIdContent);
|
|
2691
3165
|
if (opts.rbac) {
|
|
2692
|
-
const
|
|
2693
|
-
|
|
3166
|
+
const rolesListMeta = mkMeta(opts, {
|
|
3167
|
+
GET: { description: "List all roles (admin only).", response: `{ roles: [] }` },
|
|
3168
|
+
POST: { description: "Create a new role (admin only).", request: `{ name: 'editor', description: 'Can edit content', permissions: ['content:write'] }`, response: `{ message: 'Role created', role: { id: 'new-id', name: 'editor', permissions: ['content:write'] } }`, status: 201 }
|
|
3169
|
+
});
|
|
3170
|
+
const rolesListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3171
|
+
${rolesListMeta}${mwAdmin3}
|
|
2694
3172
|
export const GET = async (_req: Request, res: Response) => {
|
|
2695
3173
|
// TODO: fetch all roles
|
|
2696
3174
|
res.json({ roles: [] });
|
|
@@ -2702,7 +3180,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
2702
3180
|
// TODO: create role
|
|
2703
3181
|
res.status(201).json({ message: 'Role created', role: { id: 'new-id', name, permissions: permissions ?? [] } });
|
|
2704
3182
|
};
|
|
2705
|
-
` : `${RA}${
|
|
3183
|
+
` : `${RA}${rolesListMeta}${mwAdmin3}
|
|
2706
3184
|
export const GET = async (_req, res) => {
|
|
2707
3185
|
// TODO: fetch all roles
|
|
2708
3186
|
res.json({ roles: [] });
|
|
@@ -2716,8 +3194,13 @@ export const POST = async (req, res) => {
|
|
|
2716
3194
|
};
|
|
2717
3195
|
`;
|
|
2718
3196
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "roles", `index.${ext}`), rolesListContent);
|
|
2719
|
-
const
|
|
2720
|
-
|
|
3197
|
+
const roleByIdMeta = mkMeta(opts, {
|
|
3198
|
+
GET: { description: "Fetch a single role by ID (admin only).", params: `{ id: 'role_01HXZ' }`, response: `{ role: { id: 'role_01HXZ', name: 'editor', permissions: ['content:write'] } }` },
|
|
3199
|
+
PUT: { description: "Update a role by ID (admin only).", params: `{ id: 'role_01HXZ' }`, request: `{ name: 'editor', permissions: ['content:write'] }`, response: `{ message: 'Role updated', role: { id: 'role_01HXZ', name: 'editor', permissions: ['content:write'] } }` },
|
|
3200
|
+
DELETE: { description: "Delete a role by ID (admin only).", params: `{ id: 'role_01HXZ' }`, response: `{ message: 'Role role_01HXZ deleted' }` }
|
|
3201
|
+
});
|
|
3202
|
+
const roleByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3203
|
+
${roleByIdMeta}${mwAdmin3}
|
|
2721
3204
|
export const GET = async (req: Request, res: Response) => {
|
|
2722
3205
|
const { id } = req.params;
|
|
2723
3206
|
// TODO: fetch role by id
|
|
@@ -2735,7 +3218,7 @@ export const DELETE = async (req: Request, res: Response) => {
|
|
|
2735
3218
|
// TODO: delete role
|
|
2736
3219
|
res.json({ message: \`Role \${id} deleted\` });
|
|
2737
3220
|
};
|
|
2738
|
-
` : `${RA}${
|
|
3221
|
+
` : `${RA}${roleByIdMeta}${mwAdmin3}
|
|
2739
3222
|
export const GET = async (req, res) => {
|
|
2740
3223
|
const { id } = req.params;
|
|
2741
3224
|
// TODO: fetch role by id
|
|
@@ -2756,8 +3239,12 @@ export const DELETE = async (req, res) => {
|
|
|
2756
3239
|
`;
|
|
2757
3240
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "roles", `[id].${ext}`), roleByIdContent);
|
|
2758
3241
|
}
|
|
2759
|
-
const
|
|
2760
|
-
|
|
3242
|
+
const adminNotifMeta = mkMeta(opts, {
|
|
3243
|
+
GET: { description: "List all sent notifications (admin only).", response: `{ notifications: [], total: 0 }` },
|
|
3244
|
+
POST: { description: "Send a notification to a specific user (admin only).", request: `{ userId: 'usr_01HXZ', title: 'Welcome', message: 'Thanks for joining!', type: 'info' }`, response: `{ message: 'Notification sent' }`, status: 201 }
|
|
3245
|
+
});
|
|
3246
|
+
const adminNotifContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3247
|
+
${adminNotifMeta}${mwAdmin3}
|
|
2761
3248
|
export const GET = async (_req: Request, res: Response) => {
|
|
2762
3249
|
${roleGuard} // TODO: fetch sent notifications
|
|
2763
3250
|
res.json({ notifications: [], total: 0 });
|
|
@@ -2769,7 +3256,7 @@ ${roleGuard} const { userId, title, message, type } = req.body;
|
|
|
2769
3256
|
// TODO: create and send notification to user
|
|
2770
3257
|
res.status(201).json({ message: 'Notification sent' });
|
|
2771
3258
|
};
|
|
2772
|
-
` : `${RA}${
|
|
3259
|
+
` : `${RA}${adminNotifMeta}${mwAdmin3}
|
|
2773
3260
|
export const GET = async (_req, res) => {
|
|
2774
3261
|
${roleGuard} // TODO: fetch sent notifications
|
|
2775
3262
|
res.json({ notifications: [], total: 0 });
|
|
@@ -2783,8 +3270,10 @@ ${roleGuard} const { userId, title, message, type } = req.body;
|
|
|
2783
3270
|
};
|
|
2784
3271
|
`;
|
|
2785
3272
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "notifications", `index.${ext}`), adminNotifContent);
|
|
2786
|
-
const broadcastMeta = mkMeta(opts,
|
|
2787
|
-
|
|
3273
|
+
const broadcastMeta = mkMeta(opts, {
|
|
3274
|
+
POST: { description: "Broadcast a notification to all users (admin only).", request: `{ title: 'Announcement', message: 'Hello everyone!', type: 'info' }`, response: `{ message: 'Broadcast sent', count: 0 }` }
|
|
3275
|
+
});
|
|
3276
|
+
const broadcastContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2788
3277
|
${broadcastMeta}${mwAdmin3}
|
|
2789
3278
|
export const POST = async (req: Request, res: Response) => {
|
|
2790
3279
|
${roleGuard} const { title, message, type } = req.body;
|
|
@@ -2792,7 +3281,7 @@ ${roleGuard} const { title, message, type } = req.body;
|
|
|
2792
3281
|
// TODO: send notification to all users
|
|
2793
3282
|
res.json({ message: 'Broadcast sent', count: 0 });
|
|
2794
3283
|
};
|
|
2795
|
-
` : `${RA}${
|
|
3284
|
+
` : `${RA}${broadcastMeta}${mwAdmin3}
|
|
2796
3285
|
export const POST = async (req, res) => {
|
|
2797
3286
|
${roleGuard} const { title, message, type } = req.body;
|
|
2798
3287
|
if (!title || !message) return res.status(400).json({ error: 'title and message are required' });
|
|
@@ -2802,15 +3291,17 @@ ${roleGuard} const { title, message, type } = req.body;
|
|
|
2802
3291
|
`;
|
|
2803
3292
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "notifications", `broadcast.${ext}`), broadcastContent);
|
|
2804
3293
|
const mkLogContent = (label) => {
|
|
2805
|
-
const meta = mkMeta(opts,
|
|
2806
|
-
|
|
3294
|
+
const meta = mkMeta(opts, {
|
|
3295
|
+
GET: { description: `Paginated ${label} log entries (admin only).`, query: `{ page: '1', limit: '50' }`, response: `{ logs: [], total: 0, page: 1, limit: 50 }` }
|
|
3296
|
+
});
|
|
3297
|
+
return ts ? `${RA}import type { Request, Response } from 'express';
|
|
2807
3298
|
${meta}${mwAdmin3}
|
|
2808
3299
|
export const GET = async (req: Request, res: Response) => {
|
|
2809
3300
|
${roleGuard} const { page = 1, limit = 50 } = req.query;
|
|
2810
3301
|
// TODO: fetch ${label} logs with pagination
|
|
2811
3302
|
res.json({ logs: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
2812
3303
|
};
|
|
2813
|
-
` : `${RA}${
|
|
3304
|
+
` : `${RA}${meta}${mwAdmin3}
|
|
2814
3305
|
export const GET = async (req, res) => {
|
|
2815
3306
|
${roleGuard} const { page = 1, limit = 50 } = req.query;
|
|
2816
3307
|
// TODO: fetch ${label} logs with pagination
|
|
@@ -2821,8 +3312,11 @@ ${roleGuard} const { page = 1, limit = 50 } = req.query;
|
|
|
2821
3312
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "logs", `audit.${ext}`), mkLogContent("audit"));
|
|
2822
3313
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "logs", `activity.${ext}`), mkLogContent("activity"));
|
|
2823
3314
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "logs", `security.${ext}`), mkLogContent("security"));
|
|
2824
|
-
const settingsMeta = mkMeta(opts,
|
|
2825
|
-
|
|
3315
|
+
const settingsMeta = mkMeta(opts, {
|
|
3316
|
+
GET: { description: "Get system-wide settings (admin only).", response: `{ settings: {} }` },
|
|
3317
|
+
PUT: { description: "Update system-wide settings (admin only).", request: `{ maintenanceMode: false }`, response: `{ message: 'Settings updated', settings: { maintenanceMode: false } }` }
|
|
3318
|
+
});
|
|
3319
|
+
const settingsContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2826
3320
|
${settingsMeta}${mwAdmin3}
|
|
2827
3321
|
export const GET = async (_req: Request, res: Response) => {
|
|
2828
3322
|
${roleGuard} // TODO: fetch system settings from DB
|
|
@@ -2833,7 +3327,7 @@ export const PUT = async (req: Request, res: Response) => {
|
|
|
2833
3327
|
${roleGuard} // TODO: update system settings
|
|
2834
3328
|
res.json({ message: 'Settings updated', settings: req.body });
|
|
2835
3329
|
};
|
|
2836
|
-
` : `${RA}${
|
|
3330
|
+
` : `${RA}${settingsMeta}${mwAdmin3}
|
|
2837
3331
|
export const GET = async (_req, res) => {
|
|
2838
3332
|
${roleGuard} // TODO: fetch system settings from DB
|
|
2839
3333
|
res.json({ settings: {} });
|
|
@@ -2845,43 +3339,49 @@ ${roleGuard} // TODO: update system settings
|
|
|
2845
3339
|
};
|
|
2846
3340
|
`;
|
|
2847
3341
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "settings", `index.${ext}`), settingsContent);
|
|
2848
|
-
const healthMeta = mkMeta(opts,
|
|
2849
|
-
|
|
3342
|
+
const healthMeta = mkMeta(opts, {
|
|
3343
|
+
GET: { description: "System health check: DB, queue, cache status (admin only).", response: `{ status: 'ok', db: 'ok', queue: 'ok', uptime: 12345 }` }
|
|
3344
|
+
});
|
|
3345
|
+
const healthContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2850
3346
|
${healthMeta}${mwAdmin3}
|
|
2851
3347
|
export const GET = async (_req: Request, res: Response) => {
|
|
2852
3348
|
${roleGuard} // TODO: check DB connection, queue, cache health
|
|
2853
3349
|
res.json({ status: 'ok', db: 'ok', queue: 'ok', uptime: process.uptime() });
|
|
2854
3350
|
};
|
|
2855
|
-
` : `${RA}${
|
|
3351
|
+
` : `${RA}${healthMeta}${mwAdmin3}
|
|
2856
3352
|
export const GET = async (_req, res) => {
|
|
2857
3353
|
${roleGuard} // TODO: check DB connection, queue, cache health
|
|
2858
3354
|
res.json({ status: 'ok', db: 'ok', queue: 'ok', uptime: process.uptime() });
|
|
2859
3355
|
};
|
|
2860
3356
|
`;
|
|
2861
3357
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "system", `health.${ext}`), healthContent);
|
|
2862
|
-
const cacheMeta = mkMeta(opts,
|
|
2863
|
-
|
|
3358
|
+
const cacheMeta = mkMeta(opts, {
|
|
3359
|
+
DELETE: { description: "Flush the application cache (admin only).", response: `{ message: 'Cache cleared' }` }
|
|
3360
|
+
});
|
|
3361
|
+
const cacheContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2864
3362
|
${cacheMeta}${mwAdmin3}
|
|
2865
3363
|
export const DELETE = async (_req: Request, res: Response) => {
|
|
2866
3364
|
${roleGuard} // TODO: flush Redis or in-memory cache
|
|
2867
3365
|
res.json({ message: 'Cache cleared' });
|
|
2868
3366
|
};
|
|
2869
|
-
` : `${RA}${
|
|
3367
|
+
` : `${RA}${cacheMeta}${mwAdmin3}
|
|
2870
3368
|
export const DELETE = async (_req, res) => {
|
|
2871
3369
|
${roleGuard} // TODO: flush Redis or in-memory cache
|
|
2872
3370
|
res.json({ message: 'Cache cleared' });
|
|
2873
3371
|
};
|
|
2874
3372
|
`;
|
|
2875
3373
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "system", `cache.${ext}`), cacheContent);
|
|
2876
|
-
const adminTicketsListMeta = mkMeta(opts,
|
|
2877
|
-
|
|
3374
|
+
const adminTicketsListMeta = mkMeta(opts, {
|
|
3375
|
+
GET: { description: "List all support tickets with filters (admin only).", query: `{ status: 'open', priority: 'normal', page: '1', limit: '20' }`, response: `{ tickets: [], total: 0, page: 1, limit: 20 }` }
|
|
3376
|
+
});
|
|
3377
|
+
const adminTicketsListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2878
3378
|
${adminTicketsListMeta}${mwAdmin3}
|
|
2879
3379
|
export const GET = async (req: Request, res: Response) => {
|
|
2880
3380
|
${roleGuard} const { status, priority, page = 1, limit = 20 } = req.query;
|
|
2881
3381
|
// TODO: fetch all tickets with filters
|
|
2882
3382
|
res.json({ tickets: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
2883
3383
|
};
|
|
2884
|
-
` : `${RA}${
|
|
3384
|
+
` : `${RA}${adminTicketsListMeta}${mwAdmin3}
|
|
2885
3385
|
export const GET = async (req, res) => {
|
|
2886
3386
|
${roleGuard} const { status, priority, page = 1, limit = 20 } = req.query;
|
|
2887
3387
|
// TODO: fetch all tickets with filters
|
|
@@ -2889,8 +3389,12 @@ ${roleGuard} const { status, priority, page = 1, limit = 20 } = req.query;
|
|
|
2889
3389
|
};
|
|
2890
3390
|
`;
|
|
2891
3391
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "tickets", `index.${ext}`), adminTicketsListContent);
|
|
2892
|
-
const
|
|
2893
|
-
|
|
3392
|
+
const adminTicketByIdMeta = mkMeta(opts, {
|
|
3393
|
+
GET: { description: "Fetch a single support ticket by ID (admin only).", params: `{ id: 'tk_01HXZ' }`, response: `{ ticket: { id: 'tk_01HXZ' } }` },
|
|
3394
|
+
PUT: { description: "Assign, reply to, or change the status of a support ticket (admin only).", params: `{ id: 'tk_01HXZ' }`, request: `{ reply: 'Looking into it.', status: 'in_progress', assignedTo: 'adm_01HXZ' }`, response: `{ message: 'Ticket updated', ticket: { id: 'tk_01HXZ', status: 'in_progress', assignedTo: 'adm_01HXZ' } }` }
|
|
3395
|
+
});
|
|
3396
|
+
const adminTicketByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3397
|
+
${adminTicketByIdMeta}${mwAdmin3}
|
|
2894
3398
|
export const GET = async (req: Request, res: Response) => {
|
|
2895
3399
|
${roleGuard} const { id } = req.params;
|
|
2896
3400
|
// TODO: fetch ticket by id
|
|
@@ -2903,7 +3407,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
2903
3407
|
// TODO: update ticket (assign, change status, add reply)
|
|
2904
3408
|
res.json({ message: 'Ticket updated', ticket: { id, status, assignedTo } });
|
|
2905
3409
|
};
|
|
2906
|
-
` : `${RA}${
|
|
3410
|
+
` : `${RA}${adminTicketByIdMeta}${mwAdmin3}
|
|
2907
3411
|
export const GET = async (req, res) => {
|
|
2908
3412
|
${roleGuard} const { id } = req.params;
|
|
2909
3413
|
// TODO: fetch ticket by id
|
|
@@ -2919,8 +3423,16 @@ ${roleGuard} const { id } = req.params;
|
|
|
2919
3423
|
`;
|
|
2920
3424
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "tickets", `[id].${ext}`), adminTicketByIdContent);
|
|
2921
3425
|
const mkContentCrud = (name, namePlural, dir, createFields) => {
|
|
2922
|
-
const
|
|
2923
|
-
const
|
|
3426
|
+
const lower = name.toLowerCase();
|
|
3427
|
+
const firstField = createFields.split(", ")[0];
|
|
3428
|
+
const sampleCreateBody = `{ ${createFields.split(", ").map((f) => `${f}: 'sample-${f}'`).join(", ")} }`;
|
|
3429
|
+
const sampleCreatedRecord = `{ id: 'new-id', ${firstField}: 'sample-${firstField}' }`;
|
|
3430
|
+
const sampleFullRecord = `{ id: '${lower}_01HXZ', ${createFields.split(", ").map((f) => `${f}: 'sample-${f}'`).join(", ")} }`;
|
|
3431
|
+
const listMeta = mkMeta(opts, {
|
|
3432
|
+
GET: { description: `List all ${namePlural} (admin only).`, response: `{ ${namePlural}: [], total: 0 }` },
|
|
3433
|
+
POST: { description: `Create a new ${lower} (admin only).`, request: sampleCreateBody, response: `{ message: '${name} created', ${lower}: ${sampleCreatedRecord} }`, status: 201 }
|
|
3434
|
+
});
|
|
3435
|
+
const listContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2924
3436
|
${listMeta}${mwAdmin4}
|
|
2925
3437
|
export const GET = async (_req: Request, res: Response) => {
|
|
2926
3438
|
${roleGuard} // TODO: fetch ${namePlural}
|
|
@@ -2930,9 +3442,9 @@ ${roleGuard} // TODO: fetch ${namePlural}
|
|
|
2930
3442
|
export const POST = async (req: Request, res: Response) => {
|
|
2931
3443
|
${roleGuard} const { ${createFields} } = req.body;
|
|
2932
3444
|
// TODO: create ${name}
|
|
2933
|
-
res.status(201).json({ message: '${name} created', ${
|
|
3445
|
+
res.status(201).json({ message: '${name} created', ${lower}: { id: 'new-id', ${firstField} } });
|
|
2934
3446
|
};
|
|
2935
|
-
` : `${RA}${
|
|
3447
|
+
` : `${RA}${listMeta}${mwAdmin4}
|
|
2936
3448
|
export const GET = async (_req, res) => {
|
|
2937
3449
|
${roleGuard} // TODO: fetch ${namePlural}
|
|
2938
3450
|
res.json({ ${namePlural}: [], total: 0 });
|
|
@@ -2941,21 +3453,26 @@ ${roleGuard} // TODO: fetch ${namePlural}
|
|
|
2941
3453
|
export const POST = async (req, res) => {
|
|
2942
3454
|
${roleGuard} const { ${createFields} } = req.body;
|
|
2943
3455
|
// TODO: create ${name}
|
|
2944
|
-
res.status(201).json({ message: '${name} created', ${
|
|
3456
|
+
res.status(201).json({ message: '${name} created', ${lower}: { id: 'new-id' } });
|
|
2945
3457
|
};
|
|
2946
3458
|
`;
|
|
2947
|
-
const
|
|
2948
|
-
${
|
|
3459
|
+
const byIdMeta = mkMeta(opts, {
|
|
3460
|
+
GET: { description: `Fetch a single ${lower} by ID (admin only).`, params: `{ id: '${lower}_01HXZ' }`, response: `{ ${lower}: ${sampleFullRecord} }` },
|
|
3461
|
+
PUT: { description: `Update a ${lower} by ID (admin only).`, params: `{ id: '${lower}_01HXZ' }`, request: sampleCreateBody, response: `{ message: '${name} updated', ${lower}: ${sampleFullRecord} }` },
|
|
3462
|
+
DELETE: { description: `Delete a ${lower} by ID (admin only).`, params: `{ id: '${lower}_01HXZ' }`, response: `{ message: '${name} ${lower}_01HXZ deleted' }` }
|
|
3463
|
+
});
|
|
3464
|
+
const byIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3465
|
+
${byIdMeta}${mwAdmin4}
|
|
2949
3466
|
export const GET = async (req: Request, res: Response) => {
|
|
2950
3467
|
${roleGuard} const { id } = req.params;
|
|
2951
3468
|
// TODO: fetch ${name} by id
|
|
2952
|
-
res.json({ ${
|
|
3469
|
+
res.json({ ${lower}: { id } });
|
|
2953
3470
|
};
|
|
2954
3471
|
|
|
2955
3472
|
export const PUT = async (req: Request, res: Response) => {
|
|
2956
3473
|
${roleGuard} const { id } = req.params;
|
|
2957
3474
|
// TODO: update ${name}
|
|
2958
|
-
res.json({ message: '${name} updated', ${
|
|
3475
|
+
res.json({ message: '${name} updated', ${lower}: { id, ...req.body } });
|
|
2959
3476
|
};
|
|
2960
3477
|
|
|
2961
3478
|
export const DELETE = async (req: Request, res: Response) => {
|
|
@@ -2963,17 +3480,17 @@ ${roleGuard} const { id } = req.params;
|
|
|
2963
3480
|
// TODO: delete ${name}
|
|
2964
3481
|
res.json({ message: \`${name} \${id} deleted\` });
|
|
2965
3482
|
};
|
|
2966
|
-
` : `${RA}${
|
|
3483
|
+
` : `${RA}${byIdMeta}${mwAdmin4}
|
|
2967
3484
|
export const GET = async (req, res) => {
|
|
2968
3485
|
${roleGuard} const { id } = req.params;
|
|
2969
3486
|
// TODO: fetch ${name} by id
|
|
2970
|
-
res.json({ ${
|
|
3487
|
+
res.json({ ${lower}: { id } });
|
|
2971
3488
|
};
|
|
2972
3489
|
|
|
2973
3490
|
export const PUT = async (req, res) => {
|
|
2974
3491
|
${roleGuard} const { id } = req.params;
|
|
2975
3492
|
// TODO: update ${name}
|
|
2976
|
-
res.json({ message: '${name} updated', ${
|
|
3493
|
+
res.json({ message: '${name} updated', ${lower}: { id, ...req.body } });
|
|
2977
3494
|
};
|
|
2978
3495
|
|
|
2979
3496
|
export const DELETE = async (req, res) => {
|
|
@@ -2999,15 +3516,17 @@ ${roleGuard} const { id } = req.params;
|
|
|
2999
3516
|
const { listContent: couponList, byIdContent: couponById } = mkContentCrud("Coupon", "coupons", [], "code, type, value");
|
|
3000
3517
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "billing", "coupons", `index.${ext}`), couponList);
|
|
3001
3518
|
await fs.outputFile(path.join(dest, "src", "api", "admin", "billing", "coupons", `[id].${ext}`), couponById);
|
|
3002
|
-
const adminSubsMeta = mkMeta(opts,
|
|
3003
|
-
|
|
3519
|
+
const adminSubsMeta = mkMeta(opts, {
|
|
3520
|
+
GET: { description: "List all subscriptions with filters (admin only).", query: `{ status: 'active', page: '1', limit: '20' }`, response: `{ subscriptions: [], total: 0, page: 1, limit: 20 }` }
|
|
3521
|
+
});
|
|
3522
|
+
const adminSubsContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3004
3523
|
${adminSubsMeta}${mwAdmin4}
|
|
3005
3524
|
export const GET = async (req: Request, res: Response) => {
|
|
3006
3525
|
${roleGuard} const { status, page = 1, limit = 20 } = req.query;
|
|
3007
3526
|
// TODO: fetch all subscriptions with filters
|
|
3008
3527
|
res.json({ subscriptions: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
3009
3528
|
};
|
|
3010
|
-
` : `${RA}${
|
|
3529
|
+
` : `${RA}${adminSubsMeta}${mwAdmin4}
|
|
3011
3530
|
export const GET = async (req, res) => {
|
|
3012
3531
|
${roleGuard} const { status, page = 1, limit = 20 } = req.query;
|
|
3013
3532
|
// TODO: fetch all subscriptions with filters
|
|
@@ -3064,7 +3583,7 @@ async function main() {
|
|
|
3064
3583
|
{ value: "routeDocs", label: "API route documentation", hint: "meta exports + dashboard" },
|
|
3065
3584
|
{ value: "userPortal", label: "User portal", hint: "auth, profile, billing routes" },
|
|
3066
3585
|
{ value: "adminPortal", label: "Admin portal", hint: "dashboard, user mgmt, analytics" },
|
|
3067
|
-
{ value: "rbac", label: "Role-based access control", hint: "
|
|
3586
|
+
{ value: "rbac", label: "Role-based access control", hint: "requireAuth('role') middleware" },
|
|
3068
3587
|
{ value: "mailer", label: "Mailer", hint: "nodemailer + SMTP" }
|
|
3069
3588
|
],
|
|
3070
3589
|
initialValues: ["cluster", "tasks", "routeDocs", "userPortal", "adminPortal", "rbac"],
|
|
@@ -3177,6 +3696,13 @@ async function main() {
|
|
|
3177
3696
|
await npmInstallGlobal().catch(() => {
|
|
3178
3697
|
});
|
|
3179
3698
|
spinner2.stop("efc CLI ready");
|
|
3699
|
+
if (opts.mailer) {
|
|
3700
|
+
p.note(
|
|
3701
|
+
`SMTP_USER and SMTP_PASS were written to ${projectName}/.env \u2014 that file is gitignored, never commit it.
|
|
3702
|
+
` + (opts.smtpProvider === "gmail" ? "If this Gmail app password ever leaks, revoke it and generate a new one." : "Rotate SMTP_PASS immediately if it is ever exposed."),
|
|
3703
|
+
"Mailer credentials"
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3180
3706
|
p.outro(
|
|
3181
3707
|
pc.green(`
|
|
3182
3708
|
Your project is ready!
|