@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
|
@@ -3,6 +3,11 @@ 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
5
|
import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
|
|
6
|
+
import {
|
|
7
|
+
resolveUserFeatures,
|
|
8
|
+
runIntegrationMutationGuardAfterSuccess,
|
|
9
|
+
runIntegrationMutationGuards
|
|
10
|
+
} from "../../guards.js";
|
|
6
11
|
const idParamsSchema = z.object({ id: z.string().min(1) });
|
|
7
12
|
const metadata = {
|
|
8
13
|
POST: { requireAuth: true, requireFeatures: ["integrations.manage"] }
|
|
@@ -26,11 +31,39 @@ async function POST(req, ctx) {
|
|
|
26
31
|
return NextResponse.json({ error: "Integration not found" }, { status: 404 });
|
|
27
32
|
}
|
|
28
33
|
const container = await createRequestContainer();
|
|
34
|
+
const guardResult = await runIntegrationMutationGuards(
|
|
35
|
+
container,
|
|
36
|
+
{
|
|
37
|
+
tenantId: auth.tenantId,
|
|
38
|
+
organizationId: auth.orgId,
|
|
39
|
+
userId: auth.sub ?? "",
|
|
40
|
+
resourceKind: "integrations.integration",
|
|
41
|
+
resourceId: integration.id,
|
|
42
|
+
operation: "update",
|
|
43
|
+
requestMethod: req.method,
|
|
44
|
+
requestHeaders: req.headers,
|
|
45
|
+
mutationPayload: { integrationId: integration.id }
|
|
46
|
+
},
|
|
47
|
+
resolveUserFeatures(auth)
|
|
48
|
+
);
|
|
49
|
+
if (!guardResult.ok) {
|
|
50
|
+
return NextResponse.json(guardResult.errorBody ?? { error: "Operation blocked by guard" }, { status: guardResult.errorStatus ?? 422 });
|
|
51
|
+
}
|
|
29
52
|
const healthService = container.resolve("integrationHealthService");
|
|
30
53
|
const result = await healthService.runHealthCheck(
|
|
31
54
|
integration.id,
|
|
32
55
|
{ organizationId: auth.orgId, tenantId: auth.tenantId }
|
|
33
56
|
);
|
|
57
|
+
await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
58
|
+
tenantId: auth.tenantId,
|
|
59
|
+
organizationId: auth.orgId,
|
|
60
|
+
userId: auth.sub ?? "",
|
|
61
|
+
resourceKind: "integrations.integration",
|
|
62
|
+
resourceId: integration.id,
|
|
63
|
+
operation: "update",
|
|
64
|
+
requestMethod: req.method,
|
|
65
|
+
requestHeaders: req.headers
|
|
66
|
+
});
|
|
34
67
|
return NextResponse.json({
|
|
35
68
|
status: result.status,
|
|
36
69
|
message: result.message ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/integrations/api/%5Bid%5D/health/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 { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { IntegrationHealthService } from '../../../lib/health-service'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['integrations.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Run health check for an integration',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const healthService = container.resolve('integrationHealthService') as IntegrationHealthService\n\n const result = await healthService.runHealthCheck(\n integration.id,\n { organizationId: auth.orgId as string, tenantId: auth.tenantId },\n )\n\n return NextResponse.json({\n status: result.status,\n message: result.message ?? null,\n details: result.details ?? null,\n latencyMs: result.latencyMs,\n checkedAt: result.checkedAt,\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;
|
|
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 { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { IntegrationHealthService } from '../../../lib/health-service'\nimport {\n resolveUserFeatures,\n runIntegrationMutationGuardAfterSuccess,\n runIntegrationMutationGuards,\n} from '../../guards'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['integrations.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Run health check for an integration',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runIntegrationMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { integrationId: integration.id },\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n const healthService = container.resolve('integrationHealthService') as IntegrationHealthService\n\n const result = await healthService.runHealthCheck(\n integration.id,\n { organizationId: auth.orgId as string, tenantId: auth.tenantId },\n )\n\n await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n\n return NextResponse.json({\n status: result.status,\n message: result.message ?? null,\n details: result.details ?? null,\n latencyMs: result.latencyMs,\n checkedAt: result.checkedAt,\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;AAE/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAElD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,cAAc;AAAA,EACrB,SAAS;AACX;AAEA,eAAsB,KAAK,KAAc,KAA8D;AACrG,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,YAAa,IAAI,UAAU,OAAQ,IAAI,OAA4B,SAAS,aAC9E,MAAO,IAAI,SACV,IAAI;AAET,QAAM,eAAe,eAAe,UAAU,SAAS;AACvD,MAAI,CAAC,aAAa,SAAS;AACzB,WAAO,aAAa,KAAK,EAAE,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/E;AAEA,QAAM,cAAc,eAAe,aAAa,KAAK,EAAE;AACvD,MAAI,CAAC,aAAa;AAChB,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK,OAAO;AAAA,MACpB,cAAc;AAAA,MACd,YAAY,YAAY;AAAA,MACxB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,EAAE,eAAe,YAAY,GAAG;AAAA,IACnD;AAAA,IACA,oBAAoB,IAAI;AAAA,EAC1B;AACA,MAAI,CAAC,YAAY,IAAI;AACnB,WAAO,aAAa,KAAK,YAAY,aAAa,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,YAAY,eAAe,IAAI,CAAC;AAAA,EACvI;AAEA,QAAM,gBAAgB,UAAU,QAAQ,0BAA0B;AAElE,QAAM,SAAS,MAAM,cAAc;AAAA,IACjC,YAAY;AAAA,IACZ,EAAE,gBAAgB,KAAK,OAAiB,UAAU,KAAK,SAAS;AAAA,EAClE;AAEA,QAAM,wCAAwC,YAAY,uBAAuB;AAAA,IAC/E,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,IACrB,QAAQ,KAAK,OAAO;AAAA,IACpB,cAAc;AAAA,IACd,YAAY,YAAY;AAAA,IACxB,WAAW;AAAA,IACX,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,EACtB,CAAC;AAED,SAAO,aAAa,KAAK;AAAA,IACvB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO,WAAW;AAAA,IAC3B,SAAS,OAAO,WAAW;AAAA,IAC3B,WAAW,OAAO;AAAA,IAClB,WAAW,OAAO;AAAA,EACpB,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,9 +22,12 @@ async function GET(req) {
|
|
|
22
22
|
const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null;
|
|
23
23
|
const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null;
|
|
24
24
|
if (cache && cacheKey) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
try {
|
|
26
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey));
|
|
27
|
+
if (typeof cached === "number") {
|
|
28
|
+
return Response.json({ unreadCount: cached });
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
const count = await em.count(Notification, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/notifications/api/unread-count/route.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(userId: string): string {\n return `notifications:unread-count:u=${userId}`\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null\n const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAAwB;AACxD,SAAO,gCAAgC,MAAM;AAC/C;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,QAAQ,UAAU,mBAAmB,IAAI,iBAAiB,IAAI,SAAS,IAAI;AACjF,QAAM,WAAW,SAAS,SAAS,yBAAyB,MAAM,IAAI;AAEtE,MAAI,SAAS,UAAU;AACrB,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(userId: string): string {\n return `notifications:unread-count:u=${userId}`\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null\n const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (typeof cached === 'number') {\n return Response.json({ unreadCount: cached })\n }\n } catch {}\n }\n\n const count = await em.count(Notification, {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n status: 'unread',\n })\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, count, {\n ttl: UNREAD_COUNT_TTL_MS,\n tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [null]),\n }),\n )\n } catch {}\n }\n\n return Response.json({ unreadCount: count })\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get unread notification count',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Unread count',\n content: {\n 'application/json': {\n schema: unreadCountResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAAwB;AACxD,SAAO,gCAAgC,MAAM;AAC/C;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,QAAQ,UAAU,mBAAmB,IAAI,iBAAiB,IAAI,SAAS,IAAI;AACjF,QAAM,WAAW,SAAS,SAAS,yBAAyB,MAAM,IAAI;AAEtE,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM,SAAS,MAAM,mBAAmB,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,CAAC;AACjF,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO,SAAS,KAAK,EAAE,aAAa,OAAO,CAAC;AAAA,MAC9C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,QAAM,QAAQ,MAAM,GAAG,MAAM,cAAc;AAAA,IACzC,iBAAiB;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM;AAAA,QAAmB,MAAM;AAAA,QAAU,MACvC,MAAM,IAAI,UAAU,OAAO;AAAA,UACzB,KAAK;AAAA,UACL,MAAM,oBAAoB,uBAAuB,MAAM,UAAU,CAAC,IAAI,CAAC;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,SAAO,SAAS,KAAK,EAAE,aAAa,MAAM,CAAC;AAC7C;AAEO,MAAM,UAAU;AAAA,EACrB,KAAK;AAAA,IACH,SAAS;AAAA,IACT,MAAM,CAAC,eAAe;AAAA,IACtB,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6140.1.d3fbb77495",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6140.1.d3fbb77495",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6140.1.d3fbb77495",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6140.1.d3fbb77495",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6140.1.d3fbb77495",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6140.1.d3fbb77495",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6140.1.d3fbb77495",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -262,6 +262,7 @@ Declared in `events.ts` via `createModuleEvents`. Emit with `emitCustomerAccount
|
|
|
262
262
|
| `customer_accounts.role.created` | crud | No |
|
|
263
263
|
| `customer_accounts.role.updated` | crud | No |
|
|
264
264
|
| `customer_accounts.role.deleted` | crud | No |
|
|
265
|
+
| `customer_accounts.user.invited` | lifecycle | Yes |
|
|
265
266
|
| `customer_accounts.invitation.accepted` | lifecycle | Yes |
|
|
266
267
|
|
|
267
268
|
## Subscribers
|
|
@@ -5,6 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
|
|
8
|
+
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
8
9
|
import { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
9
10
|
import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
10
11
|
import {
|
|
@@ -63,6 +64,15 @@ export async function POST(req: Request) {
|
|
|
63
64
|
},
|
|
64
65
|
)
|
|
65
66
|
|
|
67
|
+
void emitCustomerAccountsEvent('customer_accounts.user.invited', {
|
|
68
|
+
invitationId: invitation.id,
|
|
69
|
+
email: invitation.email,
|
|
70
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
71
|
+
invitedByType: 'staff',
|
|
72
|
+
tenantId: auth.tenantId!,
|
|
73
|
+
organizationId: auth.orgId!,
|
|
74
|
+
}).catch(() => undefined)
|
|
75
|
+
|
|
66
76
|
return NextResponse.json({
|
|
67
77
|
ok: true,
|
|
68
78
|
invitation: {
|
|
@@ -4,6 +4,7 @@ import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib
|
|
|
4
4
|
import { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
|
|
7
|
+
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
7
8
|
import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
|
|
8
9
|
import { CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
9
10
|
import { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
@@ -95,6 +96,15 @@ export async function POST(req: Request) {
|
|
|
95
96
|
},
|
|
96
97
|
)
|
|
97
98
|
|
|
99
|
+
void emitCustomerAccountsEvent('customer_accounts.user.invited', {
|
|
100
|
+
invitationId: invitation.id,
|
|
101
|
+
email: invitation.email,
|
|
102
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
103
|
+
invitedByType: 'portal',
|
|
104
|
+
tenantId: auth.tenantId,
|
|
105
|
+
organizationId: auth.orgId,
|
|
106
|
+
}).catch(() => undefined)
|
|
107
|
+
|
|
98
108
|
return NextResponse.json({
|
|
99
109
|
ok: true,
|
|
100
110
|
invitation: {
|
|
@@ -16,6 +16,7 @@ const events = [
|
|
|
16
16
|
{ id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },
|
|
17
17
|
{ id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },
|
|
18
18
|
{ id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },
|
|
19
|
+
{ id: 'customer_accounts.user.invited', label: 'Customer User Invited', entity: 'user', category: 'lifecycle', clientBroadcast: true },
|
|
19
20
|
{ id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },
|
|
20
21
|
{ id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },
|
|
21
22
|
// Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
|
|
@@ -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 type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
13
|
import {
|
|
10
14
|
dashboardsTag,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
} from '../../openapi'
|
|
15
19
|
|
|
16
20
|
const DEFAULT_SIZE = 'md'
|
|
21
|
+
const RESOURCE_KIND = 'dashboards.layout'
|
|
17
22
|
|
|
18
23
|
export const metadata = {
|
|
19
24
|
PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },
|
|
@@ -39,7 +44,8 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
39
44
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const
|
|
47
|
+
const container = await createRequestContainer()
|
|
48
|
+
const { resolve } = container
|
|
43
49
|
const em = resolve('em') as any
|
|
44
50
|
const rbac = resolve('rbacService') as any
|
|
45
51
|
|
|
@@ -54,6 +60,21 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
54
60
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
64
|
+
tenantId: scope.tenantId ?? '',
|
|
65
|
+
organizationId: scope.organizationId,
|
|
66
|
+
userId: scope.userId,
|
|
67
|
+
resourceKind: RESOURCE_KIND,
|
|
68
|
+
resourceId: layoutItemId,
|
|
69
|
+
operation: 'update',
|
|
70
|
+
requestMethod: req.method,
|
|
71
|
+
requestHeaders: req.headers,
|
|
72
|
+
mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings },
|
|
73
|
+
})
|
|
74
|
+
if (guardResult && !guardResult.ok) {
|
|
75
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
const layout = await em.findOne(DashboardLayout, {
|
|
58
79
|
userId: scope.userId,
|
|
59
80
|
tenantId: scope.tenantId,
|
|
@@ -77,6 +98,20 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
77
98
|
}
|
|
78
99
|
await em.flush()
|
|
79
100
|
|
|
101
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
102
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
103
|
+
tenantId: scope.tenantId ?? '',
|
|
104
|
+
organizationId: scope.organizationId,
|
|
105
|
+
userId: scope.userId,
|
|
106
|
+
resourceKind: RESOURCE_KIND,
|
|
107
|
+
resourceId: layoutItemId,
|
|
108
|
+
operation: 'update',
|
|
109
|
+
requestMethod: req.method,
|
|
110
|
+
requestHeaders: req.headers,
|
|
111
|
+
metadata: guardResult.metadata ?? null,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
return NextResponse.json({ ok: true })
|
|
81
116
|
}
|
|
82
117
|
|
|
@@ -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 type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
import {
|
|
14
18
|
dashboardsTag,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
} from '../openapi'
|
|
19
23
|
|
|
20
24
|
const DEFAULT_SIZE = 'md'
|
|
25
|
+
const RESOURCE_KIND = 'dashboards.layout'
|
|
21
26
|
|
|
22
27
|
export const metadata = {
|
|
23
28
|
GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },
|
|
@@ -206,7 +211,8 @@ export async function PUT(req: Request) {
|
|
|
206
211
|
return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
const
|
|
214
|
+
const container = await createRequestContainer()
|
|
215
|
+
const { resolve } = container
|
|
210
216
|
const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })
|
|
211
217
|
const rbac = resolve('rbacService') as any
|
|
212
218
|
|
|
@@ -221,6 +227,21 @@ export async function PUT(req: Request) {
|
|
|
221
227
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
231
|
+
tenantId: scope.tenantId ?? '',
|
|
232
|
+
organizationId: scope.organizationId,
|
|
233
|
+
userId: scope.userId,
|
|
234
|
+
resourceKind: RESOURCE_KIND,
|
|
235
|
+
resourceId: scope.userId,
|
|
236
|
+
operation: 'update',
|
|
237
|
+
requestMethod: req.method,
|
|
238
|
+
requestHeaders: req.headers,
|
|
239
|
+
mutationPayload: { items: parsed.data.items },
|
|
240
|
+
})
|
|
241
|
+
if (guardResult && !guardResult.ok) {
|
|
242
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
243
|
+
}
|
|
244
|
+
|
|
224
245
|
const widgets = await loadAllWidgets()
|
|
225
246
|
const allowedIds = await resolveAllowedWidgetIds(
|
|
226
247
|
em,
|
|
@@ -266,6 +287,20 @@ export async function PUT(req: Request) {
|
|
|
266
287
|
}
|
|
267
288
|
await em.flush()
|
|
268
289
|
|
|
290
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
291
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
292
|
+
tenantId: scope.tenantId ?? '',
|
|
293
|
+
organizationId: scope.organizationId,
|
|
294
|
+
userId: scope.userId,
|
|
295
|
+
resourceKind: RESOURCE_KIND,
|
|
296
|
+
resourceId: scope.userId,
|
|
297
|
+
operation: 'update',
|
|
298
|
+
requestMethod: req.method,
|
|
299
|
+
requestHeaders: req.headers,
|
|
300
|
+
metadata: guardResult.metadata ?? null,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
269
304
|
return NextResponse.json({ ok: true })
|
|
270
305
|
}
|
|
271
306
|
|
|
@@ -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 type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
16
|
import {
|
|
13
17
|
dashboardsTag,
|
|
@@ -17,6 +21,7 @@ import {
|
|
|
17
21
|
} from '../../openapi'
|
|
18
22
|
|
|
19
23
|
const FEATURE = 'dashboards.admin.assign-widgets'
|
|
24
|
+
const RESOURCE_KIND = 'dashboards.roleWidgets'
|
|
20
25
|
|
|
21
26
|
function pickBestRecord(records: DashboardRoleWidgets[], tenantId: string | null, organizationId: string | null): DashboardRoleWidgets | null {
|
|
22
27
|
let best: DashboardRoleWidgets | null = null
|
|
@@ -99,7 +104,8 @@ export async function PUT(req: Request) {
|
|
|
99
104
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
const
|
|
107
|
+
const container = await createRequestContainer()
|
|
108
|
+
const { resolve } = container
|
|
103
109
|
const em = resolve('em') as any
|
|
104
110
|
const rbac = resolve('rbacService') as any
|
|
105
111
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })
|
|
@@ -119,6 +125,21 @@ export async function PUT(req: Request) {
|
|
|
119
125
|
return NextResponse.json({ error: 'Role not found' }, { status: 404 })
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
129
|
+
tenantId: tenantId ?? '',
|
|
130
|
+
organizationId,
|
|
131
|
+
userId: String(auth.sub),
|
|
132
|
+
resourceKind: RESOURCE_KIND,
|
|
133
|
+
resourceId: parsed.data.roleId,
|
|
134
|
+
operation: 'update',
|
|
135
|
+
requestMethod: req.method,
|
|
136
|
+
requestHeaders: req.headers,
|
|
137
|
+
mutationPayload: { roleId: parsed.data.roleId, widgetIds },
|
|
138
|
+
})
|
|
139
|
+
if (guardResult && !guardResult.ok) {
|
|
140
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
let record = await em.findOne(DashboardRoleWidgets, {
|
|
123
144
|
roleId: parsed.data.roleId,
|
|
124
145
|
tenantId,
|
|
@@ -130,6 +151,19 @@ export async function PUT(req: Request) {
|
|
|
130
151
|
if (record) {
|
|
131
152
|
await em.remove(record).flush()
|
|
132
153
|
}
|
|
154
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
155
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
156
|
+
tenantId: tenantId ?? '',
|
|
157
|
+
organizationId,
|
|
158
|
+
userId: String(auth.sub),
|
|
159
|
+
resourceKind: RESOURCE_KIND,
|
|
160
|
+
resourceId: parsed.data.roleId,
|
|
161
|
+
operation: 'update',
|
|
162
|
+
requestMethod: req.method,
|
|
163
|
+
requestHeaders: req.headers,
|
|
164
|
+
metadata: guardResult.metadata ?? null,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
133
167
|
return NextResponse.json({ ok: true, widgetIds: [] })
|
|
134
168
|
}
|
|
135
169
|
|
|
@@ -146,6 +180,20 @@ export async function PUT(req: Request) {
|
|
|
146
180
|
}
|
|
147
181
|
await em.flush()
|
|
148
182
|
|
|
183
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
184
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
185
|
+
tenantId: tenantId ?? '',
|
|
186
|
+
organizationId,
|
|
187
|
+
userId: String(auth.sub),
|
|
188
|
+
resourceKind: RESOURCE_KIND,
|
|
189
|
+
resourceId: parsed.data.roleId,
|
|
190
|
+
operation: 'update',
|
|
191
|
+
requestMethod: req.method,
|
|
192
|
+
requestHeaders: req.headers,
|
|
193
|
+
metadata: guardResult.metadata ?? null,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
return NextResponse.json({ ok: true, widgetIds })
|
|
150
198
|
}
|
|
151
199
|
|
|
@@ -9,6 +9,10 @@ import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widget
|
|
|
9
9
|
import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'
|
|
10
10
|
import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
|
|
11
11
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
12
16
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
import {
|
|
14
18
|
dashboardsTag,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
} from '../../openapi'
|
|
19
23
|
|
|
20
24
|
const FEATURE = 'dashboards.admin.assign-widgets'
|
|
25
|
+
const RESOURCE_KIND = 'dashboards.userWidgets'
|
|
21
26
|
|
|
22
27
|
export const metadata = {
|
|
23
28
|
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
@@ -99,7 +104,8 @@ export async function PUT(req: Request) {
|
|
|
99
104
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
const
|
|
107
|
+
const container = await createRequestContainer()
|
|
108
|
+
const { resolve } = container
|
|
103
109
|
const em = resolve('em') as any
|
|
104
110
|
const rbac = resolve('rbacService') as any
|
|
105
111
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })
|
|
@@ -119,6 +125,21 @@ export async function PUT(req: Request) {
|
|
|
119
125
|
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
129
|
+
tenantId: tenantId ?? '',
|
|
130
|
+
organizationId,
|
|
131
|
+
userId: String(auth.sub),
|
|
132
|
+
resourceKind: RESOURCE_KIND,
|
|
133
|
+
resourceId: parsed.data.userId,
|
|
134
|
+
operation: 'update',
|
|
135
|
+
requestMethod: req.method,
|
|
136
|
+
requestHeaders: req.headers,
|
|
137
|
+
mutationPayload: { userId: parsed.data.userId, mode: parsed.data.mode, widgetIds },
|
|
138
|
+
})
|
|
139
|
+
if (guardResult && !guardResult.ok) {
|
|
140
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
let record = await em.findOne(DashboardUserWidgets, {
|
|
123
144
|
userId: parsed.data.userId,
|
|
124
145
|
tenantId,
|
|
@@ -130,6 +151,19 @@ export async function PUT(req: Request) {
|
|
|
130
151
|
if (record) {
|
|
131
152
|
await em.remove(record).flush()
|
|
132
153
|
}
|
|
154
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
155
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
156
|
+
tenantId: tenantId ?? '',
|
|
157
|
+
organizationId,
|
|
158
|
+
userId: String(auth.sub),
|
|
159
|
+
resourceKind: RESOURCE_KIND,
|
|
160
|
+
resourceId: parsed.data.userId,
|
|
161
|
+
operation: 'update',
|
|
162
|
+
requestMethod: req.method,
|
|
163
|
+
requestHeaders: req.headers,
|
|
164
|
+
metadata: guardResult.metadata ?? null,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
133
167
|
return NextResponse.json({ ok: true, mode: 'inherit', widgetIds: [] })
|
|
134
168
|
}
|
|
135
169
|
|
|
@@ -148,6 +182,20 @@ export async function PUT(req: Request) {
|
|
|
148
182
|
}
|
|
149
183
|
await em.flush()
|
|
150
184
|
|
|
185
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
186
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
187
|
+
tenantId: tenantId ?? '',
|
|
188
|
+
organizationId,
|
|
189
|
+
userId: String(auth.sub),
|
|
190
|
+
resourceKind: RESOURCE_KIND,
|
|
191
|
+
resourceId: parsed.data.userId,
|
|
192
|
+
operation: 'update',
|
|
193
|
+
requestMethod: req.method,
|
|
194
|
+
requestHeaders: req.headers,
|
|
195
|
+
metadata: guardResult.metadata ?? null,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
151
199
|
return NextResponse.json({ ok: true, mode: 'override', widgetIds })
|
|
152
200
|
}
|
|
153
201
|
|
|
@@ -6,6 +6,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
6
6
|
import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
7
7
|
import { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'
|
|
8
8
|
import { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
9
|
+
import { resolveRecordIdsForCustomFieldFilters } from './tenant-cf-filter'
|
|
9
10
|
import { E } from '#generated/entities.ids.generated'
|
|
10
11
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
11
12
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
@@ -87,11 +88,6 @@ const toRow = (tenant: Tenant, cf: Record<string, unknown>): TenantRow => ({
|
|
|
87
88
|
...cf,
|
|
88
89
|
})
|
|
89
90
|
|
|
90
|
-
const matchesValue = (current: unknown, expected: unknown): boolean => {
|
|
91
|
-
if (Array.isArray(current)) return current.some((item) => item === expected)
|
|
92
|
-
return current === expected
|
|
93
|
-
}
|
|
94
|
-
|
|
95
91
|
export async function GET(req: Request) {
|
|
96
92
|
const auth = await getAuthFromRequest(req)
|
|
97
93
|
if (!auth) {
|
|
@@ -180,28 +176,31 @@ export async function GET(req: Request) {
|
|
|
180
176
|
let cfValues: Record<string, Record<string, unknown>>
|
|
181
177
|
|
|
182
178
|
if (cfFilterEntries.length) {
|
|
183
|
-
// Custom-field
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const value = payload[`cf_${key}`]
|
|
192
|
-
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
|
193
|
-
const maybeIn = (expected as { $in?: unknown[] }).$in
|
|
194
|
-
if (Array.isArray(maybeIn)) {
|
|
195
|
-
if (value === undefined || value === null) return false
|
|
196
|
-
if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))
|
|
197
|
-
return maybeIn.includes(value)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return matchesValue(value, expected)
|
|
201
|
-
})
|
|
179
|
+
// Custom-field values live in a separate store, so first resolve the bounded
|
|
180
|
+
// set of matching tenant ids from that store, then push pagination to the
|
|
181
|
+
// database via findAndCount instead of loading the whole tenant table.
|
|
182
|
+
const matchedIds = await resolveRecordIdsForCustomFieldFilters({
|
|
183
|
+
em,
|
|
184
|
+
entityId: E.directory.tenant,
|
|
185
|
+
tenantId: auth.tenantId ?? null,
|
|
186
|
+
filters: cfFilterEntries,
|
|
202
187
|
})
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
const existingId = where.id
|
|
189
|
+
const idFilter = typeof existingId === 'string'
|
|
190
|
+
? (matchedIds.has(existingId) ? [existingId] : [])
|
|
191
|
+
: Array.from(matchedIds)
|
|
192
|
+
|
|
193
|
+
if (!idFilter.length) {
|
|
194
|
+
total = 0
|
|
195
|
+
pageTenants = []
|
|
196
|
+
cfValues = {}
|
|
197
|
+
} else {
|
|
198
|
+
where.id = { $in: idFilter }
|
|
199
|
+
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })
|
|
200
|
+
total = count
|
|
201
|
+
pageTenants = rows
|
|
202
|
+
cfValues = await loadCfValues(rows)
|
|
203
|
+
}
|
|
205
204
|
} else {
|
|
206
205
|
// No JS-side filtering applies, so pagination is pushed to the database
|
|
207
206
|
// instead of fetching the whole table and slicing in memory.
|