@open-mercato/core 0.6.6-develop.6133.1.f01540250e → 0.6.6-develop.6140.1.d3fbb77495
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/customer_accounts/api/admin/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +1 -0
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +34 -1
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +34 -1
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +47 -1
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +47 -1
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/directory/api/tenants/route.js +19 -23
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/api/tenants/tenant-cf-filter.js +72 -0
- package/dist/modules/directory/api/tenants/tenant-cf-filter.js.map +7 -0
- package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js +8 -6
- package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js.map +2 -2
- package/dist/modules/integrations/api/[id]/health/route.js +33 -0
- package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +6 -3
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/AGENTS.md +1 -0
- package/src/modules/customer_accounts/api/admin/users-invite.ts +10 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +10 -0
- package/src/modules/customer_accounts/events.ts +1 -0
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +36 -1
- package/src/modules/dashboards/api/layout/route.ts +36 -1
- package/src/modules/dashboards/api/roles/widgets/route.ts +49 -1
- package/src/modules/dashboards/api/users/widgets/route.ts +49 -1
- package/src/modules/directory/api/tenants/route.ts +25 -26
- package/src/modules/directory/api/tenants/tenant-cf-filter.ts +97 -0
- package/src/modules/feature_toggles/components/FeatureToggleDetailsCard.tsx +10 -8
- package/src/modules/integrations/api/[id]/health/route.ts +35 -0
- package/src/modules/notifications/api/unread-count/route.ts +6 -4
package/.turbo/turbo-build.log
CHANGED
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
5
6
|
import { inviteUserSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
6
7
|
import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
7
8
|
import {
|
|
@@ -51,6 +52,14 @@ async function POST(req) {
|
|
|
51
52
|
displayName: parsed.data.displayName || null
|
|
52
53
|
}
|
|
53
54
|
);
|
|
55
|
+
void emitCustomerAccountsEvent("customer_accounts.user.invited", {
|
|
56
|
+
invitationId: invitation.id,
|
|
57
|
+
email: invitation.email,
|
|
58
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
59
|
+
invitedByType: "staff",
|
|
60
|
+
tenantId: auth.tenantId,
|
|
61
|
+
organizationId: auth.orgId
|
|
62
|
+
}).catch(() => void 0);
|
|
54
63
|
return NextResponse.json({
|
|
55
64
|
ok: true,
|
|
56
65
|
invitation: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/admin/users-invite.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata = {}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.invite'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId!, organizationId: auth.orgId! },\n {\n customerEntityId: parsed.data.customerEntityId || null,\n roleIds: parsed.data.roleIds,\n invitedByUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite customer user (admin)',\n description: 'Creates a staff-initiated invitation for a new customer user. The invitedByUserId is set from the staff auth context.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite customer user (admin)',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAGvC,SAAS,wBAAwB;AACjC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAW,CAAC;AAEzB,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3F;AAEA,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,QAAM,YAAY,MAAM,YAAY,mBAAmB,KAAK,KAAK,CAAC,0BAA0B,GAAG,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM,CAAC;AACtJ,MAAI,CAAC,WAAW;AACd,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5F;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AAEA,QAAM,SAAS,iBAAiB,UAAU,IAAI;AAC9C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClI;AAEA,QAAM,4BAA4B,UAAU,QAAQ,2BAA2B;AAE/E,QAAM,EAAE,WAAW,IAAI,MAAM,0BAA0B;AAAA,IACrD,OAAO,KAAK;AAAA,IACZ,EAAE,UAAU,KAAK,UAAW,gBAAgB,KAAK,MAAO;AAAA,IACxD;AAAA,MACE,kBAAkB,OAAO,KAAK,oBAAoB;AAAA,MAClD,SAAS,OAAO,KAAK;AAAA,MACrB,iBAAiB,KAAK;AAAA,MACtB,aAAa,OAAO,KAAK,eAAe;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO,aAAa,KAAK;AAAA,IACvB,IAAI;AAAA,IACJ,YAAY;AAAA,MACV,IAAI,WAAW;AAAA,MACf,OAAO,WAAW;AAAA,MAClB,WAAW,WAAW;AAAA,IACxB;AAAA,EACF,GAAG,EAAE,QAAQ,IAAI,CAAC;AACpB;AAEA,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,YAAY,EAAE,OAAO;AAAA,IACnB,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,IACpB,OAAO,EAAE,OAAO;AAAA,IAChB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,CAAC;AACH,CAAC;AACD,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAExE,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa,EAAE,QAAQ,iBAAiB;AAAA,EACxC,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,cAAc,CAAC;AAAA,EACrF,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,YAAY;AAAA,IACrE,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,YAAY;AAAA,IACrE,EAAE,QAAQ,KAAK,aAAa,4BAA4B,QAAQ,YAAY;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,qBAAqB;AAAA,EAC3F;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,UAAU;AAC7B;",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata = {}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.invite'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId!, organizationId: auth.orgId! },\n {\n customerEntityId: parsed.data.customerEntityId || null,\n roleIds: parsed.data.roleIds,\n invitedByUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n void emitCustomerAccountsEvent('customer_accounts.user.invited', {\n invitationId: invitation.id,\n email: invitation.email,\n customerEntityId: invitation.customerEntityId || null,\n invitedByType: 'staff',\n tenantId: auth.tenantId!,\n organizationId: auth.orgId!,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite customer user (admin)',\n description: 'Creates a staff-initiated invitation for a new customer user. The invitedByUserId is set from the staff auth context.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite customer user (admin)',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getCustomerAuthFromRequest, requireCustomerFeature } from "@open-mercato/core/modules/customer_accounts/lib/customerAuth";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
5
6
|
import { CustomerRole } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
6
7
|
import { inviteUserSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
7
8
|
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
@@ -76,6 +77,14 @@ async function POST(req) {
|
|
|
76
77
|
displayName: parsed.data.displayName || null
|
|
77
78
|
}
|
|
78
79
|
);
|
|
80
|
+
void emitCustomerAccountsEvent("customer_accounts.user.invited", {
|
|
81
|
+
invitationId: invitation.id,
|
|
82
|
+
email: invitation.email,
|
|
83
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
84
|
+
invitedByType: "portal",
|
|
85
|
+
tenantId: auth.tenantId,
|
|
86
|
+
organizationId: auth.orgId
|
|
87
|
+
}).catch(() => void 0);
|
|
79
88
|
return NextResponse.json({
|
|
80
89
|
ok: true,
|
|
81
90
|
invitation: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/portal/users-invite.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, ['portal.users.manage'], customerRbacService)\n } catch (response) {\n return response as NextResponse\n }\n\n if (!auth.customerEntityId) {\n return NextResponse.json({ ok: false, error: 'No company association' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n // Validate all roles are customer_assignable\n const requestedRoleIds = parsed.data.roleIds\n const roles = requestedRoleIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerRole,\n { id: { $in: requestedRoleIds }, tenantId: auth.tenantId, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n : []\n const rolesById = new Map(roles.map((role) => [role.id, role]))\n for (const roleId of requestedRoleIds) {\n const role = rolesById.get(roleId)\n if (!role) {\n return NextResponse.json({ ok: false, error: `Role ${roleId} not found` }, { status: 400 })\n }\n if (!role.customerAssignable) {\n return NextResponse.json({ ok: false, error: `Role \"${role.name}\" cannot be assigned by portal users` }, { status: 403 })\n }\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n {\n customerEntityId: auth.customerEntityId,\n roleIds: parsed.data.roleIds,\n invitedByCustomerUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite a user to the company portal',\n description: 'Creates an invitation for a new user to join the company portal.',\n tags: ['Customer Portal'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions or non-assignable role', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite portal user',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,4BAA4B,8BAA8B;AACnE,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, ['portal.users.manage'], customerRbacService)\n } catch (response) {\n return response as NextResponse\n }\n\n if (!auth.customerEntityId) {\n return NextResponse.json({ ok: false, error: 'No company association' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n // Validate all roles are customer_assignable\n const requestedRoleIds = parsed.data.roleIds\n const roles = requestedRoleIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerRole,\n { id: { $in: requestedRoleIds }, tenantId: auth.tenantId, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n : []\n const rolesById = new Map(roles.map((role) => [role.id, role]))\n for (const roleId of requestedRoleIds) {\n const role = rolesById.get(roleId)\n if (!role) {\n return NextResponse.json({ ok: false, error: `Role ${roleId} not found` }, { status: 400 })\n }\n if (!role.customerAssignable) {\n return NextResponse.json({ ok: false, error: `Role \"${role.name}\" cannot be assigned by portal users` }, { status: 403 })\n }\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n {\n customerEntityId: auth.customerEntityId,\n roleIds: parsed.data.roleIds,\n invitedByCustomerUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n void emitCustomerAccountsEvent('customer_accounts.user.invited', {\n invitationId: invitation.id,\n email: invitation.email,\n customerEntityId: invitation.customerEntityId || null,\n invitedByType: 'portal',\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite a user to the company portal',\n description: 'Creates an invitation for a new user to join the company portal.',\n tags: ['Customer Portal'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions or non-assignable role', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite portal user',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,6 +15,7 @@ const events = [
|
|
|
15
15
|
{ id: "customer_accounts.role.created", label: "Customer Role Created", entity: "role", category: "crud" },
|
|
16
16
|
{ id: "customer_accounts.role.updated", label: "Customer Role Updated", entity: "role", category: "crud", portalBroadcast: true },
|
|
17
17
|
{ id: "customer_accounts.role.deleted", label: "Customer Role Deleted", entity: "role", category: "crud" },
|
|
18
|
+
{ id: "customer_accounts.user.invited", label: "Customer User Invited", entity: "user", category: "lifecycle", clientBroadcast: true },
|
|
18
19
|
{ id: "customer_accounts.invitation.accepted", label: "Customer Invitation Accepted", category: "lifecycle", clientBroadcast: true },
|
|
19
20
|
{ id: "customer_accounts.password_reset.requested", label: "Customer Password Reset Requested", category: "lifecycle" },
|
|
20
21
|
// Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/customer_accounts/events.ts"],
|
|
4
|
-
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA;AAAA,EAEtH,EAAE,IAAI,4CAA4C,OAAO,4BAA4B,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACvJ,EAAE,IAAI,6CAA6C,OAAO,8BAA8B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC/J,EAAE,IAAI,8CAA8C,OAAO,wBAAwB,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC1J,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,4CAA4C,OAAO,yBAAyB,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACpJ,EAAE,IAAI,6CAA6C,OAAO,0BAA0B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAC7J;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,4BAA4B,aAAa;AAItD,IAAO,iBAAQ;",
|
|
4
|
+
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.user.invited', label: 'Customer User Invited', entity: 'user', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACrI,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA;AAAA,EAEtH,EAAE,IAAI,4CAA4C,OAAO,4BAA4B,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACvJ,EAAE,IAAI,6CAA6C,OAAO,8BAA8B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC/J,EAAE,IAAI,8CAA8C,OAAO,wBAAwB,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC1J,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,4CAA4C,OAAO,yBAAyB,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACpJ,EAAE,IAAI,6CAA6C,OAAO,0BAA0B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAC7J;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,4BAA4B,aAAa;AAItD,IAAO,iBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,10 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
5
5
|
import { DashboardLayout } from "@open-mercato/core/modules/dashboards/data/entities";
|
|
6
6
|
import { dashboardLayoutItemPatchSchema } from "@open-mercato/core/modules/dashboards/data/validators";
|
|
7
7
|
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
8
|
+
import {
|
|
9
|
+
runCrudMutationGuardAfterSuccess,
|
|
10
|
+
validateCrudMutationGuard
|
|
11
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
8
12
|
import {
|
|
9
13
|
dashboardsTag,
|
|
10
14
|
dashboardsErrorSchema,
|
|
@@ -12,6 +16,7 @@ import {
|
|
|
12
16
|
dashboardLayoutItemUpdateSchema
|
|
13
17
|
} from "../../openapi.js";
|
|
14
18
|
const DEFAULT_SIZE = "md";
|
|
19
|
+
const RESOURCE_KIND = "dashboards.layout";
|
|
15
20
|
const metadata = {
|
|
16
21
|
PATCH: { requireAuth: true, requireFeatures: ["dashboards.configure"] }
|
|
17
22
|
};
|
|
@@ -33,7 +38,8 @@ async function PATCH(req, ctx) {
|
|
|
33
38
|
if (!parsed.success) {
|
|
34
39
|
return NextResponse.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 });
|
|
35
40
|
}
|
|
36
|
-
const
|
|
41
|
+
const container = await createRequestContainer();
|
|
42
|
+
const { resolve } = container;
|
|
37
43
|
const em = resolve("em");
|
|
38
44
|
const rbac = resolve("rbacService");
|
|
39
45
|
const scope = {
|
|
@@ -45,6 +51,20 @@ async function PATCH(req, ctx) {
|
|
|
45
51
|
if (!acl.isSuperAdmin && !hasFeature(acl.features, "dashboards.configure")) {
|
|
46
52
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
47
53
|
}
|
|
54
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
55
|
+
tenantId: scope.tenantId ?? "",
|
|
56
|
+
organizationId: scope.organizationId,
|
|
57
|
+
userId: scope.userId,
|
|
58
|
+
resourceKind: RESOURCE_KIND,
|
|
59
|
+
resourceId: layoutItemId,
|
|
60
|
+
operation: "update",
|
|
61
|
+
requestMethod: req.method,
|
|
62
|
+
requestHeaders: req.headers,
|
|
63
|
+
mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings }
|
|
64
|
+
});
|
|
65
|
+
if (guardResult && !guardResult.ok) {
|
|
66
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
67
|
+
}
|
|
48
68
|
const layout = await em.findOne(DashboardLayout, {
|
|
49
69
|
userId: scope.userId,
|
|
50
70
|
tenantId: scope.tenantId,
|
|
@@ -65,6 +85,19 @@ async function PATCH(req, ctx) {
|
|
|
65
85
|
settings: parsed.data.settings ?? current.settings
|
|
66
86
|
};
|
|
67
87
|
await em.flush();
|
|
88
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
89
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
90
|
+
tenantId: scope.tenantId ?? "",
|
|
91
|
+
organizationId: scope.organizationId,
|
|
92
|
+
userId: scope.userId,
|
|
93
|
+
resourceKind: RESOURCE_KIND,
|
|
94
|
+
resourceId: layoutItemId,
|
|
95
|
+
operation: "update",
|
|
96
|
+
requestMethod: req.method,
|
|
97
|
+
requestHeaders: req.headers,
|
|
98
|
+
metadata: guardResult.metadata ?? null
|
|
99
|
+
});
|
|
100
|
+
}
|
|
68
101
|
return NextResponse.json({ ok: true });
|
|
69
102
|
}
|
|
70
103
|
const layoutItemParamsSchema = z.object({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/api/layout/%5BitemId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutItemUpdateSchema,\n} from '../../openapi'\n\nconst DEFAULT_SIZE = 'md'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { itemId?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const layoutItemId = ctx.params?.itemId\n if (!layoutItemId) return NextResponse.json({ error: 'Missing layout item id' }, { status: 400 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const parsed = dashboardLayoutItemPatchSchema.safeParse({ ...(body as Record<string, unknown>), id: layoutItemId })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAChC,SAAS,sCAAsC;AAC/C,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutItemUpdateSchema,\n} from '../../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { itemId?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const layoutItemId = ctx.params?.itemId\n if (!layoutItemId) return NextResponse.json({ error: 'Missing layout item id' }, { status: 400 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const parsed = dashboardLayoutItemPatchSchema.safeParse({ ...(body as Record<string, unknown>), id: layoutItemId })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n\n const scope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, 'dashboards.configure')) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: layoutItemId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const layout = await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n if (!layout) {\n return NextResponse.json({ error: 'Layout not found' }, { status: 404 })\n }\n\n const idx = layout.layoutJson.findIndex((item: { id?: string | null }) => item?.id === layoutItemId)\n if (idx === -1) {\n return NextResponse.json({ error: 'Layout item not found' }, { status: 404 })\n }\n\n const current = layout.layoutJson[idx]\n layout.layoutJson[idx] = {\n ...current,\n size: parsed.data.size ?? current.size ?? DEFAULT_SIZE,\n settings: parsed.data.settings ?? current.settings,\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: layoutItemId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutItemParamsSchema = z.object({\n itemId: z.string().uuid(),\n})\n\nconst layoutItemPatchDoc: OpenApiMethodDoc = {\n summary: 'Update a dashboard layout item',\n description: 'Adjusts the size or settings for a single widget within the dashboard layout.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutItemUpdateSchema,\n description: 'Payload containing the new size or settings for the widget.',\n },\n responses: [\n { status: 200, description: 'Layout item updated.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing item id', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n { status: 404, description: 'Item not found', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Update dashboard layout item',\n pathParams: layoutItemParamsSchema,\n methods: {\n PATCH: layoutItemPatchDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,6 +9,10 @@ import { resolveAllowedWidgetIds } from "@open-mercato/core/modules/dashboards/l
|
|
|
9
9
|
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
10
10
|
import { User } from "@open-mercato/core/modules/auth/data/entities";
|
|
11
11
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard
|
|
15
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
12
16
|
import {
|
|
13
17
|
dashboardsTag,
|
|
14
18
|
dashboardsErrorSchema,
|
|
@@ -16,6 +20,7 @@ import {
|
|
|
16
20
|
dashboardLayoutStateSchema
|
|
17
21
|
} from "../openapi.js";
|
|
18
22
|
const DEFAULT_SIZE = "md";
|
|
23
|
+
const RESOURCE_KIND = "dashboards.layout";
|
|
19
24
|
const metadata = {
|
|
20
25
|
GET: { requireAuth: true, requireFeatures: ["dashboards.view"] },
|
|
21
26
|
PUT: { requireAuth: true, requireFeatures: ["dashboards.configure"] }
|
|
@@ -172,7 +177,8 @@ async function PUT(req) {
|
|
|
172
177
|
if (!parsed.success) {
|
|
173
178
|
return NextResponse.json({ error: "Invalid layout payload", issues: parsed.error.issues }, { status: 400 });
|
|
174
179
|
}
|
|
175
|
-
const
|
|
180
|
+
const container = await createRequestContainer();
|
|
181
|
+
const { resolve } = container;
|
|
176
182
|
const em = resolve("em").fork({ clear: true, freshEventManager: true, useContext: true });
|
|
177
183
|
const rbac = resolve("rbacService");
|
|
178
184
|
const scope = {
|
|
@@ -184,6 +190,20 @@ async function PUT(req) {
|
|
|
184
190
|
if (!acl.isSuperAdmin && !hasFeature(acl.features, "dashboards.configure")) {
|
|
185
191
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
186
192
|
}
|
|
193
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
194
|
+
tenantId: scope.tenantId ?? "",
|
|
195
|
+
organizationId: scope.organizationId,
|
|
196
|
+
userId: scope.userId,
|
|
197
|
+
resourceKind: RESOURCE_KIND,
|
|
198
|
+
resourceId: scope.userId,
|
|
199
|
+
operation: "update",
|
|
200
|
+
requestMethod: req.method,
|
|
201
|
+
requestHeaders: req.headers,
|
|
202
|
+
mutationPayload: { items: parsed.data.items }
|
|
203
|
+
});
|
|
204
|
+
if (guardResult && !guardResult.ok) {
|
|
205
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
206
|
+
}
|
|
187
207
|
const widgets = await loadAllWidgets();
|
|
188
208
|
const allowedIds = await resolveAllowedWidgetIds(
|
|
189
209
|
em,
|
|
@@ -223,6 +243,19 @@ async function PUT(req) {
|
|
|
223
243
|
layout.layoutJson = sanitized;
|
|
224
244
|
}
|
|
225
245
|
await em.flush();
|
|
246
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
247
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
248
|
+
tenantId: scope.tenantId ?? "",
|
|
249
|
+
organizationId: scope.organizationId,
|
|
250
|
+
userId: scope.userId,
|
|
251
|
+
resourceKind: RESOURCE_KIND,
|
|
252
|
+
resourceId: scope.userId,
|
|
253
|
+
operation: "update",
|
|
254
|
+
requestMethod: req.method,
|
|
255
|
+
requestHeaders: req.headers,
|
|
256
|
+
metadata: guardResult.metadata ?? null
|
|
257
|
+
});
|
|
258
|
+
}
|
|
226
259
|
return NextResponse.json({ ok: true });
|
|
227
260
|
}
|
|
228
261
|
const layoutGetDoc = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/dashboards/api/layout/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { randomUUID } from 'node:crypto'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutStateSchema,\n} from '../openapi'\n\nconst DEFAULT_SIZE = 'md'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },\n PUT: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\ntype LayoutScope = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nasync function loadScopeLayout(em: any, scope: LayoutScope): Promise<DashboardLayout | null> {\n return await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n}\n\nfunction normalizeLayoutItems(items: any[]) {\n const list = Array.isArray(items) ? items : []\n const seenIds = new Set<string>()\n const sanitized = list\n .filter((item) => item && typeof item === 'object')\n .map((item) => ({\n id: String(item.id),\n widgetId: String(item.widgetId),\n order: Number.isInteger(item.order) ? Number(item.order) : undefined,\n priority: Number.isInteger(item.priority) ? Number(item.priority) : undefined,\n size: typeof item.size === 'string' ? item.size : undefined,\n settings: item.settings,\n }))\n .filter((item) => {\n if (!item.id || !item.widgetId) return false\n if (seenIds.has(item.id)) return false\n seenIds.add(item.id)\n return true\n })\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, idx) => ({ ...item, order: idx, priority: idx }))\n return sanitized\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n // Use a fresh fork to avoid carrying over pending entities from other operations\n const em = (container.resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = container.resolve('rbacService') as any\n const url = new URL(req.url)\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: acl.features ?? [],\n isSuperAdmin: !!acl.isSuperAdmin,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n let layout = await loadScopeLayout(em, scope)\n let items = layout ? normalizeLayoutItems(layout.layoutJson) : []\n let hasChanged = false\n\n if (!layout) {\n const defaults = allowedWidgets.filter((widget) => widget.metadata.defaultEnabled)\n items = defaults.map((widget, index) => ({\n id: randomUUID(),\n widgetId: widget.metadata.id,\n order: index,\n priority: index,\n size: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n settings: widget.metadata.defaultSettings ?? undefined,\n }))\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: items,\n })\n em.persist(layout)\n hasChanged = true\n } else {\n const existingLayout = layout\n const filtered = items.filter((item) => allowedIds.includes(item.widgetId))\n if (filtered.length !== items.length) {\n hasChanged = true\n items = filtered\n }\n items = items.map((item, index) => (item.order !== index || item.priority !== index ? { ...item, order: index, priority: index } : item))\n if (\n existingLayout.layoutJson.length !== items.length ||\n items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)\n ) {\n hasChanged = true\n }\n existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged) {\n await em.flush()\n }\n\n const canConfigure = acl.isSuperAdmin || hasFeature(acl.features, 'dashboards.configure')\n\n let userEmail: string | null = null\n let userName: string | null = null\n let userLabel: string | null = null\n const user = await findOneWithDecryption(\n em,\n User,\n { id: scope.userId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n )\n if (user) {\n userName = user.name?.trim() ?? null\n userEmail = user.email ?? null\n userLabel = (userName && userName.length > 0 ? userName : userEmail) ?? null\n }\n if (!userLabel) {\n userLabel = scope.userId\n }\n\n const response = {\n layout: { items },\n allowedWidgetIds: allowedIds,\n canConfigure,\n context: {\n ...scope,\n userName,\n userEmail,\n userLabel,\n },\n widgets: allowedWidgets.map((widget) => ({\n id: widget.metadata.id,\n title: widget.metadata.title,\n description: widget.metadata.description ?? null,\n defaultSize: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n defaultEnabled: !!widget.metadata.defaultEnabled,\n defaultSettings: widget.metadata.defaultSettings ?? null,\n features: widget.metadata.features ?? [],\n moduleId: widget.moduleId,\n icon: widget.metadata.icon ?? null,\n loaderKey: widget.key,\n supportsRefresh: !!widget.metadata.supportsRefresh,\n })),\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = dashboardLayoutSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const { resolve } = await createRequestContainer()\n const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = resolve('rbacService') as any\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, 'dashboards.configure')) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: acl.features ?? [],\n isSuperAdmin: !!acl.isSuperAdmin,\n },\n widgets,\n )\n const allowedSet = new Set(allowedIds)\n\n const payloadItems = parsed.data.items\n const sanitized = payloadItems\n .map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings,\n }))\n .filter((item) => allowedSet.has(item.widgetId))\n\n const uniqueIds = new Set(sanitized.map((item) => item.id))\n if (uniqueIds.size !== sanitized.length) {\n return NextResponse.json({ error: 'Layout item IDs must be unique' }, { status: 400 })\n }\n\n let layout = await loadScopeLayout(em, scope)\n if (!layout) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: sanitized,\n })\n em.persist(layout)\n } else {\n layout.layoutJson = sanitized\n }\n await em.flush()\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutGetDoc: OpenApiMethodDoc = {\n summary: 'Load the current dashboard layout',\n description: 'Returns the saved widget layout together with the widgets the current user is allowed to place.',\n tags: [dashboardsTag],\n responses: [\n {\n status: 200,\n description: 'Current dashboard layout and available widgets.',\n schema: dashboardLayoutStateSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n ],\n}\n\nconst layoutPutDoc: OpenApiMethodDoc = {\n summary: 'Persist dashboard layout changes',\n description: 'Saves the provided widget ordering, sizes, and settings for the current user.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutSchema,\n description: 'List of dashboard widgets with ordering, sizing, and settings.',\n },\n responses: [\n { status: 200, description: 'Layout updated successfully.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid layout payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage personal dashboard layout',\n methods: {\n GET: layoutGetDoc,\n PUT: layoutPutDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AACtC,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,6BAA6B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { randomUUID } from 'node:crypto'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutStateSchema,\n} from '../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },\n PUT: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\ntype LayoutScope = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nasync function loadScopeLayout(em: any, scope: LayoutScope): Promise<DashboardLayout | null> {\n return await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n}\n\nfunction normalizeLayoutItems(items: any[]) {\n const list = Array.isArray(items) ? items : []\n const seenIds = new Set<string>()\n const sanitized = list\n .filter((item) => item && typeof item === 'object')\n .map((item) => ({\n id: String(item.id),\n widgetId: String(item.widgetId),\n order: Number.isInteger(item.order) ? Number(item.order) : undefined,\n priority: Number.isInteger(item.priority) ? Number(item.priority) : undefined,\n size: typeof item.size === 'string' ? item.size : undefined,\n settings: item.settings,\n }))\n .filter((item) => {\n if (!item.id || !item.widgetId) return false\n if (seenIds.has(item.id)) return false\n seenIds.add(item.id)\n return true\n })\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, idx) => ({ ...item, order: idx, priority: idx }))\n return sanitized\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n // Use a fresh fork to avoid carrying over pending entities from other operations\n const em = (container.resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = container.resolve('rbacService') as any\n const url = new URL(req.url)\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: acl.features ?? [],\n isSuperAdmin: !!acl.isSuperAdmin,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n let layout = await loadScopeLayout(em, scope)\n let items = layout ? normalizeLayoutItems(layout.layoutJson) : []\n let hasChanged = false\n\n if (!layout) {\n const defaults = allowedWidgets.filter((widget) => widget.metadata.defaultEnabled)\n items = defaults.map((widget, index) => ({\n id: randomUUID(),\n widgetId: widget.metadata.id,\n order: index,\n priority: index,\n size: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n settings: widget.metadata.defaultSettings ?? undefined,\n }))\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: items,\n })\n em.persist(layout)\n hasChanged = true\n } else {\n const existingLayout = layout\n const filtered = items.filter((item) => allowedIds.includes(item.widgetId))\n if (filtered.length !== items.length) {\n hasChanged = true\n items = filtered\n }\n items = items.map((item, index) => (item.order !== index || item.priority !== index ? { ...item, order: index, priority: index } : item))\n if (\n existingLayout.layoutJson.length !== items.length ||\n items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)\n ) {\n hasChanged = true\n }\n existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged) {\n await em.flush()\n }\n\n const canConfigure = acl.isSuperAdmin || hasFeature(acl.features, 'dashboards.configure')\n\n let userEmail: string | null = null\n let userName: string | null = null\n let userLabel: string | null = null\n const user = await findOneWithDecryption(\n em,\n User,\n { id: scope.userId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n )\n if (user) {\n userName = user.name?.trim() ?? null\n userEmail = user.email ?? null\n userLabel = (userName && userName.length > 0 ? userName : userEmail) ?? null\n }\n if (!userLabel) {\n userLabel = scope.userId\n }\n\n const response = {\n layout: { items },\n allowedWidgetIds: allowedIds,\n canConfigure,\n context: {\n ...scope,\n userName,\n userEmail,\n userLabel,\n },\n widgets: allowedWidgets.map((widget) => ({\n id: widget.metadata.id,\n title: widget.metadata.title,\n description: widget.metadata.description ?? null,\n defaultSize: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n defaultEnabled: !!widget.metadata.defaultEnabled,\n defaultSettings: widget.metadata.defaultSettings ?? null,\n features: widget.metadata.features ?? [],\n moduleId: widget.moduleId,\n icon: widget.metadata.icon ?? null,\n loaderKey: widget.key,\n supportsRefresh: !!widget.metadata.supportsRefresh,\n })),\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = dashboardLayoutSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = resolve('rbacService') as any\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const acl = await rbac.loadAcl(scope.userId, { tenantId: scope.tenantId, organizationId: scope.organizationId })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, 'dashboards.configure')) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: parsed.data.items },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: acl.features ?? [],\n isSuperAdmin: !!acl.isSuperAdmin,\n },\n widgets,\n )\n const allowedSet = new Set(allowedIds)\n\n const payloadItems = parsed.data.items\n const sanitized = payloadItems\n .map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings,\n }))\n .filter((item) => allowedSet.has(item.widgetId))\n\n const uniqueIds = new Set(sanitized.map((item) => item.id))\n if (uniqueIds.size !== sanitized.length) {\n return NextResponse.json({ error: 'Layout item IDs must be unique' }, { status: 400 })\n }\n\n let layout = await loadScopeLayout(em, scope)\n if (!layout) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: sanitized,\n })\n em.persist(layout)\n } else {\n layout.layoutJson = sanitized\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutGetDoc: OpenApiMethodDoc = {\n summary: 'Load the current dashboard layout',\n description: 'Returns the saved widget layout together with the widgets the current user is allowed to place.',\n tags: [dashboardsTag],\n responses: [\n {\n status: 200,\n description: 'Current dashboard layout and available widgets.',\n schema: dashboardLayoutStateSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n ],\n}\n\nconst layoutPutDoc: OpenApiMethodDoc = {\n summary: 'Persist dashboard layout changes',\n description: 'Saves the provided widget ordering, sizes, and settings for the current user.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutSchema,\n description: 'List of dashboard widgets with ordering, sizing, and settings.',\n },\n responses: [\n { status: 200, description: 'Layout updated successfully.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid layout payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage personal dashboard layout',\n methods: {\n GET: layoutGetDoc,\n PUT: layoutPutDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,10 @@ import { roleWidgetSettingsSchema } from "@open-mercato/core/modules/dashboards/
|
|
|
8
8
|
import { loadAllWidgets } from "@open-mercato/core/modules/dashboards/lib/widgets";
|
|
9
9
|
import { resolveWidgetAssignmentReadScope } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
|
|
10
10
|
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
11
|
+
import {
|
|
12
|
+
runCrudMutationGuardAfterSuccess,
|
|
13
|
+
validateCrudMutationGuard
|
|
14
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
11
15
|
import {
|
|
12
16
|
dashboardsTag,
|
|
13
17
|
dashboardsErrorSchema,
|
|
@@ -15,6 +19,7 @@ import {
|
|
|
15
19
|
dashboardRoleWidgetsUpdateResponseSchema
|
|
16
20
|
} from "../../openapi.js";
|
|
17
21
|
const FEATURE = "dashboards.admin.assign-widgets";
|
|
22
|
+
const RESOURCE_KIND = "dashboards.roleWidgets";
|
|
18
23
|
function pickBestRecord(records, tenantId, organizationId) {
|
|
19
24
|
let best = null;
|
|
20
25
|
let bestScore = -1;
|
|
@@ -84,7 +89,8 @@ async function PUT(req) {
|
|
|
84
89
|
if (!parsed.success) {
|
|
85
90
|
return NextResponse.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 });
|
|
86
91
|
}
|
|
87
|
-
const
|
|
92
|
+
const container = await createRequestContainer();
|
|
93
|
+
const { resolve } = container;
|
|
88
94
|
const em = resolve("em");
|
|
89
95
|
const rbac = resolve("rbacService");
|
|
90
96
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
|
|
@@ -100,6 +106,20 @@ async function PUT(req) {
|
|
|
100
106
|
if (!role || tenantId && role.tenantId !== tenantId) {
|
|
101
107
|
return NextResponse.json({ error: "Role not found" }, { status: 404 });
|
|
102
108
|
}
|
|
109
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
110
|
+
tenantId: tenantId ?? "",
|
|
111
|
+
organizationId,
|
|
112
|
+
userId: String(auth.sub),
|
|
113
|
+
resourceKind: RESOURCE_KIND,
|
|
114
|
+
resourceId: parsed.data.roleId,
|
|
115
|
+
operation: "update",
|
|
116
|
+
requestMethod: req.method,
|
|
117
|
+
requestHeaders: req.headers,
|
|
118
|
+
mutationPayload: { roleId: parsed.data.roleId, widgetIds }
|
|
119
|
+
});
|
|
120
|
+
if (guardResult && !guardResult.ok) {
|
|
121
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
122
|
+
}
|
|
103
123
|
let record = await em.findOne(DashboardRoleWidgets, {
|
|
104
124
|
roleId: parsed.data.roleId,
|
|
105
125
|
tenantId,
|
|
@@ -110,6 +130,19 @@ async function PUT(req) {
|
|
|
110
130
|
if (record) {
|
|
111
131
|
await em.remove(record).flush();
|
|
112
132
|
}
|
|
133
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
134
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
135
|
+
tenantId: tenantId ?? "",
|
|
136
|
+
organizationId,
|
|
137
|
+
userId: String(auth.sub),
|
|
138
|
+
resourceKind: RESOURCE_KIND,
|
|
139
|
+
resourceId: parsed.data.roleId,
|
|
140
|
+
operation: "update",
|
|
141
|
+
requestMethod: req.method,
|
|
142
|
+
requestHeaders: req.headers,
|
|
143
|
+
metadata: guardResult.metadata ?? null
|
|
144
|
+
});
|
|
145
|
+
}
|
|
113
146
|
return NextResponse.json({ ok: true, widgetIds: [] });
|
|
114
147
|
}
|
|
115
148
|
if (!record) {
|
|
@@ -124,6 +157,19 @@ async function PUT(req) {
|
|
|
124
157
|
record.widgetIdsJson = widgetIds;
|
|
125
158
|
}
|
|
126
159
|
await em.flush();
|
|
160
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
161
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
162
|
+
tenantId: tenantId ?? "",
|
|
163
|
+
organizationId,
|
|
164
|
+
userId: String(auth.sub),
|
|
165
|
+
resourceKind: RESOURCE_KIND,
|
|
166
|
+
resourceId: parsed.data.roleId,
|
|
167
|
+
operation: "update",
|
|
168
|
+
requestMethod: req.method,
|
|
169
|
+
requestHeaders: req.headers,
|
|
170
|
+
metadata: guardResult.metadata ?? null
|
|
171
|
+
});
|
|
172
|
+
}
|
|
127
173
|
return NextResponse.json({ ok: true, widgetIds });
|
|
128
174
|
}
|
|
129
175
|
const roleWidgetsQuerySchema = z.object({
|