create-efc-app 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +361 -147
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,12 +30,70 @@ var NO_USER_FEATURES = {
|
|
|
30
30
|
apiAndBilling: false,
|
|
31
31
|
support: false
|
|
32
32
|
};
|
|
33
|
+
function usesTables(opts) {
|
|
34
|
+
return opts.database === "postgresql" && opts.modelApi !== "defineModel";
|
|
35
|
+
}
|
|
36
|
+
function orm(opts) {
|
|
37
|
+
const t = usesTables(opts);
|
|
38
|
+
const where = (filter) => filter.replace(/\s/g, "") === "{}" ? "" : `{ where: ${filter} }`;
|
|
39
|
+
const byId = (id) => id === "id" ? "{ id }" : `{ id: ${id} }`;
|
|
40
|
+
return {
|
|
41
|
+
findOne: (m, filter) => t ? `${m}.findFirst({ where: ${filter} })` : `${m}.findOne(${filter})`,
|
|
42
|
+
findById: (m, id) => t ? `${m}.findUnique({ where: ${byId(id)} })` : `${m}.findById(${id})`,
|
|
43
|
+
find: (m, filter) => t ? `${m}.findMany(${where(filter)})` : `${m}.find(${filter})`,
|
|
44
|
+
count: (m, filter) => t ? `${m}.count(${where(filter)})` : `${m}.count(${filter})`,
|
|
45
|
+
create: (m, data) => t ? `${m}.create({ data: ${data} })` : `${m}.create(${data})`,
|
|
46
|
+
update: (m, id, data) => t ? `${m}.update({ where: ${byId(id)}, data: ${data} })` : `${m}.update(${id}, ${data})`,
|
|
47
|
+
delete: (m, id) => t ? `${m}.delete({ where: ${byId(id)} })` : `${m}.delete(${id})`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function generatedModels(opts) {
|
|
51
|
+
const uf = opts.userFeatures;
|
|
52
|
+
const af = opts.adminFeatures;
|
|
53
|
+
const user = opts.userPortal;
|
|
54
|
+
const admin = opts.adminPortal;
|
|
55
|
+
const m = /* @__PURE__ */ new Set();
|
|
56
|
+
if (user || admin && af.userManagement) m.add("User");
|
|
57
|
+
if (admin) m.add("Admin");
|
|
58
|
+
if (user && uf.accountSecurity) m.add("Session");
|
|
59
|
+
if (user && uf.notifications || admin && af.notificationsAndLogs) m.add("Notification");
|
|
60
|
+
if (user && uf.filesAndMedia) m.add("File");
|
|
61
|
+
if (user && uf.support || admin && af.supportManagement) m.add("SupportTicket");
|
|
62
|
+
if (admin && af.notificationsAndLogs) m.add("AuditLog");
|
|
63
|
+
if (user && uf.apiAndBilling) m.add("Subscription").add("Invoice").add("ApiKey");
|
|
64
|
+
if (admin && af.billingManagement) m.add("Plan").add("Coupon");
|
|
65
|
+
if (opts.rbac && admin && af.adminManagement) m.add("Role");
|
|
66
|
+
if (admin && af.contentManagement) m.add("FAQ").add("Blog").add("Category");
|
|
67
|
+
return m;
|
|
68
|
+
}
|
|
69
|
+
function modelApi(opts) {
|
|
70
|
+
const name = usesTables(opts) ? "defineTable" : "defineModel";
|
|
71
|
+
return { name, import: `import { ${name} } from 'express-file-cluster';` };
|
|
72
|
+
}
|
|
73
|
+
function refField(opts, r) {
|
|
74
|
+
if (!usesTables(opts) || !generatedModels(opts).has(r.target)) return r.legacy;
|
|
75
|
+
const opts2 = `${r.required ? ", required: true" : ""}${r.onDelete === "restrict" ? "" : `, onDelete: '${r.onDelete}'`}`;
|
|
76
|
+
return ` ${(r.field + ":").padEnd(13)}{ type: 'uuid', ref: '${r.target}'${opts2} },`;
|
|
77
|
+
}
|
|
78
|
+
function relationsOption(opts, rels) {
|
|
79
|
+
if (!usesTables(opts)) return "";
|
|
80
|
+
const models = generatedModels(opts);
|
|
81
|
+
const live = rels.filter((r) => models.has(r.target));
|
|
82
|
+
if (live.length === 0) return "";
|
|
83
|
+
const lines = live.map(
|
|
84
|
+
(r) => ` ${r.name}: { ${r.kind}: '${r.target}', foreignKey: '${r.foreignKey}' },`
|
|
85
|
+
);
|
|
86
|
+
return `, {
|
|
87
|
+
relations: {
|
|
88
|
+
${lines.join("\n")}
|
|
89
|
+
},
|
|
90
|
+
}`;
|
|
91
|
+
}
|
|
33
92
|
async function scaffold(opts) {
|
|
34
93
|
const dest = path.resolve(process.cwd(), opts.projectName);
|
|
35
94
|
await fs.ensureDir(dest);
|
|
36
95
|
const uf = opts.userFeatures;
|
|
37
|
-
const
|
|
38
|
-
const needsUserModel = opts.userPortal || opts.adminPortal && af.userManagement;
|
|
96
|
+
const models = generatedModels(opts);
|
|
39
97
|
await writePackageJson(dest, opts);
|
|
40
98
|
await writeTsConfig(dest, opts);
|
|
41
99
|
await writeEfcConfig(dest, opts);
|
|
@@ -44,28 +102,30 @@ async function scaffold(opts) {
|
|
|
44
102
|
await writeAgentsMd(dest, opts);
|
|
45
103
|
await writeEnvFiles(dest, opts);
|
|
46
104
|
await writeExampleRoute(dest, opts);
|
|
47
|
-
if (
|
|
48
|
-
if (
|
|
105
|
+
if (models.has("User")) await writeUserModel(dest, opts);
|
|
106
|
+
if (models.has("Admin")) await writeAdminModel(dest, opts);
|
|
49
107
|
await writeAuthRoutes(dest, opts);
|
|
50
108
|
if (opts.adminPortal) await writeAdminRoutes(dest, opts);
|
|
51
109
|
if (opts.userPortal && uf.profileViewing) await writeUserRoutes(dest, opts);
|
|
52
110
|
if (opts.tasks) await writeExampleTask(dest, opts);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
111
|
+
const extended = [
|
|
112
|
+
["Session", writeSessionModel],
|
|
113
|
+
["Notification", writeNotificationModel],
|
|
114
|
+
["File", writeFileModel],
|
|
115
|
+
["SupportTicket", writeSupportTicketModel],
|
|
116
|
+
["AuditLog", writeAuditLogModel],
|
|
117
|
+
["Subscription", writeSubscriptionModel],
|
|
118
|
+
["Plan", writePlanModel],
|
|
119
|
+
["Invoice", writeInvoiceModel],
|
|
120
|
+
["ApiKey", writeApiKeyModel],
|
|
121
|
+
["Role", writeRoleModel],
|
|
122
|
+
["FAQ", writeFAQModel],
|
|
123
|
+
["Blog", writeBlogModel],
|
|
124
|
+
["Category", writeCategoryModel],
|
|
125
|
+
["Coupon", writeCouponModel]
|
|
126
|
+
];
|
|
127
|
+
for (const [name, write] of extended) if (models.has(name)) await write(dest, opts);
|
|
128
|
+
if (usesTables(opts)) await writeModelIndex(dest, opts);
|
|
69
129
|
if (opts.userPortal) await writeAuthExtendedRoutes(dest, opts);
|
|
70
130
|
if (opts.userPortal) await writeUserExtendedRoutes(dest, opts);
|
|
71
131
|
if (opts.userPortal && uf.apiAndBilling) await writeUserBillingRoutes(dest, opts);
|
|
@@ -75,7 +135,7 @@ async function scaffold(opts) {
|
|
|
75
135
|
}
|
|
76
136
|
async function writePackageJson(dest, opts) {
|
|
77
137
|
const deps = {
|
|
78
|
-
"express-file-cluster": "^0.7.
|
|
138
|
+
"express-file-cluster": "^0.7.1"
|
|
79
139
|
};
|
|
80
140
|
if (opts.database === "mongodb") deps["mongoose"] = "^9.7.2";
|
|
81
141
|
if (opts.database === "postgresql") {
|
|
@@ -229,7 +289,10 @@ Optional \u2014 the same docs over MCP, with search and scaffolding tools:
|
|
|
229
289
|
- \`src/api/**\` \u2014 every file is a route (\`users/[id].${ext}\` \u2192 \`/v1/api/users/:id\`); export
|
|
230
290
|
uppercase \`GET\`/\`POST\`/\u2026 handlers.
|
|
231
291
|
- \`src/tasks/*.${ext}\` \u2014 background tasks; the file name is the task name (not recursive).
|
|
232
|
-
|
|
292
|
+
${usesTables(opts) ? `- \`src/model/*.${ext}\` \u2014 \`defineTable\` models (PostgreSQL-native, Prisma-style queries:
|
|
293
|
+
\`findMany({ where, include })\`, \`create({ data })\`, \`update({ where, data })\`). A \`ref\` is a
|
|
294
|
+
foreign key; back-relations are in each model's \`relations\`. \`src/model/index.${ext}\` imports
|
|
295
|
+
them all${opts.language === "typescript" ? " and declares `Tables` (typed `include`)" : ""} \u2014 add new models there.` : `- \`src/model/*.${ext}\` \u2014 \`defineModel\` models.`}
|
|
233
296
|
- \`src/index.${ext}\` is the entry point; it imports \`efc.config.${ext}\` and passes it to \`ignite()\`.
|
|
234
297
|
- Only three import paths exist: \`express-file-cluster\`, \`express-file-cluster/auth\`,
|
|
235
298
|
\`express-file-cluster/tasks\`.
|
|
@@ -391,9 +454,9 @@ export default defineTask(async (payload) => {
|
|
|
391
454
|
}
|
|
392
455
|
async function writeUserModel(dest, opts) {
|
|
393
456
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
394
|
-
const content =
|
|
457
|
+
const content = `${modelApi(opts).import}
|
|
395
458
|
|
|
396
|
-
export const User =
|
|
459
|
+
export const User = ${modelApi(opts).name}('User', {
|
|
397
460
|
name: { type: 'string', required: true },
|
|
398
461
|
email: { type: 'string', required: true, unique: true },
|
|
399
462
|
password: { type: 'string', required: true },
|
|
@@ -406,15 +469,23 @@ export const User = defineModel('User', {
|
|
|
406
469
|
resetTokenExpiry: { type: 'date' },
|
|
407
470
|
refreshToken: { type: 'string' },
|
|
408
471
|
refreshTokenExpiry: { type: 'date' },
|
|
409
|
-
}
|
|
472
|
+
}${relationsOption(opts, [
|
|
473
|
+
{ name: "sessions", kind: "hasMany", target: "Session", foreignKey: "user" },
|
|
474
|
+
{ name: "notifications", kind: "hasMany", target: "Notification", foreignKey: "user" },
|
|
475
|
+
{ name: "files", kind: "hasMany", target: "File", foreignKey: "user" },
|
|
476
|
+
{ name: "tickets", kind: "hasMany", target: "SupportTicket", foreignKey: "user" },
|
|
477
|
+
{ name: "subscriptions", kind: "hasMany", target: "Subscription", foreignKey: "user" },
|
|
478
|
+
{ name: "invoices", kind: "hasMany", target: "Invoice", foreignKey: "user" },
|
|
479
|
+
{ name: "apiKeys", kind: "hasMany", target: "ApiKey", foreignKey: "user" }
|
|
480
|
+
])});
|
|
410
481
|
`;
|
|
411
482
|
await fs.outputFile(path.join(dest, "src", "model", `User.${ext}`), content);
|
|
412
483
|
}
|
|
413
484
|
async function writeAdminModel(dest, opts) {
|
|
414
485
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
415
|
-
const content =
|
|
486
|
+
const content = `${modelApi(opts).import}
|
|
416
487
|
|
|
417
|
-
export const Admin =
|
|
488
|
+
export const Admin = ${modelApi(opts).name}('Admin', {
|
|
418
489
|
name: { type: 'string', required: true },
|
|
419
490
|
email: { type: 'string', required: true, unique: true },
|
|
420
491
|
password: { type: 'string', required: true },
|
|
@@ -425,11 +496,16 @@ export const Admin = defineModel('Admin', {
|
|
|
425
496
|
resetTokenExpiry: { type: 'date' },
|
|
426
497
|
refreshToken: { type: 'string' },
|
|
427
498
|
refreshTokenExpiry: { type: 'date' },
|
|
428
|
-
}
|
|
499
|
+
}${relationsOption(opts, [
|
|
500
|
+
{ name: "assignedTickets", kind: "hasMany", target: "SupportTicket", foreignKey: "assignedTo" },
|
|
501
|
+
{ name: "auditLogs", kind: "hasMany", target: "AuditLog", foreignKey: "admin" },
|
|
502
|
+
{ name: "posts", kind: "hasMany", target: "Blog", foreignKey: "author" }
|
|
503
|
+
])});
|
|
429
504
|
`;
|
|
430
505
|
await fs.outputFile(path.join(dest, "src", "model", `Admin.${ext}`), content);
|
|
431
506
|
}
|
|
432
507
|
async function writeAuthRoutes(dest, opts) {
|
|
508
|
+
const db = orm(opts);
|
|
433
509
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
434
510
|
const ts = opts.language === "typescript";
|
|
435
511
|
const loginMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
@@ -459,7 +535,7 @@ import crypto from 'node:crypto';
|
|
|
459
535
|
${hasUser ? `import { User } from '../../model/User.js';
|
|
460
536
|
` : ""}${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
461
537
|
` : ""}` : "";
|
|
462
|
-
const loginBody = hasAdmin && hasUser ? ` const admin = await
|
|
538
|
+
const loginBody = hasAdmin && hasUser ? ` const admin = await ${db.findOne("Admin", `{ email }`)};
|
|
463
539
|
if (admin) {
|
|
464
540
|
const match = await bcrypt.compare(password, admin.password);
|
|
465
541
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
@@ -469,21 +545,21 @@ ${hasUser ? `import { User } from '../../model/User.js';
|
|
|
469
545
|
return res.json({ message: 'Logged in as admin' });
|
|
470
546
|
}
|
|
471
547
|
|
|
472
|
-
const user = await
|
|
548
|
+
const user = await ${db.findOne("User", `{ email }`)};
|
|
473
549
|
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
474
550
|
const match = await bcrypt.compare(password, user.password);
|
|
475
551
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
476
552
|
if (!user.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
477
553
|
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
478
554
|
await issueRefreshToken(res, User, user.id);
|
|
479
|
-
res.json({ message: 'Logged in' });` : hasAdmin ? ` const admin = await
|
|
555
|
+
res.json({ message: 'Logged in' });` : hasAdmin ? ` const admin = await ${db.findOne("Admin", `{ email }`)};
|
|
480
556
|
if (!admin) return res.status(401).json({ error: 'Invalid credentials' });
|
|
481
557
|
const match = await bcrypt.compare(password, admin.password);
|
|
482
558
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
483
559
|
if (!admin.isActive) return res.status(403).json({ error: 'Account suspended' });
|
|
484
560
|
await issueToken(res, { id: admin.id, role: admin.role, email: admin.email });
|
|
485
561
|
await issueRefreshToken(res, Admin, admin.id);
|
|
486
|
-
res.json({ message: 'Logged in as admin' });` : ` const user = await
|
|
562
|
+
res.json({ message: 'Logged in as admin' });` : ` const user = await ${db.findOne("User", `{ email }`)};
|
|
487
563
|
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
|
|
488
564
|
const match = await bcrypt.compare(password, user.password);
|
|
489
565
|
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
|
|
@@ -497,11 +573,11 @@ ${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 *
|
|
|
497
573
|
|
|
498
574
|
async function issueRefreshToken(
|
|
499
575
|
res: Response,
|
|
500
|
-
model: { update: (id: string, data: Record<string, unknown>) => Promise<unknown> },
|
|
576
|
+
model: ${usesTables(opts) ? "{ update: (args: { where: { id: string }; data: Record<string, unknown> }) => Promise<unknown> }" : "{ update: (id: string, data: Record<string, unknown>) => Promise<unknown> }"},
|
|
501
577
|
id: string,
|
|
502
578
|
): Promise<void> {
|
|
503
579
|
const refreshToken = crypto.randomBytes(40).toString('hex');
|
|
504
|
-
await
|
|
580
|
+
await ${db.update("model", "id", "{ refreshToken, refreshTokenExpiry: new Date(Date.now() + REFRESH_TOKEN_TTL_MS) }")};
|
|
505
581
|
res.cookie('efc_refresh_token', refreshToken, {
|
|
506
582
|
httpOnly: true,
|
|
507
583
|
secure: process.env.NODE_ENV === 'production',
|
|
@@ -521,7 +597,7 @@ ${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 *
|
|
|
521
597
|
|
|
522
598
|
async function issueRefreshToken(res, model, id) {
|
|
523
599
|
const refreshToken = crypto.randomBytes(40).toString('hex');
|
|
524
|
-
await
|
|
600
|
+
await ${db.update("model", "id", "{ refreshToken, refreshTokenExpiry: new Date(Date.now() + REFRESH_TOKEN_TTL_MS) }")};
|
|
525
601
|
res.cookie('efc_refresh_token', refreshToken, {
|
|
526
602
|
httpOnly: true,
|
|
527
603
|
secure: process.env.NODE_ENV === 'production',
|
|
@@ -625,11 +701,11 @@ ${registerDbImports}${registerMeta}export const POST = async (req: Request, res:
|
|
|
625
701
|
if (!name || !email || !password) {
|
|
626
702
|
return res.status(400).json({ error: 'name, email and password are required' });
|
|
627
703
|
}
|
|
628
|
-
const existing = await
|
|
704
|
+
const existing = await ${db.findOne("User", `{ email }`)};
|
|
629
705
|
if (existing) return res.status(409).json({ error: 'Email already in use' });
|
|
630
706
|
const hashed = await bcrypt.hash(password, 10);
|
|
631
707
|
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
632
|
-
const user = await
|
|
708
|
+
const user = await ${db.create("User", `{ name, email, password: hashed, verifyToken }`)};
|
|
633
709
|
${sendVerifyEmail}
|
|
634
710
|
const { password: _, verifyToken: __, ...safe } = user;
|
|
635
711
|
res.status(201).json({ message: 'Account created successfully', user: safe });
|
|
@@ -639,11 +715,11 @@ ${sendVerifyEmail}
|
|
|
639
715
|
if (!name || !email || !password) {
|
|
640
716
|
return res.status(400).json({ error: 'name, email and password are required' });
|
|
641
717
|
}
|
|
642
|
-
const existing = await
|
|
718
|
+
const existing = await ${db.findOne("User", `{ email }`)};
|
|
643
719
|
if (existing) return res.status(409).json({ error: 'Email already in use' });
|
|
644
720
|
const hashed = await bcrypt.hash(password, 10);
|
|
645
721
|
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
646
|
-
const user = await
|
|
722
|
+
const user = await ${db.create("User", `{ name, email, password: hashed, verifyToken }`)};
|
|
647
723
|
${sendVerifyEmail}
|
|
648
724
|
const { password: _, verifyToken: __, ...safe } = user;
|
|
649
725
|
res.status(201).json({ message: 'Account created successfully', user: safe });
|
|
@@ -685,6 +761,7 @@ ${meMeta}${meMiddlewares}export const GET = async (req, res) => {
|
|
|
685
761
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `me.${ext}`), meContent);
|
|
686
762
|
}
|
|
687
763
|
async function writeAdminRoutes(dest, opts) {
|
|
764
|
+
const db = orm(opts);
|
|
688
765
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
689
766
|
const ts = opts.language === "typescript";
|
|
690
767
|
const middlewares = opts.rbac ? `export const middlewares = [requireAuth('admin')];
|
|
@@ -726,9 +803,9 @@ import type { Request, Response } from 'express';
|
|
|
726
803
|
${dashboardDbImport}${dashboardMeta}${middlewares}
|
|
727
804
|
export const GET = async (_req: Request, res: Response) => {
|
|
728
805
|
${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all([
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
806
|
+
${db.count("User", `{}`)},
|
|
807
|
+
${db.count("User", `{ isActive: true }`)},
|
|
808
|
+
${db.count("User", `{ isVerified: true }`)},
|
|
732
809
|
]);
|
|
733
810
|
res.json({ stats: { totalUsers, activeUsers, verifiedUsers } });
|
|
734
811
|
};
|
|
@@ -736,9 +813,9 @@ ${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all
|
|
|
736
813
|
${dashboardDbImport}${dashboardMeta}${middlewares}
|
|
737
814
|
export const GET = async (_req, res) => {
|
|
738
815
|
${roleGuard} const [totalUsers, activeUsers, verifiedUsers] = await Promise.all([
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
816
|
+
${db.count("User", `{}`)},
|
|
817
|
+
${db.count("User", `{ isActive: true }`)},
|
|
818
|
+
${db.count("User", `{ isVerified: true }`)},
|
|
742
819
|
]);
|
|
743
820
|
res.json({ stats: { totalUsers, activeUsers, verifiedUsers } });
|
|
744
821
|
};
|
|
@@ -798,7 +875,7 @@ ${adminUsersDbImport}${usersListMeta}${middlewares}
|
|
|
798
875
|
export const GET = async (req: Request, res: Response) => {
|
|
799
876
|
${roleGuard} const page = Math.max(1, Number(req.query.page) || 1);
|
|
800
877
|
const limit = Math.min(100, Number(req.query.limit) || 20);
|
|
801
|
-
const [all, total] = await Promise.all([
|
|
878
|
+
const [all, total] = await Promise.all([${db.find("User", `{}`)}, ${db.count("User", `{}`)}]);
|
|
802
879
|
const users = all.slice((page - 1) * limit, page * limit).map(({ password: _, ...u }) => u);
|
|
803
880
|
res.json({ users, total, page, limit });
|
|
804
881
|
};
|
|
@@ -806,10 +883,10 @@ ${roleGuard} const page = Math.max(1, Number(req.query.page) || 1);
|
|
|
806
883
|
export const POST = async (req: Request, res: Response) => {
|
|
807
884
|
${roleGuard} const { name, email, password, role } = req.body;
|
|
808
885
|
if (!name || !email || !password) return res.status(400).json({ error: 'name, email and password are required' });
|
|
809
|
-
const existing = await
|
|
886
|
+
const existing = await ${db.findOne("User", `{ email }`)};
|
|
810
887
|
if (existing) return res.status(409).json({ error: 'Email already in use' });
|
|
811
888
|
const hashed = await bcrypt.hash(password, 10);
|
|
812
|
-
const user = await
|
|
889
|
+
const user = await ${db.create("User", `{ name, email, password: hashed, role: role ?? 'user' }`)};
|
|
813
890
|
const { password: _, ...safe } = user;
|
|
814
891
|
res.status(201).json({ message: 'User created', user: safe });
|
|
815
892
|
};
|
|
@@ -818,7 +895,7 @@ ${adminUsersDbImport}${usersListMeta}${middlewares}
|
|
|
818
895
|
export const GET = async (req, res) => {
|
|
819
896
|
${roleGuard} const page = Math.max(1, Number(req.query.page) || 1);
|
|
820
897
|
const limit = Math.min(100, Number(req.query.limit) || 20);
|
|
821
|
-
const [all, total] = await Promise.all([
|
|
898
|
+
const [all, total] = await Promise.all([${db.find("User", `{}`)}, ${db.count("User", `{}`)}]);
|
|
822
899
|
const users = all.slice((page - 1) * limit, page * limit).map(({ password: _, ...u }) => u);
|
|
823
900
|
res.json({ users, total, page, limit });
|
|
824
901
|
};
|
|
@@ -826,10 +903,10 @@ ${roleGuard} const page = Math.max(1, Number(req.query.page) || 1);
|
|
|
826
903
|
export const POST = async (req, res) => {
|
|
827
904
|
${roleGuard} const { name, email, password, role } = req.body;
|
|
828
905
|
if (!name || !email || !password) return res.status(400).json({ error: 'name, email and password are required' });
|
|
829
|
-
const existing = await
|
|
906
|
+
const existing = await ${db.findOne("User", `{ email }`)};
|
|
830
907
|
if (existing) return res.status(409).json({ error: 'Email already in use' });
|
|
831
908
|
const hashed = await bcrypt.hash(password, 10);
|
|
832
|
-
const user = await
|
|
909
|
+
const user = await ${db.create("User", `{ name, email, password: hashed, role: role ?? 'user' }`)};
|
|
833
910
|
const { password: _, ...safe } = user;
|
|
834
911
|
res.status(201).json({ message: 'User created', user: safe });
|
|
835
912
|
};
|
|
@@ -884,7 +961,7 @@ import type { Request, Response } from 'express';
|
|
|
884
961
|
${adminUserByIdDbImport}${userByIdMeta}${middlewares}
|
|
885
962
|
export const GET = async (req: Request, res: Response) => {
|
|
886
963
|
${roleGuard} const { id } = req.params;
|
|
887
|
-
const user = await
|
|
964
|
+
const user = await ${db.findById("User", `id`)};
|
|
888
965
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
889
966
|
const { password: _, ...safe } = user;
|
|
890
967
|
res.json({ user: safe });
|
|
@@ -893,7 +970,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
893
970
|
export const PUT = async (req: Request, res: Response) => {
|
|
894
971
|
${roleGuard} const { id } = req.params;
|
|
895
972
|
const { name, email, role, isActive } = req.body;
|
|
896
|
-
const updated = await
|
|
973
|
+
const updated = await ${db.update("User", `id`, `{ name, email, role, isActive }`)};
|
|
897
974
|
if (!updated) return res.status(404).json({ error: 'User not found' });
|
|
898
975
|
const { password: _, ...safe } = updated;
|
|
899
976
|
res.json({ message: 'User updated', user: safe });
|
|
@@ -901,16 +978,16 @@ ${roleGuard} const { id } = req.params;
|
|
|
901
978
|
|
|
902
979
|
export const DELETE = async (req: Request, res: Response) => {
|
|
903
980
|
${roleGuard} const { id } = req.params;
|
|
904
|
-
const user = await
|
|
981
|
+
const user = await ${db.findById("User", `id`)};
|
|
905
982
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
906
|
-
await
|
|
983
|
+
await ${db.delete("User", `id`)};
|
|
907
984
|
res.json({ message: \`User \${id} deleted\` });
|
|
908
985
|
};
|
|
909
986
|
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
910
987
|
${adminUserByIdDbImport}${userByIdMeta}${middlewares}
|
|
911
988
|
export const GET = async (req, res) => {
|
|
912
989
|
${roleGuard} const { id } = req.params;
|
|
913
|
-
const user = await
|
|
990
|
+
const user = await ${db.findById("User", `id`)};
|
|
914
991
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
915
992
|
const { password: _, ...safe } = user;
|
|
916
993
|
res.json({ user: safe });
|
|
@@ -919,7 +996,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
919
996
|
export const PUT = async (req, res) => {
|
|
920
997
|
${roleGuard} const { id } = req.params;
|
|
921
998
|
const { name, email, role, isActive } = req.body;
|
|
922
|
-
const updated = await
|
|
999
|
+
const updated = await ${db.update("User", `id`, `{ name, email, role, isActive }`)};
|
|
923
1000
|
if (!updated) return res.status(404).json({ error: 'User not found' });
|
|
924
1001
|
const { password: _, ...safe } = updated;
|
|
925
1002
|
res.json({ message: 'User updated', user: safe });
|
|
@@ -927,9 +1004,9 @@ ${roleGuard} const { id } = req.params;
|
|
|
927
1004
|
|
|
928
1005
|
export const DELETE = async (req, res) => {
|
|
929
1006
|
${roleGuard} const { id } = req.params;
|
|
930
|
-
const user = await
|
|
1007
|
+
const user = await ${db.findById("User", `id`)};
|
|
931
1008
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
932
|
-
await
|
|
1009
|
+
await ${db.delete("User", `id`)};
|
|
933
1010
|
res.json({ message: \`User \${id} deleted\` });
|
|
934
1011
|
};
|
|
935
1012
|
`;
|
|
@@ -939,6 +1016,7 @@ ${roleGuard} const { id } = req.params;
|
|
|
939
1016
|
);
|
|
940
1017
|
}
|
|
941
1018
|
async function writeUserRoutes(dest, opts) {
|
|
1019
|
+
const db = orm(opts);
|
|
942
1020
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
943
1021
|
const ts = opts.language === "typescript";
|
|
944
1022
|
const middlewares = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
@@ -978,7 +1056,7 @@ import type { Request, Response } from 'express';
|
|
|
978
1056
|
${profileDbImport}${profileMeta}${middlewares}
|
|
979
1057
|
export const GET = async (req: Request, res: Response) => {
|
|
980
1058
|
const { id } = (req as any).user;
|
|
981
|
-
const user = await
|
|
1059
|
+
const user = await ${db.findById("User", `id`)};
|
|
982
1060
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
983
1061
|
const { password: _, ...safe } = user;
|
|
984
1062
|
res.json({ user: safe });
|
|
@@ -987,7 +1065,7 @@ export const GET = async (req: Request, res: Response) => {
|
|
|
987
1065
|
export const PUT = async (req: Request, res: Response) => {
|
|
988
1066
|
const { id } = (req as any).user;
|
|
989
1067
|
const { name, email } = req.body;
|
|
990
|
-
const updated = await
|
|
1068
|
+
const updated = await ${db.update("User", `id`, `{ name, email }`)};
|
|
991
1069
|
if (!updated) return res.status(404).json({ error: 'User not found' });
|
|
992
1070
|
const { password: _, ...safe } = updated;
|
|
993
1071
|
res.json({ message: 'Profile updated', user: safe });
|
|
@@ -996,7 +1074,7 @@ export const PUT = async (req: Request, res: Response) => {
|
|
|
996
1074
|
${profileDbImport}${profileMeta}${middlewares}
|
|
997
1075
|
export const GET = async (req, res) => {
|
|
998
1076
|
const { id } = req.user;
|
|
999
|
-
const user = await
|
|
1077
|
+
const user = await ${db.findById("User", `id`)};
|
|
1000
1078
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
1001
1079
|
const { password: _, ...safe } = user;
|
|
1002
1080
|
res.json({ user: safe });
|
|
@@ -1005,7 +1083,7 @@ export const GET = async (req, res) => {
|
|
|
1005
1083
|
export const PUT = async (req, res) => {
|
|
1006
1084
|
const { id } = req.user;
|
|
1007
1085
|
const { name, email } = req.body;
|
|
1008
|
-
const updated = await
|
|
1086
|
+
const updated = await ${db.update("User", `id`, `{ name, email }`)};
|
|
1009
1087
|
if (!updated) return res.status(404).json({ error: 'User not found' });
|
|
1010
1088
|
const { password: _, ...safe } = updated;
|
|
1011
1089
|
res.json({ message: 'Profile updated', user: safe });
|
|
@@ -1045,10 +1123,10 @@ ${blocks}
|
|
|
1045
1123
|
}
|
|
1046
1124
|
async function writeSessionModel(dest, opts) {
|
|
1047
1125
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1048
|
-
const content =
|
|
1126
|
+
const content = `${modelApi(opts).import}
|
|
1049
1127
|
|
|
1050
|
-
export const Session =
|
|
1051
|
-
userId: { type: 'string', required: true },
|
|
1128
|
+
export const Session = ${modelApi(opts).name}('Session', {
|
|
1129
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1052
1130
|
token: { type: 'string', required: true, unique: true },
|
|
1053
1131
|
ip: { type: 'string', required: true },
|
|
1054
1132
|
userAgent: { type: 'string', required: true },
|
|
@@ -1060,10 +1138,10 @@ export const Session = defineModel('Session', {
|
|
|
1060
1138
|
}
|
|
1061
1139
|
async function writeNotificationModel(dest, opts) {
|
|
1062
1140
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1063
|
-
const content =
|
|
1141
|
+
const content = `${modelApi(opts).import}
|
|
1064
1142
|
|
|
1065
|
-
export const Notification =
|
|
1066
|
-
userId: { type: 'string', required: true },
|
|
1143
|
+
export const Notification = ${modelApi(opts).name}('Notification', {
|
|
1144
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1067
1145
|
title: { type: 'string', required: true },
|
|
1068
1146
|
message: { type: 'string', required: true },
|
|
1069
1147
|
type: { type: 'string', required: true, default: 'info' },
|
|
@@ -1075,10 +1153,10 @@ export const Notification = defineModel('Notification', {
|
|
|
1075
1153
|
}
|
|
1076
1154
|
async function writeFileModel(dest, opts) {
|
|
1077
1155
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1078
|
-
const content =
|
|
1156
|
+
const content = `${modelApi(opts).import}
|
|
1079
1157
|
|
|
1080
|
-
export const File =
|
|
1081
|
-
userId: { type: 'string', required: true },
|
|
1158
|
+
export const File = ${modelApi(opts).name}('File', {
|
|
1159
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1082
1160
|
filename: { type: 'string', required: true },
|
|
1083
1161
|
originalName: { type: 'string', required: true },
|
|
1084
1162
|
mimeType: { type: 'string', required: true },
|
|
@@ -1091,25 +1169,25 @@ export const File = defineModel('File', {
|
|
|
1091
1169
|
}
|
|
1092
1170
|
async function writeSupportTicketModel(dest, opts) {
|
|
1093
1171
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1094
|
-
const content =
|
|
1172
|
+
const content = `${modelApi(opts).import}
|
|
1095
1173
|
|
|
1096
|
-
export const SupportTicket =
|
|
1097
|
-
userId: { type: 'string', required: true },
|
|
1174
|
+
export const SupportTicket = ${modelApi(opts).name}('SupportTicket', {
|
|
1175
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1098
1176
|
subject: { type: 'string', required: true },
|
|
1099
1177
|
message: { type: 'string', required: true },
|
|
1100
1178
|
status: { type: 'string', required: true, default: 'open' },
|
|
1101
1179
|
priority: { type: 'string', required: true, default: 'normal' },
|
|
1102
|
-
assignedTo: { type: 'string' }
|
|
1180
|
+
${refField(opts, { field: "assignedTo", target: "Admin", legacy: ` assignedTo: { type: 'string' },`, onDelete: "setNull" })}
|
|
1103
1181
|
});
|
|
1104
1182
|
`;
|
|
1105
1183
|
await fs.outputFile(path.join(dest, "src", "model", `SupportTicket.${ext}`), content);
|
|
1106
1184
|
}
|
|
1107
1185
|
async function writeAuditLogModel(dest, opts) {
|
|
1108
1186
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1109
|
-
const content =
|
|
1187
|
+
const content = `${modelApi(opts).import}
|
|
1110
1188
|
|
|
1111
|
-
export const AuditLog =
|
|
1112
|
-
adminId: { type: 'string', required: true }
|
|
1189
|
+
export const AuditLog = ${modelApi(opts).name}('AuditLog', {
|
|
1190
|
+
${refField(opts, { field: "admin", target: "Admin", legacy: ` adminId: { type: 'string', required: true },`, onDelete: "setNull" })}
|
|
1113
1191
|
action: { type: 'string', required: true },
|
|
1114
1192
|
entity: { type: 'string', required: true },
|
|
1115
1193
|
entityId: { type: 'string', required: true },
|
|
@@ -1121,41 +1199,41 @@ export const AuditLog = defineModel('AuditLog', {
|
|
|
1121
1199
|
}
|
|
1122
1200
|
async function writeSubscriptionModel(dest, opts) {
|
|
1123
1201
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1124
|
-
const content =
|
|
1202
|
+
const content = `${modelApi(opts).import}
|
|
1125
1203
|
|
|
1126
|
-
export const Subscription =
|
|
1127
|
-
userId: { type: 'string', required: true },
|
|
1128
|
-
planId: { type: 'string', required: true },
|
|
1204
|
+
export const Subscription = ${modelApi(opts).name}('Subscription', {
|
|
1205
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1206
|
+
${refField(opts, { field: "plan", target: "Plan", legacy: ` planId: { type: 'string', required: true },`, required: true, onDelete: "restrict" })}
|
|
1129
1207
|
status: { type: 'string', required: true, default: 'active' },
|
|
1130
1208
|
startDate: { type: 'date', required: true },
|
|
1131
1209
|
endDate: { type: 'date', required: true },
|
|
1132
1210
|
cancelledAt: { type: 'date' },
|
|
1133
|
-
});
|
|
1211
|
+
}${relationsOption(opts, [{ name: "invoices", kind: "hasMany", target: "Invoice", foreignKey: "subscription" }])});
|
|
1134
1212
|
`;
|
|
1135
1213
|
await fs.outputFile(path.join(dest, "src", "model", `Subscription.${ext}`), content);
|
|
1136
1214
|
}
|
|
1137
1215
|
async function writePlanModel(dest, opts) {
|
|
1138
1216
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1139
|
-
const content =
|
|
1217
|
+
const content = `${modelApi(opts).import}
|
|
1140
1218
|
|
|
1141
|
-
export const Plan =
|
|
1219
|
+
export const Plan = ${modelApi(opts).name}('Plan', {
|
|
1142
1220
|
name: { type: 'string', required: true },
|
|
1143
1221
|
description: { type: 'string', required: true },
|
|
1144
1222
|
price: { type: 'number', required: true },
|
|
1145
1223
|
interval: { type: 'string', required: true, default: 'monthly' },
|
|
1146
1224
|
features: { type: 'array', of: 'string', default: [] },
|
|
1147
1225
|
isActive: { type: 'boolean', default: true },
|
|
1148
|
-
});
|
|
1226
|
+
}${relationsOption(opts, [{ name: "subscriptions", kind: "hasMany", target: "Subscription", foreignKey: "plan" }])});
|
|
1149
1227
|
`;
|
|
1150
1228
|
await fs.outputFile(path.join(dest, "src", "model", `Plan.${ext}`), content);
|
|
1151
1229
|
}
|
|
1152
1230
|
async function writeInvoiceModel(dest, opts) {
|
|
1153
1231
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1154
|
-
const content =
|
|
1232
|
+
const content = `${modelApi(opts).import}
|
|
1155
1233
|
|
|
1156
|
-
export const Invoice =
|
|
1157
|
-
userId: { type: 'string', required: true },
|
|
1158
|
-
subscriptionId: { type: 'string', required: true }
|
|
1234
|
+
export const Invoice = ${modelApi(opts).name}('Invoice', {
|
|
1235
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1236
|
+
${refField(opts, { field: "subscription", target: "Subscription", legacy: ` subscriptionId: { type: 'string', required: true },`, onDelete: "setNull" })}
|
|
1159
1237
|
amount: { type: 'number', required: true },
|
|
1160
1238
|
currency: { type: 'string', required: true, default: 'USD' },
|
|
1161
1239
|
status: { type: 'string', required: true, default: 'pending' },
|
|
@@ -1166,10 +1244,10 @@ export const Invoice = defineModel('Invoice', {
|
|
|
1166
1244
|
}
|
|
1167
1245
|
async function writeApiKeyModel(dest, opts) {
|
|
1168
1246
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1169
|
-
const content =
|
|
1247
|
+
const content = `${modelApi(opts).import}
|
|
1170
1248
|
|
|
1171
|
-
export const ApiKey =
|
|
1172
|
-
userId: { type: 'string', required: true },
|
|
1249
|
+
export const ApiKey = ${modelApi(opts).name}('ApiKey', {
|
|
1250
|
+
${refField(opts, { field: "user", target: "User", legacy: ` userId: { type: 'string', required: true },`, required: true, onDelete: "cascade" })}
|
|
1173
1251
|
name: { type: 'string', required: true },
|
|
1174
1252
|
key: { type: 'string', required: true, unique: true },
|
|
1175
1253
|
lastUsed: { type: 'date' },
|
|
@@ -1181,9 +1259,9 @@ export const ApiKey = defineModel('ApiKey', {
|
|
|
1181
1259
|
}
|
|
1182
1260
|
async function writeRoleModel(dest, opts) {
|
|
1183
1261
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1184
|
-
const content =
|
|
1262
|
+
const content = `${modelApi(opts).import}
|
|
1185
1263
|
|
|
1186
|
-
export const Role =
|
|
1264
|
+
export const Role = ${modelApi(opts).name}('Role', {
|
|
1187
1265
|
name: { type: 'string', required: true, unique: true },
|
|
1188
1266
|
description: { type: 'string', required: true },
|
|
1189
1267
|
permissions: { type: 'array', of: 'string', default: [] },
|
|
@@ -1193,9 +1271,9 @@ export const Role = defineModel('Role', {
|
|
|
1193
1271
|
}
|
|
1194
1272
|
async function writeFAQModel(dest, opts) {
|
|
1195
1273
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1196
|
-
const content =
|
|
1274
|
+
const content = `${modelApi(opts).import}
|
|
1197
1275
|
|
|
1198
|
-
export const FAQ =
|
|
1276
|
+
export const FAQ = ${modelApi(opts).name}('FAQ', {
|
|
1199
1277
|
question: { type: 'string', required: true },
|
|
1200
1278
|
answer: { type: 'string', required: true },
|
|
1201
1279
|
category: { type: 'string', required: true, default: 'general' },
|
|
@@ -1207,13 +1285,13 @@ export const FAQ = defineModel('FAQ', {
|
|
|
1207
1285
|
}
|
|
1208
1286
|
async function writeBlogModel(dest, opts) {
|
|
1209
1287
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1210
|
-
const content =
|
|
1288
|
+
const content = `${modelApi(opts).import}
|
|
1211
1289
|
|
|
1212
|
-
export const Blog =
|
|
1290
|
+
export const Blog = ${modelApi(opts).name}('Blog', {
|
|
1213
1291
|
title: { type: 'string', required: true },
|
|
1214
1292
|
slug: { type: 'string', required: true, unique: true },
|
|
1215
1293
|
content: { type: 'string', required: true },
|
|
1216
|
-
authorId: { type: 'string', required: true }
|
|
1294
|
+
${refField(opts, { field: "author", target: "Admin", legacy: ` authorId: { type: 'string', required: true },`, onDelete: "setNull" })}
|
|
1217
1295
|
category: { type: 'string', required: true, default: 'general' },
|
|
1218
1296
|
tags: { type: 'array', of: 'string', default: [] },
|
|
1219
1297
|
status: { type: 'string', required: true, default: 'draft' },
|
|
@@ -1224,22 +1302,22 @@ export const Blog = defineModel('Blog', {
|
|
|
1224
1302
|
}
|
|
1225
1303
|
async function writeCategoryModel(dest, opts) {
|
|
1226
1304
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1227
|
-
const content =
|
|
1305
|
+
const content = `${modelApi(opts).import}
|
|
1228
1306
|
|
|
1229
|
-
export const Category =
|
|
1307
|
+
export const Category = ${modelApi(opts).name}('Category', {
|
|
1230
1308
|
name: { type: 'string', required: true },
|
|
1231
1309
|
slug: { type: 'string', required: true, unique: true },
|
|
1232
1310
|
description: { type: 'string' },
|
|
1233
|
-
parentId: { type: 'string' }
|
|
1234
|
-
});
|
|
1311
|
+
${refField(opts, { field: "parent", target: "Category", legacy: ` parentId: { type: 'string' },`, onDelete: "setNull" })}
|
|
1312
|
+
}${relationsOption(opts, [{ name: "children", kind: "hasMany", target: "Category", foreignKey: "parent" }])});
|
|
1235
1313
|
`;
|
|
1236
1314
|
await fs.outputFile(path.join(dest, "src", "model", `Category.${ext}`), content);
|
|
1237
1315
|
}
|
|
1238
1316
|
async function writeCouponModel(dest, opts) {
|
|
1239
1317
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1240
|
-
const content =
|
|
1318
|
+
const content = `${modelApi(opts).import}
|
|
1241
1319
|
|
|
1242
|
-
export const Coupon =
|
|
1320
|
+
export const Coupon = ${modelApi(opts).name}('Coupon', {
|
|
1243
1321
|
code: { type: 'string', required: true, unique: true },
|
|
1244
1322
|
type: { type: 'string', required: true, default: 'percent' },
|
|
1245
1323
|
value: { type: 'number', required: true },
|
|
@@ -1252,6 +1330,7 @@ export const Coupon = defineModel('Coupon', {
|
|
|
1252
1330
|
await fs.outputFile(path.join(dest, "src", "model", `Coupon.${ext}`), content);
|
|
1253
1331
|
}
|
|
1254
1332
|
async function writeAuthExtendedRoutes(dest, opts) {
|
|
1333
|
+
const db = orm(opts);
|
|
1255
1334
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1256
1335
|
const ts = opts.language === "typescript";
|
|
1257
1336
|
const mwUser2 = opts.rbac ? `export const middlewares = [requireAuth('user', 'admin')];
|
|
@@ -1290,12 +1369,12 @@ async function writeAuthExtendedRoutes(dest, opts) {
|
|
|
1290
1369
|
const refreshModelImports = `import { User } from '../../model/User.js';
|
|
1291
1370
|
${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
1292
1371
|
` : ""}`;
|
|
1293
|
-
const refreshLookup = hasAdmin ? ` const user = await
|
|
1294
|
-
const admin = user ? null : await
|
|
1295
|
-
const account = user || admin;` : ` const user = await
|
|
1372
|
+
const refreshLookup = hasAdmin ? ` const user = await ${db.findOne("User", `{ refreshToken: token }`)};
|
|
1373
|
+
const admin = user ? null : await ${db.findOne("Admin", `{ refreshToken: token }`)};
|
|
1374
|
+
const account = user || admin;` : ` const user = await ${db.findOne("User", `{ refreshToken: token }`)};
|
|
1296
1375
|
const account = user;`;
|
|
1297
|
-
const refreshPersist = hasAdmin ? ` if (user) await
|
|
1298
|
-
else if (admin) await
|
|
1376
|
+
const refreshPersist = hasAdmin ? ` if (user) await ${db.update("User", `user.id`, `{ refreshToken: newRefreshToken, refreshTokenExpiry }`)};
|
|
1377
|
+
else if (admin) await ${db.update("Admin", `admin.id`, `{ refreshToken: newRefreshToken, refreshTokenExpiry }`)};` : ` await ${db.update("User", `user.id`, `{ refreshToken: newRefreshToken, refreshTokenExpiry }`)};`;
|
|
1299
1378
|
const refreshContent = ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
1300
1379
|
import type { Request, Response } from 'express';
|
|
1301
1380
|
import crypto from 'node:crypto';
|
|
@@ -1376,10 +1455,10 @@ ${veMeta}export const GET = async (req: Request, res: Response) => {
|
|
|
1376
1455
|
const { token } = req.query;
|
|
1377
1456
|
if (!token || typeof token !== 'string') return res.status(400).json({ error: 'token is required' });
|
|
1378
1457
|
|
|
1379
|
-
const user = await
|
|
1458
|
+
const user = await ${db.findOne("User", `{ verifyToken: token }`)};
|
|
1380
1459
|
if (!user) return res.status(400).json({ error: 'Invalid or expired verification token' });
|
|
1381
1460
|
|
|
1382
|
-
await
|
|
1461
|
+
await ${db.update("User", `user.id`, `{ isVerified: true, verifyToken: '' }`)};
|
|
1383
1462
|
res.json({ message: 'Email verified' });
|
|
1384
1463
|
};
|
|
1385
1464
|
|
|
@@ -1387,10 +1466,10 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
1387
1466
|
const { email } = req.body;
|
|
1388
1467
|
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
1389
1468
|
|
|
1390
|
-
const user = await
|
|
1469
|
+
const user = await ${db.findOne("User", `{ email }`)};
|
|
1391
1470
|
if (user && !user.isVerified) {
|
|
1392
1471
|
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
1393
|
-
await
|
|
1472
|
+
await ${db.update("User", `user.id`, `{ verifyToken }`)};
|
|
1394
1473
|
${sendVerifyEmailResend} }
|
|
1395
1474
|
|
|
1396
1475
|
res.json({ message: 'Verification email sent' });
|
|
@@ -1401,10 +1480,10 @@ ${veMeta}export const GET = async (req, res) => {
|
|
|
1401
1480
|
const { token } = req.query;
|
|
1402
1481
|
if (!token || typeof token !== 'string') return res.status(400).json({ error: 'token is required' });
|
|
1403
1482
|
|
|
1404
|
-
const user = await
|
|
1483
|
+
const user = await ${db.findOne("User", `{ verifyToken: token }`)};
|
|
1405
1484
|
if (!user) return res.status(400).json({ error: 'Invalid or expired verification token' });
|
|
1406
1485
|
|
|
1407
|
-
await
|
|
1486
|
+
await ${db.update("User", `user.id`, `{ isVerified: true, verifyToken: '' }`)};
|
|
1408
1487
|
res.json({ message: 'Email verified' });
|
|
1409
1488
|
};
|
|
1410
1489
|
|
|
@@ -1412,10 +1491,10 @@ export const POST = async (req, res) => {
|
|
|
1412
1491
|
const { email } = req.body;
|
|
1413
1492
|
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
1414
1493
|
|
|
1415
|
-
const user = await
|
|
1494
|
+
const user = await ${db.findOne("User", `{ email }`)};
|
|
1416
1495
|
if (user && !user.isVerified) {
|
|
1417
1496
|
const verifyToken = crypto.randomBytes(32).toString('hex');
|
|
1418
|
-
await
|
|
1497
|
+
await ${db.update("User", `user.id`, `{ verifyToken }`)};
|
|
1419
1498
|
${sendVerifyEmailResend} }
|
|
1420
1499
|
|
|
1421
1500
|
res.json({ message: 'Verification email sent' });
|
|
@@ -1434,11 +1513,11 @@ ${sendVerifyEmailResend} }
|
|
|
1434
1513
|
const fpModelImports = `import { User } from '../../model/User.js';
|
|
1435
1514
|
${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
1436
1515
|
` : ""}`;
|
|
1437
|
-
const fpLookup = hasAdmin ? ` const user = await
|
|
1438
|
-
const admin = user ? null : await
|
|
1516
|
+
const fpLookup = hasAdmin ? ` const user = await ${db.findOne("User", `{ email }`)};
|
|
1517
|
+
const admin = user ? null : await ${db.findOne("Admin", `{ email }`)};` : ` const user = await ${db.findOne("User", `{ email }`)};`;
|
|
1439
1518
|
const fpGuard = hasAdmin ? "user || admin" : "user";
|
|
1440
|
-
const fpPersist = hasAdmin ? ` if (user) await
|
|
1441
|
-
else if (admin) await
|
|
1519
|
+
const fpPersist = hasAdmin ? ` if (user) await ${db.update("User", `user.id`, `{ resetToken, resetTokenExpiry }`)};
|
|
1520
|
+
else if (admin) await ${db.update("Admin", `admin.id`, `{ resetToken, resetTokenExpiry }`)};` : ` await ${db.update("User", `user.id`, `{ resetToken, resetTokenExpiry }`)};`;
|
|
1442
1521
|
const fpContent = ts ? `import type { Request, Response } from 'express';
|
|
1443
1522
|
import crypto from 'node:crypto';
|
|
1444
1523
|
${mailerTaskImport}${fpModelImports}${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
@@ -1490,12 +1569,12 @@ ${sendResetEmail} }
|
|
|
1490
1569
|
const rpModelImports = `import { User } from '../../model/User.js';
|
|
1491
1570
|
${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
1492
1571
|
` : ""}`;
|
|
1493
|
-
const rpLookup = hasAdmin ? ` const user = await
|
|
1494
|
-
const admin = user ? null : await
|
|
1495
|
-
const account = user || admin;` : ` const user = await
|
|
1572
|
+
const rpLookup = hasAdmin ? ` const user = await ${db.findOne("User", `{ resetToken: token }`)};
|
|
1573
|
+
const admin = user ? null : await ${db.findOne("Admin", `{ resetToken: token }`)};
|
|
1574
|
+
const account = user || admin;` : ` const user = await ${db.findOne("User", `{ resetToken: token }`)};
|
|
1496
1575
|
const account = user;`;
|
|
1497
|
-
const rpPersist = hasAdmin ? ` if (user) await
|
|
1498
|
-
else if (admin) await
|
|
1576
|
+
const rpPersist = hasAdmin ? ` if (user) await ${db.update("User", `user.id`, `{ password: hashed, resetToken: '' }`)};
|
|
1577
|
+
else if (admin) await ${db.update("Admin", `admin.id`, `{ password: hashed, resetToken: '' }`)};` : ` await ${db.update("User", `user.id`, `{ password: hashed, resetToken: '' }`)};`;
|
|
1499
1578
|
const rpContent = ts ? `import type { Request, Response } from 'express';
|
|
1500
1579
|
import bcrypt from 'bcrypt';
|
|
1501
1580
|
import { revokeAllSessions } from 'express-file-cluster/auth';
|
|
@@ -2204,7 +2283,10 @@ export const GET = async (req, res) => {
|
|
|
2204
2283
|
}
|
|
2205
2284
|
if (uf.apiAndBilling) {
|
|
2206
2285
|
const akListMeta = mkMeta(opts, {
|
|
2207
|
-
GET: {
|
|
2286
|
+
GET: {
|
|
2287
|
+
description: "List API keys for the authenticated user.",
|
|
2288
|
+
response: `{ apiKeys: [] }`
|
|
2289
|
+
},
|
|
2208
2290
|
POST: {
|
|
2209
2291
|
description: "Generate a new API key for the authenticated user.",
|
|
2210
2292
|
request: `{ name: 'CI key' }`,
|
|
@@ -3447,12 +3529,53 @@ ${roleGuard} const { status, page = 1, limit = 20 } = req.query;
|
|
|
3447
3529
|
);
|
|
3448
3530
|
}
|
|
3449
3531
|
}
|
|
3532
|
+
async function writeModelIndex(dest, opts) {
|
|
3533
|
+
const ts = opts.language === "typescript";
|
|
3534
|
+
const ext = ts ? "ts" : "js";
|
|
3535
|
+
const names = [...generatedModels(opts)].sort();
|
|
3536
|
+
if (names.length === 0) return;
|
|
3537
|
+
const exports = names.map((n) => `export { ${n} } from './${n}.js';`).join("\n");
|
|
3538
|
+
const tables = ts ? `
|
|
3539
|
+
${names.map((n) => `import type { ${n} } from './${n}.js';`).join("\n")}
|
|
3540
|
+
|
|
3541
|
+
// Types \`include\` results: relations name models by string, this maps the names to models.
|
|
3542
|
+
declare module 'express-file-cluster' {
|
|
3543
|
+
interface Tables {
|
|
3544
|
+
${names.map((n) => ` ${n}: typeof ${n};`).join("\n")}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
` : "";
|
|
3548
|
+
const content = `// Every model, in one import: \`import { User } from '../model/index.js'\` also registers the
|
|
3549
|
+
// models its relations reach, which \`include\` needs.
|
|
3550
|
+
${exports}
|
|
3551
|
+
${tables}`;
|
|
3552
|
+
await fs.outputFile(path.join(dest, "src", "model", `index.${ext}`), content);
|
|
3553
|
+
}
|
|
3554
|
+
var MODEL_ORDER = [
|
|
3555
|
+
"User",
|
|
3556
|
+
"Admin",
|
|
3557
|
+
"Plan",
|
|
3558
|
+
"Category",
|
|
3559
|
+
"Role",
|
|
3560
|
+
"FAQ",
|
|
3561
|
+
"Coupon",
|
|
3562
|
+
"Session",
|
|
3563
|
+
"Notification",
|
|
3564
|
+
"File",
|
|
3565
|
+
"SupportTicket",
|
|
3566
|
+
"AuditLog",
|
|
3567
|
+
"Subscription",
|
|
3568
|
+
"Invoice",
|
|
3569
|
+
"ApiKey",
|
|
3570
|
+
"Blog"
|
|
3571
|
+
];
|
|
3450
3572
|
async function writeInitMigration(dest, opts) {
|
|
3451
3573
|
const modelDir = path.join(dest, "src", "model");
|
|
3452
3574
|
if (!await fs.pathExists(modelDir)) return;
|
|
3453
3575
|
const ts = opts.language === "typescript";
|
|
3454
3576
|
const ext = ts ? "ts" : "js";
|
|
3455
|
-
const
|
|
3577
|
+
const rank = (n) => MODEL_ORDER.includes(n) ? MODEL_ORDER.indexOf(n) : MODEL_ORDER.length;
|
|
3578
|
+
const names = (await fs.readdir(modelDir)).filter((f) => f.endsWith(`.${ext}`) && f !== `index.${ext}`).map((f) => f.slice(0, -(ext.length + 1))).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
3456
3579
|
if (names.length === 0) return;
|
|
3457
3580
|
const imports = names.map((n) => `import { ${n} } from '../model/${n}.js';`).join("\n");
|
|
3458
3581
|
const typeImport = ts ? "import type { MigrationContext } from 'express-file-cluster';\n" : "";
|
|
@@ -3503,6 +3626,22 @@ async function main() {
|
|
|
3503
3626
|
]
|
|
3504
3627
|
});
|
|
3505
3628
|
if (p.isCancel(database)) cancel2();
|
|
3629
|
+
let modelApi2;
|
|
3630
|
+
if (database === "postgresql") {
|
|
3631
|
+
const api = await p.select({
|
|
3632
|
+
message: "Models:",
|
|
3633
|
+
options: [
|
|
3634
|
+
{
|
|
3635
|
+
value: "defineTable",
|
|
3636
|
+
label: "defineTable",
|
|
3637
|
+
hint: "PostgreSQL-native \u2014 foreign keys, relations, include, Prisma-style queries"
|
|
3638
|
+
},
|
|
3639
|
+
{ value: "defineModel", label: "defineModel", hint: "portable \u2014 the same API on MongoDB" }
|
|
3640
|
+
]
|
|
3641
|
+
});
|
|
3642
|
+
if (p.isCancel(api)) cancel2();
|
|
3643
|
+
modelApi2 = api;
|
|
3644
|
+
}
|
|
3506
3645
|
const authStrategy = await p.select({
|
|
3507
3646
|
message: "Authentication strategy:",
|
|
3508
3647
|
options: [
|
|
@@ -3532,17 +3671,55 @@ async function main() {
|
|
|
3532
3671
|
const chosen = await p.multiselect({
|
|
3533
3672
|
message: "User portal features: (space to toggle, enter to confirm)",
|
|
3534
3673
|
options: [
|
|
3535
|
-
{
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3674
|
+
{
|
|
3675
|
+
value: "profileViewing",
|
|
3676
|
+
label: "Profile viewing & editing",
|
|
3677
|
+
hint: "GET/PUT /user/profile"
|
|
3678
|
+
},
|
|
3679
|
+
{
|
|
3680
|
+
value: "forgotPassword",
|
|
3681
|
+
label: "Forgot / reset password",
|
|
3682
|
+
hint: "forgot-password, reset-password"
|
|
3683
|
+
},
|
|
3684
|
+
{
|
|
3685
|
+
value: "accountSecurity",
|
|
3686
|
+
label: "Change password, 2FA & sessions",
|
|
3687
|
+
hint: "change-password, 2fa/*, sessions/*, token refresh"
|
|
3688
|
+
},
|
|
3689
|
+
{
|
|
3690
|
+
value: "emailVerification",
|
|
3691
|
+
label: "Email verification",
|
|
3692
|
+
hint: "verify-email route + registration flow"
|
|
3693
|
+
},
|
|
3694
|
+
{
|
|
3695
|
+
value: "accountSettings",
|
|
3696
|
+
label: "Account settings",
|
|
3697
|
+
hint: "avatar, settings, account, dashboard, activity"
|
|
3698
|
+
},
|
|
3540
3699
|
{ value: "notifications", label: "Notifications", hint: "user/notifications" },
|
|
3541
|
-
{
|
|
3542
|
-
|
|
3700
|
+
{
|
|
3701
|
+
value: "filesAndMedia",
|
|
3702
|
+
label: "Files, favorites & bookmarks",
|
|
3703
|
+
hint: "files, favorites, bookmarks, search"
|
|
3704
|
+
},
|
|
3705
|
+
{
|
|
3706
|
+
value: "apiAndBilling",
|
|
3707
|
+
label: "API keys & billing",
|
|
3708
|
+
hint: "api-keys, plans, subscription, invoices"
|
|
3709
|
+
},
|
|
3543
3710
|
{ value: "support", label: "Support tickets", hint: "create & view own tickets" }
|
|
3544
3711
|
],
|
|
3545
|
-
initialValues: [
|
|
3712
|
+
initialValues: [
|
|
3713
|
+
"profileViewing",
|
|
3714
|
+
"forgotPassword",
|
|
3715
|
+
"accountSecurity",
|
|
3716
|
+
"emailVerification",
|
|
3717
|
+
"accountSettings",
|
|
3718
|
+
"notifications",
|
|
3719
|
+
"filesAndMedia",
|
|
3720
|
+
"apiAndBilling",
|
|
3721
|
+
"support"
|
|
3722
|
+
],
|
|
3546
3723
|
required: false
|
|
3547
3724
|
});
|
|
3548
3725
|
if (p.isCancel(chosen)) cancel2();
|
|
@@ -3564,16 +3741,49 @@ async function main() {
|
|
|
3564
3741
|
const chosen = await p.multiselect({
|
|
3565
3742
|
message: "Admin panel features: (space to toggle, enter to confirm)",
|
|
3566
3743
|
options: [
|
|
3567
|
-
{
|
|
3568
|
-
|
|
3744
|
+
{
|
|
3745
|
+
value: "userManagement",
|
|
3746
|
+
label: "User management",
|
|
3747
|
+
hint: "list/create/suspend/verify/export users + dashboard stats"
|
|
3748
|
+
},
|
|
3749
|
+
{
|
|
3750
|
+
value: "adminManagement",
|
|
3751
|
+
label: "Admin & role management",
|
|
3752
|
+
hint: "manage other admins" + (selected.has("rbac") ? " + roles" : "")
|
|
3753
|
+
},
|
|
3569
3754
|
{ value: "analytics", label: "Analytics", hint: "users, revenue, traffic dashboards" },
|
|
3570
|
-
{
|
|
3571
|
-
|
|
3755
|
+
{
|
|
3756
|
+
value: "contentManagement",
|
|
3757
|
+
label: "Content management",
|
|
3758
|
+
hint: "FAQs, blog posts, categories"
|
|
3759
|
+
},
|
|
3760
|
+
{
|
|
3761
|
+
value: "billingManagement",
|
|
3762
|
+
label: "Billing management",
|
|
3763
|
+
hint: "plans, coupons, subscriptions"
|
|
3764
|
+
},
|
|
3572
3765
|
{ value: "supportManagement", label: "Support tickets", hint: "view & respond to tickets" },
|
|
3573
|
-
{
|
|
3574
|
-
|
|
3766
|
+
{
|
|
3767
|
+
value: "notificationsAndLogs",
|
|
3768
|
+
label: "Notifications & audit logs",
|
|
3769
|
+
hint: "broadcast + audit/activity/security logs"
|
|
3770
|
+
},
|
|
3771
|
+
{
|
|
3772
|
+
value: "systemSettings",
|
|
3773
|
+
label: "System settings & health",
|
|
3774
|
+
hint: "app settings, health check, cache"
|
|
3775
|
+
}
|
|
3776
|
+
],
|
|
3777
|
+
initialValues: [
|
|
3778
|
+
"userManagement",
|
|
3779
|
+
"adminManagement",
|
|
3780
|
+
"analytics",
|
|
3781
|
+
"contentManagement",
|
|
3782
|
+
"billingManagement",
|
|
3783
|
+
"supportManagement",
|
|
3784
|
+
"notificationsAndLogs",
|
|
3785
|
+
"systemSettings"
|
|
3575
3786
|
],
|
|
3576
|
-
initialValues: ["userManagement", "adminManagement", "analytics", "contentManagement", "billingManagement", "supportManagement", "notificationsAndLogs", "systemSettings"],
|
|
3577
3787
|
required: false
|
|
3578
3788
|
});
|
|
3579
3789
|
if (p.isCancel(chosen)) cancel2();
|
|
@@ -3676,6 +3886,7 @@ async function main() {
|
|
|
3676
3886
|
adminFeatures,
|
|
3677
3887
|
rbac: selected.has("rbac"),
|
|
3678
3888
|
mailer: selected.has("mailer"),
|
|
3889
|
+
...modelApi2 !== void 0 && { modelApi: modelApi2 },
|
|
3679
3890
|
...taskBackend !== void 0 && { taskBackend },
|
|
3680
3891
|
...smtpProvider !== void 0 && { smtpProvider },
|
|
3681
3892
|
...smtpHost !== void 0 && { smtpHost },
|
|
@@ -3727,7 +3938,10 @@ function npmInstall(projectDir) {
|
|
|
3727
3938
|
function npmInstallGlobal() {
|
|
3728
3939
|
return new Promise((resolve, reject) => {
|
|
3729
3940
|
const child = spawn("npm", ["install", "-g", "express-file-cluster"], { stdio: "ignore" });
|
|
3730
|
-
child.on(
|
|
3941
|
+
child.on(
|
|
3942
|
+
"exit",
|
|
3943
|
+
(code) => code === 0 ? resolve() : reject(new Error("global install failed"))
|
|
3944
|
+
);
|
|
3731
3945
|
});
|
|
3732
3946
|
}
|
|
3733
3947
|
main().catch((err) => {
|