@open-mercato/core 0.10.1-develop.7762.1.657bb7236b → 0.10.1-develop.7764.1.7505d7340b
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/auth/api/profile/route.js +16 -2
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/lib/userDataSubject.js +26 -0
- package/dist/modules/auth/lib/userDataSubject.js.map +7 -0
- package/dist/modules/customers/api/companies/route.js +2 -1
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +2 -1
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +21 -3
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/listSorting.js +2 -1
- package/dist/modules/customers/backend/customers/listSorting.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +17 -1
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/lib/createdAtDayFilter.js +64 -0
- package/dist/modules/customers/lib/createdAtDayFilter.js.map +7 -0
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +11 -4
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +25 -13
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/profile/route.ts +15 -1
- package/src/modules/auth/lib/userDataSubject.ts +30 -0
- package/src/modules/customers/api/companies/route.ts +2 -1
- package/src/modules/customers/api/people/route.ts +2 -1
- package/src/modules/customers/backend/customers/companies/page.tsx +26 -3
- package/src/modules/customers/backend/customers/listSorting.ts +1 -0
- package/src/modules/customers/backend/customers/people/page.tsx +22 -1
- package/src/modules/customers/i18n/de.json +2 -0
- package/src/modules/customers/i18n/en.json +2 -0
- package/src/modules/customers/i18n/es.json +2 -0
- package/src/modules/customers/i18n/ko.json +2 -0
- package/src/modules/customers/i18n/pl.json +2 -0
- package/src/modules/customers/lib/createdAtDayFilter.ts +68 -0
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +12 -4
- package/src/modules/dashboards/api/layout/route.ts +25 -12
|
@@ -9,6 +9,8 @@ import { resolveAllowedWidgetIds } from "@open-mercato/core/modules/dashboards/l
|
|
|
9
9
|
import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
|
|
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 { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
13
|
+
import { resolveUserDataSubject } from "@open-mercato/core/modules/auth/lib/userDataSubject";
|
|
12
14
|
import {
|
|
13
15
|
runCrudMutationGuardAfterSuccess,
|
|
14
16
|
validateCrudMutationGuard
|
|
@@ -61,13 +63,17 @@ async function GET(req) {
|
|
|
61
63
|
const container = await createRequestContainer();
|
|
62
64
|
const em = container.resolve("em").fork({ clear: true, freshEventManager: true, useContext: true });
|
|
63
65
|
const rbac = container.resolve("rbacService");
|
|
64
|
-
const
|
|
66
|
+
const userId = await resolveUserDataSubject(em, auth);
|
|
67
|
+
if (!userId) {
|
|
68
|
+
const { translate } = await resolveTranslations();
|
|
69
|
+
return NextResponse.json({ error: translate("api.errors.forbidden", "Forbidden") }, { status: 403 });
|
|
70
|
+
}
|
|
65
71
|
const scope = {
|
|
66
|
-
userId
|
|
72
|
+
userId,
|
|
67
73
|
tenantId: auth.tenantId ?? null,
|
|
68
74
|
organizationId: auth.orgId ?? null
|
|
69
75
|
};
|
|
70
|
-
const effectiveFeatures = await rbac.getEffectiveFeatures(
|
|
76
|
+
const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {
|
|
71
77
|
tenantId: scope.tenantId,
|
|
72
78
|
organizationId: scope.organizationId
|
|
73
79
|
});
|
|
@@ -98,7 +104,7 @@ async function GET(req) {
|
|
|
98
104
|
size: widget.metadata.defaultSize ?? DEFAULT_SIZE,
|
|
99
105
|
settings: widget.metadata.defaultSettings ?? void 0
|
|
100
106
|
}));
|
|
101
|
-
if (hasRegisteredWidgets) {
|
|
107
|
+
if (hasRegisteredWidgets && !auth.isApiKey) {
|
|
102
108
|
layout = em.create(DashboardLayout, {
|
|
103
109
|
userId: scope.userId,
|
|
104
110
|
tenantId: scope.tenantId,
|
|
@@ -108,7 +114,7 @@ async function GET(req) {
|
|
|
108
114
|
em.persist(layout);
|
|
109
115
|
hasChanged = true;
|
|
110
116
|
}
|
|
111
|
-
} else if (hasRegisteredWidgets) {
|
|
117
|
+
} else if (hasRegisteredWidgets || auth.isApiKey) {
|
|
112
118
|
const existingLayout = layout;
|
|
113
119
|
const filtered = items.filter((item) => allowedIds.includes(item.widgetId));
|
|
114
120
|
if (filtered.length !== items.length) {
|
|
@@ -119,10 +125,10 @@ async function GET(req) {
|
|
|
119
125
|
if (existingLayout.layoutJson.length !== items.length || items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)) {
|
|
120
126
|
hasChanged = true;
|
|
121
127
|
}
|
|
122
|
-
existingLayout.layoutJson = items;
|
|
128
|
+
if (!auth.isApiKey) existingLayout.layoutJson = items;
|
|
123
129
|
layout = existingLayout;
|
|
124
130
|
}
|
|
125
|
-
if (hasChanged) {
|
|
131
|
+
if (hasChanged && !auth.isApiKey) {
|
|
126
132
|
await em.flush();
|
|
127
133
|
}
|
|
128
134
|
const canConfigure = authorizeFeatures(["dashboards.configure"], {
|
|
@@ -189,13 +195,18 @@ async function PUT(req) {
|
|
|
189
195
|
const { resolve } = container;
|
|
190
196
|
const em = resolve("em").fork({ clear: true, freshEventManager: true, useContext: true });
|
|
191
197
|
const rbac = resolve("rbacService");
|
|
198
|
+
const userId = await resolveUserDataSubject(em, auth);
|
|
199
|
+
if (!userId) {
|
|
200
|
+
const { translate } = await resolveTranslations();
|
|
201
|
+
return NextResponse.json({ error: translate("api.errors.forbidden", "Forbidden") }, { status: 403 });
|
|
202
|
+
}
|
|
192
203
|
const scope = {
|
|
193
|
-
userId
|
|
204
|
+
userId,
|
|
194
205
|
tenantId: auth.tenantId ?? null,
|
|
195
206
|
organizationId: auth.orgId ?? null
|
|
196
207
|
};
|
|
197
208
|
const canConfigure = await rbac.userHasAllFeatures(
|
|
198
|
-
|
|
209
|
+
auth.sub,
|
|
199
210
|
["dashboards.configure"],
|
|
200
211
|
{ tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
201
212
|
);
|
|
@@ -205,7 +216,7 @@ async function PUT(req) {
|
|
|
205
216
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
206
217
|
tenantId: scope.tenantId ?? "",
|
|
207
218
|
organizationId: scope.organizationId,
|
|
208
|
-
userId:
|
|
219
|
+
userId: auth.sub,
|
|
209
220
|
resourceKind: RESOURCE_KIND,
|
|
210
221
|
resourceId: scope.userId,
|
|
211
222
|
operation: "update",
|
|
@@ -220,7 +231,7 @@ async function PUT(req) {
|
|
|
220
231
|
if (widgets.length === 0) {
|
|
221
232
|
return NextResponse.json({ error: "Widget registry unavailable" }, { status: 503 });
|
|
222
233
|
}
|
|
223
|
-
const effectiveFeatures = await rbac.getEffectiveFeatures(
|
|
234
|
+
const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {
|
|
224
235
|
tenantId: scope.tenantId,
|
|
225
236
|
organizationId: scope.organizationId
|
|
226
237
|
});
|
|
@@ -266,7 +277,7 @@ async function PUT(req) {
|
|
|
266
277
|
await runCrudMutationGuardAfterSuccess(container, {
|
|
267
278
|
tenantId: scope.tenantId ?? "",
|
|
268
279
|
organizationId: scope.organizationId,
|
|
269
|
-
userId:
|
|
280
|
+
userId: auth.sub,
|
|
270
281
|
resourceKind: RESOURCE_KIND,
|
|
271
282
|
resourceId: scope.userId,
|
|
272
283
|
operation: "update",
|
|
@@ -289,7 +300,8 @@ const layoutGetDoc = {
|
|
|
289
300
|
}
|
|
290
301
|
],
|
|
291
302
|
errors: [
|
|
292
|
-
{ status: 401, description: "Authentication required", schema: dashboardsErrorSchema }
|
|
303
|
+
{ status: 401, description: "Authentication required", schema: dashboardsErrorSchema },
|
|
304
|
+
{ status: 403, description: "No user binding in the authenticated scope", schema: dashboardsErrorSchema }
|
|
293
305
|
]
|
|
294
306
|
};
|
|
295
307
|
const layoutPutDoc = {
|
|
@@ -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 { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\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 effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\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: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n // An empty widget registry means the module registry has not been populated yet\n // (a boot race), not that this app has no widgets. Every write below is derived\n // from it, so a read request must persist nothing until it recovers \u2014 otherwise a\n // restart-recoverable glitch is written into saved layouts for good (#5041).\n // Note this keys off the registry, not off `allowedIds`: a user whose allowlist is\n // legitimately empty on a healthy registry is still pruned and seeded as before.\n const hasRegisteredWidgets = widgets.length > 0\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 if (hasRegisteredWidgets) {\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 }\n } else if (hasRegisteredWidgets) {\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 = authorizeFeatures(['dashboards.configure'], {\n grantedFeatures: effectiveFeatures,\n })\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 canConfigure = await rbac.userHasAllFeatures(\n scope.userId,\n ['dashboards.configure'],\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!canConfigure) {\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 // The allowlist below is derived from the registry, and this handler persists the\n // filtered result unconditionally. An empty registry \u2014 the boot race behind #5041 \u2014\n // would drop every submitted item and save `[]` while answering `{ ok: true }`, so a\n // stale tab reordering a widget could erase the layout it just rendered. Fail the\n // write instead: the save is explicit, so the client must learn it did not happen\n // and retry, rather than be told a wipe succeeded.\n if (widgets.length === 0) {\n return NextResponse.json({ error: 'Widget registry unavailable' }, { status: 503 })\n }\n const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\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 { status: 503, description: 'Widget registry unavailable \u2014 the layout was not saved', 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,yBAAyB;AAClC,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAEf,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AAAA,EAC/D,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAQA,eAAe,gBAAgB,IAAS,OAAqD;AAC3F,SAAO,MAAM,GAAG,QAAQ,iBAAiB;AAAA,IACvC,QAAQ,MAAM;AAAA,IACd,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,WAAW;AAAA,EACb,CAAC;AACH;AAEA,SAAS,qBAAqB,OAAc;AAC1C,QAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;AAC7C,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,YAAY,KACf,OAAO,CAAC,SAAS,QAAQ,OAAO,SAAS,QAAQ,EACjD,IAAI,CAAC,UAAU;AAAA,IACd,IAAI,OAAO,KAAK,EAAE;AAAA,IAClB,UAAU,OAAO,KAAK,QAAQ;AAAA,IAC9B,OAAO,OAAO,UAAU,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK,IAAI;AAAA,IAC3D,UAAU,OAAO,UAAU,KAAK,QAAQ,IAAI,OAAO,KAAK,QAAQ,IAAI;AAAA,IACpE,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,IAClD,UAAU,KAAK;AAAA,EACjB,EAAE,EACD,OAAO,CAAC,SAAS;AAChB,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,SAAU,QAAO;AACvC,QAAI,QAAQ,IAAI,KAAK,EAAE,EAAG,QAAO;AACjC,YAAQ,IAAI,KAAK,EAAE;AACnB,WAAO;AAAA,EACT,CAAC,EACA,KAAK,CAAC,GAAG,MAAM;AACd,UAAM,SAAS,EAAE,SAAS,EAAE,YAAY;AACxC,UAAM,SAAS,EAAE,SAAS,EAAE,YAAY;AACxC,WAAO,SAAS;AAAA,EAClB,CAAC,EACA,IAAI,CAAC,MAAM,SAAS,EAAE,GAAG,MAAM,OAAO,KAAK,UAAU,IAAI,EAAE;AAC9D,SAAO;AACT;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE9E,QAAM,YAAY,MAAM,uBAAuB;AAE/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAU,KAAK,EAAE,OAAO,MAAM,mBAAmB,MAAM,YAAY,KAAK,CAAC;AAC3G,QAAM,OAAO,UAAU,QAAQ,aAAa;AAC5C,QAAM,MAAM,
|
|
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 { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveUserDataSubject } from '@open-mercato/core/modules/auth/lib/userDataSubject'\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 userId = await resolveUserDataSubject(em, auth)\n if (!userId) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n\n const scope: LayoutScope = {\n userId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\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: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n // An empty widget registry means the module registry has not been populated yet\n // (a boot race), not that this app has no widgets. Every write below is derived\n // from it, so a read request must persist nothing until it recovers \u2014 otherwise a\n // restart-recoverable glitch is written into saved layouts for good (#5041).\n // Note this keys off the registry, not off `allowedIds`: a user whose allowlist is\n // legitimately empty on a healthy registry is still pruned and seeded as before.\n const hasRegisteredWidgets = widgets.length > 0\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 if (hasRegisteredWidgets && !auth.isApiKey) {\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 }\n } else if (hasRegisteredWidgets || auth.isApiKey) {\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 if (!auth.isApiKey) existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged && !auth.isApiKey) {\n await em.flush()\n }\n\n const canConfigure = authorizeFeatures(['dashboards.configure'], {\n grantedFeatures: effectiveFeatures,\n })\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 userId = await resolveUserDataSubject(em, auth)\n if (!userId) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n\n const scope: LayoutScope = {\n userId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const canConfigure = await rbac.userHasAllFeatures(\n auth.sub,\n ['dashboards.configure'],\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!canConfigure) {\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: auth.sub,\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 // The allowlist below is derived from the registry, and this handler persists the\n // filtered result unconditionally. An empty registry \u2014 the boot race behind #5041 \u2014\n // would drop every submitted item and save `[]` while answering `{ ok: true }`, so a\n // stale tab reordering a widget could erase the layout it just rendered. Fail the\n // write instead: the save is explicit, so the client must learn it did not happen\n // and retry, rather than be told a wipe succeeded.\n if (widgets.length === 0) {\n return NextResponse.json({ error: 'Widget registry unavailable' }, { status: 503 })\n }\n const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\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: auth.sub,\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 { status: 403, description: 'No user binding in the authenticated scope', 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 { status: 503, description: 'Widget registry unavailable \u2014 the layout was not saved', 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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7764.1.7505d7340b",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7764.1.7505d7340b",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7764.1.7505d7340b",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7764.1.7505d7340b",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7764.1.7505d7340b",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7764.1.7505d7340b",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7764.1.7505d7340b",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -14,6 +14,7 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
|
14
14
|
import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
15
15
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
16
16
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
17
|
+
import { resolveUserDataSubject } from '@open-mercato/core/modules/auth/lib/userDataSubject'
|
|
17
18
|
|
|
18
19
|
const logger = createLogger('auth').child({ component: 'profile' })
|
|
19
20
|
|
|
@@ -98,10 +99,18 @@ export async function GET(req: Request) {
|
|
|
98
99
|
try {
|
|
99
100
|
const container = await createRequestContainer()
|
|
100
101
|
const em = (container.resolve('em') as EntityManager)
|
|
102
|
+
const userId = await resolveUserDataSubject(em, auth)
|
|
103
|
+
if (!userId) {
|
|
104
|
+
return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })
|
|
105
|
+
}
|
|
101
106
|
const user = await findOneWithDecryption(
|
|
102
107
|
em,
|
|
103
108
|
User,
|
|
104
|
-
{
|
|
109
|
+
{
|
|
110
|
+
id: userId,
|
|
111
|
+
deletedAt: null,
|
|
112
|
+
...(auth.isApiKey ? { tenantId: auth.tenantId, ...(auth.orgId ? { organizationId: auth.orgId } : {}) } : {}),
|
|
113
|
+
},
|
|
105
114
|
undefined,
|
|
106
115
|
{ tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },
|
|
107
116
|
)
|
|
@@ -126,6 +135,9 @@ export async function PUT(req: Request) {
|
|
|
126
135
|
if (!auth?.sub) {
|
|
127
136
|
return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
|
|
128
137
|
}
|
|
138
|
+
if (auth.isApiKey) {
|
|
139
|
+
return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })
|
|
140
|
+
}
|
|
129
141
|
try {
|
|
130
142
|
const body = await req.json().catch(() => ({}))
|
|
131
143
|
const parsed = buildUpdateSchema(translate).safeParse(body)
|
|
@@ -222,6 +234,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
222
234
|
responses: [
|
|
223
235
|
{ status: 200, description: 'Profile payload', schema: profileResponseSchema },
|
|
224
236
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
237
|
+
{ status: 403, description: 'No user binding in the authenticated scope', schema: z.object({ error: z.string() }) },
|
|
225
238
|
{ status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },
|
|
226
239
|
],
|
|
227
240
|
},
|
|
@@ -236,6 +249,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
236
249
|
{ status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },
|
|
237
250
|
{ status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
|
|
238
251
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
252
|
+
{ status: 403, description: 'Interactive session required', schema: z.object({ error: z.string() }) },
|
|
239
253
|
],
|
|
240
254
|
},
|
|
241
255
|
},
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { User } from '../data/entities'
|
|
6
|
+
|
|
7
|
+
const uuidSchema = z.string().uuid()
|
|
8
|
+
|
|
9
|
+
export async function resolveUserDataSubject(
|
|
10
|
+
em: EntityManager,
|
|
11
|
+
auth: NonNullable<AuthContext>,
|
|
12
|
+
): Promise<string | null> {
|
|
13
|
+
if (!auth.isApiKey) return auth.sub
|
|
14
|
+
if (!uuidSchema.safeParse(auth.userId).success || !uuidSchema.safeParse(auth.tenantId).success) return null
|
|
15
|
+
if (auth.orgId !== null && !uuidSchema.safeParse(auth.orgId).success) return null
|
|
16
|
+
|
|
17
|
+
const user = await findOneWithDecryption(
|
|
18
|
+
em,
|
|
19
|
+
User,
|
|
20
|
+
{
|
|
21
|
+
id: auth.userId,
|
|
22
|
+
tenantId: auth.tenantId,
|
|
23
|
+
...(auth.orgId ? { organizationId: auth.orgId } : {}),
|
|
24
|
+
deletedAt: null,
|
|
25
|
+
},
|
|
26
|
+
undefined,
|
|
27
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
28
|
+
)
|
|
29
|
+
return user?.id ?? null
|
|
30
|
+
}
|
|
@@ -28,6 +28,7 @@ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
|
28
28
|
import { isUuidListParam, splitIdListParam } from '@open-mercato/shared/lib/crud/ids'
|
|
29
29
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
30
30
|
import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
|
|
31
|
+
import { expandCreatedAtDayRules } from '../../lib/createdAtDayFilter'
|
|
31
32
|
import {
|
|
32
33
|
createCustomersCrudOpenApi,
|
|
33
34
|
createPagedListResponseSchema,
|
|
@@ -135,7 +136,7 @@ const crud = makeCrudRoute({
|
|
|
135
136
|
updatedAt: 'updated_at',
|
|
136
137
|
},
|
|
137
138
|
buildFilters: async (query, ctx) => {
|
|
138
|
-
const advancedFilterTree = consumeAdvancedFilterState(query)
|
|
139
|
+
const advancedFilterTree = expandCreatedAtDayRules(consumeAdvancedFilterState(query), 'UTC')
|
|
139
140
|
const filters: Record<string, unknown> = { kind: { $eq: 'company' } }
|
|
140
141
|
if (query.id) filters.id = { $eq: query.id }
|
|
141
142
|
if (query.search) {
|
|
@@ -24,6 +24,7 @@ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
|
24
24
|
import { isUuidListParam, splitIdListParam } from '@open-mercato/shared/lib/crud/ids'
|
|
25
25
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
26
26
|
import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
|
|
27
|
+
import { expandCreatedAtDayRules } from '../../lib/createdAtDayFilter'
|
|
27
28
|
import {
|
|
28
29
|
createCustomersCrudOpenApi,
|
|
29
30
|
createPagedListResponseSchema,
|
|
@@ -128,7 +129,7 @@ const crud = makeCrudRoute({
|
|
|
128
129
|
updatedAt: 'updated_at',
|
|
129
130
|
},
|
|
130
131
|
buildFilters: async (query, ctx) => {
|
|
131
|
-
const advancedFilterTree = consumeAdvancedFilterState(query)
|
|
132
|
+
const advancedFilterTree = expandCreatedAtDayRules(consumeAdvancedFilterState(query), 'UTC')
|
|
132
133
|
const filters: Record<string, unknown> = { kind: { $eq: 'person' } }
|
|
133
134
|
if (query.id) filters.id = { $eq: query.id }
|
|
134
135
|
if (query.search) {
|
|
@@ -58,6 +58,9 @@ import {
|
|
|
58
58
|
} from '../../../components/detail/assignableStaff'
|
|
59
59
|
import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
|
|
60
60
|
import { appendCustomerListSortParams } from '../listSorting'
|
|
61
|
+
import { expandCreatedAtDayRules } from '../../../lib/createdAtDayFilter'
|
|
62
|
+
import { USER_TIMEZONE } from '../../../lib/localDay'
|
|
63
|
+
import { formatInTimeZone } from 'date-fns-tz'
|
|
61
64
|
|
|
62
65
|
type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
|
|
63
66
|
|
|
@@ -74,8 +77,9 @@ function makeCompaniesPresets(): FilterPreset[] {
|
|
|
74
77
|
labelKey: 'customers.companies.presets.recentlyCreated',
|
|
75
78
|
iconName: 'clock',
|
|
76
79
|
build: ({ now }) => {
|
|
77
|
-
const
|
|
78
|
-
|
|
80
|
+
const dayBeforeWindow = new Date(`${formatInTimeZone(now, USER_TIMEZONE, 'yyyy-MM-dd')}T00:00:00.000Z`)
|
|
81
|
+
dayBeforeWindow.setUTCDate(dayBeforeWindow.getUTCDate() - 8)
|
|
82
|
+
return makeRuleTree({ field: 'created_at', operator: 'is_after', value: dayBeforeWindow.toISOString().slice(0, 10) })
|
|
79
83
|
},
|
|
80
84
|
},
|
|
81
85
|
{
|
|
@@ -112,6 +116,7 @@ type CompanyRow = {
|
|
|
112
116
|
organizationId?: string | null
|
|
113
117
|
source?: string | null
|
|
114
118
|
ownerUserId?: string | null
|
|
119
|
+
createdAt?: string | null
|
|
115
120
|
} & Record<string, unknown>
|
|
116
121
|
|
|
117
122
|
type CompaniesResponse = {
|
|
@@ -159,6 +164,7 @@ function mapApiItem(item: Record<string, unknown>): CompanyRow | null {
|
|
|
159
164
|
const nextInteractionColor = typeof item.next_interaction_color === 'string' ? item.next_interaction_color : null
|
|
160
165
|
const organizationId = typeof item.organization_id === 'string' ? item.organization_id : null
|
|
161
166
|
const source = typeof item.source === 'string' ? item.source : null
|
|
167
|
+
const createdAt = typeof item.created_at === 'string' ? item.created_at : null
|
|
162
168
|
const ownerUserId = typeof item.owner_user_id === 'string' ? item.owner_user_id : null
|
|
163
169
|
const customFields: Record<string, unknown> = {}
|
|
164
170
|
for (const [key, value] of Object.entries(item)) {
|
|
@@ -188,6 +194,7 @@ function mapApiItem(item: Record<string, unknown>): CompanyRow | null {
|
|
|
188
194
|
organizationId,
|
|
189
195
|
source,
|
|
190
196
|
ownerUserId,
|
|
197
|
+
createdAt,
|
|
191
198
|
...customFields,
|
|
192
199
|
}, item)
|
|
193
200
|
}
|
|
@@ -364,7 +371,7 @@ export default function CustomersCompaniesPage() {
|
|
|
364
371
|
params.set('pageSize', String(pageSize))
|
|
365
372
|
appendCustomerListSortParams(params, sorting)
|
|
366
373
|
if (search.trim()) params.set('search', search.trim())
|
|
367
|
-
const advancedParams = serializeTree(advancedFilterState)
|
|
374
|
+
const advancedParams = serializeTree(expandCreatedAtDayRules(advancedFilterState, USER_TIMEZONE))
|
|
368
375
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
369
376
|
params.set(key, val)
|
|
370
377
|
}
|
|
@@ -707,6 +714,22 @@ export default function CustomersCompaniesPage() {
|
|
|
707
714
|
)
|
|
708
715
|
: noValue,
|
|
709
716
|
},
|
|
717
|
+
{
|
|
718
|
+
accessorKey: 'createdAt',
|
|
719
|
+
header: t('customers.companies.list.columns.createdAt', 'Created'),
|
|
720
|
+
meta: {
|
|
721
|
+
columnChooserGroup: t('customers.columnGroups.dates', 'Dates'),
|
|
722
|
+
filterKey: 'created_at',
|
|
723
|
+
filterType: 'date' as const,
|
|
724
|
+
filterGroup: t('customers.columnGroups.activity', 'Activity'),
|
|
725
|
+
filterIconName: 'calendar',
|
|
726
|
+
},
|
|
727
|
+
cell: ({ row }) => (
|
|
728
|
+
<span className="text-sm">
|
|
729
|
+
{formatDate(row.original.createdAt, t('customers.companies.list.noValue'))}
|
|
730
|
+
</span>
|
|
731
|
+
),
|
|
732
|
+
},
|
|
710
733
|
{
|
|
711
734
|
accessorKey: 'source',
|
|
712
735
|
header: t('customers.companies.list.columns.source'),
|
|
@@ -7,6 +7,7 @@ const CUSTOMER_LIST_SORT_FIELDS: Record<string, string> = {
|
|
|
7
7
|
lifecycleStage: 'lifecycleStage',
|
|
8
8
|
source: 'source',
|
|
9
9
|
nextInteractionAt: 'nextInteractionAt',
|
|
10
|
+
createdAt: 'createdAt',
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function resolveCustomerListSortField(columnId: string): string | null {
|
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
} from '../../../components/detail/assignableStaff'
|
|
59
59
|
import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
|
|
60
60
|
import { appendCustomerListSortParams } from '../listSorting'
|
|
61
|
+
import { expandCreatedAtDayRules } from '../../../lib/createdAtDayFilter'
|
|
62
|
+
import { USER_TIMEZONE } from '../../../lib/localDay'
|
|
61
63
|
|
|
62
64
|
type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
|
|
63
65
|
|
|
@@ -119,6 +121,7 @@ type PersonRow = {
|
|
|
119
121
|
organizationId?: string | null
|
|
120
122
|
source?: string | null
|
|
121
123
|
ownerUserId?: string | null
|
|
124
|
+
createdAt?: string | null
|
|
122
125
|
} & Record<string, unknown>
|
|
123
126
|
|
|
124
127
|
type PeopleResponse = {
|
|
@@ -166,6 +169,7 @@ function mapApiItem(item: Record<string, unknown>): PersonRow | null {
|
|
|
166
169
|
const nextInteractionColor = typeof item.next_interaction_color === 'string' ? item.next_interaction_color : null
|
|
167
170
|
const organizationId = typeof item.organization_id === 'string' ? item.organization_id : null
|
|
168
171
|
const source = typeof item.source === 'string' ? item.source : null
|
|
172
|
+
const createdAt = typeof item.created_at === 'string' ? item.created_at : null
|
|
169
173
|
const customFields: Record<string, unknown> = {}
|
|
170
174
|
for (const [key, value] of Object.entries(item)) {
|
|
171
175
|
if (key.startsWith('cf_')) {
|
|
@@ -196,6 +200,7 @@ function mapApiItem(item: Record<string, unknown>): PersonRow | null {
|
|
|
196
200
|
nextInteractionColor,
|
|
197
201
|
organizationId,
|
|
198
202
|
source,
|
|
203
|
+
createdAt,
|
|
199
204
|
...customFields,
|
|
200
205
|
}, item)
|
|
201
206
|
}
|
|
@@ -375,7 +380,7 @@ export default function CustomersPeoplePage() {
|
|
|
375
380
|
params.set('pageSize', String(pageSize))
|
|
376
381
|
appendCustomerListSortParams(params, sorting)
|
|
377
382
|
if (search.trim()) params.set('search', search.trim())
|
|
378
|
-
const advancedParams = serializeTree(advancedFilterState)
|
|
383
|
+
const advancedParams = serializeTree(expandCreatedAtDayRules(advancedFilterState, USER_TIMEZONE))
|
|
379
384
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
380
385
|
params.set(key, val)
|
|
381
386
|
}
|
|
@@ -713,6 +718,22 @@ export default function CustomersPeoplePage() {
|
|
|
713
718
|
)
|
|
714
719
|
: <span className="text-muted-foreground text-sm">{t('customers.people.list.noValue')}</span>,
|
|
715
720
|
},
|
|
721
|
+
{
|
|
722
|
+
accessorKey: 'createdAt',
|
|
723
|
+
header: t('customers.people.list.columns.createdAt', 'Created'),
|
|
724
|
+
meta: {
|
|
725
|
+
columnChooserGroup: t('customers.columnGroups.dates', 'Dates'),
|
|
726
|
+
filterKey: 'created_at',
|
|
727
|
+
filterType: 'date' as const,
|
|
728
|
+
filterGroup: t('customers.columnGroups.activity', 'Activity'),
|
|
729
|
+
filterIconName: 'calendar',
|
|
730
|
+
},
|
|
731
|
+
cell: ({ row }) => (
|
|
732
|
+
<span className="text-sm">
|
|
733
|
+
{formatDate(row.original.createdAt, t('customers.people.list.noValue'))}
|
|
734
|
+
</span>
|
|
735
|
+
),
|
|
736
|
+
},
|
|
716
737
|
{
|
|
717
738
|
accessorKey: 'source',
|
|
718
739
|
header: t('customers.people.list.columns.source'),
|
|
@@ -797,6 +797,7 @@
|
|
|
797
797
|
"customers.companies.list.bulkDelete.progressName": "Ausgewählte Unternehmen löschen",
|
|
798
798
|
"customers.companies.list.bulkDelete.success": "{count} companies deleted",
|
|
799
799
|
"customers.companies.list.bulkDelete.title": "Delete {count} companies?",
|
|
800
|
+
"customers.companies.list.columns.createdAt": "Erstellt am",
|
|
800
801
|
"customers.companies.list.columns.email": "E-Mail",
|
|
801
802
|
"customers.companies.list.columns.lifecycleStage": "Lebenszyklusphase",
|
|
802
803
|
"customers.companies.list.columns.name": "Name",
|
|
@@ -2512,6 +2513,7 @@
|
|
|
2512
2513
|
"customers.people.list.bulkDelete.progressName": "Ausgewählte Personen löschen",
|
|
2513
2514
|
"customers.people.list.bulkDelete.success": "{count} people deleted",
|
|
2514
2515
|
"customers.people.list.bulkDelete.title": "Delete {count} people?",
|
|
2516
|
+
"customers.people.list.columns.createdAt": "Erstellt am",
|
|
2515
2517
|
"customers.people.list.columns.email": "E-Mail",
|
|
2516
2518
|
"customers.people.list.columns.lifecycleStage": "Lebenszyklusphase",
|
|
2517
2519
|
"customers.people.list.columns.name": "Name",
|
|
@@ -797,6 +797,7 @@
|
|
|
797
797
|
"customers.companies.list.bulkDelete.progressName": "Delete selected companies",
|
|
798
798
|
"customers.companies.list.bulkDelete.success": "{count} companies deleted",
|
|
799
799
|
"customers.companies.list.bulkDelete.title": "Delete {count} companies?",
|
|
800
|
+
"customers.companies.list.columns.createdAt": "Created",
|
|
800
801
|
"customers.companies.list.columns.email": "Email",
|
|
801
802
|
"customers.companies.list.columns.lifecycleStage": "Lifecycle stage",
|
|
802
803
|
"customers.companies.list.columns.name": "Name",
|
|
@@ -2512,6 +2513,7 @@
|
|
|
2512
2513
|
"customers.people.list.bulkDelete.progressName": "Delete selected people",
|
|
2513
2514
|
"customers.people.list.bulkDelete.success": "{count} people deleted",
|
|
2514
2515
|
"customers.people.list.bulkDelete.title": "Delete {count} people?",
|
|
2516
|
+
"customers.people.list.columns.createdAt": "Created",
|
|
2515
2517
|
"customers.people.list.columns.email": "Email",
|
|
2516
2518
|
"customers.people.list.columns.lifecycleStage": "Lifecycle stage",
|
|
2517
2519
|
"customers.people.list.columns.name": "Name",
|
|
@@ -797,6 +797,7 @@
|
|
|
797
797
|
"customers.companies.list.bulkDelete.progressName": "Eliminar empresas seleccionadas",
|
|
798
798
|
"customers.companies.list.bulkDelete.success": "{count} companies deleted",
|
|
799
799
|
"customers.companies.list.bulkDelete.title": "Delete {count} companies?",
|
|
800
|
+
"customers.companies.list.columns.createdAt": "Fecha de creación",
|
|
800
801
|
"customers.companies.list.columns.email": "Correo electrónico",
|
|
801
802
|
"customers.companies.list.columns.lifecycleStage": "Etapa del ciclo de vida",
|
|
802
803
|
"customers.companies.list.columns.name": "Nombre",
|
|
@@ -2512,6 +2513,7 @@
|
|
|
2512
2513
|
"customers.people.list.bulkDelete.progressName": "Eliminar personas seleccionadas",
|
|
2513
2514
|
"customers.people.list.bulkDelete.success": "{count} people deleted",
|
|
2514
2515
|
"customers.people.list.bulkDelete.title": "Delete {count} people?",
|
|
2516
|
+
"customers.people.list.columns.createdAt": "Fecha de creación",
|
|
2515
2517
|
"customers.people.list.columns.email": "Correo electrónico",
|
|
2516
2518
|
"customers.people.list.columns.lifecycleStage": "Etapa del ciclo de vida",
|
|
2517
2519
|
"customers.people.list.columns.name": "Nombre",
|
|
@@ -797,6 +797,7 @@
|
|
|
797
797
|
"customers.companies.list.bulkDelete.progressName": "선택한 회사 삭제",
|
|
798
798
|
"customers.companies.list.bulkDelete.success": "회사 {count}개가 삭제되었습니다",
|
|
799
799
|
"customers.companies.list.bulkDelete.title": "회사 {count}개를 삭제하시겠습니까?",
|
|
800
|
+
"customers.companies.list.columns.createdAt": "생성일",
|
|
800
801
|
"customers.companies.list.columns.email": "이메일",
|
|
801
802
|
"customers.companies.list.columns.lifecycleStage": "라이프사이클 단계",
|
|
802
803
|
"customers.companies.list.columns.name": "이름",
|
|
@@ -2512,6 +2513,7 @@
|
|
|
2512
2513
|
"customers.people.list.bulkDelete.progressName": "선택한 연락처 삭제",
|
|
2513
2514
|
"customers.people.list.bulkDelete.success": "{count}명의 연락처가 삭제되었습니다",
|
|
2514
2515
|
"customers.people.list.bulkDelete.title": "{count}명의 연락처를 삭제하시겠습니까?",
|
|
2516
|
+
"customers.people.list.columns.createdAt": "생성일",
|
|
2515
2517
|
"customers.people.list.columns.email": "이메일",
|
|
2516
2518
|
"customers.people.list.columns.lifecycleStage": "라이프사이클 단계",
|
|
2517
2519
|
"customers.people.list.columns.name": "이름",
|
|
@@ -797,6 +797,7 @@
|
|
|
797
797
|
"customers.companies.list.bulkDelete.progressName": "Usuń zaznaczone firmy",
|
|
798
798
|
"customers.companies.list.bulkDelete.success": "Usunięto {count} firm",
|
|
799
799
|
"customers.companies.list.bulkDelete.title": "Usunąć {count} firm?",
|
|
800
|
+
"customers.companies.list.columns.createdAt": "Data utworzenia",
|
|
800
801
|
"customers.companies.list.columns.email": "E-mail",
|
|
801
802
|
"customers.companies.list.columns.lifecycleStage": "Etap cyklu życia",
|
|
802
803
|
"customers.companies.list.columns.name": "Nazwa",
|
|
@@ -2512,6 +2513,7 @@
|
|
|
2512
2513
|
"customers.people.list.bulkDelete.progressName": "Usuń zaznaczone osoby",
|
|
2513
2514
|
"customers.people.list.bulkDelete.success": "Usunięto {count} osób",
|
|
2514
2515
|
"customers.people.list.bulkDelete.title": "Usunąć {count} osób?",
|
|
2516
|
+
"customers.people.list.columns.createdAt": "Data utworzenia",
|
|
2515
2517
|
"customers.people.list.columns.email": "E-mail",
|
|
2516
2518
|
"customers.people.list.columns.lifecycleStage": "Etap cyklu życia",
|
|
2517
2519
|
"customers.people.list.columns.name": "Nazwa",
|