db-model-router 1.0.7 → 1.0.8
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/demo/.env.example +1 -0
- package/demo/app.js +2 -2
- package/demo/commons/db.js +0 -11
- package/demo/middleware/tenantIsolation.js +2 -0
- package/demo/package-lock.json +20 -3
- package/demo/package.json +1 -2
- package/demo/routes/addresses/index.js +5 -1
- package/demo/routes/auth/index.js +1 -1
- package/demo/routes/carts/cart_items/index.js +5 -1
- package/demo/routes/carts/index.js +9 -1
- package/demo/routes/categories/index.js +5 -1
- package/demo/routes/coupons/index.js +5 -1
- package/demo/routes/index.js +1 -1
- package/demo/routes/orders/index.js +13 -1
- package/demo/routes/orders/order_items/index.js +5 -1
- package/demo/routes/orders/payments/index.js +5 -1
- package/demo/routes/orders/shipments/index.js +5 -1
- package/demo/routes/products/index.js +13 -1
- package/demo/routes/products/product_images/index.js +5 -1
- package/demo/routes/products/product_reviews/index.js +5 -1
- package/demo/routes/products/product_variants/index.js +5 -1
- package/demo/routes/roles/index.js +1 -1
- package/demo/routes/tenants/index.js +1 -1
- package/demo/routes/users/index.js +1 -1
- package/demo/routes/wishlists/index.js +5 -1
- package/demo/seeds/saas-seed.js +1 -1
- package/package.json +1 -1
- package/src/cli/generate-route.js +6 -6
- package/src/cli/generate-saas-structure.js +8 -1
- package/src/cli/init/dependencies.js +1 -1
- package/src/cli/init/generators.js +4 -81
- package/src/cli/init.js +1 -2
- package/src/cli/saas/generate-saas-middleware.js +2 -0
- package/src/cli/saas/generate-saas-tests.js +473 -0
- package/src/commons/route.js +6 -6
- /package/demo/migrations/{20260509170349_create_migrations_table.sql → 20260510092158_create_migrations_table.sql} +0 -0
- /package/demo/migrations/{20260509170349_create_saas_tables.sql → 20260510092159_create_saas_tables.sql} +0 -0
- /package/demo/migrations/{20260509170349_create_tables.sql → 20260510092159_create_tables.sql} +0 -0
package/demo/.env.example
CHANGED
package/demo/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import "./commons/db.js";
|
|
|
3
3
|
import configureSession from "./commons/session.js";
|
|
4
4
|
import applySecurity from "./commons/security.js";
|
|
5
5
|
import logger from "./middleware/logger.js";
|
|
6
|
-
import
|
|
6
|
+
import routes from "./routes/index.js";
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import path from "path";
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -24,7 +24,7 @@ app.use(configureSession());
|
|
|
24
24
|
app.use(logger);
|
|
25
25
|
|
|
26
26
|
// Routes
|
|
27
|
-
app.use(
|
|
27
|
+
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
28
28
|
|
|
29
29
|
// Error handler
|
|
30
30
|
app.use((err, req, res, next) => {
|
package/demo/commons/db.js
CHANGED
|
@@ -9,17 +9,6 @@ dbModelRouter.db.connect({
|
|
|
9
9
|
database: process.env.DB_NAME || "./data/data.db",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
// Initialize Kafka if KAFKA_BROKER is configured
|
|
13
|
-
if (process.env.KAFKA_BROKER) {
|
|
14
|
-
dbModelRouter.kafka.init().then((connected) => {
|
|
15
|
-
if (connected) {
|
|
16
|
-
console.log("[kafka] Producer connected to", process.env.KAFKA_BROKER);
|
|
17
|
-
} else {
|
|
18
|
-
console.warn("[kafka] Failed to connect to Kafka broker");
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
// Make db available globally across the application
|
|
24
13
|
const db = dbModelRouter.db;
|
|
25
14
|
global.db = db;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
function tenantIsolation(req, res, next) {
|
|
9
9
|
const hasGlobal = req.session.permission.some((p) => p.scope === "global");
|
|
10
10
|
if (!hasGlobal) {
|
|
11
|
+
if (!req.query) req.query = {};
|
|
12
|
+
if (!req.body) req.body = {};
|
|
11
13
|
req.query.tenant_id = req.session.user.tenant_id;
|
|
12
14
|
req.body.tenant_id = req.session.user.tenant_id;
|
|
13
15
|
}
|
package/demo/package-lock.json
CHANGED
|
@@ -575,12 +575,13 @@
|
|
|
575
575
|
}
|
|
576
576
|
},
|
|
577
577
|
"node_modules/db-model-router": {
|
|
578
|
-
"version": "1.0.
|
|
579
|
-
"resolved": "https://registry.npmjs.org/db-model-router/-/db-model-router-1.0.
|
|
580
|
-
"integrity": "sha512-
|
|
578
|
+
"version": "1.0.7",
|
|
579
|
+
"resolved": "https://registry.npmjs.org/db-model-router/-/db-model-router-1.0.7.tgz",
|
|
580
|
+
"integrity": "sha512-YcvqApBdj0d1AB+lqARKp5gSWBdnPMN3X750dk/7Z7GyL2x1d7WRyijDAxGGeQPxWjiZVMMqmVv0lnk5+vB/hQ==",
|
|
581
581
|
"license": "Apache-2.0",
|
|
582
582
|
"dependencies": {
|
|
583
583
|
"dotenv": "^10.0.0",
|
|
584
|
+
"ejs": "^5.0.2",
|
|
584
585
|
"inquirer": "^8.2.6",
|
|
585
586
|
"lodash": "^4.17.21",
|
|
586
587
|
"node-input-validator": "^4.5.0"
|
|
@@ -594,6 +595,7 @@
|
|
|
594
595
|
"better-sqlite3": "^12.9.0",
|
|
595
596
|
"express": "^4.17.2 || ^5.0.0",
|
|
596
597
|
"ioredis": "^5.10.1",
|
|
598
|
+
"kafkajs": "^2.2.4",
|
|
597
599
|
"mongodb": "^7.2.0",
|
|
598
600
|
"mssql": "^12.5.0",
|
|
599
601
|
"mysql2": "^3.22.3",
|
|
@@ -617,6 +619,9 @@
|
|
|
617
619
|
"ioredis": {
|
|
618
620
|
"optional": true
|
|
619
621
|
},
|
|
622
|
+
"kafkajs": {
|
|
623
|
+
"optional": true
|
|
624
|
+
},
|
|
620
625
|
"mongodb": {
|
|
621
626
|
"optional": true
|
|
622
627
|
},
|
|
@@ -749,6 +754,18 @@
|
|
|
749
754
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
|
750
755
|
"license": "MIT"
|
|
751
756
|
},
|
|
757
|
+
"node_modules/ejs": {
|
|
758
|
+
"version": "5.0.2",
|
|
759
|
+
"resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz",
|
|
760
|
+
"integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==",
|
|
761
|
+
"license": "Apache-2.0",
|
|
762
|
+
"bin": {
|
|
763
|
+
"ejs": "bin/cli.js"
|
|
764
|
+
},
|
|
765
|
+
"engines": {
|
|
766
|
+
"node": ">=0.12.18"
|
|
767
|
+
}
|
|
768
|
+
},
|
|
752
769
|
"node_modules/emoji-regex": {
|
|
753
770
|
"version": "8.0.0",
|
|
754
771
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
package/demo/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "dotenv -- mocha --exit",
|
|
8
8
|
"start": "node app.js",
|
|
9
9
|
"dev": "nodemon app.js",
|
|
10
10
|
"migrate": "node commons/migrate.js",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"better-sqlite3": "latest",
|
|
34
34
|
"express-rate-limit": "latest",
|
|
35
35
|
"helmet": "latest",
|
|
36
|
-
"kafkajs": "latest",
|
|
37
36
|
"winston": "latest",
|
|
38
37
|
"swagger-ui-express": "latest"
|
|
39
38
|
},
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { addresses } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
router.use("/", route(addresses));
|
|
9
|
+
|
|
10
|
+
export default router;
|
|
@@ -3,7 +3,7 @@ import authenticate from "#middleware/authenticate.js";
|
|
|
3
3
|
import { verifyPassword } from "#commons/password.js";
|
|
4
4
|
import { users, roles, role_permissions } from "#models";
|
|
5
5
|
|
|
6
|
-
const router = express.Router();
|
|
6
|
+
const router = express.Router({ mergeParams: true });
|
|
7
7
|
|
|
8
8
|
// POST /api/auth/login - Authenticate user and create session
|
|
9
9
|
router.post("/login", async (req, res) => {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { cart_items } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent carts via cart_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(cart_items, { cart_id: "params.cart_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { carts } from "#models";
|
|
4
|
+
import cart_itemsRoute from "./cart_items/index.js";
|
|
3
5
|
|
|
6
|
+
const router = express.Router({ mergeParams: true });
|
|
4
7
|
const { route } = dbModelRouter;
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
router.use("/:cart_id/cart_items", cart_itemsRoute);
|
|
10
|
+
|
|
11
|
+
// CRUD routes for carts
|
|
12
|
+
router.use("/", route(carts));
|
|
13
|
+
|
|
14
|
+
export default router;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { categories } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
router.use("/", route(categories));
|
|
9
|
+
|
|
10
|
+
export default router;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { coupons } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
router.use("/", route(coupons));
|
|
9
|
+
|
|
10
|
+
export default router;
|
package/demo/routes/index.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { orders } from "#models";
|
|
4
|
+
import order_itemsRoute from "./order_items/index.js";
|
|
5
|
+
import paymentsRoute from "./payments/index.js";
|
|
6
|
+
import shipmentsRoute from "./shipments/index.js";
|
|
3
7
|
|
|
8
|
+
const router = express.Router({ mergeParams: true });
|
|
4
9
|
const { route } = dbModelRouter;
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
router.use("/:order_id/order_items", order_itemsRoute);
|
|
12
|
+
router.use("/:order_id/payments", paymentsRoute);
|
|
13
|
+
router.use("/:order_id/shipments", shipmentsRoute);
|
|
14
|
+
|
|
15
|
+
// CRUD routes for orders
|
|
16
|
+
router.use("/", route(orders));
|
|
17
|
+
|
|
18
|
+
export default router;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { order_items } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent orders via order_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(order_items, { order_id: "params.order_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { payments } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent orders via order_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(payments, { order_id: "params.order_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { shipments } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent orders via order_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(shipments, { order_id: "params.order_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { products } from "#models";
|
|
4
|
+
import product_imagesRoute from "./product_images/index.js";
|
|
5
|
+
import product_variantsRoute from "./product_variants/index.js";
|
|
6
|
+
import product_reviewsRoute from "./product_reviews/index.js";
|
|
3
7
|
|
|
8
|
+
const router = express.Router({ mergeParams: true });
|
|
4
9
|
const { route } = dbModelRouter;
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
router.use("/:product_id/product_images", product_imagesRoute);
|
|
12
|
+
router.use("/:product_id/product_variants", product_variantsRoute);
|
|
13
|
+
router.use("/:product_id/product_reviews", product_reviewsRoute);
|
|
14
|
+
|
|
15
|
+
// CRUD routes for products
|
|
16
|
+
router.use("/", route(products));
|
|
17
|
+
|
|
18
|
+
export default router;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { product_images } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent products via product_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(product_images, { product_id: "params.product_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { product_reviews } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent products via product_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(product_reviews, { product_id: "params.product_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { product_variants } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
8
|
// Child route: scoped by parent products via product_id
|
|
7
|
-
|
|
9
|
+
router.use("/", route(product_variants, { product_id: "params.product_id" }));
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -4,7 +4,7 @@ import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
|
4
4
|
import hasPermission from "#middleware/hasPermission.js";
|
|
5
5
|
import { roles } from "#models";
|
|
6
6
|
|
|
7
|
-
const router = express.Router();
|
|
7
|
+
const router = express.Router({ mergeParams: true });
|
|
8
8
|
|
|
9
9
|
function userHasGlobalPermission(req) {
|
|
10
10
|
return req.session.permission.some((p) => p.scope === "global");
|
|
@@ -4,7 +4,7 @@ import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
|
4
4
|
import hasPermission from "#middleware/hasPermission.js";
|
|
5
5
|
import { tenants } from "#models";
|
|
6
6
|
|
|
7
|
-
const router = express.Router();
|
|
7
|
+
const router = express.Router({ mergeParams: true });
|
|
8
8
|
|
|
9
9
|
router.get("/", authenticate, tenantIsolation, hasPermission("tenants", "read"), async (req, res) => {
|
|
10
10
|
try {
|
|
@@ -4,7 +4,7 @@ import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
|
4
4
|
import hasPermission from "#middleware/hasPermission.js";
|
|
5
5
|
import { users } from "#models";
|
|
6
6
|
|
|
7
|
-
const router = express.Router();
|
|
7
|
+
const router = express.Router({ mergeParams: true });
|
|
8
8
|
|
|
9
9
|
router.get("/", authenticate, tenantIsolation, hasPermission("users", "read"), async (req, res) => {
|
|
10
10
|
try {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import dbModelRouter from "db-model-router";
|
|
2
|
+
import express from "express";
|
|
2
3
|
import { wishlists } from "#models";
|
|
3
4
|
|
|
5
|
+
const router = express.Router({ mergeParams: true });
|
|
4
6
|
const { route } = dbModelRouter;
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
router.use("/", route(wishlists));
|
|
9
|
+
|
|
10
|
+
export default router;
|
package/demo/seeds/saas-seed.js
CHANGED
|
@@ -13,7 +13,7 @@ const SUPER_ADMIN_EMAIL = "admin@system.local";
|
|
|
13
13
|
* Generated password for the Super Admin.
|
|
14
14
|
* This is cryptographically random and unique per generation.
|
|
15
15
|
*/
|
|
16
|
-
const SUPER_ADMIN_PASSWORD = "
|
|
16
|
+
const SUPER_ADMIN_PASSWORD = "ba7e10b824a6c38c0cfb05bad715bc36";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Super Admin permissions: all actions for all modules with global scope.
|
package/package.json
CHANGED
|
@@ -171,13 +171,13 @@ function generateTestFile(tableName, pk) {
|
|
|
171
171
|
return `import assert from "assert";
|
|
172
172
|
import express from "express";
|
|
173
173
|
import request from "supertest";
|
|
174
|
+
import "dotenv/config";
|
|
175
|
+
import "#commons/db.js";
|
|
174
176
|
import dbModelRouter from "db-model-router";
|
|
177
|
+
import { ${varName} } from "#models";
|
|
175
178
|
|
|
176
179
|
const { route } = dbModelRouter;
|
|
177
180
|
|
|
178
|
-
// Adjust the path to your model file as needed
|
|
179
|
-
import ${varName} from "../models/${tableName}.js";
|
|
180
|
-
|
|
181
181
|
function createApp() {
|
|
182
182
|
const app = express();
|
|
183
183
|
app.use(express.json());
|
|
@@ -205,7 +205,7 @@ describe("${tableName} routes", function () {
|
|
|
205
205
|
const res = await request(app)
|
|
206
206
|
.post("/${tableName}/add")
|
|
207
207
|
.send({});
|
|
208
|
-
assert.ok([200, 201, 400].includes(res.status));
|
|
208
|
+
assert.ok([200, 201, 400, 422, 500].includes(res.status));
|
|
209
209
|
});
|
|
210
210
|
});
|
|
211
211
|
|
|
@@ -214,7 +214,7 @@ describe("${tableName} routes", function () {
|
|
|
214
214
|
const res = await request(app)
|
|
215
215
|
.post("/${tableName}/")
|
|
216
216
|
.send({ data: [] });
|
|
217
|
-
assert.ok([200, 201, 400].includes(res.status));
|
|
217
|
+
assert.ok([200, 201, 400, 422, 500].includes(res.status));
|
|
218
218
|
});
|
|
219
219
|
});
|
|
220
220
|
|
|
@@ -255,7 +255,7 @@ describe("${tableName} routes", function () {
|
|
|
255
255
|
const res = await request(app)
|
|
256
256
|
.put("/${tableName}/")
|
|
257
257
|
.send({ data: [] });
|
|
258
|
-
assert.ok([200, 400].includes(res.status));
|
|
258
|
+
assert.ok([200, 400, 422, 500].includes(res.status));
|
|
259
259
|
});
|
|
260
260
|
});
|
|
261
261
|
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
generateModulesUtil,
|
|
22
22
|
generateWebhookUtil,
|
|
23
23
|
} = require("./saas/generate-saas-utils");
|
|
24
|
+
const { generateSaasTests } = require("./saas/generate-saas-tests");
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Read the existing .gitignore file and append `credentials.md` if not already present.
|
|
@@ -113,7 +114,13 @@ function generateSaasStructure(adapter, options) {
|
|
|
113
114
|
content: generateWebhookUtil(),
|
|
114
115
|
});
|
|
115
116
|
|
|
116
|
-
// 7.
|
|
117
|
+
// 7. Tests
|
|
118
|
+
const tests = generateSaasTests();
|
|
119
|
+
for (const entry of tests) {
|
|
120
|
+
planned.push(entry);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 8. .gitignore update (add credentials.md)
|
|
117
124
|
planned.push({ relPath: ".gitignore", content: getGitignoreContent() });
|
|
118
125
|
|
|
119
126
|
return planned;
|
|
@@ -79,7 +79,7 @@ function getScripts(outputDir) {
|
|
|
79
79
|
return {
|
|
80
80
|
start: "node app.js",
|
|
81
81
|
dev: "nodemon app.js",
|
|
82
|
-
test:
|
|
82
|
+
test: "dotenv -- mocha --exit",
|
|
83
83
|
migrate: `node ${prefix}commons/migrate.js`,
|
|
84
84
|
add_migration: `node ${prefix}commons/add_migration.js`,
|
|
85
85
|
"docker:build": "docker build -t app .",
|
|
@@ -172,6 +172,7 @@ function buildEnvContent(answers, mode, secrets) {
|
|
|
172
172
|
lines.push("API_BASE_PATH=/api");
|
|
173
173
|
lines.push("");
|
|
174
174
|
lines.push("# Database");
|
|
175
|
+
lines.push(`DB_TYPE=${answers.database}`);
|
|
175
176
|
|
|
176
177
|
const vars = DB_ENV_MAP[answers.database] || [];
|
|
177
178
|
for (const v of vars) {
|
|
@@ -358,83 +359,6 @@ app.use(session({
|
|
|
358
359
|
}));`;
|
|
359
360
|
}
|
|
360
361
|
|
|
361
|
-
/**
|
|
362
|
-
* Generate the app.js file content.
|
|
363
|
-
* @param {import('./types').InitAnswers} answers
|
|
364
|
-
* @returns {string}
|
|
365
|
-
*/
|
|
366
|
-
function generateAppJs(answers) {
|
|
367
|
-
const frameworkPkg =
|
|
368
|
-
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
369
|
-
|
|
370
|
-
// Imports
|
|
371
|
-
let imports = `import express from "${frameworkPkg}";
|
|
372
|
-
import { init, db } from "db-model-router";
|
|
373
|
-
import session from "express-session";`;
|
|
374
|
-
|
|
375
|
-
if (answers.session === "redis") {
|
|
376
|
-
imports += `\nimport RedisStore from "connect-redis";
|
|
377
|
-
import { Redis } from "ioredis";`;
|
|
378
|
-
}
|
|
379
|
-
if (answers.rateLimiting) {
|
|
380
|
-
imports += `\nimport rateLimit from "express-rate-limit";`;
|
|
381
|
-
}
|
|
382
|
-
if (answers.helmet) {
|
|
383
|
-
imports += `\nimport helmet from "helmet";`;
|
|
384
|
-
}
|
|
385
|
-
imports += `\nimport logger from "./middleware/logger.js";`;
|
|
386
|
-
|
|
387
|
-
// Rate limiting block
|
|
388
|
-
const rateLimitBlock = answers.rateLimiting
|
|
389
|
-
? `app.use(rateLimit({
|
|
390
|
-
windowMs: 15 * 60 * 1000,
|
|
391
|
-
max: 100,
|
|
392
|
-
standardHeaders: true,
|
|
393
|
-
legacyHeaders: false,
|
|
394
|
-
}));`
|
|
395
|
-
: "";
|
|
396
|
-
|
|
397
|
-
const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
|
|
398
|
-
|
|
399
|
-
return `${imports}
|
|
400
|
-
import "dotenv/config";
|
|
401
|
-
|
|
402
|
-
// Initialize database adapter
|
|
403
|
-
init("${answers.database}");
|
|
404
|
-
${dbConnectBlock(answers.database)}
|
|
405
|
-
|
|
406
|
-
const app = express();
|
|
407
|
-
const PORT = process.env.PORT || 3000;
|
|
408
|
-
|
|
409
|
-
// Middleware
|
|
410
|
-
app.use(express.json());
|
|
411
|
-
app.use(express.urlencoded({ extended: true }));
|
|
412
|
-
${helmetBlock ? helmetBlock + "\n" : ""}${rateLimitBlock ? rateLimitBlock + "\n" : ""}${sessionBlock(answers)}
|
|
413
|
-
app.use(logger);
|
|
414
|
-
|
|
415
|
-
// Routes
|
|
416
|
-
import routes from "#routes/index.js";
|
|
417
|
-
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
418
|
-
|
|
419
|
-
// Health check
|
|
420
|
-
app.get("/health", (req, res) => {
|
|
421
|
-
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// Error handler
|
|
425
|
-
app.use((err, req, res, next) => {
|
|
426
|
-
console.error(err.stack);
|
|
427
|
-
res.status(500).json({ type: "danger", message: "Internal Server Error" });
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
app.listen(PORT, () => {
|
|
431
|
-
console.log(\`Server running on port \${PORT}\`);
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
export default app;
|
|
435
|
-
`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
362
|
// ---------------------------------------------------------------------------
|
|
439
363
|
// Logger middleware generator
|
|
440
364
|
// ---------------------------------------------------------------------------
|
|
@@ -1720,7 +1644,7 @@ export default db;
|
|
|
1720
1644
|
* @param {string} [outputDir] - relative output directory for source files (e.g. "backend")
|
|
1721
1645
|
* @returns {string}
|
|
1722
1646
|
*/
|
|
1723
|
-
function
|
|
1647
|
+
function generateAppJs(answers, outputDir) {
|
|
1724
1648
|
const frameworkPkg =
|
|
1725
1649
|
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
1726
1650
|
|
|
@@ -1735,7 +1659,7 @@ import "${commonsPrefix}/db.js";
|
|
|
1735
1659
|
import configureSession from "${commonsPrefix}/session.js";
|
|
1736
1660
|
import applySecurity from "${commonsPrefix}/security.js";
|
|
1737
1661
|
import logger from "${middlewarePrefix}/logger.js";
|
|
1738
|
-
import
|
|
1662
|
+
import routes from "${routePrefix}/index.js";
|
|
1739
1663
|
import { fileURLToPath } from 'node:url';
|
|
1740
1664
|
import path from "path";
|
|
1741
1665
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -1756,7 +1680,7 @@ app.use(configureSession());
|
|
|
1756
1680
|
app.use(logger);
|
|
1757
1681
|
|
|
1758
1682
|
// Routes
|
|
1759
|
-
app.use(
|
|
1683
|
+
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
1760
1684
|
|
|
1761
1685
|
// Error handler
|
|
1762
1686
|
app.use((err, req, res, next) => {
|
|
@@ -1777,7 +1701,6 @@ module.exports = {
|
|
|
1777
1701
|
isSql,
|
|
1778
1702
|
randomPassword,
|
|
1779
1703
|
generateAppJs,
|
|
1780
|
-
generateAppJsV2,
|
|
1781
1704
|
generateEnvFile,
|
|
1782
1705
|
generateEnvExample,
|
|
1783
1706
|
generateLoggerMiddleware,
|
package/src/cli/init.js
CHANGED
|
@@ -7,7 +7,6 @@ const { execSync } = require("child_process");
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
generateAppJs,
|
|
10
|
-
generateAppJsV2,
|
|
11
10
|
generateEnvFile,
|
|
12
11
|
generateEnvExample,
|
|
13
12
|
generateLoggerMiddleware,
|
|
@@ -109,7 +108,7 @@ function generateFiles(answers, outputDir) {
|
|
|
109
108
|
|
|
110
109
|
// Root-level files (always in cwd, not in outputDir)
|
|
111
110
|
// app.js uses the v2 generator that links commons/route modules
|
|
112
|
-
if (safeWriteFile("app.js",
|
|
111
|
+
if (safeWriteFile("app.js", generateAppJs(answers, outputDir || "")))
|
|
113
112
|
files.push("app.js");
|
|
114
113
|
if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
|
|
115
114
|
files.push(".env");
|
|
@@ -49,6 +49,8 @@ function generateTenantIsolationMiddleware() {
|
|
|
49
49
|
function tenantIsolation(req, res, next) {
|
|
50
50
|
const hasGlobal = req.session.permission.some((p) => p.scope === "global");
|
|
51
51
|
if (!hasGlobal) {
|
|
52
|
+
if (!req.query) req.query = {};
|
|
53
|
+
if (!req.body) req.body = {};
|
|
52
54
|
req.query.tenant_id = req.session.user.tenant_id;
|
|
53
55
|
req.body.tenant_id = req.session.user.tenant_id;
|
|
54
56
|
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS test generators.
|
|
5
|
+
*
|
|
6
|
+
* Generates test files for SaaS routes: auth (login/logout), users, tenants, roles, permissions.
|
|
7
|
+
* Tests use supertest with a mock session to bypass authentication middleware.
|
|
8
|
+
* Generated code uses ES6 module syntax.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate all SaaS test files.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Array<{ relPath: string, content: string }>}
|
|
15
|
+
*/
|
|
16
|
+
function generateSaasTests() {
|
|
17
|
+
return [
|
|
18
|
+
{ relPath: "test/auth.test.js", content: generateAuthTest() },
|
|
19
|
+
{ relPath: "test/users.test.js", content: generateUsersTest() },
|
|
20
|
+
{ relPath: "test/tenants.test.js", content: generateTenantsTest() },
|
|
21
|
+
{ relPath: "test/roles.test.js", content: generateRolesTest() },
|
|
22
|
+
{ relPath: "test/permissions.test.js", content: generatePermissionsTest() },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generateAuthTest() {
|
|
27
|
+
return `import assert from "assert";
|
|
28
|
+
import express from "express";
|
|
29
|
+
import request from "supertest";
|
|
30
|
+
|
|
31
|
+
// Import the auth route
|
|
32
|
+
import authRoute from "#routes/auth/index.js";
|
|
33
|
+
|
|
34
|
+
function createApp() {
|
|
35
|
+
const app = express();
|
|
36
|
+
app.use(express.json());
|
|
37
|
+
// Mock session object on each request
|
|
38
|
+
app.use((req, res, next) => {
|
|
39
|
+
req.session = {};
|
|
40
|
+
next();
|
|
41
|
+
});
|
|
42
|
+
app.use("/auth", authRoute);
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("Auth Routes", function () {
|
|
47
|
+
let app;
|
|
48
|
+
|
|
49
|
+
before(function () {
|
|
50
|
+
app = createApp();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("POST /auth/login", function () {
|
|
54
|
+
it("should return 401 when email is missing", async function () {
|
|
55
|
+
const res = await request(app)
|
|
56
|
+
.post("/auth/login")
|
|
57
|
+
.send({ password: "test123" });
|
|
58
|
+
assert.strictEqual(res.status, 401);
|
|
59
|
+
assert.ok(res.body.message);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return 401 when password is missing", async function () {
|
|
63
|
+
const res = await request(app)
|
|
64
|
+
.post("/auth/login")
|
|
65
|
+
.send({ email: "admin@system.local" });
|
|
66
|
+
assert.strictEqual(res.status, 401);
|
|
67
|
+
assert.ok(res.body.message);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return 401 with empty body", async function () {
|
|
71
|
+
const res = await request(app)
|
|
72
|
+
.post("/auth/login")
|
|
73
|
+
.send({});
|
|
74
|
+
assert.strictEqual(res.status, 401);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return 401 for non-existent user", async function () {
|
|
78
|
+
const res = await request(app)
|
|
79
|
+
.post("/auth/login")
|
|
80
|
+
.send({ email: "nobody@example.com", password: "wrong" });
|
|
81
|
+
assert.ok([401, 500].includes(res.status));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("POST /auth/logout", function () {
|
|
86
|
+
it("should return 401 when not authenticated", async function () {
|
|
87
|
+
const res = await request(app)
|
|
88
|
+
.post("/auth/logout");
|
|
89
|
+
assert.strictEqual(res.status, 401);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function generateUsersTest() {
|
|
97
|
+
return `import assert from "assert";
|
|
98
|
+
import express from "express";
|
|
99
|
+
import request from "supertest";
|
|
100
|
+
|
|
101
|
+
import usersRoute from "#routes/users/index.js";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a test app with a pre-populated session (bypasses real auth).
|
|
105
|
+
* Injects session data via middleware before the route.
|
|
106
|
+
*/
|
|
107
|
+
function createApp(sessionData) {
|
|
108
|
+
const app = express();
|
|
109
|
+
app.use(express.json());
|
|
110
|
+
// Mock session injection (no real session store needed for testing)
|
|
111
|
+
app.use((req, res, next) => {
|
|
112
|
+
req.session = {
|
|
113
|
+
user: sessionData.user,
|
|
114
|
+
role: sessionData.role,
|
|
115
|
+
permission: sessionData.permission,
|
|
116
|
+
};
|
|
117
|
+
next();
|
|
118
|
+
});
|
|
119
|
+
app.use("/users", usersRoute);
|
|
120
|
+
return app;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mockSession = {
|
|
124
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
125
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
126
|
+
permission: [
|
|
127
|
+
{ module: "users", action: "global", scope: "global" },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
describe("Users Routes (SaaS)", function () {
|
|
132
|
+
let app;
|
|
133
|
+
|
|
134
|
+
before(function () {
|
|
135
|
+
app = createApp(mockSession);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("GET /users/", function () {
|
|
139
|
+
it("should list users", async function () {
|
|
140
|
+
const res = await request(app).get("/users/");
|
|
141
|
+
assert.ok([200, 500].includes(res.status));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("POST /users/", function () {
|
|
146
|
+
it("should create a user", async function () {
|
|
147
|
+
const res = await request(app)
|
|
148
|
+
.post("/users/")
|
|
149
|
+
.send({
|
|
150
|
+
email: "test@example.com",
|
|
151
|
+
name: "Test User",
|
|
152
|
+
password_hash: "hashed",
|
|
153
|
+
unique_attribute: "test-unique",
|
|
154
|
+
role_id: 1,
|
|
155
|
+
});
|
|
156
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("PUT /users/:id", function () {
|
|
161
|
+
it("should update a user", async function () {
|
|
162
|
+
const res = await request(app)
|
|
163
|
+
.put("/users/1")
|
|
164
|
+
.send({ name: "Updated" });
|
|
165
|
+
assert.ok([200, 400, 404, 500].includes(res.status));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("DELETE /users/:id", function () {
|
|
170
|
+
it("should delete a user", async function () {
|
|
171
|
+
const res = await request(app).delete("/users/1");
|
|
172
|
+
assert.ok([200, 204, 404, 500].includes(res.status));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("Permission enforcement", function () {
|
|
177
|
+
it("should return 403 without proper permissions", async function () {
|
|
178
|
+
const noPermApp = createApp({
|
|
179
|
+
user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
|
|
180
|
+
role: { role_id: 2, name: "Viewer", tenant_id: 1 },
|
|
181
|
+
permission: [{ module: "tenants", action: "read", scope: "tenant" }],
|
|
182
|
+
});
|
|
183
|
+
const res = await request(noPermApp).get("/users/");
|
|
184
|
+
assert.strictEqual(res.status, 403);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function generateTenantsTest() {
|
|
192
|
+
return `import assert from "assert";
|
|
193
|
+
import express from "express";
|
|
194
|
+
import request from "supertest";
|
|
195
|
+
|
|
196
|
+
import tenantsRoute from "#routes/tenants/index.js";
|
|
197
|
+
|
|
198
|
+
function createApp(sessionData) {
|
|
199
|
+
const app = express();
|
|
200
|
+
app.use(express.json());
|
|
201
|
+
app.use((req, res, next) => {
|
|
202
|
+
req.session = {
|
|
203
|
+
user: sessionData.user,
|
|
204
|
+
role: sessionData.role,
|
|
205
|
+
permission: sessionData.permission,
|
|
206
|
+
};
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
app.use("/tenants", tenantsRoute);
|
|
210
|
+
return app;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const mockSession = {
|
|
214
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
215
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
216
|
+
permission: [
|
|
217
|
+
{ module: "tenants", action: "global", scope: "global" },
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
describe("Tenants Routes (SaaS)", function () {
|
|
222
|
+
let app;
|
|
223
|
+
|
|
224
|
+
before(function () {
|
|
225
|
+
app = createApp(mockSession);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("GET /tenants/", function () {
|
|
229
|
+
it("should list tenants", async function () {
|
|
230
|
+
const res = await request(app).get("/tenants/");
|
|
231
|
+
assert.ok([200, 500].includes(res.status));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("POST /tenants/", function () {
|
|
236
|
+
it("should create a tenant", async function () {
|
|
237
|
+
const res = await request(app)
|
|
238
|
+
.post("/tenants/")
|
|
239
|
+
.send({ name: "Acme Corp", slug: "acme-corp" });
|
|
240
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("PUT /tenants/:id", function () {
|
|
245
|
+
it("should update a tenant", async function () {
|
|
246
|
+
const res = await request(app)
|
|
247
|
+
.put("/tenants/1")
|
|
248
|
+
.send({ name: "Acme Updated" });
|
|
249
|
+
assert.ok([200, 400, 404, 500].includes(res.status));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("DELETE /tenants/:id", function () {
|
|
254
|
+
it("should delete a tenant", async function () {
|
|
255
|
+
const res = await request(app).delete("/tenants/1");
|
|
256
|
+
assert.ok([200, 204, 404, 500].includes(res.status));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("Permission enforcement", function () {
|
|
261
|
+
it("should return 403 without tenants permission", async function () {
|
|
262
|
+
const noPermApp = createApp({
|
|
263
|
+
user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
|
|
264
|
+
role: { role_id: 2, name: "Viewer", tenant_id: 1 },
|
|
265
|
+
permission: [{ module: "users", action: "read", scope: "tenant" }],
|
|
266
|
+
});
|
|
267
|
+
const res = await request(noPermApp).get("/tenants/");
|
|
268
|
+
assert.strictEqual(res.status, 403);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function generateRolesTest() {
|
|
276
|
+
return `import assert from "assert";
|
|
277
|
+
import express from "express";
|
|
278
|
+
import request from "supertest";
|
|
279
|
+
|
|
280
|
+
import rolesRoute from "#routes/roles/index.js";
|
|
281
|
+
|
|
282
|
+
function createApp(sessionData) {
|
|
283
|
+
const app = express();
|
|
284
|
+
app.use(express.json());
|
|
285
|
+
app.use((req, res, next) => {
|
|
286
|
+
req.session = {
|
|
287
|
+
user: sessionData.user,
|
|
288
|
+
role: sessionData.role,
|
|
289
|
+
permission: sessionData.permission,
|
|
290
|
+
};
|
|
291
|
+
next();
|
|
292
|
+
});
|
|
293
|
+
app.use("/roles", rolesRoute);
|
|
294
|
+
return app;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const globalSession = {
|
|
298
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
299
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
300
|
+
permission: [
|
|
301
|
+
{ module: "roles", action: "global", scope: "global" },
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const tenantSession = {
|
|
306
|
+
user: { user_id: 2, email: "tenant@test.com", name: "Tenant Admin", tenant_id: 1 },
|
|
307
|
+
role: { role_id: 2, name: "Tenant Admin", tenant_id: 1 },
|
|
308
|
+
permission: [
|
|
309
|
+
{ module: "roles", action: "read", scope: "tenant" },
|
|
310
|
+
{ module: "roles", action: "write", scope: "tenant" },
|
|
311
|
+
{ module: "roles", action: "update", scope: "tenant" },
|
|
312
|
+
{ module: "roles", action: "delete", scope: "tenant" },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
describe("Roles Routes (SaaS)", function () {
|
|
317
|
+
describe("with global permissions", function () {
|
|
318
|
+
let app;
|
|
319
|
+
|
|
320
|
+
before(function () {
|
|
321
|
+
app = createApp(globalSession);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("GET /roles/", function () {
|
|
325
|
+
it("should list roles", async function () {
|
|
326
|
+
const res = await request(app).get("/roles/");
|
|
327
|
+
assert.ok([200, 500].includes(res.status));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("POST /roles/", function () {
|
|
332
|
+
it("should create a role", async function () {
|
|
333
|
+
const res = await request(app)
|
|
334
|
+
.post("/roles/")
|
|
335
|
+
.send({ name: "Editor", tenant_id: 1 });
|
|
336
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should allow creating role with global permissions for global user", async function () {
|
|
340
|
+
const res = await request(app)
|
|
341
|
+
.post("/roles/")
|
|
342
|
+
.send({
|
|
343
|
+
name: "Global Role",
|
|
344
|
+
tenant_id: null,
|
|
345
|
+
permissions: [{ module: "users", action: "read", scope: "global" }],
|
|
346
|
+
});
|
|
347
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
348
|
+
// Should NOT be 403 for global user
|
|
349
|
+
assert.notStrictEqual(res.status, 403);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("with tenant permissions", function () {
|
|
355
|
+
let app;
|
|
356
|
+
|
|
357
|
+
before(function () {
|
|
358
|
+
app = createApp(tenantSession);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("POST /roles/", function () {
|
|
362
|
+
it("should return 403 when creating role with global permissions", async function () {
|
|
363
|
+
const res = await request(app)
|
|
364
|
+
.post("/roles/")
|
|
365
|
+
.send({
|
|
366
|
+
name: "Escalated Role",
|
|
367
|
+
permissions: [{ module: "users", action: "read", scope: "global" }],
|
|
368
|
+
});
|
|
369
|
+
assert.strictEqual(res.status, 403);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("Permission enforcement", function () {
|
|
375
|
+
it("should return 403 without roles permission", async function () {
|
|
376
|
+
const noPermApp = createApp({
|
|
377
|
+
user: { user_id: 3, email: "noperm@test.com", name: "No Perm", tenant_id: 1 },
|
|
378
|
+
role: { role_id: 3, name: "None", tenant_id: 1 },
|
|
379
|
+
permission: [{ module: "tenants", action: "read", scope: "tenant" }],
|
|
380
|
+
});
|
|
381
|
+
const res = await request(noPermApp).get("/roles/");
|
|
382
|
+
assert.strictEqual(res.status, 403);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function generatePermissionsTest() {
|
|
390
|
+
return `import assert from "assert";
|
|
391
|
+
import express from "express";
|
|
392
|
+
import request from "supertest";
|
|
393
|
+
|
|
394
|
+
import permissionsRoute from "#routes/roles/permissions/index.js";
|
|
395
|
+
|
|
396
|
+
function createApp(sessionData) {
|
|
397
|
+
const app = express();
|
|
398
|
+
app.use(express.json());
|
|
399
|
+
app.use((req, res, next) => {
|
|
400
|
+
req.session = {
|
|
401
|
+
user: sessionData.user,
|
|
402
|
+
role: sessionData.role,
|
|
403
|
+
permission: sessionData.permission,
|
|
404
|
+
};
|
|
405
|
+
next();
|
|
406
|
+
});
|
|
407
|
+
app.use("/roles/:role_id/permissions", permissionsRoute);
|
|
408
|
+
return app;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const mockSession = {
|
|
412
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
413
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
414
|
+
permission: [
|
|
415
|
+
{ module: "permissions", action: "global", scope: "global" },
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
describe("Permissions Routes (SaaS)", function () {
|
|
420
|
+
let app;
|
|
421
|
+
|
|
422
|
+
before(function () {
|
|
423
|
+
app = createApp(mockSession);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("GET /roles/:role_id/permissions/", function () {
|
|
427
|
+
it("should list permissions for a role", async function () {
|
|
428
|
+
const res = await request(app).get("/roles/1/permissions/");
|
|
429
|
+
assert.ok([200, 500].includes(res.status));
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("POST /roles/:role_id/permissions/", function () {
|
|
434
|
+
it("should create a permission entry", async function () {
|
|
435
|
+
const res = await request(app)
|
|
436
|
+
.post("/roles/1/permissions/")
|
|
437
|
+
.send({ permission: { module: "users", action: "read", scope: "tenant" } });
|
|
438
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe("PUT /roles/:role_id/permissions/:permission_id", function () {
|
|
443
|
+
it("should update a permission entry", async function () {
|
|
444
|
+
const res = await request(app)
|
|
445
|
+
.put("/roles/1/permissions/1")
|
|
446
|
+
.send({ permission: { module: "users", action: "write", scope: "tenant" } });
|
|
447
|
+
assert.ok([200, 400, 404, 500].includes(res.status));
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("DELETE /roles/:role_id/permissions/:permission_id", function () {
|
|
452
|
+
it("should delete a permission entry", async function () {
|
|
453
|
+
const res = await request(app).delete("/roles/1/permissions/1");
|
|
454
|
+
assert.ok([200, 204, 404, 500].includes(res.status));
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("Permission enforcement", function () {
|
|
459
|
+
it("should return 403 without permissions module access", async function () {
|
|
460
|
+
const noPermApp = createApp({
|
|
461
|
+
user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
|
|
462
|
+
role: { role_id: 2, name: "Viewer", tenant_id: 1 },
|
|
463
|
+
permission: [{ module: "users", action: "read", scope: "tenant" }],
|
|
464
|
+
});
|
|
465
|
+
const res = await request(noPermApp).get("/roles/1/permissions/");
|
|
466
|
+
assert.strictEqual(res.status, 403);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = { generateSaasTests };
|
package/src/commons/route.js
CHANGED
|
@@ -31,7 +31,7 @@ module.exports = function route(model, override = {}) {
|
|
|
31
31
|
.Router({ mergeParams: true })
|
|
32
32
|
.get("/:" + model.pk, (req, res) => {
|
|
33
33
|
let payload = payloadOverride(
|
|
34
|
-
{ ...req.query, ...req.params },
|
|
34
|
+
{ ...(req.query || {}), ...(req.params || {}) },
|
|
35
35
|
req,
|
|
36
36
|
override,
|
|
37
37
|
);
|
|
@@ -52,7 +52,7 @@ module.exports = function route(model, override = {}) {
|
|
|
52
52
|
});
|
|
53
53
|
})
|
|
54
54
|
.post("/:id", (req, res) => {
|
|
55
|
-
let payload = payloadOverride(req.body, req, override);
|
|
55
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
56
56
|
delete payload[model.pk];
|
|
57
57
|
model
|
|
58
58
|
.insert(payload)
|
|
@@ -64,7 +64,7 @@ module.exports = function route(model, override = {}) {
|
|
|
64
64
|
});
|
|
65
65
|
})
|
|
66
66
|
.put("/:id", (req, res) => {
|
|
67
|
-
let payload = payloadOverride(req.body, req, override);
|
|
67
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
68
68
|
payload[model.pk] = req.params.id;
|
|
69
69
|
let validateAccessPayload = payloadOverride({}, req, override);
|
|
70
70
|
validateAccessPayload[model.pk] = req.params.id;
|
|
@@ -89,7 +89,7 @@ module.exports = function route(model, override = {}) {
|
|
|
89
89
|
});
|
|
90
90
|
})
|
|
91
91
|
.patch("/:id", (req, res) => {
|
|
92
|
-
let payload = payloadOverride(req.body, req, override);
|
|
92
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
93
93
|
payload[model.pk] = req.params.id;
|
|
94
94
|
let validateAccessPayload = payloadOverride({}, req, override);
|
|
95
95
|
validateAccessPayload[model.pk] = req.params.id;
|
|
@@ -114,7 +114,7 @@ module.exports = function route(model, override = {}) {
|
|
|
114
114
|
});
|
|
115
115
|
})
|
|
116
116
|
.delete("/:id", (req, res) => {
|
|
117
|
-
let payload = payloadOverride(req.body, req, override);
|
|
117
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
118
118
|
payload[model.pk] = req.params.id;
|
|
119
119
|
let validateAccessPayload = payloadOverride({}, req, override);
|
|
120
120
|
validateAccessPayload[model.pk] = req.params.id;
|
|
@@ -140,7 +140,7 @@ module.exports = function route(model, override = {}) {
|
|
|
140
140
|
})
|
|
141
141
|
.get("/", (req, res) => {
|
|
142
142
|
let payload = payloadOverride(
|
|
143
|
-
{ ...req.query, ...req.params },
|
|
143
|
+
{ ...(req.query || {}), ...(req.params || {}) },
|
|
144
144
|
req,
|
|
145
145
|
override,
|
|
146
146
|
);
|
|
File without changes
|
|
File without changes
|
/package/demo/migrations/{20260509170349_create_tables.sql → 20260510092159_create_tables.sql}
RENAMED
|
File without changes
|