create-efc-app 0.4.3 → 0.4.5
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 +1062 -417
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,9 +9,33 @@ import { spawn } from "child_process";
|
|
|
9
9
|
import fs from "fs-extra";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import crypto from "crypto";
|
|
12
|
+
var NO_ADMIN_FEATURES = {
|
|
13
|
+
userManagement: false,
|
|
14
|
+
adminManagement: false,
|
|
15
|
+
analytics: false,
|
|
16
|
+
contentManagement: false,
|
|
17
|
+
billingManagement: false,
|
|
18
|
+
supportManagement: false,
|
|
19
|
+
notificationsAndLogs: false,
|
|
20
|
+
systemSettings: false
|
|
21
|
+
};
|
|
22
|
+
var NO_USER_FEATURES = {
|
|
23
|
+
profileViewing: false,
|
|
24
|
+
forgotPassword: false,
|
|
25
|
+
accountSecurity: false,
|
|
26
|
+
emailVerification: false,
|
|
27
|
+
accountSettings: false,
|
|
28
|
+
notifications: false,
|
|
29
|
+
filesAndMedia: false,
|
|
30
|
+
apiAndBilling: false,
|
|
31
|
+
support: false
|
|
32
|
+
};
|
|
12
33
|
async function scaffold(opts) {
|
|
13
34
|
const dest = path.resolve(process.cwd(), opts.projectName);
|
|
14
35
|
await fs.ensureDir(dest);
|
|
36
|
+
const uf = opts.userFeatures;
|
|
37
|
+
const af = opts.adminFeatures;
|
|
38
|
+
const needsUserModel = opts.userPortal || opts.adminPortal && af.userManagement;
|
|
15
39
|
await writePackageJson(dest, opts);
|
|
16
40
|
await writeTsConfig(dest, opts);
|
|
17
41
|
await writeEfcConfig(dest, opts);
|
|
@@ -19,30 +43,32 @@ async function scaffold(opts) {
|
|
|
19
43
|
await writeGitignore(dest);
|
|
20
44
|
await writeEnvFiles(dest, opts);
|
|
21
45
|
await writeExampleRoute(dest, opts);
|
|
22
|
-
if (
|
|
46
|
+
if (needsUserModel) await writeUserModel(dest, opts);
|
|
23
47
|
if (opts.adminPortal) await writeAdminModel(dest, opts);
|
|
24
48
|
await writeAuthRoutes(dest, opts);
|
|
25
49
|
if (opts.adminPortal) await writeAdminRoutes(dest, opts);
|
|
26
|
-
if (opts.userPortal) await writeUserRoutes(dest, opts);
|
|
50
|
+
if (opts.userPortal && uf.profileViewing) await writeUserRoutes(dest, opts);
|
|
27
51
|
if (opts.tasks) await writeExampleTask(dest, opts);
|
|
28
|
-
if (opts.userPortal) await writeSessionModel(dest, opts);
|
|
29
|
-
if (opts.userPortal
|
|
30
|
-
|
|
31
|
-
if (opts.userPortal
|
|
32
|
-
if (opts.
|
|
33
|
-
|
|
34
|
-
if (opts.adminPortal) await
|
|
35
|
-
if (opts.userPortal) await
|
|
36
|
-
if (opts.
|
|
37
|
-
if (opts.
|
|
38
|
-
if (opts.
|
|
39
|
-
if (opts.adminPortal) await
|
|
40
|
-
if (opts.adminPortal) await
|
|
41
|
-
if (opts.adminPortal) await
|
|
52
|
+
if (opts.userPortal && uf.accountSecurity) await writeSessionModel(dest, opts);
|
|
53
|
+
if (opts.userPortal && uf.notifications || opts.adminPortal && af.notificationsAndLogs)
|
|
54
|
+
await writeNotificationModel(dest, opts);
|
|
55
|
+
if (opts.userPortal && uf.filesAndMedia) await writeFileModel(dest, opts);
|
|
56
|
+
if (opts.userPortal && uf.support || opts.adminPortal && af.supportManagement)
|
|
57
|
+
await writeSupportTicketModel(dest, opts);
|
|
58
|
+
if (opts.adminPortal && af.notificationsAndLogs) await writeAuditLogModel(dest, opts);
|
|
59
|
+
if (opts.userPortal && uf.apiAndBilling) await writeSubscriptionModel(dest, opts);
|
|
60
|
+
if (opts.adminPortal && af.billingManagement) await writePlanModel(dest, opts);
|
|
61
|
+
if (opts.userPortal && uf.apiAndBilling) await writeInvoiceModel(dest, opts);
|
|
62
|
+
if (opts.userPortal && uf.apiAndBilling) await writeApiKeyModel(dest, opts);
|
|
63
|
+
if (opts.rbac && opts.adminPortal && af.adminManagement) await writeRoleModel(dest, opts);
|
|
64
|
+
if (opts.adminPortal && af.contentManagement) await writeFAQModel(dest, opts);
|
|
65
|
+
if (opts.adminPortal && af.contentManagement) await writeBlogModel(dest, opts);
|
|
66
|
+
if (opts.adminPortal && af.contentManagement) await writeCategoryModel(dest, opts);
|
|
67
|
+
if (opts.adminPortal && af.billingManagement) await writeCouponModel(dest, opts);
|
|
42
68
|
if (opts.userPortal) await writeAuthExtendedRoutes(dest, opts);
|
|
43
69
|
if (opts.userPortal) await writeUserExtendedRoutes(dest, opts);
|
|
44
|
-
if (opts.userPortal) await writeUserBillingRoutes(dest, opts);
|
|
45
|
-
if (opts.userPortal) await writeSupportRoutes(dest, opts);
|
|
70
|
+
if (opts.userPortal && uf.apiAndBilling) await writeUserBillingRoutes(dest, opts);
|
|
71
|
+
if (opts.userPortal && uf.support) await writeSupportRoutes(dest, opts);
|
|
46
72
|
if (opts.adminPortal) await writeAdminExtendedRoutes(dest, opts);
|
|
47
73
|
}
|
|
48
74
|
async function writePackageJson(dest, opts) {
|
|
@@ -440,16 +466,47 @@ export const meta: RouteMeta = {
|
|
|
440
466
|
};
|
|
441
467
|
|
|
442
468
|
` : "";
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
` : `import bcrypt from 'bcrypt';
|
|
469
|
+
const hasUser = opts.userPortal;
|
|
470
|
+
const hasAdmin = opts.adminPortal;
|
|
471
|
+
const mongo = opts.database === "mongodb" && (hasUser || hasAdmin);
|
|
472
|
+
const loginDbImports = mongo ? `import bcrypt from 'bcrypt';
|
|
448
473
|
import crypto from 'node:crypto';
|
|
449
|
-
import { User } from '../../model/User.js';
|
|
450
|
-
import { Admin } from '../../model/Admin.js';
|
|
451
|
-
` : "";
|
|
452
|
-
const
|
|
474
|
+
${hasUser ? `import { User } from '../../model/User.js';
|
|
475
|
+
` : ""}${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
476
|
+
` : ""}` : "";
|
|
477
|
+
const loginBody = hasAdmin && hasUser ? ` const admin = await Admin.findOne({ email });
|
|
478
|
+
if (admin) {
|
|
479
|
+
const match = await bcrypt.compare(password, admin.password);
|
|
480
|
+
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
481
|
+
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
482
|
+
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
483
|
+
await issueRefreshToken(res, Admin, admin.id);
|
|
484
|
+
return res.json({ message: 'Logged in as admin' });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const user = await User.findOne({ email });
|
|
488
|
+
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
489
|
+
const match = await bcrypt.compare(password, user.password);
|
|
490
|
+
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
491
|
+
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
492
|
+
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
493
|
+
await issueRefreshToken(res, User, user.id);
|
|
494
|
+
res.json({ message: 'Logged in' });` : hasAdmin ? ` const admin = await Admin.findOne({ email });
|
|
495
|
+
if (!admin) return res.status(401).json({ error: 'Invalid credentials' });
|
|
496
|
+
const match = await bcrypt.compare(password, admin.password);
|
|
497
|
+
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
498
|
+
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
499
|
+
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
500
|
+
await issueRefreshToken(res, Admin, admin.id);
|
|
501
|
+
res.json({ message: 'Logged in as admin' });` : ` const user = await User.findOne({ email });
|
|
502
|
+
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
503
|
+
const match = await bcrypt.compare(password, user.password);
|
|
504
|
+
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
505
|
+
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
506
|
+
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
507
|
+
await issueRefreshToken(res, User, user.id);
|
|
508
|
+
res.json({ message: 'Logged in' });`;
|
|
509
|
+
const loginContent = mongo ? ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
453
510
|
import type { Request, Response } from 'express';
|
|
454
511
|
${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
455
512
|
|
|
@@ -472,24 +529,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
472
529
|
const { email, password } = req.body;
|
|
473
530
|
if (!email || !password) return res.status(400).json({ error: 'email and password are required' });
|
|
474
531
|
|
|
475
|
-
|
|
476
|
-
if (admin) {
|
|
477
|
-
const match = await bcrypt.compare(password, admin.password);
|
|
478
|
-
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
479
|
-
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
480
|
-
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
481
|
-
await issueRefreshToken(res, Admin, admin.id);
|
|
482
|
-
return res.json({ message: 'Logged in as admin' });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const user = await User.findOne({ email });
|
|
486
|
-
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
487
|
-
const match = await bcrypt.compare(password, user.password);
|
|
488
|
-
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
489
|
-
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
490
|
-
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
491
|
-
await issueRefreshToken(res, User, user.id);
|
|
492
|
-
res.json({ message: 'Logged in' });
|
|
532
|
+
${loginBody}
|
|
493
533
|
};
|
|
494
534
|
` : `import { issueToken } from 'express-file-cluster/auth';
|
|
495
535
|
${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
@@ -509,24 +549,7 @@ export const POST = async (req, res) => {
|
|
|
509
549
|
const { email, password } = req.body;
|
|
510
550
|
if (!email || !password) return res.status(400).json({ error: 'email and password are required' });
|
|
511
551
|
|
|
512
|
-
|
|
513
|
-
if (admin) {
|
|
514
|
-
const match = await bcrypt.compare(password, admin.password);
|
|
515
|
-
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
516
|
-
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
517
|
-
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
518
|
-
await issueRefreshToken(res, Admin, admin.id);
|
|
519
|
-
return res.json({ message: 'Logged in as admin' });
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const user = await User.findOne({ email });
|
|
523
|
-
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
524
|
-
const match = await bcrypt.compare(password, user.password);
|
|
525
|
-
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
526
|
-
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
527
|
-
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
528
|
-
await issueRefreshToken(res, User, user.id);
|
|
529
|
-
res.json({ message: 'Logged in' });
|
|
552
|
+
${loginBody}
|
|
530
553
|
};
|
|
531
554
|
` : ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
532
555
|
import type { Request, Response } from 'express';
|
|
@@ -725,9 +748,10 @@ export const meta: RouteMeta = {
|
|
|
725
748
|
};
|
|
726
749
|
|
|
727
750
|
` : "";
|
|
728
|
-
const
|
|
751
|
+
const dashboardHasUserModel = opts.database === "mongodb" && opts.adminFeatures.userManagement;
|
|
752
|
+
const dashboardDbImport = dashboardHasUserModel ? `import { User } from '../../model/User.js';
|
|
729
753
|
` : "";
|
|
730
|
-
const dashboardContent =
|
|
754
|
+
const dashboardContent = dashboardHasUserModel ? ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
731
755
|
import type { Request, Response } from 'express';
|
|
732
756
|
${dashboardDbImport}${dashboardMeta}${middlewares}
|
|
733
757
|
export const GET = async (_req: Request, res: Response) => {
|
|
@@ -763,6 +787,7 @@ ${roleGuard} // TODO: aggregate stats from DB
|
|
|
763
787
|
};
|
|
764
788
|
`;
|
|
765
789
|
await fs.outputFile(path.join(dest, "src", "api", "admin", `dashboard.${ext}`), dashboardContent);
|
|
790
|
+
if (!opts.adminFeatures.userManagement) return;
|
|
766
791
|
const usersListMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
767
792
|
|
|
768
793
|
export const meta: RouteMeta = {
|
|
@@ -866,7 +891,10 @@ ${roleGuard} const { name, email, role } = req.body;
|
|
|
866
891
|
res.status(201).json({ message: 'User created', user: { id: 'new-id', name, email, role: role ?? 'user' } });
|
|
867
892
|
};
|
|
868
893
|
`;
|
|
869
|
-
await fs.outputFile(
|
|
894
|
+
await fs.outputFile(
|
|
895
|
+
path.join(dest, "src", "api", "admin", "users", `index.${ext}`),
|
|
896
|
+
usersListContent
|
|
897
|
+
);
|
|
870
898
|
const userByIdMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
871
899
|
|
|
872
900
|
export const meta: RouteMeta = {
|
|
@@ -1001,7 +1029,10 @@ ${roleGuard} const { id } = req.params;
|
|
|
1001
1029
|
res.json({ message: \`User \${id} deleted\` });
|
|
1002
1030
|
};
|
|
1003
1031
|
`;
|
|
1004
|
-
await fs.outputFile(
|
|
1032
|
+
await fs.outputFile(
|
|
1033
|
+
path.join(dest, "src", "api", "admin", "users", `[id].${ext}`),
|
|
1034
|
+
userByIdContent
|
|
1035
|
+
);
|
|
1005
1036
|
}
|
|
1006
1037
|
async function writeUserRoutes(dest, opts) {
|
|
1007
1038
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
@@ -1850,6 +1881,8 @@ async function writeAuthExtendedRoutes(dest, opts) {
|
|
|
1850
1881
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
1851
1882
|
`;
|
|
1852
1883
|
const mongo = opts.database === "mongodb";
|
|
1884
|
+
const hasAdmin = opts.adminPortal;
|
|
1885
|
+
const uf = opts.userFeatures;
|
|
1853
1886
|
const sendResetEmail = opts.mailer ? ` var appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
1854
1887
|
await enqueue('SendEmail', {
|
|
1855
1888
|
to: email,
|
|
@@ -1868,23 +1901,32 @@ async function writeAuthExtendedRoutes(dest, opts) {
|
|
|
1868
1901
|
`;
|
|
1869
1902
|
const mailerTaskImport = opts.mailer ? `import { enqueue } from 'express-file-cluster/tasks';
|
|
1870
1903
|
` : "";
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1904
|
+
if (uf.accountSecurity) {
|
|
1905
|
+
const refreshMeta = mkMeta(opts, {
|
|
1906
|
+
POST: {
|
|
1907
|
+
description: "Refresh the JWT using the refresh-token cookie and issue a new access token.",
|
|
1908
|
+
response: `{ message: 'Token refreshed' }`
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
const refreshModelImports = `import { User } from '../../model/User.js';
|
|
1912
|
+
${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
1913
|
+
` : ""}`;
|
|
1914
|
+
const refreshLookup = hasAdmin ? ` const user = await User.findOne({ refreshToken: token });
|
|
1915
|
+
const admin = user ? null : await Admin.findOne({ refreshToken: token });
|
|
1916
|
+
const account = user || admin;` : ` const user = await User.findOne({ refreshToken: token });
|
|
1917
|
+
const account = user;`;
|
|
1918
|
+
const refreshPersist = hasAdmin ? ` if (user) await User.update(user.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1919
|
+
else if (admin) await Admin.update(admin.id, { refreshToken: newRefreshToken, refreshTokenExpiry });` : ` await User.update(user.id, { refreshToken: newRefreshToken, refreshTokenExpiry });`;
|
|
1920
|
+
const refreshContent = mongo ? ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
1875
1921
|
import type { Request, Response } from 'express';
|
|
1876
1922
|
import crypto from 'node:crypto';
|
|
1877
|
-
|
|
1878
|
-
import { Admin } from '../../model/Admin.js';
|
|
1879
|
-
${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
1923
|
+
${refreshModelImports}${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
1880
1924
|
|
|
1881
1925
|
export const POST = async (req: Request, res: Response) => {
|
|
1882
1926
|
const token = req.cookies?.['efc_refresh_token'] || req.body?.refreshToken;
|
|
1883
1927
|
if (!token) return res.status(401).json({ error: 'Refresh token required' });
|
|
1884
1928
|
|
|
1885
|
-
|
|
1886
|
-
const admin = user ? null : await Admin.findOne({ refreshToken: token });
|
|
1887
|
-
const account = user || admin;
|
|
1929
|
+
${refreshLookup}
|
|
1888
1930
|
|
|
1889
1931
|
if (!account || !account.refreshTokenExpiry || new Date(account.refreshTokenExpiry) < new Date()) {
|
|
1890
1932
|
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
@@ -1892,8 +1934,7 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
1892
1934
|
|
|
1893
1935
|
const newRefreshToken = crypto.randomBytes(40).toString('hex');
|
|
1894
1936
|
const refreshTokenExpiry = new Date(Date.now() + REFRESH_TOKEN_TTL_MS);
|
|
1895
|
-
|
|
1896
|
-
else if (admin) await Admin.update(admin.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1937
|
+
${refreshPersist}
|
|
1897
1938
|
|
|
1898
1939
|
res.cookie('efc_refresh_token', newRefreshToken, {
|
|
1899
1940
|
httpOnly: true,
|
|
@@ -1907,17 +1948,13 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
1907
1948
|
};
|
|
1908
1949
|
` : `import { issueToken } from 'express-file-cluster/auth';
|
|
1909
1950
|
import crypto from 'node:crypto';
|
|
1910
|
-
|
|
1911
|
-
import { Admin } from '../../model/Admin.js';
|
|
1912
|
-
${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
1951
|
+
${refreshModelImports}${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
1913
1952
|
|
|
1914
1953
|
export const POST = async (req, res) => {
|
|
1915
1954
|
const token = req.cookies?.efc_refresh_token || req.body?.refreshToken;
|
|
1916
1955
|
if (!token) return res.status(401).json({ error: 'Refresh token required' });
|
|
1917
1956
|
|
|
1918
|
-
|
|
1919
|
-
const admin = user ? null : await Admin.findOne({ refreshToken: token });
|
|
1920
|
-
const account = user || admin;
|
|
1957
|
+
${refreshLookup}
|
|
1921
1958
|
|
|
1922
1959
|
if (!account || !account.refreshTokenExpiry || new Date(account.refreshTokenExpiry) < new Date()) {
|
|
1923
1960
|
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
@@ -1925,8 +1962,7 @@ export const POST = async (req, res) => {
|
|
|
1925
1962
|
|
|
1926
1963
|
const newRefreshToken = crypto.randomBytes(40).toString('hex');
|
|
1927
1964
|
const refreshTokenExpiry = new Date(Date.now() + REFRESH_TOKEN_TTL_MS);
|
|
1928
|
-
|
|
1929
|
-
else if (admin) await Admin.update(admin.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1965
|
+
${refreshPersist}
|
|
1930
1966
|
|
|
1931
1967
|
res.cookie('efc_refresh_token', newRefreshToken, {
|
|
1932
1968
|
httpOnly: true,
|
|
@@ -1948,12 +1984,22 @@ ${refreshMeta}export const POST = async (_req: Request, res: Response) => {
|
|
|
1948
1984
|
res.json({ message: 'Token refreshed' });
|
|
1949
1985
|
};
|
|
1950
1986
|
`;
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1987
|
+
await fs.outputFile(path.join(dest, "src", "api", "auth", `refresh.${ext}`), refreshContent);
|
|
1988
|
+
}
|
|
1989
|
+
if (uf.emailVerification) {
|
|
1990
|
+
const veMeta = mkMeta(opts, {
|
|
1991
|
+
GET: {
|
|
1992
|
+
description: "Verify an email address using the token from the verification link.",
|
|
1993
|
+
query: `{ token: 'a1b2c3d4' }`,
|
|
1994
|
+
response: `{ message: 'Email verified' }`
|
|
1995
|
+
},
|
|
1996
|
+
POST: {
|
|
1997
|
+
description: "Resend the verification email to a given address.",
|
|
1998
|
+
request: `{ email: 'user@example.com' }`,
|
|
1999
|
+
response: `{ message: 'Verification email sent' }`
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
const veContent = mongo ? ts ? `import type { Request, Response } from 'express';
|
|
1957
2003
|
import crypto from 'node:crypto';
|
|
1958
2004
|
${mailerTaskImport}import { User } from '../../model/User.js';
|
|
1959
2005
|
${veMeta}export const GET = async (req: Request, res: Response) => {
|
|
@@ -2026,53 +2072,59 @@ export const POST = async (_req, res) => {
|
|
|
2026
2072
|
res.json({ message: 'Verification email sent' });
|
|
2027
2073
|
};
|
|
2028
2074
|
`;
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2075
|
+
await fs.outputFile(path.join(dest, "src", "api", "auth", `verify-email.${ext}`), veContent);
|
|
2076
|
+
}
|
|
2077
|
+
if (uf.forgotPassword) {
|
|
2078
|
+
const fpMeta = mkMeta(opts, {
|
|
2079
|
+
POST: {
|
|
2080
|
+
description: "Send a password reset email to the given address.",
|
|
2081
|
+
request: `{ email: 'user@example.com' }`,
|
|
2082
|
+
response: `{ message: 'Reset email sent' }`
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
const fpModelImports = `import { User } from '../../model/User.js';
|
|
2086
|
+
${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
2087
|
+
` : ""}`;
|
|
2088
|
+
const fpLookup = hasAdmin ? ` const user = await User.findOne({ email });
|
|
2089
|
+
const admin = user ? null : await Admin.findOne({ email });` : ` const user = await User.findOne({ email });`;
|
|
2090
|
+
const fpGuard = hasAdmin ? "user || admin" : "user";
|
|
2091
|
+
const fpPersist = hasAdmin ? ` if (user) await User.update(user.id, { resetToken, resetTokenExpiry });
|
|
2092
|
+
else if (admin) await Admin.update(admin.id, { resetToken, resetTokenExpiry });` : ` await User.update(user.id, { resetToken, resetTokenExpiry });`;
|
|
2093
|
+
const fpContent = mongo ? ts ? `import type { Request, Response } from 'express';
|
|
2034
2094
|
import crypto from 'node:crypto';
|
|
2035
|
-
${mailerTaskImport}
|
|
2036
|
-
import { Admin } from '../../model/Admin.js';
|
|
2037
|
-
${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2095
|
+
${mailerTaskImport}${fpModelImports}${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2038
2096
|
|
|
2039
2097
|
export const POST = async (req: Request, res: Response) => {
|
|
2040
2098
|
const { email } = req.body;
|
|
2041
2099
|
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
2042
2100
|
|
|
2043
|
-
|
|
2044
|
-
const admin = user ? null : await Admin.findOne({ email });
|
|
2101
|
+
${fpLookup}
|
|
2045
2102
|
|
|
2046
2103
|
// Always respond the same way whether or not the account exists, so this
|
|
2047
2104
|
// endpoint can't be used to enumerate registered emails.
|
|
2048
|
-
if (
|
|
2105
|
+
if (${fpGuard}) {
|
|
2049
2106
|
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
2050
2107
|
const resetTokenExpiry = new Date(Date.now() + RESET_TOKEN_TTL_MS);
|
|
2051
|
-
|
|
2052
|
-
else if (admin) await Admin.update(admin.id, { resetToken, resetTokenExpiry });
|
|
2108
|
+
${fpPersist}
|
|
2053
2109
|
${sendResetEmail} }
|
|
2054
2110
|
|
|
2055
2111
|
res.json({ message: 'Reset email sent' });
|
|
2056
2112
|
};
|
|
2057
2113
|
` : `import crypto from 'node:crypto';
|
|
2058
|
-
${mailerTaskImport}
|
|
2059
|
-
import { Admin } from '../../model/Admin.js';
|
|
2060
|
-
${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2114
|
+
${mailerTaskImport}${fpModelImports}${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2061
2115
|
|
|
2062
2116
|
export const POST = async (req, res) => {
|
|
2063
2117
|
const { email } = req.body;
|
|
2064
2118
|
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
2065
2119
|
|
|
2066
|
-
|
|
2067
|
-
const admin = user ? null : await Admin.findOne({ email });
|
|
2120
|
+
${fpLookup}
|
|
2068
2121
|
|
|
2069
2122
|
// Always respond the same way whether or not the account exists, so this
|
|
2070
2123
|
// endpoint can't be used to enumerate registered emails.
|
|
2071
|
-
if (
|
|
2124
|
+
if (${fpGuard}) {
|
|
2072
2125
|
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
2073
2126
|
const resetTokenExpiry = new Date(Date.now() + RESET_TOKEN_TTL_MS);
|
|
2074
|
-
|
|
2075
|
-
else if (admin) await Admin.update(admin.id, { resetToken, resetTokenExpiry });
|
|
2127
|
+
${fpPersist}
|
|
2076
2128
|
${sendResetEmail} }
|
|
2077
2129
|
|
|
2078
2130
|
res.json({ message: 'Reset email sent' });
|
|
@@ -2091,50 +2143,53 @@ ${fpMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
2091
2143
|
res.json({ message: 'Reset email sent' });
|
|
2092
2144
|
};
|
|
2093
2145
|
`;
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2146
|
+
await fs.outputFile(path.join(dest, "src", "api", "auth", `forgot-password.${ext}`), fpContent);
|
|
2147
|
+
const rpMeta = mkMeta(opts, {
|
|
2148
|
+
POST: {
|
|
2149
|
+
description: "Reset password using a valid reset token.",
|
|
2150
|
+
request: `{ token: 'reset-token', password: 'newpassword' }`,
|
|
2151
|
+
response: `{ message: 'Password reset successfully' }`
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
const rpModelImports = `import { User } from '../../model/User.js';
|
|
2155
|
+
${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
2156
|
+
` : ""}`;
|
|
2157
|
+
const rpLookup = hasAdmin ? ` const user = await User.findOne({ resetToken: token });
|
|
2158
|
+
const admin = user ? null : await Admin.findOne({ resetToken: token });
|
|
2159
|
+
const account = user || admin;` : ` const user = await User.findOne({ resetToken: token });
|
|
2160
|
+
const account = user;`;
|
|
2161
|
+
const rpPersist = hasAdmin ? ` if (user) await User.update(user.id, { password: hashed, resetToken: '' });
|
|
2162
|
+
else if (admin) await Admin.update(admin.id, { password: hashed, resetToken: '' });` : ` await User.update(user.id, { password: hashed, resetToken: '' });`;
|
|
2163
|
+
const rpContent = mongo ? ts ? `import type { Request, Response } from 'express';
|
|
2099
2164
|
import bcrypt from 'bcrypt';
|
|
2100
|
-
|
|
2101
|
-
import { Admin } from '../../model/Admin.js';
|
|
2102
|
-
${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
2165
|
+
${rpModelImports}${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
2103
2166
|
const { token, password } = req.body;
|
|
2104
2167
|
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
2105
2168
|
|
|
2106
|
-
|
|
2107
|
-
const admin = user ? null : await Admin.findOne({ resetToken: token });
|
|
2108
|
-
const account = user || admin;
|
|
2169
|
+
${rpLookup}
|
|
2109
2170
|
|
|
2110
2171
|
if (!account || !account.resetTokenExpiry || new Date(account.resetTokenExpiry) < new Date()) {
|
|
2111
2172
|
return res.status(400).json({ error: 'Invalid or expired reset token' });
|
|
2112
2173
|
}
|
|
2113
2174
|
|
|
2114
2175
|
const hashed = await bcrypt.hash(password, 10);
|
|
2115
|
-
|
|
2116
|
-
else if (admin) await Admin.update(admin.id, { password: hashed, resetToken: '' });
|
|
2176
|
+
${rpPersist}
|
|
2117
2177
|
|
|
2118
2178
|
res.json({ message: 'Password reset successfully' });
|
|
2119
2179
|
};
|
|
2120
2180
|
` : `import bcrypt from 'bcrypt';
|
|
2121
|
-
|
|
2122
|
-
import { Admin } from '../../model/Admin.js';
|
|
2123
|
-
${rpMeta}export const POST = async (req, res) => {
|
|
2181
|
+
${rpModelImports}${rpMeta}export const POST = async (req, res) => {
|
|
2124
2182
|
const { token, password } = req.body;
|
|
2125
2183
|
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
2126
2184
|
|
|
2127
|
-
|
|
2128
|
-
const admin = user ? null : await Admin.findOne({ resetToken: token });
|
|
2129
|
-
const account = user || admin;
|
|
2185
|
+
${rpLookup}
|
|
2130
2186
|
|
|
2131
2187
|
if (!account || !account.resetTokenExpiry || new Date(account.resetTokenExpiry) < new Date()) {
|
|
2132
2188
|
return res.status(400).json({ error: 'Invalid or expired reset token' });
|
|
2133
2189
|
}
|
|
2134
2190
|
|
|
2135
2191
|
const hashed = await bcrypt.hash(password, 10);
|
|
2136
|
-
|
|
2137
|
-
else if (admin) await Admin.update(admin.id, { password: hashed, resetToken: '' });
|
|
2192
|
+
${rpPersist}
|
|
2138
2193
|
|
|
2139
2194
|
res.json({ message: 'Password reset successfully' });
|
|
2140
2195
|
};
|
|
@@ -2152,11 +2207,17 @@ ${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
2152
2207
|
res.json({ message: 'Password reset successfully' });
|
|
2153
2208
|
};
|
|
2154
2209
|
`;
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2210
|
+
await fs.outputFile(path.join(dest, "src", "api", "auth", `reset-password.${ext}`), rpContent);
|
|
2211
|
+
}
|
|
2212
|
+
if (uf.accountSecurity) {
|
|
2213
|
+
const cpMeta = mkMeta(opts, {
|
|
2214
|
+
POST: {
|
|
2215
|
+
description: "Change password for the authenticated user.",
|
|
2216
|
+
request: `{ oldPassword: 'current', newPassword: 'newpassword' }`,
|
|
2217
|
+
response: `{ message: 'Password changed successfully' }`
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
const cpContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2160
2221
|
${cpMeta}${mwUser2}
|
|
2161
2222
|
export const POST = async (req: Request, res: Response) => {
|
|
2162
2223
|
const { oldPassword, newPassword } = req.body;
|
|
@@ -2172,12 +2233,19 @@ export const POST = async (req, res) => {
|
|
|
2172
2233
|
res.json({ message: 'Password changed successfully' });
|
|
2173
2234
|
};
|
|
2174
2235
|
`;
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2236
|
+
await fs.outputFile(path.join(dest, "src", "api", "auth", `change-password.${ext}`), cpContent);
|
|
2237
|
+
const tfaSetupMeta = mkMeta(opts, {
|
|
2238
|
+
GET: {
|
|
2239
|
+
description: "Generate a TOTP secret and QR code to set up 2FA.",
|
|
2240
|
+
response: `{ qrCode: 'otpauth://totp/...', secret: 'BASE32SECRET' }`
|
|
2241
|
+
},
|
|
2242
|
+
POST: {
|
|
2243
|
+
description: "Confirm a TOTP code to enable 2FA for the authenticated user.",
|
|
2244
|
+
request: `{ code: '123456' }`,
|
|
2245
|
+
response: `{ message: '2FA enabled' }`
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
const tfaSetupContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2181
2249
|
${tfaSetupMeta}${mwUser3}
|
|
2182
2250
|
export const GET = async (req: Request, res: Response) => {
|
|
2183
2251
|
// TODO: generate TOTP secret and return QR code URL
|
|
@@ -2203,11 +2271,18 @@ export const POST = async (req, res) => {
|
|
|
2203
2271
|
res.json({ message: '2FA enabled' });
|
|
2204
2272
|
};
|
|
2205
2273
|
`;
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2274
|
+
await fs.outputFile(
|
|
2275
|
+
path.join(dest, "src", "api", "auth", "2fa", `setup.${ext}`),
|
|
2276
|
+
tfaSetupContent
|
|
2277
|
+
);
|
|
2278
|
+
const tfaVerifyMeta = mkMeta(opts, {
|
|
2279
|
+
POST: {
|
|
2280
|
+
description: "Verify a TOTP code during login.",
|
|
2281
|
+
request: `{ code: '123456' }`,
|
|
2282
|
+
response: `{ message: '2FA verified' }`
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
const tfaVerifyContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2211
2286
|
${tfaVerifyMeta}export const POST = async (req: Request, res: Response) => {
|
|
2212
2287
|
const { code } = req.body;
|
|
2213
2288
|
if (!code) return res.status(400).json({ error: 'code is required' });
|
|
@@ -2221,11 +2296,18 @@ ${tfaVerifyMeta}export const POST = async (req: Request, res: Response) => {
|
|
|
2221
2296
|
res.json({ message: '2FA verified' });
|
|
2222
2297
|
};
|
|
2223
2298
|
`;
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2299
|
+
await fs.outputFile(
|
|
2300
|
+
path.join(dest, "src", "api", "auth", "2fa", `verify.${ext}`),
|
|
2301
|
+
tfaVerifyContent
|
|
2302
|
+
);
|
|
2303
|
+
const tfaDisableMeta = mkMeta(opts, {
|
|
2304
|
+
POST: {
|
|
2305
|
+
description: "Disable 2FA for the authenticated user.",
|
|
2306
|
+
request: `{ code: '123456' }`,
|
|
2307
|
+
response: `{ message: '2FA disabled' }`
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
const tfaDisableContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2229
2311
|
${tfaDisableMeta}${mwUser3}
|
|
2230
2312
|
export const POST = async (req: Request, res: Response) => {
|
|
2231
2313
|
const { code } = req.body;
|
|
@@ -2241,11 +2323,17 @@ export const POST = async (req, res) => {
|
|
|
2241
2323
|
res.json({ message: '2FA disabled' });
|
|
2242
2324
|
};
|
|
2243
2325
|
`;
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2326
|
+
await fs.outputFile(
|
|
2327
|
+
path.join(dest, "src", "api", "auth", "2fa", `disable.${ext}`),
|
|
2328
|
+
tfaDisableContent
|
|
2329
|
+
);
|
|
2330
|
+
const sessListMeta = mkMeta(opts, {
|
|
2331
|
+
GET: {
|
|
2332
|
+
description: "List all active sessions for the authenticated user.",
|
|
2333
|
+
response: `{ sessions: [] }`
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
const sessListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2249
2337
|
${sessListMeta}${mwUser3}
|
|
2250
2338
|
export const GET = async (req: Request, res: Response) => {
|
|
2251
2339
|
// TODO: fetch sessions for req.user.id
|
|
@@ -2257,11 +2345,18 @@ export const GET = async (req, res) => {
|
|
|
2257
2345
|
res.json({ sessions: [] });
|
|
2258
2346
|
};
|
|
2259
2347
|
`;
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2348
|
+
await fs.outputFile(
|
|
2349
|
+
path.join(dest, "src", "api", "auth", "sessions", `index.${ext}`),
|
|
2350
|
+
sessListContent
|
|
2351
|
+
);
|
|
2352
|
+
const sessRevokeMeta = mkMeta(opts, {
|
|
2353
|
+
DELETE: {
|
|
2354
|
+
description: "Revoke a single active session by ID.",
|
|
2355
|
+
params: `{ id: 'sess_01HXZ' }`,
|
|
2356
|
+
response: `{ message: 'Session sess_01HXZ revoked' }`
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
const sessRevokeContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2265
2360
|
${sessRevokeMeta}${mwUser3}
|
|
2266
2361
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2267
2362
|
const { id } = req.params;
|
|
@@ -2275,7 +2370,11 @@ export const DELETE = async (req, res) => {
|
|
|
2275
2370
|
res.json({ message: \`Session \${id} revoked\` });
|
|
2276
2371
|
};
|
|
2277
2372
|
`;
|
|
2278
|
-
|
|
2373
|
+
await fs.outputFile(
|
|
2374
|
+
path.join(dest, "src", "api", "auth", "sessions", `[id].${ext}`),
|
|
2375
|
+
sessRevokeContent
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2279
2378
|
}
|
|
2280
2379
|
async function writeUserExtendedRoutes(dest, opts) {
|
|
2281
2380
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
@@ -2287,11 +2386,19 @@ async function writeUserExtendedRoutes(dest, opts) {
|
|
|
2287
2386
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
2288
2387
|
`;
|
|
2289
2388
|
const user = ts ? `(req as any).user` : `req.user`;
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2389
|
+
const uf = opts.userFeatures;
|
|
2390
|
+
if (uf.accountSettings) {
|
|
2391
|
+
const avatarMeta = mkMeta(opts, {
|
|
2392
|
+
POST: {
|
|
2393
|
+
description: "Upload a new avatar image for the authenticated user.",
|
|
2394
|
+
response: `{ message: 'Avatar updated', url: 'https://example.com/avatar.jpg' }`
|
|
2395
|
+
},
|
|
2396
|
+
DELETE: {
|
|
2397
|
+
description: "Remove the authenticated user's avatar.",
|
|
2398
|
+
response: `{ message: 'Avatar removed' }`
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
const avatarContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2295
2402
|
${avatarMeta}${mw2}
|
|
2296
2403
|
export const POST = async (req: Request, res: Response) => {
|
|
2297
2404
|
// TODO: handle multipart upload, store file, update user.avatar
|
|
@@ -2313,12 +2420,19 @@ export const DELETE = async (req, res) => {
|
|
|
2313
2420
|
res.json({ message: 'Avatar removed' });
|
|
2314
2421
|
};
|
|
2315
2422
|
`;
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2423
|
+
await fs.outputFile(path.join(dest, "src", "api", "user", `avatar.${ext}`), avatarContent);
|
|
2424
|
+
const settingsMeta = mkMeta(opts, {
|
|
2425
|
+
GET: {
|
|
2426
|
+
description: "Get account settings (notifications, language, theme, privacy) for the authenticated user.",
|
|
2427
|
+
response: `{ settings: { notifications: true, language: 'en', theme: 'system', privacy: 'public' } }`
|
|
2428
|
+
},
|
|
2429
|
+
PUT: {
|
|
2430
|
+
description: "Update account settings for the authenticated user.",
|
|
2431
|
+
request: `{ notifications: true, language: 'en', theme: 'system', privacy: 'public' }`,
|
|
2432
|
+
response: `{ message: 'Settings updated', settings: { notifications: true, language: 'en', theme: 'system', privacy: 'public' } }`
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
const settingsContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2322
2436
|
${settingsMeta}${mw2}
|
|
2323
2437
|
export const GET = async (req: Request, res: Response) => {
|
|
2324
2438
|
// TODO: fetch user settings from DB
|
|
@@ -2342,12 +2456,19 @@ export const PUT = async (req, res) => {
|
|
|
2342
2456
|
res.json({ message: 'Settings updated', settings: { notifications, language, theme, privacy } });
|
|
2343
2457
|
};
|
|
2344
2458
|
`;
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2459
|
+
await fs.outputFile(path.join(dest, "src", "api", "user", `settings.${ext}`), settingsContent);
|
|
2460
|
+
const accountMeta = mkMeta(opts, {
|
|
2461
|
+
GET: {
|
|
2462
|
+
description: "Download a personal data export for the authenticated user.",
|
|
2463
|
+
response: `{ data: { user: { id: '1', role: 'user', email: 'user@example.com' }, exportedAt: '2026-01-01T00:00:00.000Z' } }`
|
|
2464
|
+
},
|
|
2465
|
+
DELETE: {
|
|
2466
|
+
description: "Schedule the authenticated account for deletion (requires password confirmation).",
|
|
2467
|
+
request: `{ password: 'current' }`,
|
|
2468
|
+
response: `{ message: 'Account scheduled for deletion' }`
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
const accountContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2351
2472
|
${accountMeta}${mw2}
|
|
2352
2473
|
export const GET = async (req: Request, res: Response) => {
|
|
2353
2474
|
// TODO: compile and return personal data export
|
|
@@ -2373,11 +2494,14 @@ export const DELETE = async (req, res) => {
|
|
|
2373
2494
|
res.json({ message: 'Account scheduled for deletion' });
|
|
2374
2495
|
};
|
|
2375
2496
|
`;
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2497
|
+
await fs.outputFile(path.join(dest, "src", "api", "user", `account.${ext}`), accountContent);
|
|
2498
|
+
const dashMeta = mkMeta(opts, {
|
|
2499
|
+
GET: {
|
|
2500
|
+
description: "Personal dashboard: stats, recent activity, and quick actions.",
|
|
2501
|
+
response: `{ stats: {}, recentActivity: [], quickActions: [] }`
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
const dashContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2381
2505
|
${dashMeta}${mw2}
|
|
2382
2506
|
export const GET = async (req: Request, res: Response) => {
|
|
2383
2507
|
// TODO: aggregate personal stats and activity
|
|
@@ -2389,11 +2513,15 @@ export const GET = async (req, res) => {
|
|
|
2389
2513
|
res.json({ stats: {}, recentActivity: [], quickActions: [] });
|
|
2390
2514
|
};
|
|
2391
2515
|
`;
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2516
|
+
await fs.outputFile(path.join(dest, "src", "api", "user", `dashboard.${ext}`), dashContent);
|
|
2517
|
+
const actMeta = mkMeta(opts, {
|
|
2518
|
+
GET: {
|
|
2519
|
+
description: "Paginated activity history for the authenticated user.",
|
|
2520
|
+
query: `{ page: '1', limit: '20' }`,
|
|
2521
|
+
response: `{ activities: [], total: 0, page: 1, limit: 20 }`
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
const actContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2397
2525
|
${actMeta}${mw2}
|
|
2398
2526
|
export const GET = async (req: Request, res: Response) => {
|
|
2399
2527
|
const { page = 1, limit = 20 } = req.query;
|
|
@@ -2407,12 +2535,20 @@ export const GET = async (req, res) => {
|
|
|
2407
2535
|
res.json({ activities: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
2408
2536
|
};
|
|
2409
2537
|
`;
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2538
|
+
await fs.outputFile(path.join(dest, "src", "api", "user", `activity.${ext}`), actContent);
|
|
2539
|
+
}
|
|
2540
|
+
if (uf.notifications) {
|
|
2541
|
+
const notifListMeta = mkMeta(opts, {
|
|
2542
|
+
GET: {
|
|
2543
|
+
description: "List notifications for the authenticated user.",
|
|
2544
|
+
response: `{ notifications: [], total: 0, unread: 0 }`
|
|
2545
|
+
},
|
|
2546
|
+
POST: {
|
|
2547
|
+
description: "Mark all notifications as read for the authenticated user.",
|
|
2548
|
+
response: `{ message: 'All notifications marked as read' }`
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
const notifListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2416
2552
|
${notifListMeta}${mw3}
|
|
2417
2553
|
export const GET = async (req: Request, res: Response) => {
|
|
2418
2554
|
// TODO: fetch notifications for user
|
|
@@ -2434,13 +2570,28 @@ export const POST = async (req, res) => {
|
|
|
2434
2570
|
res.json({ message: 'All notifications marked as read' });
|
|
2435
2571
|
};
|
|
2436
2572
|
`;
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2573
|
+
await fs.outputFile(
|
|
2574
|
+
path.join(dest, "src", "api", "user", "notifications", `index.${ext}`),
|
|
2575
|
+
notifListContent
|
|
2576
|
+
);
|
|
2577
|
+
const notifByIdMeta = mkMeta(opts, {
|
|
2578
|
+
GET: {
|
|
2579
|
+
description: "Fetch a single notification by ID.",
|
|
2580
|
+
params: `{ id: 'notif_01HXZ' }`,
|
|
2581
|
+
response: `{ notification: { id: 'notif_01HXZ' } }`
|
|
2582
|
+
},
|
|
2583
|
+
PATCH: {
|
|
2584
|
+
description: "Mark a single notification as read.",
|
|
2585
|
+
params: `{ id: 'notif_01HXZ' }`,
|
|
2586
|
+
response: `{ message: 'Notification marked as read', id: 'notif_01HXZ' }`
|
|
2587
|
+
},
|
|
2588
|
+
DELETE: {
|
|
2589
|
+
description: "Delete a single notification.",
|
|
2590
|
+
params: `{ id: 'notif_01HXZ' }`,
|
|
2591
|
+
response: `{ message: 'Notification notif_01HXZ deleted' }`
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
const notifByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2444
2595
|
${notifByIdMeta}${mw3}
|
|
2445
2596
|
export const GET = async (req: Request, res: Response) => {
|
|
2446
2597
|
const { id } = req.params;
|
|
@@ -2478,12 +2629,24 @@ export const DELETE = async (req, res) => {
|
|
|
2478
2629
|
res.json({ message: \`Notification \${id} deleted\` });
|
|
2479
2630
|
};
|
|
2480
2631
|
`;
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2632
|
+
await fs.outputFile(
|
|
2633
|
+
path.join(dest, "src", "api", "user", "notifications", `[id].${ext}`),
|
|
2634
|
+
notifByIdContent
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
if (uf.filesAndMedia) {
|
|
2638
|
+
const filesListMeta = mkMeta(opts, {
|
|
2639
|
+
GET: {
|
|
2640
|
+
description: "List uploaded files with storage usage for the authenticated user.",
|
|
2641
|
+
response: `{ files: [], total: 0, storageUsed: 0 }`
|
|
2642
|
+
},
|
|
2643
|
+
POST: {
|
|
2644
|
+
description: "Upload a new file for the authenticated user.",
|
|
2645
|
+
response: `{ message: 'File uploaded', file: { id: 'new-id' } }`,
|
|
2646
|
+
status: 201
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
const filesListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2487
2650
|
${filesListMeta}${mw3}
|
|
2488
2651
|
export const GET = async (req: Request, res: Response) => {
|
|
2489
2652
|
// TODO: fetch user files and compute storage usage
|
|
@@ -2505,12 +2668,23 @@ export const POST = async (req, res) => {
|
|
|
2505
2668
|
res.status(201).json({ message: 'File uploaded', file: { id: 'new-id' } });
|
|
2506
2669
|
};
|
|
2507
2670
|
`;
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2671
|
+
await fs.outputFile(
|
|
2672
|
+
path.join(dest, "src", "api", "user", "files", `index.${ext}`),
|
|
2673
|
+
filesListContent
|
|
2674
|
+
);
|
|
2675
|
+
const fileByIdMeta = mkMeta(opts, {
|
|
2676
|
+
GET: {
|
|
2677
|
+
description: "Get a download/preview URL for a single file by ID.",
|
|
2678
|
+
params: `{ id: 'file_01HXZ' }`,
|
|
2679
|
+
response: `{ file: { id: 'file_01HXZ', url: 'https://example.com/files/file_01HXZ' } }`
|
|
2680
|
+
},
|
|
2681
|
+
DELETE: {
|
|
2682
|
+
description: "Delete a file from storage.",
|
|
2683
|
+
params: `{ id: 'file_01HXZ' }`,
|
|
2684
|
+
response: `{ message: 'File file_01HXZ deleted' }`
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
const fileByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2514
2688
|
${fileByIdMeta}${mw3}
|
|
2515
2689
|
export const GET = async (req: Request, res: Response) => {
|
|
2516
2690
|
const { id } = req.params;
|
|
@@ -2536,12 +2710,23 @@ export const DELETE = async (req, res) => {
|
|
|
2536
2710
|
res.json({ message: \`File \${id} deleted\` });
|
|
2537
2711
|
};
|
|
2538
2712
|
`;
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2713
|
+
await fs.outputFile(
|
|
2714
|
+
path.join(dest, "src", "api", "user", "files", `[id].${ext}`),
|
|
2715
|
+
fileByIdContent
|
|
2716
|
+
);
|
|
2717
|
+
const favListMeta = mkMeta(opts, {
|
|
2718
|
+
GET: {
|
|
2719
|
+
description: "List favorites for the authenticated user.",
|
|
2720
|
+
response: `{ favorites: [] }`
|
|
2721
|
+
},
|
|
2722
|
+
POST: {
|
|
2723
|
+
description: "Add an entity to the authenticated user's favorites.",
|
|
2724
|
+
request: `{ entityId: 'post_01HXZ', entityType: 'post' }`,
|
|
2725
|
+
response: `{ message: 'Added to favorites' }`,
|
|
2726
|
+
status: 201
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
const favListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2545
2730
|
${favListMeta}${mw3}
|
|
2546
2731
|
export const GET = async (req: Request, res: Response) => {
|
|
2547
2732
|
// TODO: fetch user favorites
|
|
@@ -2567,11 +2752,18 @@ export const POST = async (req, res) => {
|
|
|
2567
2752
|
res.status(201).json({ message: 'Added to favorites' });
|
|
2568
2753
|
};
|
|
2569
2754
|
`;
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2755
|
+
await fs.outputFile(
|
|
2756
|
+
path.join(dest, "src", "api", "user", "favorites", `index.${ext}`),
|
|
2757
|
+
favListContent
|
|
2758
|
+
);
|
|
2759
|
+
const favByIdMeta = mkMeta(opts, {
|
|
2760
|
+
DELETE: {
|
|
2761
|
+
description: "Remove an entity from the authenticated user favorites.",
|
|
2762
|
+
params: `{ id: 'fav_01HXZ' }`,
|
|
2763
|
+
response: `{ message: 'Removed favorite fav_01HXZ' }`
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
const favByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2575
2767
|
${favByIdMeta}${mw3}
|
|
2576
2768
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2577
2769
|
const { id } = req.params;
|
|
@@ -2585,12 +2777,23 @@ export const DELETE = async (req, res) => {
|
|
|
2585
2777
|
res.json({ message: \`Removed favorite \${id}\` });
|
|
2586
2778
|
};
|
|
2587
2779
|
`;
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2780
|
+
await fs.outputFile(
|
|
2781
|
+
path.join(dest, "src", "api", "user", "favorites", `[id].${ext}`),
|
|
2782
|
+
favByIdContent
|
|
2783
|
+
);
|
|
2784
|
+
const bkListMeta = mkMeta(opts, {
|
|
2785
|
+
GET: {
|
|
2786
|
+
description: "List bookmarks for the authenticated user.",
|
|
2787
|
+
response: `{ bookmarks: [] }`
|
|
2788
|
+
},
|
|
2789
|
+
POST: {
|
|
2790
|
+
description: "Save a new bookmark for the authenticated user.",
|
|
2791
|
+
request: `{ url: 'https://example.com', title: 'Example' }`,
|
|
2792
|
+
response: `{ message: 'Bookmark saved' }`,
|
|
2793
|
+
status: 201
|
|
2794
|
+
}
|
|
2795
|
+
});
|
|
2796
|
+
const bkListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2594
2797
|
${bkListMeta}${mw3}
|
|
2595
2798
|
export const GET = async (req: Request, res: Response) => {
|
|
2596
2799
|
// TODO: fetch user bookmarks
|
|
@@ -2616,11 +2819,18 @@ export const POST = async (req, res) => {
|
|
|
2616
2819
|
res.status(201).json({ message: 'Bookmark saved' });
|
|
2617
2820
|
};
|
|
2618
2821
|
`;
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2822
|
+
await fs.outputFile(
|
|
2823
|
+
path.join(dest, "src", "api", "user", "bookmarks", `index.${ext}`),
|
|
2824
|
+
bkListContent
|
|
2825
|
+
);
|
|
2826
|
+
const bkByIdMeta = mkMeta(opts, {
|
|
2827
|
+
DELETE: {
|
|
2828
|
+
description: "Delete a bookmark by ID.",
|
|
2829
|
+
params: `{ id: 'bm_01HXZ' }`,
|
|
2830
|
+
response: `{ message: 'Bookmark bm_01HXZ removed' }`
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
const bkByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2624
2834
|
${bkByIdMeta}${mw3}
|
|
2625
2835
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2626
2836
|
const { id } = req.params;
|
|
@@ -2634,11 +2844,18 @@ export const DELETE = async (req, res) => {
|
|
|
2634
2844
|
res.json({ message: \`Bookmark \${id} removed\` });
|
|
2635
2845
|
};
|
|
2636
2846
|
`;
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2847
|
+
await fs.outputFile(
|
|
2848
|
+
path.join(dest, "src", "api", "user", "bookmarks", `[id].${ext}`),
|
|
2849
|
+
bkByIdContent
|
|
2850
|
+
);
|
|
2851
|
+
const searchMeta = mkMeta(opts, {
|
|
2852
|
+
GET: {
|
|
2853
|
+
description: "Search with filters, sort, and pagination.",
|
|
2854
|
+
query: `{ q: 'query', filter: 'active', sort: 'newest', page: '1', limit: '20' }`,
|
|
2855
|
+
response: `{ results: [], total: 0, page: 1, limit: 20 }`
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
const searchContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2642
2859
|
${searchMeta}${mw2}
|
|
2643
2860
|
export const GET = async (req: Request, res: Response) => {
|
|
2644
2861
|
const { q, filter, sort, page = 1, limit = 20 } = req.query;
|
|
@@ -2652,12 +2869,19 @@ export const GET = async (req, res) => {
|
|
|
2652
2869
|
res.json({ results: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
2653
2870
|
};
|
|
2654
2871
|
`;
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2872
|
+
await fs.outputFile(path.join(dest, "src", "api", "user", `search.${ext}`), searchContent);
|
|
2873
|
+
}
|
|
2874
|
+
if (uf.apiAndBilling) {
|
|
2875
|
+
const akListMeta = mkMeta(opts, {
|
|
2876
|
+
GET: { description: "List API keys for the authenticated user.", response: `{ apiKeys: [] }` },
|
|
2877
|
+
POST: {
|
|
2878
|
+
description: "Generate a new API key for the authenticated user.",
|
|
2879
|
+
request: `{ name: 'CI key' }`,
|
|
2880
|
+
response: `{ message: 'API key created', key: 'efc_...' }`,
|
|
2881
|
+
status: 201
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
const akListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2661
2885
|
${akListMeta}${mw3}
|
|
2662
2886
|
export const GET = async (req: Request, res: Response) => {
|
|
2663
2887
|
// TODO: fetch api keys for user
|
|
@@ -2683,11 +2907,18 @@ export const POST = async (req, res) => {
|
|
|
2683
2907
|
res.status(201).json({ message: 'API key created', key: 'efc_...' });
|
|
2684
2908
|
};
|
|
2685
2909
|
`;
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2910
|
+
await fs.outputFile(
|
|
2911
|
+
path.join(dest, "src", "api", "user", "api-keys", `index.${ext}`),
|
|
2912
|
+
akListContent
|
|
2913
|
+
);
|
|
2914
|
+
const akByIdMeta = mkMeta(opts, {
|
|
2915
|
+
DELETE: {
|
|
2916
|
+
description: "Revoke and delete an API key by ID.",
|
|
2917
|
+
params: `{ id: 'key_01HXZ' }`,
|
|
2918
|
+
response: `{ message: 'API key key_01HXZ revoked' }`
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
const akByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2691
2922
|
${akByIdMeta}${mw3}
|
|
2692
2923
|
export const DELETE = async (req: Request, res: Response) => {
|
|
2693
2924
|
const { id } = req.params;
|
|
@@ -2701,7 +2932,11 @@ export const DELETE = async (req, res) => {
|
|
|
2701
2932
|
res.json({ message: \`API key \${id} revoked\` });
|
|
2702
2933
|
};
|
|
2703
2934
|
`;
|
|
2704
|
-
|
|
2935
|
+
await fs.outputFile(
|
|
2936
|
+
path.join(dest, "src", "api", "user", "api-keys", `[id].${ext}`),
|
|
2937
|
+
akByIdContent
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2705
2940
|
}
|
|
2706
2941
|
async function writeUserBillingRoutes(dest, opts) {
|
|
2707
2942
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
@@ -2727,11 +2962,25 @@ export const GET = async (_req, res) => {
|
|
|
2727
2962
|
res.json({ plans: [] });
|
|
2728
2963
|
};
|
|
2729
2964
|
`;
|
|
2730
|
-
await fs.outputFile(
|
|
2965
|
+
await fs.outputFile(
|
|
2966
|
+
path.join(dest, "src", "api", "user", "billing", `plans.${ext}`),
|
|
2967
|
+
plansContent
|
|
2968
|
+
);
|
|
2731
2969
|
const subMeta = mkMeta(opts, {
|
|
2732
|
-
GET: {
|
|
2733
|
-
|
|
2734
|
-
|
|
2970
|
+
GET: {
|
|
2971
|
+
description: "Get the current subscription for the authenticated user.",
|
|
2972
|
+
response: `{ subscription: null }`
|
|
2973
|
+
},
|
|
2974
|
+
POST: {
|
|
2975
|
+
description: "Subscribe the authenticated user to a plan.",
|
|
2976
|
+
request: `{ planId: 'plan_01HXZ' }`,
|
|
2977
|
+
response: `{ message: 'Subscribed', subscription: { planId: 'plan_01HXZ' } }`,
|
|
2978
|
+
status: 201
|
|
2979
|
+
},
|
|
2980
|
+
DELETE: {
|
|
2981
|
+
description: "Cancel the current subscription at period end.",
|
|
2982
|
+
response: `{ message: 'Subscription cancelled' }`
|
|
2983
|
+
}
|
|
2735
2984
|
});
|
|
2736
2985
|
const subContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2737
2986
|
${subMeta}${mw3}
|
|
@@ -2769,10 +3018,21 @@ export const DELETE = async (req, res) => {
|
|
|
2769
3018
|
res.json({ message: 'Subscription cancelled' });
|
|
2770
3019
|
};
|
|
2771
3020
|
`;
|
|
2772
|
-
await fs.outputFile(
|
|
3021
|
+
await fs.outputFile(
|
|
3022
|
+
path.join(dest, "src", "api", "user", "billing", `subscription.${ext}`),
|
|
3023
|
+
subContent
|
|
3024
|
+
);
|
|
2773
3025
|
const pmListMeta = mkMeta(opts, {
|
|
2774
|
-
GET: {
|
|
2775
|
-
|
|
3026
|
+
GET: {
|
|
3027
|
+
description: "List payment methods for the authenticated user.",
|
|
3028
|
+
response: `{ paymentMethods: [] }`
|
|
3029
|
+
},
|
|
3030
|
+
POST: {
|
|
3031
|
+
description: "Attach a new payment method via the payment gateway.",
|
|
3032
|
+
request: `{ token: 'tok_...' }`,
|
|
3033
|
+
response: `{ message: 'Payment method added' }`,
|
|
3034
|
+
status: 201
|
|
3035
|
+
}
|
|
2776
3036
|
});
|
|
2777
3037
|
const pmListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2778
3038
|
${pmListMeta}${mw4}
|
|
@@ -2800,9 +3060,16 @@ export const POST = async (req, res) => {
|
|
|
2800
3060
|
res.status(201).json({ message: 'Payment method added' });
|
|
2801
3061
|
};
|
|
2802
3062
|
`;
|
|
2803
|
-
await fs.outputFile(
|
|
3063
|
+
await fs.outputFile(
|
|
3064
|
+
path.join(dest, "src", "api", "user", "billing", "payment-methods", `index.${ext}`),
|
|
3065
|
+
pmListContent
|
|
3066
|
+
);
|
|
2804
3067
|
const pmByIdMeta = mkMeta(opts, {
|
|
2805
|
-
DELETE: {
|
|
3068
|
+
DELETE: {
|
|
3069
|
+
description: "Detach a payment method from the payment gateway.",
|
|
3070
|
+
params: `{ id: 'pm_01HXZ' }`,
|
|
3071
|
+
response: `{ message: 'Payment method pm_01HXZ removed' }`
|
|
3072
|
+
}
|
|
2806
3073
|
});
|
|
2807
3074
|
const pmByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2808
3075
|
${pmByIdMeta}${mw4}
|
|
@@ -2818,9 +3085,15 @@ export const DELETE = async (req, res) => {
|
|
|
2818
3085
|
res.json({ message: \`Payment method \${id} removed\` });
|
|
2819
3086
|
};
|
|
2820
3087
|
`;
|
|
2821
|
-
await fs.outputFile(
|
|
3088
|
+
await fs.outputFile(
|
|
3089
|
+
path.join(dest, "src", "api", "user", "billing", "payment-methods", `[id].${ext}`),
|
|
3090
|
+
pmByIdContent
|
|
3091
|
+
);
|
|
2822
3092
|
const invListMeta = mkMeta(opts, {
|
|
2823
|
-
GET: {
|
|
3093
|
+
GET: {
|
|
3094
|
+
description: "List all invoices for the authenticated user.",
|
|
3095
|
+
response: `{ invoices: [], total: 0 }`
|
|
3096
|
+
}
|
|
2824
3097
|
});
|
|
2825
3098
|
const invListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2826
3099
|
${invListMeta}${mw4}
|
|
@@ -2834,9 +3107,16 @@ export const GET = async (req, res) => {
|
|
|
2834
3107
|
res.json({ invoices: [], total: 0 });
|
|
2835
3108
|
};
|
|
2836
3109
|
`;
|
|
2837
|
-
await fs.outputFile(
|
|
3110
|
+
await fs.outputFile(
|
|
3111
|
+
path.join(dest, "src", "api", "user", "billing", "invoices", `index.${ext}`),
|
|
3112
|
+
invListContent
|
|
3113
|
+
);
|
|
2838
3114
|
const invByIdMeta = mkMeta(opts, {
|
|
2839
|
-
GET: {
|
|
3115
|
+
GET: {
|
|
3116
|
+
description: "Get the download URL for a single invoice by ID.",
|
|
3117
|
+
params: `{ id: 'inv_01HXZ' }`,
|
|
3118
|
+
response: `{ invoice: { id: 'inv_01HXZ', downloadUrl: 'https://example.com/invoices/inv_01HXZ.pdf' } }`
|
|
3119
|
+
}
|
|
2840
3120
|
});
|
|
2841
3121
|
const invByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2842
3122
|
${invByIdMeta}${mw4}
|
|
@@ -2852,7 +3132,10 @@ export const GET = async (req, res) => {
|
|
|
2852
3132
|
res.json({ invoice: { id, downloadUrl: 'https://...' } });
|
|
2853
3133
|
};
|
|
2854
3134
|
`;
|
|
2855
|
-
await fs.outputFile(
|
|
3135
|
+
await fs.outputFile(
|
|
3136
|
+
path.join(dest, "src", "api", "user", "billing", "invoices", `[id].${ext}`),
|
|
3137
|
+
invByIdContent
|
|
3138
|
+
);
|
|
2856
3139
|
}
|
|
2857
3140
|
async function writeSupportRoutes(dest, opts) {
|
|
2858
3141
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
@@ -2863,8 +3146,16 @@ async function writeSupportRoutes(dest, opts) {
|
|
|
2863
3146
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
2864
3147
|
`;
|
|
2865
3148
|
const ticketListMeta = mkMeta(opts, {
|
|
2866
|
-
GET: {
|
|
2867
|
-
|
|
3149
|
+
GET: {
|
|
3150
|
+
description: "List the authenticated user's own support tickets.",
|
|
3151
|
+
response: `{ tickets: [], total: 0 }`
|
|
3152
|
+
},
|
|
3153
|
+
POST: {
|
|
3154
|
+
description: "Create a new support ticket.",
|
|
3155
|
+
request: `{ subject: 'Issue with login', message: 'I cannot log in', priority: 'normal' }`,
|
|
3156
|
+
response: `{ message: 'Ticket created', ticket: { id: 'new-id', subject: 'Issue with login', status: 'open' } }`,
|
|
3157
|
+
status: 201
|
|
3158
|
+
}
|
|
2868
3159
|
});
|
|
2869
3160
|
const ticketListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2870
3161
|
${ticketListMeta}${mw3}
|
|
@@ -2892,10 +3183,22 @@ export const POST = async (req, res) => {
|
|
|
2892
3183
|
res.status(201).json({ message: 'Ticket created', ticket: { id: 'new-id', subject, status: 'open' } });
|
|
2893
3184
|
};
|
|
2894
3185
|
`;
|
|
2895
|
-
await fs.outputFile(
|
|
3186
|
+
await fs.outputFile(
|
|
3187
|
+
path.join(dest, "src", "api", "support", "tickets", `index.${ext}`),
|
|
3188
|
+
ticketListContent
|
|
3189
|
+
);
|
|
2896
3190
|
const ticketByIdMeta = mkMeta(opts, {
|
|
2897
|
-
GET: {
|
|
2898
|
-
|
|
3191
|
+
GET: {
|
|
3192
|
+
description: "Fetch a single support ticket by ID.",
|
|
3193
|
+
params: `{ id: 'tk_01HXZ' }`,
|
|
3194
|
+
response: `{ ticket: { id: 'tk_01HXZ', subject: 'Issue', status: 'open', replies: [] } }`
|
|
3195
|
+
},
|
|
3196
|
+
PUT: {
|
|
3197
|
+
description: "Add a reply to a support ticket or update its status.",
|
|
3198
|
+
params: `{ id: 'tk_01HXZ' }`,
|
|
3199
|
+
request: `{ reply: 'Thanks, resolved.', status: 'closed' }`,
|
|
3200
|
+
response: `{ message: 'Ticket updated', ticket: { id: 'tk_01HXZ', status: 'closed' } }`
|
|
3201
|
+
}
|
|
2899
3202
|
});
|
|
2900
3203
|
const ticketByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2901
3204
|
${ticketByIdMeta}${mw3}
|
|
@@ -2925,7 +3228,10 @@ export const PUT = async (req, res) => {
|
|
|
2925
3228
|
res.json({ message: 'Ticket updated', ticket: { id, status: status ?? 'open' } });
|
|
2926
3229
|
};
|
|
2927
3230
|
`;
|
|
2928
|
-
await fs.outputFile(
|
|
3231
|
+
await fs.outputFile(
|
|
3232
|
+
path.join(dest, "src", "api", "support", "tickets", `[id].${ext}`),
|
|
3233
|
+
ticketByIdContent
|
|
3234
|
+
);
|
|
2929
3235
|
}
|
|
2930
3236
|
async function writeAdminExtendedRoutes(dest, opts) {
|
|
2931
3237
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
@@ -2941,10 +3247,15 @@ async function writeAdminExtendedRoutes(dest, opts) {
|
|
|
2941
3247
|
`;
|
|
2942
3248
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
2943
3249
|
`;
|
|
2944
|
-
const
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3250
|
+
const af = opts.adminFeatures;
|
|
3251
|
+
if (af.analytics) {
|
|
3252
|
+
const analyticsOverviewMeta = mkMeta(opts, {
|
|
3253
|
+
GET: {
|
|
3254
|
+
description: "Analytics overview: users, revenue, traffic (admin only).",
|
|
3255
|
+
response: `{ users: {}, revenue: {}, traffic: {} }`
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
const analyticsOverviewContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2948
3259
|
${analyticsOverviewMeta}${mwAdmin3}
|
|
2949
3260
|
export const GET = async (req: Request, res: Response) => {
|
|
2950
3261
|
${roleGuard} // TODO: aggregate analytics overview
|
|
@@ -2956,11 +3267,18 @@ ${roleGuard} // TODO: aggregate analytics overview
|
|
|
2956
3267
|
res.json({ users: {}, revenue: {}, traffic: {} });
|
|
2957
3268
|
};
|
|
2958
3269
|
`;
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3270
|
+
await fs.outputFile(
|
|
3271
|
+
path.join(dest, "src", "api", "admin", "analytics", `index.${ext}`),
|
|
3272
|
+
analyticsOverviewContent
|
|
3273
|
+
);
|
|
3274
|
+
const analyticsUsersMeta = mkMeta(opts, {
|
|
3275
|
+
GET: {
|
|
3276
|
+
description: "User analytics: registrations, active users, churn (admin only).",
|
|
3277
|
+
query: `{ period: '30d' }`,
|
|
3278
|
+
response: `{ registrations: [], activeUsers: 0, churn: 0, period: '30d' }`
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
3281
|
+
const analyticsUsersContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2964
3282
|
${analyticsUsersMeta}${mwAdmin3}
|
|
2965
3283
|
export const GET = async (req: Request, res: Response) => {
|
|
2966
3284
|
${roleGuard} const { period = '30d' } = req.query;
|
|
@@ -2974,11 +3292,18 @@ ${roleGuard} const { period = '30d' } = req.query;
|
|
|
2974
3292
|
res.json({ registrations: [], activeUsers: 0, churn: 0, period });
|
|
2975
3293
|
};
|
|
2976
3294
|
`;
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3295
|
+
await fs.outputFile(
|
|
3296
|
+
path.join(dest, "src", "api", "admin", "analytics", `users.${ext}`),
|
|
3297
|
+
analyticsUsersContent
|
|
3298
|
+
);
|
|
3299
|
+
const analyticsRevenueMeta = mkMeta(opts, {
|
|
3300
|
+
GET: {
|
|
3301
|
+
description: "Revenue analytics: MRR, ARR, payment history (admin only).",
|
|
3302
|
+
query: `{ period: '30d' }`,
|
|
3303
|
+
response: `{ mrr: 0, arr: 0, history: [], period: '30d' }`
|
|
3304
|
+
}
|
|
3305
|
+
});
|
|
3306
|
+
const analyticsRevenueContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
2982
3307
|
${analyticsRevenueMeta}${mwAdmin3}
|
|
2983
3308
|
export const GET = async (req: Request, res: Response) => {
|
|
2984
3309
|
${roleGuard} const { period = '30d' } = req.query;
|
|
@@ -2992,11 +3317,18 @@ ${roleGuard} const { period = '30d' } = req.query;
|
|
|
2992
3317
|
res.json({ mrr: 0, arr: 0, history: [], period });
|
|
2993
3318
|
};
|
|
2994
3319
|
`;
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3320
|
+
await fs.outputFile(
|
|
3321
|
+
path.join(dest, "src", "api", "admin", "analytics", `revenue.${ext}`),
|
|
3322
|
+
analyticsRevenueContent
|
|
3323
|
+
);
|
|
3324
|
+
const analyticsTrafficMeta = mkMeta(opts, {
|
|
3325
|
+
GET: {
|
|
3326
|
+
description: "Traffic analytics: page views, devices, countries (admin only).",
|
|
3327
|
+
query: `{ period: '30d' }`,
|
|
3328
|
+
response: `{ pageViews: 0, devices: {}, countries: {}, period: '30d' }`
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
const analyticsTrafficContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3000
3332
|
${analyticsTrafficMeta}${mwAdmin3}
|
|
3001
3333
|
export const GET = async (req: Request, res: Response) => {
|
|
3002
3334
|
${roleGuard} const { period = '30d' } = req.query;
|
|
@@ -3010,11 +3342,21 @@ ${roleGuard} const { period = '30d' } = req.query;
|
|
|
3010
3342
|
res.json({ pageViews: 0, devices: {}, countries: {}, period });
|
|
3011
3343
|
};
|
|
3012
3344
|
`;
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3345
|
+
await fs.outputFile(
|
|
3346
|
+
path.join(dest, "src", "api", "admin", "analytics", `traffic.${ext}`),
|
|
3347
|
+
analyticsTrafficContent
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
if (af.userManagement) {
|
|
3351
|
+
const suspendMeta = mkMeta(opts, {
|
|
3352
|
+
POST: {
|
|
3353
|
+
description: "Suspend a user account (admin only).",
|
|
3354
|
+
params: `{ id: 'usr_01HXZ' }`,
|
|
3355
|
+
request: `{ reason: 'Terms of service violation' }`,
|
|
3356
|
+
response: `{ message: 'User usr_01HXZ suspended', reason: 'Terms of service violation' }`
|
|
3357
|
+
}
|
|
3358
|
+
});
|
|
3359
|
+
const suspendContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3018
3360
|
${suspendMeta}${mwAdmin4}
|
|
3019
3361
|
export const POST = async (req: Request, res: Response) => {
|
|
3020
3362
|
${roleGuard} const { id } = req.params;
|
|
@@ -3030,11 +3372,18 @@ ${roleGuard} const { id } = req.params;
|
|
|
3030
3372
|
res.json({ message: \`User \${id} suspended\`, reason });
|
|
3031
3373
|
};
|
|
3032
3374
|
`;
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3375
|
+
await fs.outputFile(
|
|
3376
|
+
path.join(dest, "src", "api", "admin", "users", "[id]", `suspend.${ext}`),
|
|
3377
|
+
suspendContent
|
|
3378
|
+
);
|
|
3379
|
+
const activateMeta = mkMeta(opts, {
|
|
3380
|
+
POST: {
|
|
3381
|
+
description: "Reactivate a suspended user account (admin only).",
|
|
3382
|
+
params: `{ id: 'usr_01HXZ' }`,
|
|
3383
|
+
response: `{ message: 'User usr_01HXZ activated' }`
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
const activateContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3038
3387
|
${activateMeta}${mwAdmin4}
|
|
3039
3388
|
export const POST = async (req: Request, res: Response) => {
|
|
3040
3389
|
${roleGuard} const { id } = req.params;
|
|
@@ -3048,11 +3397,18 @@ ${roleGuard} const { id } = req.params;
|
|
|
3048
3397
|
res.json({ message: \`User \${id} activated\` });
|
|
3049
3398
|
};
|
|
3050
3399
|
`;
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3400
|
+
await fs.outputFile(
|
|
3401
|
+
path.join(dest, "src", "api", "admin", "users", "[id]", `activate.${ext}`),
|
|
3402
|
+
activateContent
|
|
3403
|
+
);
|
|
3404
|
+
const verifyUserMeta = mkMeta(opts, {
|
|
3405
|
+
POST: {
|
|
3406
|
+
description: "Mark a user's email as verified (admin only).",
|
|
3407
|
+
params: `{ id: 'usr_01HXZ' }`,
|
|
3408
|
+
response: `{ message: 'User usr_01HXZ verified' }`
|
|
3409
|
+
}
|
|
3410
|
+
});
|
|
3411
|
+
const verifyUserContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3056
3412
|
${verifyUserMeta}${mwAdmin4}
|
|
3057
3413
|
export const POST = async (req: Request, res: Response) => {
|
|
3058
3414
|
${roleGuard} const { id } = req.params;
|
|
@@ -3066,11 +3422,17 @@ ${roleGuard} const { id } = req.params;
|
|
|
3066
3422
|
res.json({ message: \`User \${id} verified\` });
|
|
3067
3423
|
};
|
|
3068
3424
|
`;
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3425
|
+
await fs.outputFile(
|
|
3426
|
+
path.join(dest, "src", "api", "admin", "users", "[id]", `verify.${ext}`),
|
|
3427
|
+
verifyUserContent
|
|
3428
|
+
);
|
|
3429
|
+
const exportMeta = mkMeta(opts, {
|
|
3430
|
+
GET: {
|
|
3431
|
+
description: "Export all users as a CSV download (admin only).",
|
|
3432
|
+
response: `'id,name,email,role,createdAt\\n'`
|
|
3433
|
+
}
|
|
3434
|
+
});
|
|
3435
|
+
const exportContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3074
3436
|
${exportMeta}${mwAdmin3}
|
|
3075
3437
|
export const GET = async (_req: Request, res: Response) => {
|
|
3076
3438
|
${roleGuard} // TODO: generate CSV of all users and stream response
|
|
@@ -3086,12 +3448,25 @@ ${roleGuard} // TODO: generate CSV of all users and stream response
|
|
|
3086
3448
|
res.send('id,name,email,role,createdAt\\n');
|
|
3087
3449
|
};
|
|
3088
3450
|
`;
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3451
|
+
await fs.outputFile(
|
|
3452
|
+
path.join(dest, "src", "api", "admin", "users", `export.${ext}`),
|
|
3453
|
+
exportContent
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
if (af.adminManagement) {
|
|
3457
|
+
const adminsListMeta = mkMeta(opts, {
|
|
3458
|
+
GET: {
|
|
3459
|
+
description: "List all admin accounts (admin only).",
|
|
3460
|
+
response: `{ admins: [], total: 0 }`
|
|
3461
|
+
},
|
|
3462
|
+
POST: {
|
|
3463
|
+
description: "Create a new admin account (admin only).",
|
|
3464
|
+
request: `{ name: 'Jane Doe', email: 'jane@example.com', role: 'admin' }`,
|
|
3465
|
+
response: `{ message: 'Admin created', admin: { id: 'new-id', name: 'Jane Doe', email: 'jane@example.com', role: 'admin' } }`,
|
|
3466
|
+
status: 201
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
const adminsListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3095
3470
|
${adminsListMeta}${mwAdmin3}
|
|
3096
3471
|
export const GET = async (_req: Request, res: Response) => {
|
|
3097
3472
|
${roleGuard} // TODO: fetch admins from DB
|
|
@@ -3117,13 +3492,29 @@ ${roleGuard} const { name, email, role } = req.body;
|
|
|
3117
3492
|
res.status(201).json({ message: 'Admin created', admin: { id: 'new-id', name, email, role: role ?? 'admin' } });
|
|
3118
3493
|
};
|
|
3119
3494
|
`;
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3495
|
+
await fs.outputFile(
|
|
3496
|
+
path.join(dest, "src", "api", "admin", "admins", `index.${ext}`),
|
|
3497
|
+
adminsListContent
|
|
3498
|
+
);
|
|
3499
|
+
const adminByIdMeta = mkMeta(opts, {
|
|
3500
|
+
GET: {
|
|
3501
|
+
description: "Fetch a single admin account by ID (admin only).",
|
|
3502
|
+
params: `{ id: 'adm_01HXZ' }`,
|
|
3503
|
+
response: `{ admin: { id: 'adm_01HXZ' } }`
|
|
3504
|
+
},
|
|
3505
|
+
PUT: {
|
|
3506
|
+
description: "Update an admin account by ID (admin only).",
|
|
3507
|
+
params: `{ id: 'adm_01HXZ' }`,
|
|
3508
|
+
request: `{ name: 'Jane Doe', role: 'admin' }`,
|
|
3509
|
+
response: `{ message: 'Admin updated', admin: { id: 'adm_01HXZ', name: 'Jane Doe', role: 'admin' } }`
|
|
3510
|
+
},
|
|
3511
|
+
DELETE: {
|
|
3512
|
+
description: "Delete an admin account by ID (admin only).",
|
|
3513
|
+
params: `{ id: 'adm_01HXZ' }`,
|
|
3514
|
+
response: `{ message: 'Admin adm_01HXZ deleted' }`
|
|
3515
|
+
}
|
|
3516
|
+
});
|
|
3517
|
+
const adminByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3127
3518
|
${adminByIdMeta}${mwAdmin3}
|
|
3128
3519
|
export const GET = async (req: Request, res: Response) => {
|
|
3129
3520
|
${roleGuard} const { id } = req.params;
|
|
@@ -3161,13 +3552,21 @@ ${roleGuard} const { id } = req.params;
|
|
|
3161
3552
|
res.json({ message: \`Admin \${id} deleted\` });
|
|
3162
3553
|
};
|
|
3163
3554
|
`;
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3555
|
+
await fs.outputFile(
|
|
3556
|
+
path.join(dest, "src", "api", "admin", "admins", `[id].${ext}`),
|
|
3557
|
+
adminByIdContent
|
|
3558
|
+
);
|
|
3559
|
+
if (opts.rbac) {
|
|
3560
|
+
const rolesListMeta = mkMeta(opts, {
|
|
3561
|
+
GET: { description: "List all roles (admin only).", response: `{ roles: [] }` },
|
|
3562
|
+
POST: {
|
|
3563
|
+
description: "Create a new role (admin only).",
|
|
3564
|
+
request: `{ name: 'editor', description: 'Can edit content', permissions: ['content:write'] }`,
|
|
3565
|
+
response: `{ message: 'Role created', role: { id: 'new-id', name: 'editor', permissions: ['content:write'] } }`,
|
|
3566
|
+
status: 201
|
|
3567
|
+
}
|
|
3568
|
+
});
|
|
3569
|
+
const rolesListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3171
3570
|
${rolesListMeta}${mwAdmin3}
|
|
3172
3571
|
export const GET = async (_req: Request, res: Response) => {
|
|
3173
3572
|
// TODO: fetch all roles
|
|
@@ -3193,13 +3592,29 @@ export const POST = async (req, res) => {
|
|
|
3193
3592
|
res.status(201).json({ message: 'Role created', role: { id: 'new-id', name, permissions: permissions ?? [] } });
|
|
3194
3593
|
};
|
|
3195
3594
|
`;
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3595
|
+
await fs.outputFile(
|
|
3596
|
+
path.join(dest, "src", "api", "admin", "roles", `index.${ext}`),
|
|
3597
|
+
rolesListContent
|
|
3598
|
+
);
|
|
3599
|
+
const roleByIdMeta = mkMeta(opts, {
|
|
3600
|
+
GET: {
|
|
3601
|
+
description: "Fetch a single role by ID (admin only).",
|
|
3602
|
+
params: `{ id: 'role_01HXZ' }`,
|
|
3603
|
+
response: `{ role: { id: 'role_01HXZ', name: 'editor', permissions: ['content:write'] } }`
|
|
3604
|
+
},
|
|
3605
|
+
PUT: {
|
|
3606
|
+
description: "Update a role by ID (admin only).",
|
|
3607
|
+
params: `{ id: 'role_01HXZ' }`,
|
|
3608
|
+
request: `{ name: 'editor', permissions: ['content:write'] }`,
|
|
3609
|
+
response: `{ message: 'Role updated', role: { id: 'role_01HXZ', name: 'editor', permissions: ['content:write'] } }`
|
|
3610
|
+
},
|
|
3611
|
+
DELETE: {
|
|
3612
|
+
description: "Delete a role by ID (admin only).",
|
|
3613
|
+
params: `{ id: 'role_01HXZ' }`,
|
|
3614
|
+
response: `{ message: 'Role role_01HXZ deleted' }`
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
const roleByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3203
3618
|
${roleByIdMeta}${mwAdmin3}
|
|
3204
3619
|
export const GET = async (req: Request, res: Response) => {
|
|
3205
3620
|
const { id } = req.params;
|
|
@@ -3237,13 +3652,26 @@ export const DELETE = async (req, res) => {
|
|
|
3237
3652
|
res.json({ message: \`Role \${id} deleted\` });
|
|
3238
3653
|
};
|
|
3239
3654
|
`;
|
|
3240
|
-
|
|
3655
|
+
await fs.outputFile(
|
|
3656
|
+
path.join(dest, "src", "api", "admin", "roles", `[id].${ext}`),
|
|
3657
|
+
roleByIdContent
|
|
3658
|
+
);
|
|
3659
|
+
}
|
|
3241
3660
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3661
|
+
if (af.notificationsAndLogs) {
|
|
3662
|
+
const adminNotifMeta = mkMeta(opts, {
|
|
3663
|
+
GET: {
|
|
3664
|
+
description: "List all sent notifications (admin only).",
|
|
3665
|
+
response: `{ notifications: [], total: 0 }`
|
|
3666
|
+
},
|
|
3667
|
+
POST: {
|
|
3668
|
+
description: "Send a notification to a specific user (admin only).",
|
|
3669
|
+
request: `{ userId: 'usr_01HXZ', title: 'Welcome', message: 'Thanks for joining!', type: 'info' }`,
|
|
3670
|
+
response: `{ message: 'Notification sent' }`,
|
|
3671
|
+
status: 201
|
|
3672
|
+
}
|
|
3673
|
+
});
|
|
3674
|
+
const adminNotifContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3247
3675
|
${adminNotifMeta}${mwAdmin3}
|
|
3248
3676
|
export const GET = async (_req: Request, res: Response) => {
|
|
3249
3677
|
${roleGuard} // TODO: fetch sent notifications
|
|
@@ -3269,11 +3697,18 @@ ${roleGuard} const { userId, title, message, type } = req.body;
|
|
|
3269
3697
|
res.status(201).json({ message: 'Notification sent' });
|
|
3270
3698
|
};
|
|
3271
3699
|
`;
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3700
|
+
await fs.outputFile(
|
|
3701
|
+
path.join(dest, "src", "api", "admin", "notifications", `index.${ext}`),
|
|
3702
|
+
adminNotifContent
|
|
3703
|
+
);
|
|
3704
|
+
const broadcastMeta = mkMeta(opts, {
|
|
3705
|
+
POST: {
|
|
3706
|
+
description: "Broadcast a notification to all users (admin only).",
|
|
3707
|
+
request: `{ title: 'Announcement', message: 'Hello everyone!', type: 'info' }`,
|
|
3708
|
+
response: `{ message: 'Broadcast sent', count: 0 }`
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
const broadcastContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3277
3712
|
${broadcastMeta}${mwAdmin3}
|
|
3278
3713
|
export const POST = async (req: Request, res: Response) => {
|
|
3279
3714
|
${roleGuard} const { title, message, type } = req.body;
|
|
@@ -3289,12 +3724,19 @@ ${roleGuard} const { title, message, type } = req.body;
|
|
|
3289
3724
|
res.json({ message: 'Broadcast sent', count: 0 });
|
|
3290
3725
|
};
|
|
3291
3726
|
`;
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3727
|
+
await fs.outputFile(
|
|
3728
|
+
path.join(dest, "src", "api", "admin", "notifications", `broadcast.${ext}`),
|
|
3729
|
+
broadcastContent
|
|
3730
|
+
);
|
|
3731
|
+
const mkLogContent = (label) => {
|
|
3732
|
+
const meta = mkMeta(opts, {
|
|
3733
|
+
GET: {
|
|
3734
|
+
description: `Paginated ${label} log entries (admin only).`,
|
|
3735
|
+
query: `{ page: '1', limit: '50' }`,
|
|
3736
|
+
response: `{ logs: [], total: 0, page: 1, limit: 50 }`
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3739
|
+
return ts ? `${RA}import type { Request, Response } from 'express';
|
|
3298
3740
|
${meta}${mwAdmin3}
|
|
3299
3741
|
export const GET = async (req: Request, res: Response) => {
|
|
3300
3742
|
${roleGuard} const { page = 1, limit = 50 } = req.query;
|
|
@@ -3308,15 +3750,30 @@ ${roleGuard} const { page = 1, limit = 50 } = req.query;
|
|
|
3308
3750
|
res.json({ logs: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
3309
3751
|
};
|
|
3310
3752
|
`;
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3753
|
+
};
|
|
3754
|
+
await fs.outputFile(
|
|
3755
|
+
path.join(dest, "src", "api", "admin", "logs", `audit.${ext}`),
|
|
3756
|
+
mkLogContent("audit")
|
|
3757
|
+
);
|
|
3758
|
+
await fs.outputFile(
|
|
3759
|
+
path.join(dest, "src", "api", "admin", "logs", `activity.${ext}`),
|
|
3760
|
+
mkLogContent("activity")
|
|
3761
|
+
);
|
|
3762
|
+
await fs.outputFile(
|
|
3763
|
+
path.join(dest, "src", "api", "admin", "logs", `security.${ext}`),
|
|
3764
|
+
mkLogContent("security")
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3767
|
+
if (af.systemSettings) {
|
|
3768
|
+
const settingsMeta = mkMeta(opts, {
|
|
3769
|
+
GET: { description: "Get system-wide settings (admin only).", response: `{ settings: {} }` },
|
|
3770
|
+
PUT: {
|
|
3771
|
+
description: "Update system-wide settings (admin only).",
|
|
3772
|
+
request: `{ maintenanceMode: false }`,
|
|
3773
|
+
response: `{ message: 'Settings updated', settings: { maintenanceMode: false } }`
|
|
3774
|
+
}
|
|
3775
|
+
});
|
|
3776
|
+
const settingsContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3320
3777
|
${settingsMeta}${mwAdmin3}
|
|
3321
3778
|
export const GET = async (_req: Request, res: Response) => {
|
|
3322
3779
|
${roleGuard} // TODO: fetch system settings from DB
|
|
@@ -3338,11 +3795,17 @@ ${roleGuard} // TODO: update system settings
|
|
|
3338
3795
|
res.json({ message: 'Settings updated', settings: req.body });
|
|
3339
3796
|
};
|
|
3340
3797
|
`;
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3798
|
+
await fs.outputFile(
|
|
3799
|
+
path.join(dest, "src", "api", "admin", "settings", `index.${ext}`),
|
|
3800
|
+
settingsContent
|
|
3801
|
+
);
|
|
3802
|
+
const healthMeta = mkMeta(opts, {
|
|
3803
|
+
GET: {
|
|
3804
|
+
description: "System health check: DB, queue, cache status (admin only).",
|
|
3805
|
+
response: `{ status: 'ok', db: 'ok', queue: 'ok', uptime: 12345 }`
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
const healthContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3346
3809
|
${healthMeta}${mwAdmin3}
|
|
3347
3810
|
export const GET = async (_req: Request, res: Response) => {
|
|
3348
3811
|
${roleGuard} // TODO: check DB connection, queue, cache health
|
|
@@ -3354,11 +3817,17 @@ ${roleGuard} // TODO: check DB connection, queue, cache health
|
|
|
3354
3817
|
res.json({ status: 'ok', db: 'ok', queue: 'ok', uptime: process.uptime() });
|
|
3355
3818
|
};
|
|
3356
3819
|
`;
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3820
|
+
await fs.outputFile(
|
|
3821
|
+
path.join(dest, "src", "api", "admin", "system", `health.${ext}`),
|
|
3822
|
+
healthContent
|
|
3823
|
+
);
|
|
3824
|
+
const cacheMeta = mkMeta(opts, {
|
|
3825
|
+
DELETE: {
|
|
3826
|
+
description: "Flush the application cache (admin only).",
|
|
3827
|
+
response: `{ message: 'Cache cleared' }`
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
const cacheContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3362
3831
|
${cacheMeta}${mwAdmin3}
|
|
3363
3832
|
export const DELETE = async (_req: Request, res: Response) => {
|
|
3364
3833
|
${roleGuard} // TODO: flush Redis or in-memory cache
|
|
@@ -3370,11 +3839,20 @@ ${roleGuard} // TODO: flush Redis or in-memory cache
|
|
|
3370
3839
|
res.json({ message: 'Cache cleared' });
|
|
3371
3840
|
};
|
|
3372
3841
|
`;
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3842
|
+
await fs.outputFile(
|
|
3843
|
+
path.join(dest, "src", "api", "admin", "system", `cache.${ext}`),
|
|
3844
|
+
cacheContent
|
|
3845
|
+
);
|
|
3846
|
+
}
|
|
3847
|
+
if (af.supportManagement) {
|
|
3848
|
+
const adminTicketsListMeta = mkMeta(opts, {
|
|
3849
|
+
GET: {
|
|
3850
|
+
description: "List all support tickets with filters (admin only).",
|
|
3851
|
+
query: `{ status: 'open', priority: 'normal', page: '1', limit: '20' }`,
|
|
3852
|
+
response: `{ tickets: [], total: 0, page: 1, limit: 20 }`
|
|
3853
|
+
}
|
|
3854
|
+
});
|
|
3855
|
+
const adminTicketsListContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3378
3856
|
${adminTicketsListMeta}${mwAdmin3}
|
|
3379
3857
|
export const GET = async (req: Request, res: Response) => {
|
|
3380
3858
|
${roleGuard} const { status, priority, page = 1, limit = 20 } = req.query;
|
|
@@ -3388,12 +3866,24 @@ ${roleGuard} const { status, priority, page = 1, limit = 20 } = req.query;
|
|
|
3388
3866
|
res.json({ tickets: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
3389
3867
|
};
|
|
3390
3868
|
`;
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3869
|
+
await fs.outputFile(
|
|
3870
|
+
path.join(dest, "src", "api", "admin", "tickets", `index.${ext}`),
|
|
3871
|
+
adminTicketsListContent
|
|
3872
|
+
);
|
|
3873
|
+
const adminTicketByIdMeta = mkMeta(opts, {
|
|
3874
|
+
GET: {
|
|
3875
|
+
description: "Fetch a single support ticket by ID (admin only).",
|
|
3876
|
+
params: `{ id: 'tk_01HXZ' }`,
|
|
3877
|
+
response: `{ ticket: { id: 'tk_01HXZ' } }`
|
|
3878
|
+
},
|
|
3879
|
+
PUT: {
|
|
3880
|
+
description: "Assign, reply to, or change the status of a support ticket (admin only).",
|
|
3881
|
+
params: `{ id: 'tk_01HXZ' }`,
|
|
3882
|
+
request: `{ reply: 'Looking into it.', status: 'in_progress', assignedTo: 'adm_01HXZ' }`,
|
|
3883
|
+
response: `{ message: 'Ticket updated', ticket: { id: 'tk_01HXZ', status: 'in_progress', assignedTo: 'adm_01HXZ' } }`
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
const adminTicketByIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3397
3887
|
${adminTicketByIdMeta}${mwAdmin3}
|
|
3398
3888
|
export const GET = async (req: Request, res: Response) => {
|
|
3399
3889
|
${roleGuard} const { id } = req.params;
|
|
@@ -3421,7 +3911,11 @@ ${roleGuard} const { id } = req.params;
|
|
|
3421
3911
|
res.json({ message: 'Ticket updated', ticket: { id, status, assignedTo } });
|
|
3422
3912
|
};
|
|
3423
3913
|
`;
|
|
3424
|
-
|
|
3914
|
+
await fs.outputFile(
|
|
3915
|
+
path.join(dest, "src", "api", "admin", "tickets", `[id].${ext}`),
|
|
3916
|
+
adminTicketByIdContent
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3425
3919
|
const mkContentCrud = (name, namePlural, dir, createFields) => {
|
|
3426
3920
|
const lower = name.toLowerCase();
|
|
3427
3921
|
const firstField = createFields.split(", ")[0];
|
|
@@ -3429,8 +3923,16 @@ ${roleGuard} const { id } = req.params;
|
|
|
3429
3923
|
const sampleCreatedRecord = `{ id: 'new-id', ${firstField}: 'sample-${firstField}' }`;
|
|
3430
3924
|
const sampleFullRecord = `{ id: '${lower}_01HXZ', ${createFields.split(", ").map((f) => `${f}: 'sample-${f}'`).join(", ")} }`;
|
|
3431
3925
|
const listMeta = mkMeta(opts, {
|
|
3432
|
-
GET: {
|
|
3433
|
-
|
|
3926
|
+
GET: {
|
|
3927
|
+
description: `List all ${namePlural} (admin only).`,
|
|
3928
|
+
response: `{ ${namePlural}: [], total: 0 }`
|
|
3929
|
+
},
|
|
3930
|
+
POST: {
|
|
3931
|
+
description: `Create a new ${lower} (admin only).`,
|
|
3932
|
+
request: sampleCreateBody,
|
|
3933
|
+
response: `{ message: '${name} created', ${lower}: ${sampleCreatedRecord} }`,
|
|
3934
|
+
status: 201
|
|
3935
|
+
}
|
|
3434
3936
|
});
|
|
3435
3937
|
const listContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3436
3938
|
${listMeta}${mwAdmin4}
|
|
@@ -3457,9 +3959,22 @@ ${roleGuard} const { ${createFields} } = req.body;
|
|
|
3457
3959
|
};
|
|
3458
3960
|
`;
|
|
3459
3961
|
const byIdMeta = mkMeta(opts, {
|
|
3460
|
-
GET: {
|
|
3461
|
-
|
|
3462
|
-
|
|
3962
|
+
GET: {
|
|
3963
|
+
description: `Fetch a single ${lower} by ID (admin only).`,
|
|
3964
|
+
params: `{ id: '${lower}_01HXZ' }`,
|
|
3965
|
+
response: `{ ${lower}: ${sampleFullRecord} }`
|
|
3966
|
+
},
|
|
3967
|
+
PUT: {
|
|
3968
|
+
description: `Update a ${lower} by ID (admin only).`,
|
|
3969
|
+
params: `{ id: '${lower}_01HXZ' }`,
|
|
3970
|
+
request: sampleCreateBody,
|
|
3971
|
+
response: `{ message: '${name} updated', ${lower}: ${sampleFullRecord} }`
|
|
3972
|
+
},
|
|
3973
|
+
DELETE: {
|
|
3974
|
+
description: `Delete a ${lower} by ID (admin only).`,
|
|
3975
|
+
params: `{ id: '${lower}_01HXZ' }`,
|
|
3976
|
+
response: `{ message: '${name} ${lower}_01HXZ deleted' }`
|
|
3977
|
+
}
|
|
3463
3978
|
});
|
|
3464
3979
|
const byIdContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3465
3980
|
${byIdMeta}${mwAdmin4}
|
|
@@ -3501,25 +4016,87 @@ ${roleGuard} const { id } = req.params;
|
|
|
3501
4016
|
`;
|
|
3502
4017
|
return { listContent, byIdContent };
|
|
3503
4018
|
};
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
4019
|
+
if (af.contentManagement) {
|
|
4020
|
+
const { listContent: faqList, byIdContent: faqById } = mkContentCrud(
|
|
4021
|
+
"FAQ",
|
|
4022
|
+
"faqs",
|
|
4023
|
+
[],
|
|
4024
|
+
"question, answer, category"
|
|
4025
|
+
);
|
|
4026
|
+
await fs.outputFile(
|
|
4027
|
+
path.join(dest, "src", "api", "admin", "content", "faqs", `index.${ext}`),
|
|
4028
|
+
faqList
|
|
4029
|
+
);
|
|
4030
|
+
await fs.outputFile(
|
|
4031
|
+
path.join(dest, "src", "api", "admin", "content", "faqs", `[id].${ext}`),
|
|
4032
|
+
faqById
|
|
4033
|
+
);
|
|
4034
|
+
const { listContent: blogList, byIdContent: blogById } = mkContentCrud(
|
|
4035
|
+
"Blog",
|
|
4036
|
+
"blogs",
|
|
4037
|
+
[],
|
|
4038
|
+
"title, slug, content"
|
|
4039
|
+
);
|
|
4040
|
+
await fs.outputFile(
|
|
4041
|
+
path.join(dest, "src", "api", "admin", "content", "blogs", `index.${ext}`),
|
|
4042
|
+
blogList
|
|
4043
|
+
);
|
|
4044
|
+
await fs.outputFile(
|
|
4045
|
+
path.join(dest, "src", "api", "admin", "content", "blogs", `[id].${ext}`),
|
|
4046
|
+
blogById
|
|
4047
|
+
);
|
|
4048
|
+
const { listContent: catList, byIdContent: catById } = mkContentCrud(
|
|
4049
|
+
"Category",
|
|
4050
|
+
"categories",
|
|
4051
|
+
[],
|
|
4052
|
+
"name, slug"
|
|
4053
|
+
);
|
|
4054
|
+
await fs.outputFile(
|
|
4055
|
+
path.join(dest, "src", "api", "admin", "content", "categories", `index.${ext}`),
|
|
4056
|
+
catList
|
|
4057
|
+
);
|
|
4058
|
+
await fs.outputFile(
|
|
4059
|
+
path.join(dest, "src", "api", "admin", "content", "categories", `[id].${ext}`),
|
|
4060
|
+
catById
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
4063
|
+
if (af.billingManagement) {
|
|
4064
|
+
const { listContent: planList, byIdContent: planById } = mkContentCrud(
|
|
4065
|
+
"Plan",
|
|
4066
|
+
"plans",
|
|
4067
|
+
[],
|
|
4068
|
+
"name, price, interval"
|
|
4069
|
+
);
|
|
4070
|
+
await fs.outputFile(
|
|
4071
|
+
path.join(dest, "src", "api", "admin", "billing", "plans", `index.${ext}`),
|
|
4072
|
+
planList
|
|
4073
|
+
);
|
|
4074
|
+
await fs.outputFile(
|
|
4075
|
+
path.join(dest, "src", "api", "admin", "billing", "plans", `[id].${ext}`),
|
|
4076
|
+
planById
|
|
4077
|
+
);
|
|
4078
|
+
const { listContent: couponList, byIdContent: couponById } = mkContentCrud(
|
|
4079
|
+
"Coupon",
|
|
4080
|
+
"coupons",
|
|
4081
|
+
[],
|
|
4082
|
+
"code, type, value"
|
|
4083
|
+
);
|
|
4084
|
+
await fs.outputFile(
|
|
4085
|
+
path.join(dest, "src", "api", "admin", "billing", "coupons", `index.${ext}`),
|
|
4086
|
+
couponList
|
|
4087
|
+
);
|
|
4088
|
+
await fs.outputFile(
|
|
4089
|
+
path.join(dest, "src", "api", "admin", "billing", "coupons", `[id].${ext}`),
|
|
4090
|
+
couponById
|
|
4091
|
+
);
|
|
4092
|
+
const adminSubsMeta = mkMeta(opts, {
|
|
4093
|
+
GET: {
|
|
4094
|
+
description: "List all subscriptions with filters (admin only).",
|
|
4095
|
+
query: `{ status: 'active', page: '1', limit: '20' }`,
|
|
4096
|
+
response: `{ subscriptions: [], total: 0, page: 1, limit: 20 }`
|
|
4097
|
+
}
|
|
4098
|
+
});
|
|
4099
|
+
const adminSubsContent = ts ? `${RA}import type { Request, Response } from 'express';
|
|
3523
4100
|
${adminSubsMeta}${mwAdmin4}
|
|
3524
4101
|
export const GET = async (req: Request, res: Response) => {
|
|
3525
4102
|
${roleGuard} const { status, page = 1, limit = 20 } = req.query;
|
|
@@ -3533,7 +4110,11 @@ ${roleGuard} const { status, page = 1, limit = 20 } = req.query;
|
|
|
3533
4110
|
res.json({ subscriptions: [], total: 0, page: Number(page), limit: Number(limit) });
|
|
3534
4111
|
};
|
|
3535
4112
|
`;
|
|
3536
|
-
|
|
4113
|
+
await fs.outputFile(
|
|
4114
|
+
path.join(dest, "src", "api", "admin", "billing", "subscriptions", `index.${ext}`),
|
|
4115
|
+
adminSubsContent
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
3537
4118
|
}
|
|
3538
4119
|
|
|
3539
4120
|
// src/index.ts
|
|
@@ -3591,6 +4172,68 @@ async function main() {
|
|
|
3591
4172
|
});
|
|
3592
4173
|
if (p.isCancel(features)) cancel2();
|
|
3593
4174
|
const selected = new Set(features);
|
|
4175
|
+
let userFeatures = NO_USER_FEATURES;
|
|
4176
|
+
if (selected.has("userPortal")) {
|
|
4177
|
+
const chosen = await p.multiselect({
|
|
4178
|
+
message: "User portal features: (space to toggle, enter to confirm)",
|
|
4179
|
+
options: [
|
|
4180
|
+
{ value: "profileViewing", label: "Profile viewing & editing", hint: "GET/PUT /user/profile" },
|
|
4181
|
+
{ value: "forgotPassword", label: "Forgot / reset password", hint: "forgot-password, reset-password" },
|
|
4182
|
+
{ value: "accountSecurity", label: "Change password, 2FA & sessions", hint: "change-password, 2fa/*, sessions/*, token refresh" },
|
|
4183
|
+
{ value: "emailVerification", label: "Email verification", hint: "verify-email route + registration flow" },
|
|
4184
|
+
{ value: "accountSettings", label: "Account settings", hint: "avatar, settings, account, dashboard, activity" },
|
|
4185
|
+
{ value: "notifications", label: "Notifications", hint: "user/notifications" },
|
|
4186
|
+
{ value: "filesAndMedia", label: "Files, favorites & bookmarks", hint: "files, favorites, bookmarks, search" },
|
|
4187
|
+
{ value: "apiAndBilling", label: "API keys & billing", hint: "api-keys, plans, subscription, invoices" },
|
|
4188
|
+
{ value: "support", label: "Support tickets", hint: "create & view own tickets" }
|
|
4189
|
+
],
|
|
4190
|
+
initialValues: ["profileViewing", "forgotPassword", "accountSecurity", "emailVerification", "accountSettings", "notifications", "filesAndMedia", "apiAndBilling", "support"],
|
|
4191
|
+
required: false
|
|
4192
|
+
});
|
|
4193
|
+
if (p.isCancel(chosen)) cancel2();
|
|
4194
|
+
const uSet = new Set(chosen);
|
|
4195
|
+
userFeatures = {
|
|
4196
|
+
profileViewing: uSet.has("profileViewing"),
|
|
4197
|
+
forgotPassword: uSet.has("forgotPassword"),
|
|
4198
|
+
accountSecurity: uSet.has("accountSecurity"),
|
|
4199
|
+
emailVerification: uSet.has("emailVerification"),
|
|
4200
|
+
accountSettings: uSet.has("accountSettings"),
|
|
4201
|
+
notifications: uSet.has("notifications"),
|
|
4202
|
+
filesAndMedia: uSet.has("filesAndMedia"),
|
|
4203
|
+
apiAndBilling: uSet.has("apiAndBilling"),
|
|
4204
|
+
support: uSet.has("support")
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
let adminFeatures = NO_ADMIN_FEATURES;
|
|
4208
|
+
if (selected.has("adminPortal")) {
|
|
4209
|
+
const chosen = await p.multiselect({
|
|
4210
|
+
message: "Admin panel features: (space to toggle, enter to confirm)",
|
|
4211
|
+
options: [
|
|
4212
|
+
{ value: "userManagement", label: "User management", hint: "list/create/suspend/verify/export users + dashboard stats" },
|
|
4213
|
+
{ value: "adminManagement", label: "Admin & role management", hint: "manage other admins" + (selected.has("rbac") ? " + roles" : "") },
|
|
4214
|
+
{ value: "analytics", label: "Analytics", hint: "users, revenue, traffic dashboards" },
|
|
4215
|
+
{ value: "contentManagement", label: "Content management", hint: "FAQs, blog posts, categories" },
|
|
4216
|
+
{ value: "billingManagement", label: "Billing management", hint: "plans, coupons, subscriptions" },
|
|
4217
|
+
{ value: "supportManagement", label: "Support tickets", hint: "view & respond to tickets" },
|
|
4218
|
+
{ value: "notificationsAndLogs", label: "Notifications & audit logs", hint: "broadcast + audit/activity/security logs" },
|
|
4219
|
+
{ value: "systemSettings", label: "System settings & health", hint: "app settings, health check, cache" }
|
|
4220
|
+
],
|
|
4221
|
+
initialValues: ["userManagement", "adminManagement", "analytics", "contentManagement", "billingManagement", "supportManagement", "notificationsAndLogs", "systemSettings"],
|
|
4222
|
+
required: false
|
|
4223
|
+
});
|
|
4224
|
+
if (p.isCancel(chosen)) cancel2();
|
|
4225
|
+
const aSet = new Set(chosen);
|
|
4226
|
+
adminFeatures = {
|
|
4227
|
+
userManagement: aSet.has("userManagement"),
|
|
4228
|
+
adminManagement: aSet.has("adminManagement"),
|
|
4229
|
+
analytics: aSet.has("analytics"),
|
|
4230
|
+
contentManagement: aSet.has("contentManagement"),
|
|
4231
|
+
billingManagement: aSet.has("billingManagement"),
|
|
4232
|
+
supportManagement: aSet.has("supportManagement"),
|
|
4233
|
+
notificationsAndLogs: aSet.has("notificationsAndLogs"),
|
|
4234
|
+
systemSettings: aSet.has("systemSettings")
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
3594
4237
|
let taskBackend;
|
|
3595
4238
|
if (selected.has("tasks")) {
|
|
3596
4239
|
const backend = await p.select({
|
|
@@ -3670,6 +4313,8 @@ async function main() {
|
|
|
3670
4313
|
routeDocs: selected.has("routeDocs"),
|
|
3671
4314
|
userPortal: selected.has("userPortal"),
|
|
3672
4315
|
adminPortal: selected.has("adminPortal"),
|
|
4316
|
+
userFeatures,
|
|
4317
|
+
adminFeatures,
|
|
3673
4318
|
rbac: selected.has("rbac"),
|
|
3674
4319
|
mailer: selected.has("mailer"),
|
|
3675
4320
|
...taskBackend !== void 0 && { taskBackend },
|