@open-mercato/core 0.4.11-develop.1365.0acff7b08e → 0.4.11-develop.1366.20ce92f196

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.
@@ -1,36 +1,59 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
- import { getModules } from "@open-mercato/shared/lib/i18n/server";
3
+ import { getModules, resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
- import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
7
- import { hasAllFeatures } from "@open-mercato/shared/security/features";
8
- import { CustomEntity } from "@open-mercato/core/modules/entities/data/entities";
9
- import { slugifySidebarId } from "@open-mercato/shared/modules/navigation/sidebarPreferences";
10
- import { applySidebarPreference, loadFirstRoleSidebarPreference, loadSidebarPreference } from "../../services/sidebarPreferencesService.js";
11
- import { Role } from "../../data/entities.js";
6
+ import { resolveFeatureCheckContext } from "@open-mercato/core/modules/directory/utils/organizationScope";
7
+ import { resolveBackendChromePayload } from "../../lib/backendChrome.js";
12
8
  const metadata = {
13
9
  GET: { requireAuth: true }
14
10
  };
15
11
  const sidebarNavItemSchema = z.lazy(
16
12
  () => z.object({
13
+ id: z.string().optional(),
17
14
  href: z.string(),
18
15
  title: z.string(),
19
- defaultTitle: z.string(),
20
- enabled: z.boolean(),
16
+ defaultTitle: z.string().optional(),
17
+ enabled: z.boolean().optional(),
21
18
  hidden: z.boolean().optional(),
19
+ pageContext: z.enum(["main", "admin", "settings", "profile"]).optional(),
20
+ iconMarkup: z.string().optional(),
22
21
  children: z.array(sidebarNavItemSchema).optional()
23
22
  })
24
23
  );
24
+ const sectionItemSchema = z.lazy(
25
+ () => z.object({
26
+ id: z.string(),
27
+ label: z.string(),
28
+ labelKey: z.string().optional(),
29
+ href: z.string(),
30
+ order: z.number().optional(),
31
+ iconMarkup: z.string().optional(),
32
+ children: z.array(sectionItemSchema).optional()
33
+ })
34
+ );
35
+ const sectionGroupSchema = z.object({
36
+ id: z.string(),
37
+ label: z.string(),
38
+ labelKey: z.string().optional(),
39
+ order: z.number().optional(),
40
+ items: z.array(sectionItemSchema)
41
+ });
25
42
  const adminNavResponseSchema = z.object({
26
43
  groups: z.array(
27
44
  z.object({
28
- id: z.string(),
45
+ id: z.string().optional(),
29
46
  name: z.string(),
30
- defaultName: z.string(),
47
+ defaultName: z.string().optional(),
31
48
  items: z.array(sidebarNavItemSchema)
32
49
  })
33
- )
50
+ ),
51
+ settingsSections: z.array(sectionGroupSchema),
52
+ settingsPathPrefixes: z.array(z.string()),
53
+ profileSections: z.array(sectionGroupSchema),
54
+ profilePathPrefixes: z.array(z.string()),
55
+ grantedFeatures: z.array(z.string()),
56
+ roles: z.array(z.string())
34
57
  });
35
58
  const adminNavErrorSchema = z.object({
36
59
  error: z.string()
@@ -39,241 +62,62 @@ async function GET(req) {
39
62
  const auth = await getAuthFromRequest(req);
40
63
  if (!auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
41
64
  const { translate, locale } = await resolveTranslations();
42
- const { resolve } = await createRequestContainer();
43
- const em = resolve("em");
44
- const rbac = resolve("rbacService");
45
- const cache = resolve("cache");
46
- const cacheKey = `nav:sidebar:${locale}:${auth.sub}:${auth.tenantId || "null"}:${auth.orgId || "null"}`;
47
- const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
48
- const entries = [];
49
- function capitalize(s) {
50
- return s.charAt(0).toUpperCase() + s.slice(1);
51
- }
52
- function deriveTitleFromPath(p) {
53
- const seg = p.split("/").filter(Boolean).pop() || "";
54
- return seg ? seg.split("-").map(capitalize).join(" ") : "Home";
55
- }
56
- const ctx = { auth: { roles: auth.roles || [], sub: auth.sub, tenantId: auth.tenantId, orgId: auth.orgId } };
57
- const modules = getModules();
58
- for (const m of modules) {
59
- const groupDefault = capitalize(m.id);
60
- for (const r of m.backendRoutes || []) {
61
- const href = r.pattern ?? r.path ?? "";
62
- if (!href || href.includes("[")) continue;
63
- if (r.navHidden) continue;
64
- const title = r.title || deriveTitleFromPath(href);
65
- const titleKey = r.pageTitleKey ?? r.titleKey;
66
- const groupName = r.group || groupDefault;
67
- const groupKey = r.pageGroupKey ?? r.groupKey;
68
- const groupId = typeof groupKey === "string" && groupKey ? groupKey : slugifySidebarId(groupName);
69
- const visible = r.visible ? await Promise.resolve(r.visible(ctx)) : true;
70
- if (!visible) continue;
71
- const enabled = r.enabled ? await Promise.resolve(r.enabled(ctx)) : true;
72
- const requiredRoles = r.requireRoles || [];
73
- if (requiredRoles.length) {
74
- const roles = auth.roles || [];
75
- const ok = requiredRoles.some((role) => roles.includes(role));
76
- if (!ok) continue;
77
- }
78
- const features = r.requireFeatures;
79
- if (!acl.isSuperAdmin && !hasAllFeatures(acl.features, features)) continue;
80
- const order = r.order;
81
- const priority = r.priority ?? order;
82
- entries.push({ groupId, groupName, groupKey, title, titleKey, href, enabled, order, priority });
83
- }
84
- }
85
- const roots = [];
86
- for (const e of entries) {
87
- let parent;
88
- for (const p of entries) {
89
- if (p === e) continue;
90
- if (p.groupId !== e.groupId) continue;
91
- if (!e.href.startsWith(p.href + "/")) continue;
92
- if (!parent || p.href.length > parent.href.length) parent = p;
93
- }
94
- if (parent) {
95
- ;
96
- parent.children = parent.children || [];
97
- parent.children.push(e);
98
- } else {
99
- roots.push(e);
100
- }
101
- }
102
- const where = { isActive: true, showInSidebar: true };
103
- where.$and = [
104
- { $or: [{ organizationId: auth.orgId ?? void 0 }, { organizationId: null }] },
105
- { $or: [{ tenantId: auth.tenantId ?? void 0 }, { tenantId: null }] }
106
- ];
65
+ const container = await createRequestContainer();
66
+ const cache = container.resolve("cache");
67
+ let selectedOrganizationId;
68
+ let selectedTenantId;
107
69
  try {
108
- const entities = await em.find(CustomEntity, where, { orderBy: { label: "asc" } });
109
- const items = entities.map((e) => ({
110
- entityId: e.entityId,
111
- label: e.label,
112
- href: `/backend/entities/user/${encodeURIComponent(e.entityId)}/records`
113
- }));
114
- if (items.length) {
115
- const userEntitiesLegacyGroupKeys = /* @__PURE__ */ new Set(["settings.sections.dataDesigner", "entities.nav.group"]);
116
- const userEntitiesAnchor = entries.find((entry) => entry.href === "/backend/entities/user") ?? entries.find(
117
- (entry) => entry.titleKey === "entities.nav.userEntities" && typeof entry.groupKey === "string" && userEntitiesLegacyGroupKeys.has(entry.groupKey)
118
- );
119
- if (userEntitiesAnchor) {
120
- const existing = userEntitiesAnchor.children || [];
121
- const dynamic = items.map((it) => ({
122
- groupId: userEntitiesAnchor.groupId,
123
- groupName: userEntitiesAnchor.groupName,
124
- groupKey: userEntitiesAnchor.groupKey,
125
- title: it.label,
126
- href: it.href,
127
- enabled: true,
128
- order: 1e3,
129
- priority: 1e3
130
- }));
131
- const byHref = /* @__PURE__ */ new Map();
132
- for (const c of existing) if (!byHref.has(c.href)) byHref.set(c.href, c);
133
- for (const c of dynamic) if (!byHref.has(c.href)) byHref.set(c.href, c);
134
- userEntitiesAnchor.children = Array.from(byHref.values());
135
- }
136
- }
137
- } catch (e) {
138
- console.error("Error loading user entities", e);
70
+ const url = new URL(req.url);
71
+ const orgParam = url.searchParams.get("orgId");
72
+ const tenantParam = url.searchParams.get("tenantId");
73
+ selectedOrganizationId = orgParam === null ? void 0 : orgParam || null;
74
+ selectedTenantId = tenantParam === null ? void 0 : tenantParam || null;
75
+ } catch {
76
+ selectedOrganizationId = void 0;
77
+ selectedTenantId = void 0;
139
78
  }
140
- const sortItems = (arr) => {
141
- arr.sort((a, b) => {
142
- if (a.group !== b.group) return a.group.localeCompare(b.group);
143
- const ap = a.priority ?? a.order ?? 1e4;
144
- const bp = b.priority ?? b.order ?? 1e4;
145
- if (ap !== bp) return ap - bp;
146
- return String(a.title).localeCompare(String(b.title));
79
+ let cacheScopeTenantId = auth.tenantId ?? null;
80
+ let cacheScopeOrganizationId = auth.orgId ?? null;
81
+ try {
82
+ const { organizationId, scope } = await resolveFeatureCheckContext({
83
+ container,
84
+ auth,
85
+ selectedId: selectedOrganizationId,
86
+ tenantId: selectedTenantId,
87
+ request: req
147
88
  });
148
- for (const it of arr) if (it.children?.length) sortItems(it.children);
149
- };
150
- sortItems(roots);
151
- const groupBuckets = /* @__PURE__ */ new Map();
152
- for (const entry of roots) {
153
- const weight = entry.priority ?? entry.order ?? 1e4;
154
- if (!groupBuckets.has(entry.groupId)) {
155
- groupBuckets.set(entry.groupId, {
156
- id: entry.groupId,
157
- rawName: entry.groupName,
158
- key: entry.groupKey,
159
- weight,
160
- entries: [entry]
161
- });
162
- } else {
163
- const bucket = groupBuckets.get(entry.groupId);
164
- bucket.entries.push(entry);
165
- if (weight < bucket.weight) bucket.weight = weight;
166
- if (!bucket.key && entry.groupKey) bucket.key = entry.groupKey;
167
- if (!bucket.rawName && entry.groupName) bucket.rawName = entry.groupName;
168
- }
89
+ cacheScopeOrganizationId = organizationId;
90
+ cacheScopeTenantId = scope.tenantId ?? auth.tenantId ?? null;
91
+ } catch {
92
+ cacheScopeOrganizationId = auth.orgId ?? null;
93
+ cacheScopeTenantId = auth.tenantId ?? null;
169
94
  }
170
- const toItem = (entry) => {
171
- const defaultTitle = entry.titleKey ? translate(entry.titleKey, entry.title) : entry.title;
172
- return {
173
- href: entry.href,
174
- title: defaultTitle,
175
- defaultTitle,
176
- enabled: entry.enabled,
177
- children: entry.children?.map((child) => toItem(child))
178
- };
179
- };
180
- const groups = Array.from(groupBuckets.values()).map((bucket) => {
181
- const defaultName = bucket.key ? translate(bucket.key, bucket.rawName) : bucket.rawName;
182
- return {
183
- id: bucket.id,
184
- key: bucket.key,
185
- name: defaultName,
186
- defaultName,
187
- weight: bucket.weight,
188
- items: bucket.entries.map((entry) => toItem(entry))
189
- };
190
- });
191
- const defaultGroupOrder = [
192
- "customers.nav.group",
193
- "catalog.nav.group",
194
- "customers~sales.nav.group",
195
- "resources.nav.group",
196
- "staff.nav.group",
197
- "entities.nav.group",
198
- "directory.nav.group",
199
- "customers.storage.nav.group"
200
- ];
201
- const groupOrderIndex = new Map(defaultGroupOrder.map((id, index) => [id, index]));
202
- groups.sort((a, b) => {
203
- const aIndex = groupOrderIndex.get(a.id);
204
- const bIndex = groupOrderIndex.get(b.id);
205
- if (aIndex !== void 0 || bIndex !== void 0) {
206
- if (aIndex === void 0) return 1;
207
- if (bIndex === void 0) return -1;
208
- if (aIndex !== bIndex) return aIndex - bIndex;
209
- }
210
- if (a.weight !== b.weight) return a.weight - b.weight;
211
- return a.name.localeCompare(b.name);
212
- });
213
- const defaultGroupCount = defaultGroupOrder.length;
214
- groups.forEach((group, index) => {
215
- const rank = groupOrderIndex.get(group.id);
216
- const fallbackWeight = typeof group.weight === "number" ? group.weight : 1e4;
217
- const normalized = (rank !== void 0 ? rank : defaultGroupCount + index) * 1e6 + Math.min(Math.max(fallbackWeight, 0), 999999);
218
- group.weight = normalized;
219
- });
220
- let rolePreference = null;
221
- if (Array.isArray(auth.roles) && auth.roles.length) {
222
- const roleScope = auth.tenantId ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] } : { tenantId: null };
223
- const roleRecords = await em.find(Role, {
224
- name: { $in: auth.roles },
225
- ...roleScope
226
- });
227
- const roleIds = roleRecords.map((role) => role.id);
228
- if (roleIds.length) {
229
- rolePreference = await loadFirstRoleSidebarPreference(em, {
230
- roleIds,
231
- tenantId: auth.tenantId ?? null,
232
- locale
233
- });
95
+ const cacheKey = `nav:sidebar:${locale}:${auth.sub}:${cacheScopeTenantId || "null"}:${cacheScopeOrganizationId || "null"}`;
96
+ try {
97
+ if (cache?.get) {
98
+ const cached = await cache.get(cacheKey);
99
+ if (cached) return NextResponse.json(cached);
234
100
  }
101
+ } catch {
235
102
  }
236
- const groupsWithRole = rolePreference ? applySidebarPreference(groups, rolePreference) : groups;
237
- const baseForUser = adoptSidebarDefaults(groupsWithRole);
238
- const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub;
239
- const preference = effectiveUserId ? await loadSidebarPreference(em, {
240
- userId: effectiveUserId,
241
- tenantId: auth.tenantId ?? null,
242
- organizationId: auth.orgId ?? null,
243
- locale
244
- }) : null;
245
- const withPreference = applySidebarPreference(baseForUser, preference);
246
- const payload = {
247
- groups: withPreference.map((group) => ({
248
- id: group.id,
249
- name: group.name,
250
- defaultName: group.defaultName,
251
- items: group.items.map((item) => ({
252
- href: item.href,
253
- title: item.title,
254
- defaultTitle: item.defaultTitle,
255
- enabled: item.enabled,
256
- hidden: item.hidden,
257
- children: item.children?.map((child) => ({
258
- href: child.href,
259
- title: child.title,
260
- defaultTitle: child.defaultTitle,
261
- enabled: child.enabled,
262
- hidden: child.hidden
263
- }))
264
- }))
265
- }))
266
- };
103
+ const payload = await resolveBackendChromePayload({
104
+ auth,
105
+ locale,
106
+ modules: getModules(),
107
+ translate: (key, fallback) => key ? translate(key, fallback) : fallback,
108
+ selectedOrganizationId,
109
+ selectedTenantId
110
+ });
267
111
  try {
268
- if (cache) {
112
+ if (cache?.set) {
269
113
  const tags = [
270
114
  `rbac:user:${auth.sub}`,
271
- auth.tenantId ? `rbac:tenant:${auth.tenantId}` : void 0,
272
- `nav:entities:${auth.tenantId || "null"}`,
115
+ cacheScopeTenantId ? `rbac:tenant:${cacheScopeTenantId}` : void 0,
116
+ `nav:entities:${cacheScopeTenantId || "null"}`,
273
117
  `nav:locale:${locale}`,
274
118
  `nav:sidebar:user:${auth.sub}`,
275
- `nav:sidebar:scope:${auth.sub}:${auth.tenantId || "null"}:${auth.orgId || "null"}:${locale}`,
276
- ...Array.isArray(auth.roles) ? auth.roles.map((role) => `nav:sidebar:role:${role}`) : []
119
+ `nav:sidebar:scope:${auth.sub}:${cacheScopeTenantId || "null"}:${cacheScopeOrganizationId || "null"}:${locale}`,
120
+ ...(Array.isArray(auth.roles) ? auth.roles : []).map((role) => `nav:sidebar:role:${role}`)
277
121
  ].filter(Boolean);
278
122
  await cache.set(cacheKey, payload, { tags });
279
123
  }
@@ -281,27 +125,15 @@ async function GET(req) {
281
125
  }
282
126
  return NextResponse.json(payload);
283
127
  }
284
- function adoptSidebarDefaults(groups) {
285
- const adoptItems = (items) => items.map((item) => ({
286
- ...item,
287
- defaultTitle: item.title,
288
- children: item.children ? adoptItems(item.children) : void 0
289
- }));
290
- return groups.map((group) => ({
291
- ...group,
292
- defaultName: group.name,
293
- items: adoptItems(group.items)
294
- }));
295
- }
296
128
  const openApi = {
297
129
  tag: "Authentication & Accounts",
298
130
  summary: "Admin sidebar navigation",
299
131
  methods: {
300
132
  GET: {
301
- summary: "Resolve sidebar entries",
302
- description: "Returns the backend navigation tree available to the authenticated administrator after applying role and personal sidebar preferences.",
133
+ summary: "Resolve backend chrome bootstrap payload",
134
+ description: "Returns the backend chrome payload available to the authenticated administrator after applying scope, RBAC, role defaults, and personal sidebar preferences.",
303
135
  responses: [
304
- { status: 200, description: "Sidebar navigation structure", schema: adminNavResponseSchema },
136
+ { status: 200, description: "Backend chrome payload", schema: adminNavResponseSchema },
305
137
  { status: 401, description: "Unauthorized", schema: adminNavErrorSchema }
306
138
  ]
307
139
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/admin/nav.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { z } from 'zod'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport { CustomEntity } from '@open-mercato/core/modules/entities/data/entities'\nimport { slugifySidebarId } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { applySidebarPreference, loadFirstRoleSidebarPreference, loadSidebarPreference } from '../../services/sidebarPreferencesService'\nimport { Role } from '../../data/entities'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst sidebarNavItemSchema: z.ZodType<{ href: string; title: string; defaultTitle: string; enabled: boolean; hidden?: boolean; children?: any[] }> = z.lazy(() =>\n z.object({\n href: z.string(),\n title: z.string(),\n defaultTitle: z.string(),\n enabled: z.boolean(),\n hidden: z.boolean().optional(),\n children: z.array(sidebarNavItemSchema).optional(),\n })\n)\n\nconst adminNavResponseSchema = z.object({\n groups: z.array(\n z.object({\n id: z.string(),\n name: z.string(),\n defaultName: z.string(),\n items: z.array(sidebarNavItemSchema),\n })\n ),\n})\n\nconst adminNavErrorSchema = z.object({\n error: z.string(),\n})\n\ntype SidebarItemNode = {\n href: string\n title: string\n defaultTitle: string\n enabled: boolean\n hidden?: boolean\n children?: SidebarItemNode[]\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 { translate, locale } = await resolveTranslations()\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const cache = resolve('cache') as any\n\n // Cache key is user + tenant + organization scoped\n const cacheKey = `nav:sidebar:${locale}:${auth.sub}:${auth.tenantId || 'null'}:${auth.orgId || 'null'}`\n // try {\n // if (cache) {\n // const cached = await cache.get(cacheKey)\n // if (cached) return NextResponse.json(cached)\n // }\n // } catch {}\n\n // Load ACL once; we'll evaluate features locally without multiple calls\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n\n // Build nav entries from discovered backend routes\n type Entry = {\n groupId: string\n groupName: string\n groupKey?: string\n title: string\n titleKey?: string\n href: string\n enabled: boolean\n order?: number\n priority?: number\n children?: Entry[]\n }\n const entries: Entry[] = []\n\n function capitalize(s: string) { return s.charAt(0).toUpperCase() + s.slice(1) }\n function deriveTitleFromPath(p: string) {\n const seg = p.split('/').filter(Boolean).pop() || ''\n return seg ? seg.split('-').map(capitalize).join(' ') : 'Home'\n }\n\n const ctx = { auth: { roles: auth.roles || [], sub: auth.sub, tenantId: auth.tenantId, orgId: auth.orgId } }\n const modules = getModules()\n for (const m of (modules as any[])) {\n const groupDefault = capitalize(m.id)\n for (const r of (m.backendRoutes || [])) {\n const href = (r.pattern ?? r.path ?? '') as string\n if (!href || href.includes('[')) continue\n if ((r as any).navHidden) continue\n const title = (r.title as string) || deriveTitleFromPath(href)\n const titleKey = (r as any).pageTitleKey ?? (r as any).titleKey\n const groupName = (r.group as string) || groupDefault\n const groupKey = (r as any).pageGroupKey ?? (r as any).groupKey\n const groupId = typeof groupKey === 'string' && groupKey ? groupKey : slugifySidebarId(groupName)\n const visible = r.visible ? await Promise.resolve(r.visible(ctx)) : true\n if (!visible) continue\n const enabled = r.enabled ? await Promise.resolve(r.enabled(ctx)) : true\n const requiredRoles = (r.requireRoles as string[]) || []\n if (requiredRoles.length) {\n const roles = auth.roles || []\n const ok = requiredRoles.some((role) => roles.includes(role))\n if (!ok) continue\n }\n const features = (r as any).requireFeatures as string[] | undefined\n if (!acl.isSuperAdmin && !hasAllFeatures(acl.features, features)) continue\n const order = (r as any).order as number | undefined\n const priority = ((r as any).priority as number | undefined) ?? order\n entries.push({ groupId, groupName, groupKey, title, titleKey, href, enabled, order, priority })\n }\n }\n\n // Parent-child relationships within the same group by href prefix\n const roots: any[] = []\n for (const e of entries) {\n let parent: any | undefined\n for (const p of entries) {\n if (p === e) continue\n if (p.groupId !== e.groupId) continue\n if (!e.href.startsWith(p.href + '/')) continue\n if (!parent || p.href.length > parent.href.length) parent = p\n }\n if (parent) {\n ;(parent as any).children = (parent as any).children || []\n ;(parent as any).children.push(e)\n } else {\n roots.push(e)\n }\n }\n\n // Add dynamic user entities into Data designer > User Entities\n const where: any = { isActive: true, showInSidebar: true }\n where.$and = [\n { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] },\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n try {\n const entities = await em.find(CustomEntity as any, where as any, { orderBy: { label: 'asc' } as any })\n const items = (entities as any[]).map((e) => ({\n entityId: e.entityId,\n label: e.label,\n href: `/backend/entities/user/${encodeURIComponent(e.entityId)}/records`\n }))\n if (items.length) {\n const userEntitiesLegacyGroupKeys = new Set(['settings.sections.dataDesigner', 'entities.nav.group'])\n const userEntitiesAnchor = entries.find((entry: Entry) => entry.href === '/backend/entities/user')\n ?? entries.find((entry: Entry) =>\n entry.titleKey === 'entities.nav.userEntities' &&\n typeof entry.groupKey === 'string' &&\n userEntitiesLegacyGroupKeys.has(entry.groupKey),\n )\n if (userEntitiesAnchor) {\n const existing = userEntitiesAnchor.children || []\n const dynamic = items.map((it) => ({\n groupId: userEntitiesAnchor.groupId,\n groupName: userEntitiesAnchor.groupName,\n groupKey: userEntitiesAnchor.groupKey,\n title: it.label,\n href: it.href,\n enabled: true,\n order: 1000,\n priority: 1000,\n }))\n const byHref = new Map<string, Entry>()\n for (const c of existing) if (!byHref.has(c.href)) byHref.set(c.href, c)\n for (const c of dynamic) if (!byHref.has(c.href)) byHref.set(c.href, c)\n userEntitiesAnchor.children = Array.from(byHref.values())\n }\n }\n } catch (e) {\n console.error('Error loading user entities', e)\n }\n\n // Sort roots and children\n const sortItems = (arr: any[]) => {\n arr.sort((a, b) => {\n if (a.group !== b.group) return a.group.localeCompare(b.group)\n const ap = a.priority ?? a.order ?? 10000\n const bp = b.priority ?? b.order ?? 10000\n if (ap !== bp) return ap - bp\n return String(a.title).localeCompare(String(b.title))\n })\n for (const it of arr) if (it.children?.length) sortItems(it.children)\n }\n sortItems(roots)\n\n // Group into sidebar groups\n type GroupBucket = {\n id: string\n rawName: string\n key?: string\n weight: number\n entries: Entry[]\n }\n\n const groupBuckets = new Map<string, GroupBucket>()\n for (const entry of roots) {\n const weight = entry.priority ?? entry.order ?? 10_000\n if (!groupBuckets.has(entry.groupId)) {\n groupBuckets.set(entry.groupId, {\n id: entry.groupId,\n rawName: entry.groupName,\n key: entry.groupKey as string | undefined,\n weight,\n entries: [entry],\n })\n } else {\n const bucket = groupBuckets.get(entry.groupId)!\n bucket.entries.push(entry)\n if (weight < bucket.weight) bucket.weight = weight\n if (!bucket.key && entry.groupKey) bucket.key = entry.groupKey as string\n if (!bucket.rawName && entry.groupName) bucket.rawName = entry.groupName\n }\n }\n\n const toItem = (entry: Entry): SidebarItemNode => {\n const defaultTitle = entry.titleKey ? translate(entry.titleKey, entry.title) : entry.title\n return {\n href: entry.href,\n title: defaultTitle,\n defaultTitle,\n enabled: entry.enabled,\n children: entry.children?.map((child) => toItem(child)),\n }\n }\n\n const groups = Array.from(groupBuckets.values()).map((bucket) => {\n const defaultName = bucket.key ? translate(bucket.key, bucket.rawName) : bucket.rawName\n return {\n id: bucket.id,\n key: bucket.key,\n name: defaultName,\n defaultName,\n weight: bucket.weight,\n items: bucket.entries.map((entry) => toItem(entry)),\n }\n })\n const defaultGroupOrder = [\n 'customers.nav.group',\n 'catalog.nav.group',\n 'customers~sales.nav.group',\n 'resources.nav.group',\n 'staff.nav.group',\n 'entities.nav.group',\n 'directory.nav.group',\n 'customers.storage.nav.group',\n ]\n const groupOrderIndex = new Map(defaultGroupOrder.map((id, index) => [id, index]))\n groups.sort((a, b) => {\n const aIndex = groupOrderIndex.get(a.id)\n const bIndex = groupOrderIndex.get(b.id)\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) return 1\n if (bIndex === undefined) return -1\n if (aIndex !== bIndex) return aIndex - bIndex\n }\n if (a.weight !== b.weight) return a.weight - b.weight\n return a.name.localeCompare(b.name)\n })\n const defaultGroupCount = defaultGroupOrder.length\n groups.forEach((group, index) => {\n const rank = groupOrderIndex.get(group.id)\n const fallbackWeight = typeof group.weight === 'number' ? group.weight : 10_000\n const normalized =\n (rank !== undefined ? rank : defaultGroupCount + index) * 1_000_000 +\n Math.min(Math.max(fallbackWeight, 0), 999_999)\n group.weight = normalized\n })\n\n let rolePreference = null\n if (Array.isArray(auth.roles) && auth.roles.length) {\n const roleScope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roleRecords = await em.find(Role, {\n name: { $in: auth.roles },\n ...roleScope,\n } as any)\n const roleIds = roleRecords.map((role: Role) => role.id)\n if (roleIds.length) {\n rolePreference = await loadFirstRoleSidebarPreference(em, {\n roleIds,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n }\n }\n\n const groupsWithRole = rolePreference ? applySidebarPreference(groups, rolePreference) : groups\n const baseForUser = adoptSidebarDefaults(groupsWithRole)\n\n // For API key auth, use userId (the actual user) if available; otherwise skip user preferences\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n const preference = effectiveUserId\n ? await loadSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n const withPreference = applySidebarPreference(baseForUser, preference)\n\n const payload = {\n groups: withPreference.map((group) => ({\n id: group.id,\n name: group.name,\n defaultName: group.defaultName,\n items: (group.items as SidebarItemNode[]).map((item) => ({\n href: item.href,\n title: item.title,\n defaultTitle: item.defaultTitle,\n enabled: item.enabled,\n hidden: item.hidden,\n children: item.children?.map((child) => ({\n href: child.href,\n title: child.title,\n defaultTitle: child.defaultTitle,\n enabled: child.enabled,\n hidden: child.hidden,\n })),\n })),\n })),\n }\n\n try {\n if (cache) {\n const tags = [\n `rbac:user:${auth.sub}`,\n auth.tenantId ? `rbac:tenant:${auth.tenantId}` : undefined,\n `nav:entities:${auth.tenantId || 'null'}`,\n `nav:locale:${locale}`,\n `nav:sidebar:user:${auth.sub}`,\n `nav:sidebar:scope:${auth.sub}:${auth.tenantId || 'null'}:${auth.orgId || 'null'}:${locale}`,\n ...(Array.isArray(auth.roles) ? auth.roles.map((role: string) => `nav:sidebar:role:${role}`) : []),\n ].filter(Boolean) as string[]\n await cache.set(cacheKey, payload, { tags })\n }\n } catch {}\n\n return NextResponse.json(payload)\n}\n\nfunction adoptSidebarDefaults(groups: ReturnType<typeof applySidebarPreference>) {\n const adoptItems = <T extends { title: string; defaultTitle?: string; children?: T[] }>(items: T[]): T[] =>\n items.map((item) => ({\n ...item,\n defaultTitle: item.title,\n children: item.children ? adoptItems(item.children) : undefined,\n }))\n\n return groups.map((group) => ({\n ...group,\n defaultName: group.name,\n items: adoptItems(group.items),\n }))\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Admin sidebar navigation',\n methods: {\n GET: {\n summary: 'Resolve sidebar entries',\n description:\n 'Returns the backend navigation tree available to the authenticated administrator after applying role and personal sidebar preferences.',\n responses: [\n { status: 200, description: 'Sidebar navigation structure', schema: adminNavResponseSchema },\n { status: 401, description: 'Unauthorized', schema: adminNavErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { z } from 'zod'\nimport { getModules, resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveBackendChromePayload } from '../../lib/backendChrome'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst sidebarNavItemSchema: z.ZodType<{\n id?: string\n href: string\n title: string\n defaultTitle?: string\n enabled?: boolean\n hidden?: boolean\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n iconMarkup?: string\n children?: any[]\n}> = z.lazy(() =>\n z.object({\n id: z.string().optional(),\n href: z.string(),\n title: z.string(),\n defaultTitle: z.string().optional(),\n enabled: z.boolean().optional(),\n hidden: z.boolean().optional(),\n pageContext: z.enum(['main', 'admin', 'settings', 'profile']).optional(),\n iconMarkup: z.string().optional(),\n children: z.array(sidebarNavItemSchema).optional(),\n }),\n)\n\nconst sectionItemSchema: z.ZodType<{\n id: string\n label: string\n labelKey?: string\n href: string\n order?: number\n iconMarkup?: string\n children?: any[]\n}> = z.lazy(() =>\n z.object({\n id: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n href: z.string(),\n order: z.number().optional(),\n iconMarkup: z.string().optional(),\n children: z.array(sectionItemSchema).optional(),\n }),\n)\n\nconst sectionGroupSchema = z.object({\n id: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n order: z.number().optional(),\n items: z.array(sectionItemSchema),\n})\n\nconst adminNavResponseSchema = z.object({\n groups: z.array(\n z.object({\n id: z.string().optional(),\n name: z.string(),\n defaultName: z.string().optional(),\n items: z.array(sidebarNavItemSchema),\n }),\n ),\n settingsSections: z.array(sectionGroupSchema),\n settingsPathPrefixes: z.array(z.string()),\n profileSections: z.array(sectionGroupSchema),\n profilePathPrefixes: z.array(z.string()),\n grantedFeatures: z.array(z.string()),\n roles: z.array(z.string()),\n})\n\nconst adminNavErrorSchema = z.object({\n error: z.string(),\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 { translate, locale } = await resolveTranslations()\n const container = await createRequestContainer()\n const cache = container.resolve('cache') as {\n get?: (key: string) => Promise<unknown>\n set?: (key: string, value: unknown, options?: { tags?: string[] }) => Promise<void>\n } | null\n\n let selectedOrganizationId: string | null | undefined\n let selectedTenantId: string | null | undefined\n try {\n const url = new URL(req.url)\n const orgParam = url.searchParams.get('orgId')\n const tenantParam = url.searchParams.get('tenantId')\n selectedOrganizationId = orgParam === null ? undefined : orgParam || null\n selectedTenantId = tenantParam === null ? undefined : tenantParam || null\n } catch {\n selectedOrganizationId = undefined\n selectedTenantId = undefined\n }\n\n let cacheScopeTenantId = auth.tenantId ?? null\n let cacheScopeOrganizationId = auth.orgId ?? null\n try {\n const { organizationId, scope } = await resolveFeatureCheckContext({\n container,\n auth,\n selectedId: selectedOrganizationId,\n tenantId: selectedTenantId,\n request: req,\n })\n cacheScopeOrganizationId = organizationId\n cacheScopeTenantId = scope.tenantId ?? auth.tenantId ?? null\n } catch {\n cacheScopeOrganizationId = auth.orgId ?? null\n cacheScopeTenantId = auth.tenantId ?? null\n }\n\n const cacheKey = `nav:sidebar:${locale}:${auth.sub}:${cacheScopeTenantId || 'null'}:${cacheScopeOrganizationId || 'null'}`\n try {\n if (cache?.get) {\n const cached = await cache.get(cacheKey)\n if (cached) return NextResponse.json(cached)\n }\n } catch {\n // ignore cache read failures\n }\n\n const payload = await resolveBackendChromePayload({\n auth,\n locale,\n modules: getModules(),\n translate: (key, fallback) => (key ? translate(key, fallback) : fallback),\n selectedOrganizationId,\n selectedTenantId,\n })\n\n try {\n if (cache?.set) {\n const tags = [\n `rbac:user:${auth.sub}`,\n cacheScopeTenantId ? `rbac:tenant:${cacheScopeTenantId}` : undefined,\n `nav:entities:${cacheScopeTenantId || 'null'}`,\n `nav:locale:${locale}`,\n `nav:sidebar:user:${auth.sub}`,\n `nav:sidebar:scope:${auth.sub}:${cacheScopeTenantId || 'null'}:${cacheScopeOrganizationId || 'null'}:${locale}`,\n ...((Array.isArray(auth.roles) ? auth.roles : []).map((role) => `nav:sidebar:role:${role}`)),\n ].filter(Boolean) as string[]\n await cache.set(cacheKey, payload, { tags })\n }\n } catch {\n // ignore cache write failures\n }\n\n return NextResponse.json(payload)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Admin sidebar navigation',\n methods: {\n GET: {\n summary: 'Resolve backend chrome bootstrap payload',\n description:\n 'Returns the backend chrome payload available to the authenticated administrator after applying scope, RBAC, role defaults, and personal sidebar preferences.',\n responses: [\n { status: 200, description: 'Backend chrome payload', schema: adminNavResponseSchema },\n { status: 401, description: 'Unauthorized', schema: adminNavErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -23,15 +23,47 @@ const loginIpRateLimitConfig = readEndpointRateLimitConfig("LOGIN_IP", {
23
23
  keyPrefix: "login-ip"
24
24
  });
25
25
  const metadata = {};
26
+ function parseRequiredRoles(rawValue) {
27
+ return rawValue.split(",").map((value) => value.trim()).filter(Boolean);
28
+ }
29
+ async function parseLoginForm(req) {
30
+ const rawContentType = req.headers.get("content-type") ?? "";
31
+ const contentType = rawContentType.split(";")[0].trim().toLowerCase();
32
+ try {
33
+ if (contentType === "application/x-www-form-urlencoded") {
34
+ const body = await req.text();
35
+ const params = new URLSearchParams(body);
36
+ const requireRoleRaw2 = String(params.get("requireRole") ?? params.get("role") ?? "").trim();
37
+ return {
38
+ email: String(params.get("email") ?? ""),
39
+ password: String(params.get("password") ?? ""),
40
+ remember: parseBooleanToken(params.get("remember")) === true,
41
+ tenantIdRaw: String(params.get("tenantId") ?? params.get("tenant") ?? "").trim(),
42
+ requiredRoles: requireRoleRaw2 ? parseRequiredRoles(requireRoleRaw2) : []
43
+ };
44
+ }
45
+ const form = await req.formData();
46
+ const requireRoleRaw = String(form.get("requireRole") ?? form.get("role") ?? "").trim();
47
+ return {
48
+ email: String(form.get("email") ?? ""),
49
+ password: String(form.get("password") ?? ""),
50
+ remember: parseBooleanToken(form.get("remember")?.toString()) === true,
51
+ tenantIdRaw: String(form.get("tenantId") ?? form.get("tenant") ?? "").trim(),
52
+ requiredRoles: requireRoleRaw ? parseRequiredRoles(requireRoleRaw) : []
53
+ };
54
+ } catch {
55
+ return {
56
+ email: "",
57
+ password: "",
58
+ remember: false,
59
+ tenantIdRaw: "",
60
+ requiredRoles: []
61
+ };
62
+ }
63
+ }
26
64
  async function POST(req) {
27
65
  const { translate } = await resolveTranslations();
28
- const form = await req.formData();
29
- const email = String(form.get("email") ?? "");
30
- const password = String(form.get("password") ?? "");
31
- const remember = parseBooleanToken(form.get("remember")?.toString()) === true;
32
- const tenantIdRaw = String(form.get("tenantId") ?? form.get("tenant") ?? "").trim();
33
- const requireRoleRaw = String(form.get("requireRole") ?? form.get("role") ?? "").trim();
34
- const requiredRoles = requireRoleRaw ? requireRoleRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
66
+ const { email, password, remember, tenantIdRaw, requiredRoles } = await parseLoginForm(req);
35
67
  const { error: rateLimitError, compoundKey: rateLimitCompoundKey } = await checkAuthRateLimit({
36
68
  req,
37
69
  ipConfig: loginIpRateLimitConfig,
@@ -100,14 +132,14 @@ async function POST(req) {
100
132
  roles: userRoleNames
101
133
  });
102
134
  void emitAuthEvent("auth.login.success", { id: String(user.id), email: user.email, tenantId: resolvedTenantId, organizationId: user.organizationId ? String(user.organizationId) : null }).catch(() => void 0);
135
+ const rememberMeDays = Number(process.env.REMEMBER_ME_DAYS || "30");
103
136
  const responseData = {
104
137
  ok: true,
105
138
  token,
106
139
  redirect: "/backend"
107
140
  };
108
141
  if (remember) {
109
- const days = Number(process.env.REMEMBER_ME_DAYS || "30");
110
- const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1e3);
142
+ const expiresAt = new Date(Date.now() + rememberMeDays * 24 * 60 * 60 * 1e3);
111
143
  const sess = await auth.createSession(user, expiresAt);
112
144
  responseData.refreshToken = sess.token;
113
145
  }
@@ -145,8 +177,7 @@ async function POST(req) {
145
177
  const res = NextResponse.json(interceptedBody, { status: interceptedResponse.statusCode });
146
178
  res.cookies.set("auth_token", authTokenForCookie, { httpOnly: true, path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 60 * 60 * 8 });
147
179
  if (remember && refreshTokenForCookie) {
148
- const days = Number(process.env.REMEMBER_ME_DAYS || "30");
149
- const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1e3);
180
+ const expiresAt = new Date(Date.now() + rememberMeDays * 24 * 60 * 60 * 1e3);
150
181
  res.cookies.set("session_token", refreshTokenForCookie, { httpOnly: true, path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", expires: expiresAt });
151
182
  }
152
183
  return res;