@porulle/core 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +26 -7
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -5
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -0
- package/dist/config/types.d.ts +26 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout-completion.d.ts.map +1 -1
- package/dist/hooks/checkout-completion.js +2 -2
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +17 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/staff.js +206 -0
- package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
- package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/analytics.js +24 -0
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +39 -2
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +17 -1
- package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +97 -21
- package/dist/interfaces/rest/routes/documents.d.ts +10 -0
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/documents.js +40 -0
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +13 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +111 -1
- package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/pricing.js +36 -1
- package/dist/interfaces/rest/routes/settings.d.ts +5 -0
- package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/settings.js +50 -0
- package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
- package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/shipping.js +68 -0
- package/dist/interfaces/rest/routes/tax.d.ts +5 -0
- package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/tax.js +68 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/analytics.js +33 -0
- package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/carts.js +38 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +48 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/checkout.js +4 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
- package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/documents.js +65 -0
- package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +234 -1
- package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/pricing.js +70 -0
- package/dist/interfaces/rest/schemas/responses.d.ts +190 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
- package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/settings.js +79 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/shipping.js +146 -0
- package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
- package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/tax.js +132 -0
- package/dist/kernel/database/schema.d.ts +4 -0
- package/dist/kernel/database/schema.d.ts.map +1 -1
- package/dist/kernel/database/schema.js +8 -0
- package/dist/kernel/database/scoped-db.d.ts +2 -1
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +62 -1
- package/dist/kernel/hooks/types.d.ts +1 -1
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +26 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +17 -2
- package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
- package/dist/modules/analytics/drizzle-adapter.js +16 -3
- package/dist/modules/analytics/module.d.ts.map +1 -1
- package/dist/modules/analytics/module.js +6 -0
- package/dist/modules/analytics/reports.d.ts +58 -0
- package/dist/modules/analytics/reports.d.ts.map +1 -0
- package/dist/modules/analytics/reports.js +249 -0
- package/dist/modules/analytics/service.d.ts +10 -0
- package/dist/modules/analytics/service.d.ts.map +1 -1
- package/dist/modules/analytics/service.js +14 -0
- package/dist/modules/cart/repository/index.d.ts +7 -0
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +25 -1
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +2 -0
- package/dist/modules/cart/schemas.d.ts +1 -0
- package/dist/modules/cart/schemas.d.ts.map +1 -1
- package/dist/modules/cart/schemas.js +1 -0
- package/dist/modules/cart/service.d.ts +32 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +61 -2
- package/dist/modules/catalog/entity-service.d.ts +21 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +166 -5
- package/dist/modules/catalog/schema.d.ts +34 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +5 -0
- package/dist/modules/catalog/schemas.d.ts +3 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +3 -0
- package/dist/modules/catalog/service.d.ts +36 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +6 -0
- package/dist/modules/documents/module.d.ts +7 -0
- package/dist/modules/documents/module.d.ts.map +1 -0
- package/dist/modules/documents/module.js +12 -0
- package/dist/modules/documents/pdf.d.ts +25 -0
- package/dist/modules/documents/pdf.d.ts.map +1 -0
- package/dist/modules/documents/pdf.js +80 -0
- package/dist/modules/documents/render.d.ts +32 -0
- package/dist/modules/documents/render.d.ts.map +1 -0
- package/dist/modules/documents/render.js +190 -0
- package/dist/modules/documents/repository/index.d.ts +25 -0
- package/dist/modules/documents/repository/index.d.ts.map +1 -0
- package/dist/modules/documents/repository/index.js +47 -0
- package/dist/modules/documents/schema.d.ts +214 -0
- package/dist/modules/documents/schema.d.ts.map +1 -0
- package/dist/modules/documents/schema.js +42 -0
- package/dist/modules/documents/service.d.ts +51 -0
- package/dist/modules/documents/service.d.ts.map +1 -0
- package/dist/modules/documents/service.js +119 -0
- package/dist/modules/fulfillment/service.d.ts +18 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +85 -1
- package/dist/modules/orders/repository/index.d.ts +18 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +86 -1
- package/dist/modules/orders/schema.d.ts +391 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +56 -1
- package/dist/modules/orders/service.d.ts +102 -3
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +653 -29
- package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
- package/dist/modules/orders/stale-order-cleanup.js +13 -1
- package/dist/modules/pricing/repository/index.d.ts +5 -0
- package/dist/modules/pricing/repository/index.d.ts.map +1 -1
- package/dist/modules/pricing/repository/index.js +19 -0
- package/dist/modules/pricing/service.d.ts +18 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +43 -5
- package/dist/modules/settings/module.d.ts +6 -0
- package/dist/modules/settings/module.d.ts.map +1 -0
- package/dist/modules/settings/module.js +11 -0
- package/dist/modules/settings/repository/index.d.ts +16 -0
- package/dist/modules/settings/repository/index.d.ts.map +1 -0
- package/dist/modules/settings/repository/index.js +41 -0
- package/dist/modules/settings/schema.d.ts +121 -0
- package/dist/modules/settings/schema.d.ts.map +1 -0
- package/dist/modules/settings/schema.js +24 -0
- package/dist/modules/settings/service.d.ts +26 -0
- package/dist/modules/settings/service.d.ts.map +1 -0
- package/dist/modules/settings/service.js +51 -0
- package/dist/modules/shipping/calculator.d.ts +5 -0
- package/dist/modules/shipping/calculator.d.ts.map +1 -1
- package/dist/modules/shipping/calculator.js +10 -0
- package/dist/modules/shipping/module.d.ts +5 -1
- package/dist/modules/shipping/module.d.ts.map +1 -1
- package/dist/modules/shipping/module.js +4 -1
- package/dist/modules/shipping/repository/index.d.ts +27 -2
- package/dist/modules/shipping/repository/index.d.ts.map +1 -1
- package/dist/modules/shipping/repository/index.js +105 -1
- package/dist/modules/shipping/schema.d.ts +419 -0
- package/dist/modules/shipping/schema.d.ts.map +1 -0
- package/dist/modules/shipping/schema.js +56 -0
- package/dist/modules/shipping/service.d.ts +54 -0
- package/dist/modules/shipping/service.d.ts.map +1 -1
- package/dist/modules/shipping/service.js +161 -2
- package/dist/modules/tax/adapter.d.ts +13 -0
- package/dist/modules/tax/adapter.d.ts.map +1 -1
- package/dist/modules/tax/module.d.ts +5 -1
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +4 -1
- package/dist/modules/tax/repository/index.d.ts +38 -2
- package/dist/modules/tax/repository/index.d.ts.map +1 -1
- package/dist/modules/tax/repository/index.js +138 -1
- package/dist/modules/tax/schema.d.ts +355 -0
- package/dist/modules/tax/schema.d.ts.map +1 -0
- package/dist/modules/tax/schema.js +55 -0
- package/dist/modules/tax/service.d.ts +60 -1
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +227 -2
- package/dist/runtime/kernel-modules.d.ts +15 -2
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/kernel-modules.js +4 -0
- package/dist/runtime/kernel-types.d.ts +5 -1
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel-types.js +2 -0
- package/dist/runtime/server.js +1 -1
- package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
- package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
- package/dist/test-utils/create-plugin-test-app.js +15 -3
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +2 -0
- package/dist/test-utils/order-test-helpers.d.ts +12 -0
- package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
- package/dist/test-utils/order-test-helpers.js +18 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +7 -0
- package/dist/test-utils/test-actors.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/package.json +3 -3
- package/src/auth/middleware.ts +26 -7
- package/src/auth/setup.ts +11 -4
- package/src/config/defaults.ts +7 -0
- package/src/config/types.ts +26 -1
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout-completion.ts +22 -16
- package/src/hooks/checkout.ts +36 -11
- package/src/index.ts +1 -0
- package/src/interfaces/rest/index.ts +12 -0
- package/src/interfaces/rest/routes/admin/staff.ts +260 -0
- package/src/interfaces/rest/routes/analytics.ts +33 -0
- package/src/interfaces/rest/routes/carts.ts +52 -2
- package/src/interfaces/rest/routes/catalog.ts +34 -0
- package/src/interfaces/rest/routes/checkout.ts +114 -27
- package/src/interfaces/rest/routes/documents.ts +61 -0
- package/src/interfaces/rest/routes/inventory.ts +14 -0
- package/src/interfaces/rest/routes/orders.ts +148 -1
- package/src/interfaces/rest/routes/pricing.ts +44 -1
- package/src/interfaces/rest/routes/settings.ts +61 -0
- package/src/interfaces/rest/routes/shipping.ts +92 -0
- package/src/interfaces/rest/routes/tax.ts +90 -0
- package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
- package/src/interfaces/rest/schemas/analytics.ts +39 -0
- package/src/interfaces/rest/schemas/carts.ts +40 -0
- package/src/interfaces/rest/schemas/catalog.ts +53 -0
- package/src/interfaces/rest/schemas/checkout.ts +4 -0
- package/src/interfaces/rest/schemas/documents.ts +72 -0
- package/src/interfaces/rest/schemas/orders.ts +258 -1
- package/src/interfaces/rest/schemas/pricing.ts +75 -0
- package/src/interfaces/rest/schemas/settings.ts +93 -0
- package/src/interfaces/rest/schemas/shipping.ts +164 -0
- package/src/interfaces/rest/schemas/tax.ts +148 -0
- package/src/kernel/database/schema.ts +12 -0
- package/src/kernel/database/scoped-db.ts +70 -1
- package/src/kernel/hooks/types.ts +1 -0
- package/src/kernel/plugin/manifest.ts +41 -2
- package/src/modules/analytics/drizzle-adapter.ts +19 -3
- package/src/modules/analytics/module.ts +10 -0
- package/src/modules/analytics/reports.ts +311 -0
- package/src/modules/analytics/service.ts +23 -0
- package/src/modules/cart/repository/index.ts +30 -1
- package/src/modules/cart/schema.ts +2 -0
- package/src/modules/cart/schemas.ts +1 -0
- package/src/modules/cart/service.ts +104 -2
- package/src/modules/catalog/entity-service.ts +196 -4
- package/src/modules/catalog/schema.ts +5 -0
- package/src/modules/catalog/schemas.ts +3 -0
- package/src/modules/catalog/service.ts +30 -0
- package/src/modules/customers/service.ts +6 -0
- package/src/modules/documents/module.ts +19 -0
- package/src/modules/documents/pdf.ts +101 -0
- package/src/modules/documents/render.ts +231 -0
- package/src/modules/documents/repository/index.ts +68 -0
- package/src/modules/documents/schema.ts +44 -0
- package/src/modules/documents/service.ts +176 -0
- package/src/modules/fulfillment/service.ts +139 -1
- package/src/modules/orders/repository/index.ts +127 -1
- package/src/modules/orders/schema.ts +58 -1
- package/src/modules/orders/service.ts +1041 -65
- package/src/modules/orders/stale-order-cleanup.ts +18 -4
- package/src/modules/pricing/repository/index.ts +34 -0
- package/src/modules/pricing/service.ts +76 -6
- package/src/modules/settings/module.ts +18 -0
- package/src/modules/settings/repository/index.ts +59 -0
- package/src/modules/settings/schema.ts +25 -0
- package/src/modules/settings/service.ts +82 -0
- package/src/modules/shipping/calculator.ts +27 -0
- package/src/modules/shipping/module.ts +6 -2
- package/src/modules/shipping/repository/index.ts +136 -2
- package/src/modules/shipping/schema.ts +58 -0
- package/src/modules/shipping/service.ts +260 -1
- package/src/modules/tax/adapter.ts +9 -0
- package/src/modules/tax/module.ts +6 -2
- package/src/modules/tax/repository/index.ts +169 -2
- package/src/modules/tax/schema.ts +57 -0
- package/src/modules/tax/service.ts +313 -2
- package/src/runtime/kernel-modules.ts +4 -0
- package/src/runtime/kernel-types.ts +6 -0
- package/src/runtime/server.ts +1 -1
- package/src/test-utils/create-plugin-test-app.ts +17 -4
- package/src/test-utils/create-test-config.ts +2 -0
- package/src/test-utils/order-test-helpers.ts +26 -0
- package/src/test-utils/rest-api-test-utils.ts +7 -0
- package/src/test-utils/test-actors.ts +1 -1
- package/src/testing.ts +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { eq, and } from "drizzle-orm";
|
|
3
|
+
import { member, user, invitation } from "../../../../auth/auth-schema.js";
|
|
4
|
+
import { resolveOrgId } from "../../../../auth/org.js";
|
|
5
|
+
import { makeId } from "../../../../utils/id.js";
|
|
6
|
+
import { listStaffRoute, createStaffRoute, inviteStaffRoute, listStaffInvitationsRoute, listStaffRolesRoute, updateStaffRoleRoute, revokeStaffRoute, } from "../../schemas/admin-staff.js";
|
|
7
|
+
import { requirePerm } from "../../utils.js";
|
|
8
|
+
/**
|
|
9
|
+
* Admin staff / RBAC surface (issue #46).
|
|
10
|
+
*
|
|
11
|
+
* Surfaces the Better Auth `member` table as first-class admin REST:
|
|
12
|
+
* list staff, add an existing user with a role, invite by email, change
|
|
13
|
+
* role, revoke — plus the role → permission mapping roles resolve to.
|
|
14
|
+
*/
|
|
15
|
+
export function adminStaffRoutes(kernel) {
|
|
16
|
+
const router = new OpenAPIHono();
|
|
17
|
+
const db = kernel.database.db;
|
|
18
|
+
const config = kernel.config;
|
|
19
|
+
router.use("/staff", requirePerm("staff:manage"));
|
|
20
|
+
router.use("/staff/:id", requirePerm("staff:manage"));
|
|
21
|
+
router.use("/staff/invitations", requirePerm("staff:manage"));
|
|
22
|
+
router.use("/staff/roles", requirePerm("staff:manage"));
|
|
23
|
+
function validRoles() {
|
|
24
|
+
return new Set([
|
|
25
|
+
...Object.keys(config.auth?.roles ?? {}),
|
|
26
|
+
"owner",
|
|
27
|
+
"admin",
|
|
28
|
+
"member",
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
function invalidRole(c, role) {
|
|
32
|
+
return c.json({
|
|
33
|
+
error: {
|
|
34
|
+
code: "VALIDATION_FAILED",
|
|
35
|
+
message: `Unknown role "${role}". Valid roles: ${[...validRoles()].join(", ")}.`,
|
|
36
|
+
},
|
|
37
|
+
}, 422);
|
|
38
|
+
}
|
|
39
|
+
const BUILTIN_ROLE_RANK = {
|
|
40
|
+
owner: 3,
|
|
41
|
+
admin: 2,
|
|
42
|
+
};
|
|
43
|
+
const CUSTOM_ROLE_RANK = 1;
|
|
44
|
+
function roleRank(role) {
|
|
45
|
+
return BUILTIN_ROLE_RANK[role] ?? CUSTOM_ROLE_RANK;
|
|
46
|
+
}
|
|
47
|
+
function canGrantRole(actorRole, targetRole) {
|
|
48
|
+
return roleRank(actorRole) >= roleRank(targetRole);
|
|
49
|
+
}
|
|
50
|
+
function insufficientPrivilege(c, targetRole) {
|
|
51
|
+
return c.json({
|
|
52
|
+
error: {
|
|
53
|
+
code: "FORBIDDEN",
|
|
54
|
+
message: `Cannot assign role "${targetRole}": your role does not have sufficient privilege.`,
|
|
55
|
+
},
|
|
56
|
+
}, 403);
|
|
57
|
+
}
|
|
58
|
+
async function countOwners(orgId) {
|
|
59
|
+
const rows = await db
|
|
60
|
+
.select({ id: member.id })
|
|
61
|
+
.from(member)
|
|
62
|
+
.where(and(eq(member.organizationId, orgId), eq(member.role, "owner")));
|
|
63
|
+
return rows.length;
|
|
64
|
+
}
|
|
65
|
+
router.openapi(listStaffRoute, async (c) => {
|
|
66
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
67
|
+
const rows = await db
|
|
68
|
+
.select({
|
|
69
|
+
id: member.id,
|
|
70
|
+
userId: member.userId,
|
|
71
|
+
role: member.role,
|
|
72
|
+
createdAt: member.createdAt,
|
|
73
|
+
email: user.email,
|
|
74
|
+
name: user.name,
|
|
75
|
+
})
|
|
76
|
+
.from(member)
|
|
77
|
+
.leftJoin(user, eq(member.userId, user.id))
|
|
78
|
+
.where(eq(member.organizationId, orgId));
|
|
79
|
+
return c.json({ data: rows });
|
|
80
|
+
});
|
|
81
|
+
// @ts-expect-error -- openapi handler union return type
|
|
82
|
+
router.openapi(createStaffRoute, async (c) => {
|
|
83
|
+
const body = c.req.valid("json");
|
|
84
|
+
const actor = c.get("actor");
|
|
85
|
+
const orgId = resolveOrgId(actor);
|
|
86
|
+
if (!validRoles().has(body.role))
|
|
87
|
+
return invalidRole(c, body.role);
|
|
88
|
+
if (!canGrantRole(actor.role, body.role))
|
|
89
|
+
return insufficientPrivilege(c, body.role);
|
|
90
|
+
const users = await db.select().from(user).where(eq(user.id, body.userId));
|
|
91
|
+
if (users.length === 0) {
|
|
92
|
+
return c.json({ error: { code: "NOT_FOUND", message: "User not found." } }, 404);
|
|
93
|
+
}
|
|
94
|
+
const existing = await db
|
|
95
|
+
.select({ id: member.id })
|
|
96
|
+
.from(member)
|
|
97
|
+
.where(and(eq(member.organizationId, orgId), eq(member.userId, body.userId)));
|
|
98
|
+
if (existing.length > 0) {
|
|
99
|
+
return c.json({ error: { code: "VALIDATION_FAILED", message: "User is already a member of this organization." } }, 422);
|
|
100
|
+
}
|
|
101
|
+
const rows = await db
|
|
102
|
+
.insert(member)
|
|
103
|
+
.values({
|
|
104
|
+
id: makeId(),
|
|
105
|
+
organizationId: orgId,
|
|
106
|
+
userId: body.userId,
|
|
107
|
+
role: body.role,
|
|
108
|
+
createdAt: new Date(),
|
|
109
|
+
})
|
|
110
|
+
.returning();
|
|
111
|
+
return c.json({ data: rows[0] }, 201);
|
|
112
|
+
});
|
|
113
|
+
// @ts-expect-error -- openapi handler union return type
|
|
114
|
+
router.openapi(inviteStaffRoute, async (c) => {
|
|
115
|
+
const body = c.req.valid("json");
|
|
116
|
+
const actor = c.get("actor");
|
|
117
|
+
const orgId = resolveOrgId(actor);
|
|
118
|
+
if (!validRoles().has(body.role))
|
|
119
|
+
return invalidRole(c, body.role);
|
|
120
|
+
const rows = await db
|
|
121
|
+
.insert(invitation)
|
|
122
|
+
.values({
|
|
123
|
+
id: makeId(),
|
|
124
|
+
organizationId: orgId,
|
|
125
|
+
email: body.email,
|
|
126
|
+
role: body.role,
|
|
127
|
+
status: "pending",
|
|
128
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
129
|
+
inviterId: actor.userId,
|
|
130
|
+
})
|
|
131
|
+
.returning();
|
|
132
|
+
return c.json({ data: rows[0] }, 201);
|
|
133
|
+
});
|
|
134
|
+
router.openapi(listStaffInvitationsRoute, async (c) => {
|
|
135
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
136
|
+
const rows = await db
|
|
137
|
+
.select()
|
|
138
|
+
.from(invitation)
|
|
139
|
+
.where(and(eq(invitation.organizationId, orgId), eq(invitation.status, "pending")));
|
|
140
|
+
return c.json({ data: rows });
|
|
141
|
+
});
|
|
142
|
+
router.openapi(listStaffRolesRoute, async (c) => {
|
|
143
|
+
const roles = config.auth?.roles ?? {};
|
|
144
|
+
const data = Object.entries(roles).map(([role, def]) => ({
|
|
145
|
+
role,
|
|
146
|
+
permissions: def.permissions,
|
|
147
|
+
}));
|
|
148
|
+
return c.json({ data });
|
|
149
|
+
});
|
|
150
|
+
// @ts-expect-error -- openapi handler union return type
|
|
151
|
+
router.openapi(updateStaffRoleRoute, async (c) => {
|
|
152
|
+
const body = c.req.valid("json");
|
|
153
|
+
const actor = c.get("actor");
|
|
154
|
+
const orgId = resolveOrgId(actor);
|
|
155
|
+
const id = c.req.param("id");
|
|
156
|
+
if (!validRoles().has(body.role))
|
|
157
|
+
return invalidRole(c, body.role);
|
|
158
|
+
if (!canGrantRole(actor.role, body.role))
|
|
159
|
+
return insufficientPrivilege(c, body.role);
|
|
160
|
+
const rows = await db
|
|
161
|
+
.select()
|
|
162
|
+
.from(member)
|
|
163
|
+
.where(and(eq(member.organizationId, orgId), eq(member.id, id)));
|
|
164
|
+
const target = rows[0];
|
|
165
|
+
if (!target) {
|
|
166
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
|
|
167
|
+
}
|
|
168
|
+
// SEC-18/R-02: the actor must also outrank (or equal) the target's CURRENT
|
|
169
|
+
// role, else an admin could demote an owner they do not outrank.
|
|
170
|
+
if (!canGrantRole(actor.role, target.role))
|
|
171
|
+
return insufficientPrivilege(c, target.role);
|
|
172
|
+
if (target.role === "owner" && body.role !== "owner" && (await countOwners(orgId)) <= 1) {
|
|
173
|
+
return c.json({ error: { code: "VALIDATION_FAILED", message: "Cannot demote the organization's last owner." } }, 422);
|
|
174
|
+
}
|
|
175
|
+
const updated = await db
|
|
176
|
+
.update(member)
|
|
177
|
+
.set({ role: body.role })
|
|
178
|
+
.where(eq(member.id, id))
|
|
179
|
+
.returning();
|
|
180
|
+
return c.json({ data: updated[0] });
|
|
181
|
+
});
|
|
182
|
+
// @ts-expect-error -- openapi handler union return type
|
|
183
|
+
router.openapi(revokeStaffRoute, async (c) => {
|
|
184
|
+
const actor = c.get("actor");
|
|
185
|
+
const orgId = resolveOrgId(actor);
|
|
186
|
+
const id = c.req.param("id");
|
|
187
|
+
const rows = await db
|
|
188
|
+
.select()
|
|
189
|
+
.from(member)
|
|
190
|
+
.where(and(eq(member.organizationId, orgId), eq(member.id, id)));
|
|
191
|
+
const target = rows[0];
|
|
192
|
+
if (!target) {
|
|
193
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
|
|
194
|
+
}
|
|
195
|
+
// SEC-18/R-02: the actor must outrank (or equal) the target's current role
|
|
196
|
+
// to revoke it — an admin cannot revoke an owner.
|
|
197
|
+
if (!canGrantRole(actor.role, target.role))
|
|
198
|
+
return insufficientPrivilege(c, target.role);
|
|
199
|
+
if (target.role === "owner" && (await countOwners(orgId)) <= 1) {
|
|
200
|
+
return c.json({ error: { code: "VALIDATION_FAILED", message: "Cannot revoke the organization's last owner." } }, 422);
|
|
201
|
+
}
|
|
202
|
+
await db.delete(member).where(eq(member.id, id));
|
|
203
|
+
return c.json({ data: { deleted: true } });
|
|
204
|
+
});
|
|
205
|
+
return router;
|
|
206
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
|
+
import { type AppEnv } from "../utils.js";
|
|
4
|
+
export declare function analyticsRoutes(kernel: Kernel): OpenAPIHono<AppEnv, {}, "/">;
|
|
5
|
+
//# sourceMappingURL=analytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAE7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gCA2B7C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { getReportRoute, listReportsRoute } from "../schemas/analytics.js";
|
|
3
|
+
import { mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
|
|
4
|
+
export function analyticsRoutes(kernel) {
|
|
5
|
+
const router = new OpenAPIHono();
|
|
6
|
+
router.use("/reports", requirePerm("analytics:read"));
|
|
7
|
+
router.use("/reports/:name", requirePerm("analytics:read"));
|
|
8
|
+
router.openapi(listReportsRoute, (c) => {
|
|
9
|
+
return c.json({ data: kernel.services.analytics.listReports() });
|
|
10
|
+
});
|
|
11
|
+
// @ts-expect-error -- openapi handler union return type
|
|
12
|
+
router.openapi(getReportRoute, async (c) => {
|
|
13
|
+
const { date, from, to } = c.req.valid("query");
|
|
14
|
+
const result = await kernel.services.analytics.getReport(c.req.param("name"), {
|
|
15
|
+
...(date !== undefined ? { date } : {}),
|
|
16
|
+
...(from !== undefined ? { from } : {}),
|
|
17
|
+
...(to !== undefined ? { to } : {}),
|
|
18
|
+
}, c.get("actor"));
|
|
19
|
+
if (!result.ok)
|
|
20
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
21
|
+
return c.json({ data: result.value });
|
|
22
|
+
});
|
|
23
|
+
return router;
|
|
24
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"carts.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/carts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"carts.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/carts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,EAAyD,MAAM,aAAa,CAAC;AAEjG,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,gCA+IxC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
-
import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute } from "../schemas/carts.js";
|
|
3
|
-
import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
2
|
+
import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute, listCartsRoute, recoverCartRoute } from "../schemas/carts.js";
|
|
3
|
+
import { mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
|
|
4
4
|
export function cartRoutes(kernel) {
|
|
5
5
|
const router = new OpenAPIHono();
|
|
6
6
|
// @ts-expect-error -- openapi() enforces strict response typing but our handler
|
|
@@ -14,6 +14,43 @@ export function cartRoutes(kernel) {
|
|
|
14
14
|
return c.json({ data: result.value }, 201);
|
|
15
15
|
});
|
|
16
16
|
// @ts-expect-error -- openapi handler union return type
|
|
17
|
+
router.openapi(listCartsRoute, async (c) => {
|
|
18
|
+
const actor = c.get("actor");
|
|
19
|
+
const pagination = parsePagination(c.req.query());
|
|
20
|
+
const status = c.req.query("status");
|
|
21
|
+
const olderThanRaw = c.req.query("olderThan");
|
|
22
|
+
const hasCustomerRaw = c.req.query("hasCustomer");
|
|
23
|
+
let olderThan;
|
|
24
|
+
if (olderThanRaw) {
|
|
25
|
+
const d = new Date(olderThanRaw);
|
|
26
|
+
if (Number.isNaN(d.getTime())) {
|
|
27
|
+
return c.json({ error: { code: "VALIDATION_FAILED", message: "olderThan must be an ISO timestamp." } }, 422);
|
|
28
|
+
}
|
|
29
|
+
olderThan = d;
|
|
30
|
+
}
|
|
31
|
+
const result = await kernel.services.cart.list({
|
|
32
|
+
page: pagination.page,
|
|
33
|
+
limit: pagination.limit,
|
|
34
|
+
...(status !== undefined ? { status } : {}),
|
|
35
|
+
...(olderThan !== undefined ? { olderThan } : {}),
|
|
36
|
+
...(hasCustomerRaw === "true" ? { hasCustomer: true } : {}),
|
|
37
|
+
...(hasCustomerRaw === "false" ? { hasCustomer: false } : {}),
|
|
38
|
+
}, actor);
|
|
39
|
+
if (!result.ok)
|
|
40
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
41
|
+
return c.json({
|
|
42
|
+
data: result.value.items,
|
|
43
|
+
meta: { pagination: result.value.pagination },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
// @ts-expect-error -- openapi handler union return type
|
|
47
|
+
router.openapi(recoverCartRoute, async (c) => {
|
|
48
|
+
const result = await kernel.services.cart.recover(c.req.param("id"), c.get("actor"));
|
|
49
|
+
if (!result.ok)
|
|
50
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
51
|
+
return c.json({ data: result.value });
|
|
52
|
+
});
|
|
53
|
+
// @ts-expect-error -- openapi handler union return type
|
|
17
54
|
router.openapi(getCartRoute, async (c) => {
|
|
18
55
|
const actor = c.get("actor");
|
|
19
56
|
// Guest carts are gated by the cart secret; clients pass it as either
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAgDzD,OAAO,EACL,KAAK,MAAM,EAMZ,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,gCAuf3C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
-
import { listEntitiesRoute, getEntityRoute, getEntityAttributesRoute, listCategoriesRoute, listBrandsRoute, deleteEntityRoute, deleteCategoryRoute, deleteBrandRoute, removeEntityFromCategoryRoute, removeEntityFromBrandRoute, publishEntityRoute, archiveEntityRoute, discontinueEntityRoute, addEntityToCategoryRoute, addEntityToBrandRoute, createEntityRoute, updateEntityRoute, setEntityAttributesRoute, createCategoryRoute, archiveCategoryRoute, restoreCategoryRoute, updateCategoryRoute, createBrandRoute, updateBrandRoute, createOptionTypeRoute, createOptionValueRoute, createVariantRoute, generateVariantsRoute, } from "../schemas/catalog.js";
|
|
2
|
+
import { listEntitiesRoute, getEntityRoute, getEntityAttributesRoute, listCategoriesRoute, listBrandsRoute, deleteEntityRoute, deleteCategoryRoute, deleteBrandRoute, removeEntityFromCategoryRoute, removeEntityFromBrandRoute, publishEntityRoute, archiveEntityRoute, discontinueEntityRoute, addEntityToCategoryRoute, addEntityToBrandRoute, createEntityRoute, updateEntityRoute, setEntityAttributesRoute, createCategoryRoute, archiveCategoryRoute, restoreCategoryRoute, updateCategoryRoute, createBrandRoute, updateBrandRoute, createOptionTypeRoute, createOptionValueRoute, createVariantRoute, generateVariantsRoute, quickVariantRoute, bulkVariantsRoute, } from "../schemas/catalog.js";
|
|
3
3
|
import { mapErrorToResponse, mapErrorToStatus, parseInclude, parsePagination, parseSort, } from "../utils.js";
|
|
4
4
|
export function catalogRoutes(kernel) {
|
|
5
5
|
const router = new OpenAPIHono();
|
|
@@ -273,6 +273,22 @@ export function catalogRoutes(kernel) {
|
|
|
273
273
|
return c.json({ data: result.value }, 201);
|
|
274
274
|
});
|
|
275
275
|
// @ts-expect-error -- openapi handler union return type
|
|
276
|
+
router.openapi(quickVariantRoute, async (c) => {
|
|
277
|
+
const body = c.req.valid("json");
|
|
278
|
+
const result = await kernel.services.catalog.quickCreateVariant(c.req.param("id"), body, c.get("actor"));
|
|
279
|
+
if (!result.ok)
|
|
280
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
281
|
+
return c.json({ data: result.value }, result.value.created ? 201 : 200);
|
|
282
|
+
});
|
|
283
|
+
// @ts-expect-error -- openapi handler union return type
|
|
284
|
+
router.openapi(bulkVariantsRoute, async (c) => {
|
|
285
|
+
const body = c.req.valid("json");
|
|
286
|
+
const result = await kernel.services.catalog.bulkCreateVariants(c.req.param("id"), body, c.get("actor"));
|
|
287
|
+
if (!result.ok)
|
|
288
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
289
|
+
return c.json({ data: result.value }, 201);
|
|
290
|
+
});
|
|
291
|
+
// @ts-expect-error -- openapi handler union return type
|
|
276
292
|
router.openapi(generateVariantsRoute, async (c) => {
|
|
277
293
|
const body = c.req.valid("json");
|
|
278
294
|
const result = await kernel.services.catalog.generateVariants(c.req.param("id"), body, c.get("actor"));
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
3
|
import { type AppEnv } from "../utils.js";
|
|
4
|
+
import type { Actor } from "../../../auth/types.js";
|
|
5
|
+
/**
|
|
6
|
+
* SEC-07 — resolve the customer profile a checkout order is attributed to.
|
|
7
|
+
* A self-service actor may ONLY attribute the order to its own customer
|
|
8
|
+
* profile; only actors with org-level `customers:read` (staff/clienteling) may
|
|
9
|
+
* name an arbitrary `customerId`. Returns undefined for guests / unresolved
|
|
10
|
+
* (guest checkout). Exported for unit testing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveCheckoutCustomerUuid(customers: Kernel["services"]["customers"], actor: Actor | null, customerId: string | undefined): Promise<string | undefined>;
|
|
4
13
|
export declare function checkoutRoutes(kernel: Kernel): OpenAPIHono<AppEnv, {}, "/">;
|
|
5
14
|
//# sourceMappingURL=checkout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoBzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAKhF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoBzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAKhF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,EAC1C,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmC7B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCA2S5C"}
|
|
@@ -5,7 +5,52 @@ import { runAfterHooks, runBeforeHooks } from "../../../kernel/hooks/executor.js
|
|
|
5
5
|
import { createHookContext } from "../../../kernel/hooks/create-context.js";
|
|
6
6
|
import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
7
7
|
import { isCommerceError } from "../../../kernel/errors.js";
|
|
8
|
+
import { assertPermission } from "../../../auth/permissions.js";
|
|
8
9
|
import { makeId } from "../../../utils/id.js";
|
|
10
|
+
/**
|
|
11
|
+
* SEC-07 — resolve the customer profile a checkout order is attributed to.
|
|
12
|
+
* A self-service actor may ONLY attribute the order to its own customer
|
|
13
|
+
* profile; only actors with org-level `customers:read` (staff/clienteling) may
|
|
14
|
+
* name an arbitrary `customerId`. Returns undefined for guests / unresolved
|
|
15
|
+
* (guest checkout). Exported for unit testing.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveCheckoutCustomerUuid(customers, actor, customerId) {
|
|
18
|
+
const actorUserId = actor?.userId;
|
|
19
|
+
let canActForOthers = false;
|
|
20
|
+
if (actorUserId) {
|
|
21
|
+
try {
|
|
22
|
+
assertPermission(actor, "customers:read");
|
|
23
|
+
canActForOthers = true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
canActForOthers = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (customerId) {
|
|
30
|
+
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
31
|
+
if (uuidRe.test(customerId)) {
|
|
32
|
+
const byId = await customers.getById(customerId, actor);
|
|
33
|
+
// Honor a profile UUID only if it is the actor's own, or the actor may
|
|
34
|
+
// act for other customers.
|
|
35
|
+
if (byId.ok && (canActForOthers || byId.value.userId === actorUserId)) {
|
|
36
|
+
return byId.value.id;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (canActForOthers) {
|
|
40
|
+
// Staff may resolve/create a customer by a supplied user_id.
|
|
41
|
+
const byUser = await customers.getByUserId(customerId, actor);
|
|
42
|
+
if (byUser.ok)
|
|
43
|
+
return byUser.value.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Self-service default: the authenticated actor's own customer profile.
|
|
47
|
+
if (actorUserId && !canActForOthers) {
|
|
48
|
+
const own = await customers.getByUserId(actorUserId, actor);
|
|
49
|
+
if (own.ok)
|
|
50
|
+
return own.value.id;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
9
54
|
export function checkoutRoutes(kernel) {
|
|
10
55
|
const router = new OpenAPIHono();
|
|
11
56
|
// @ts-expect-error -- openapi() enforces strict response typing but our handler
|
|
@@ -14,6 +59,40 @@ export function checkoutRoutes(kernel) {
|
|
|
14
59
|
router.openapi(checkoutRoute, async (c) => {
|
|
15
60
|
const body = c.req.valid("json");
|
|
16
61
|
const actor = c.get("actor");
|
|
62
|
+
// Idempotent replay: a re-submitted checkout (offline POS queue, network
|
|
63
|
+
// retry) returns the already-created order BEFORE the pipeline runs — no
|
|
64
|
+
// double payment authorization, no duplicate order.
|
|
65
|
+
if (body.idempotencyKey) {
|
|
66
|
+
const replay = await kernel.services.orders.getByIdempotencyKey(body.idempotencyKey, actor);
|
|
67
|
+
if (replay.ok && replay.value) {
|
|
68
|
+
// IDOR guard: an idempotency key must only replay the requester's OWN
|
|
69
|
+
// order. Without this, a same-org customer who supplies/guesses another
|
|
70
|
+
// customer's key would receive that customer's order (PII, totals, lines).
|
|
71
|
+
let canActForOthers = false;
|
|
72
|
+
try {
|
|
73
|
+
assertPermission(actor, "customers:read");
|
|
74
|
+
canActForOthers = true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
canActForOthers = false;
|
|
78
|
+
}
|
|
79
|
+
const ownCustomer = actor
|
|
80
|
+
? await resolveCheckoutCustomerUuid(kernel.services.customers, actor, undefined)
|
|
81
|
+
: undefined;
|
|
82
|
+
const replayCustomer = replay.value.customerId ?? null;
|
|
83
|
+
const ownsOrder = (replayCustomer !== null && replayCustomer === ownCustomer) ||
|
|
84
|
+
(replayCustomer === null && !ownCustomer);
|
|
85
|
+
if (canActForOthers || ownsOrder) {
|
|
86
|
+
return c.json({ data: replay.value }, 201);
|
|
87
|
+
}
|
|
88
|
+
return c.json({
|
|
89
|
+
error: {
|
|
90
|
+
code: "IDEMPOTENCY_CONFLICT",
|
|
91
|
+
message: "Idempotency key does not belong to this account.",
|
|
92
|
+
},
|
|
93
|
+
}, 409);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
17
96
|
const checkoutData = {
|
|
18
97
|
checkoutId: makeId(),
|
|
19
98
|
cartId: body.cartId,
|
|
@@ -84,27 +163,14 @@ export function checkoutRoutes(kernel) {
|
|
|
84
163
|
// If Stripe takes 5s, the DB connection pool is not affected.
|
|
85
164
|
context.tx = null;
|
|
86
165
|
const processed = await runBeforeHooks(paymentHooks, validated, "create", context);
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (uuidRe.test(processed.customerId)) {
|
|
92
|
-
// Looks like a profile UUID — try direct lookup (no auto-create)
|
|
93
|
-
const byIdResult = await kernel.services.customers.getById(processed.customerId, actor);
|
|
94
|
-
if (byIdResult.ok) {
|
|
95
|
-
customerUuid = byIdResult.value.id;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (!customerUuid) {
|
|
99
|
-
// Fall back to user_id lookup (auto-creates customer profile if needed)
|
|
100
|
-
const byUserIdResult = await kernel.services.customers.getByUserId(processed.customerId, actor);
|
|
101
|
-
if (byUserIdResult.ok) {
|
|
102
|
-
customerUuid = byUserIdResult.value.id;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// If both lookups fail, we still allow guest checkout (customerUuid remains undefined)
|
|
106
|
-
}
|
|
166
|
+
// SEC-07: resolve the order's customer server-side. A self-service actor
|
|
167
|
+
// can only attribute the order to its own profile; a client-supplied
|
|
168
|
+
// foreign customerId is ignored unless the actor is staff.
|
|
169
|
+
const customerUuid = await resolveCheckoutCustomerUuid(kernel.services.customers, actor, processed.customerId);
|
|
107
170
|
const orderPayload = {
|
|
171
|
+
...(body.idempotencyKey !== undefined
|
|
172
|
+
? { idempotencyKey: body.idempotencyKey }
|
|
173
|
+
: {}),
|
|
108
174
|
currency: processed.currency,
|
|
109
175
|
subtotal: processed.subtotal,
|
|
110
176
|
taxTotal: processed.taxTotal,
|
|
@@ -133,6 +199,11 @@ export function checkoutRoutes(kernel) {
|
|
|
133
199
|
quantity: lineItem.quantity,
|
|
134
200
|
unitPrice: lineItem.resolvedUnitPrice ?? 0,
|
|
135
201
|
totalPrice: lineItem.resolvedTotal ?? 0,
|
|
202
|
+
// Per-line tax from product tax classes (issue #57)
|
|
203
|
+
taxAmount: lineItem.taxAmount ?? 0,
|
|
204
|
+
...(lineItem.discountAmount !== undefined
|
|
205
|
+
? { discountAmount: lineItem.discountAmount }
|
|
206
|
+
: {}),
|
|
136
207
|
};
|
|
137
208
|
return lineItem.variantId !== undefined
|
|
138
209
|
? { ...payload, variantId: lineItem.variantId }
|
|
@@ -142,7 +213,12 @@ export function checkoutRoutes(kernel) {
|
|
|
142
213
|
? { customerId: customerUuid }
|
|
143
214
|
: {}),
|
|
144
215
|
};
|
|
145
|
-
|
|
216
|
+
// Checkout is a trusted, already-server-priced pipeline (resolveCurrentPrices
|
|
217
|
+
// + promotions/tax) and reserves stock in its own after-hooks — so it hands
|
|
218
|
+
// the order primitive precomputed totals rather than re-deriving them.
|
|
219
|
+
const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
|
|
220
|
+
trustedPricing: true,
|
|
221
|
+
});
|
|
146
222
|
if (!order.ok) {
|
|
147
223
|
return c.json(mapErrorToResponse(order.error), mapErrorToStatus(order.error));
|
|
148
224
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
|
+
import { type AppEnv } from "../utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Order document rendering (issue #47). Mounted under /orders. Access
|
|
6
|
+
* control rides on OrderService.getById (orders:read, or orders:read:own
|
|
7
|
+
* for the order's customer), so no extra scope is needed here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function documentRoutes(kernel: Kernel): OpenAPIHono<AppEnv, {}, "/">;
|
|
10
|
+
//# sourceMappingURL=documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAOzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCA6C5C"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { emailInvoiceRoute, invoiceHtmlRoute, invoicePdfRoute, receiptHtmlRoute, } from "../schemas/documents.js";
|
|
3
|
+
import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Order document rendering (issue #47). Mounted under /orders. Access
|
|
6
|
+
* control rides on OrderService.getById (orders:read, or orders:read:own
|
|
7
|
+
* for the order's customer), so no extra scope is needed here.
|
|
8
|
+
*/
|
|
9
|
+
export function documentRoutes(kernel) {
|
|
10
|
+
const router = new OpenAPIHono();
|
|
11
|
+
router.openapi(invoicePdfRoute, async (c) => {
|
|
12
|
+
const result = await kernel.services.documents.renderInvoicePdf(c.req.param("id"), c.get("actor"));
|
|
13
|
+
if (!result.ok)
|
|
14
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
15
|
+
return c.body(result.value.pdf.slice().buffer, 200, {
|
|
16
|
+
"content-type": "application/pdf",
|
|
17
|
+
"content-disposition": `inline; filename="invoice-${result.value.invoiceNumber}.pdf"`,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
router.openapi(invoiceHtmlRoute, async (c) => {
|
|
21
|
+
const result = await kernel.services.documents.renderInvoiceHtml(c.req.param("id"), c.get("actor"));
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
24
|
+
return c.html(result.value.html);
|
|
25
|
+
});
|
|
26
|
+
router.openapi(receiptHtmlRoute, async (c) => {
|
|
27
|
+
const result = await kernel.services.documents.renderReceiptHtml(c.req.param("id"), c.get("actor"));
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
30
|
+
return c.html(result.value.html);
|
|
31
|
+
});
|
|
32
|
+
// @ts-expect-error -- openapi handler union return type
|
|
33
|
+
router.openapi(emailInvoiceRoute, async (c) => {
|
|
34
|
+
const result = await kernel.services.documents.emailInvoice(c.req.param("id"), c.req.valid("json").to, c.get("actor"));
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
37
|
+
return c.json({ data: result.value });
|
|
38
|
+
});
|
|
39
|
+
return router;
|
|
40
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAUzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAE7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAUzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAE7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gCA+F7C"}
|
|
@@ -5,6 +5,13 @@ export function inventoryRoutes(kernel) {
|
|
|
5
5
|
const router = new OpenAPIHono();
|
|
6
6
|
// Inventory levels listing requires inventory:read
|
|
7
7
|
router.use("/levels", requirePerm("inventory:read"));
|
|
8
|
+
// Warehouse config + reservation are staff operations, never public. Gate them
|
|
9
|
+
// at the route (reserve/release are also called internally by checkout with the
|
|
10
|
+
// customer's actor, so the service itself must stay callable — the boundary is
|
|
11
|
+
// the HTTP route). Blocks anonymous (401) and customers lacking the perm (403).
|
|
12
|
+
router.use("/warehouses", requirePerm("inventory:read"));
|
|
13
|
+
router.use("/reserve", requirePerm("inventory:adjust"));
|
|
14
|
+
router.use("/release", requirePerm("inventory:adjust"));
|
|
8
15
|
// @ts-expect-error -- openapi handler union return type
|
|
9
16
|
router.openapi(listInventoryLevelsRoute, async (c) => {
|
|
10
17
|
const actor = c.get("actor");
|
|
@@ -59,6 +66,12 @@ export function inventoryRoutes(kernel) {
|
|
|
59
66
|
router.openapi(createWarehouseRoute, async (c) => {
|
|
60
67
|
const body = c.req.valid("json");
|
|
61
68
|
const actor = c.get("actor");
|
|
69
|
+
// Creating a warehouse (write) needs the higher inventory:adjust perm; the
|
|
70
|
+
// router.use above already requires inventory:read for the /warehouses path.
|
|
71
|
+
const perms = actor?.permissions ?? [];
|
|
72
|
+
if (!(perms.includes("inventory:adjust") || perms.includes("inventory:*") || perms.includes("*:*"))) {
|
|
73
|
+
return c.json({ error: { code: "FORBIDDEN", message: "Permission 'inventory:adjust' is required." } }, 403);
|
|
74
|
+
}
|
|
62
75
|
const result = await kernel.services.inventory.createWarehouse(body, actor);
|
|
63
76
|
if (!result.ok)
|
|
64
77
|
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orders.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/orders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAiE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"orders.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/orders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,KAAK,MAAM,EAAiE,MAAM,aAAa,CAAC;AAIzG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,gCAgRzC"}
|