@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.
- package/dist/modules/auth/api/admin/nav.js +83 -251
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/login.js +42 -11
- package/dist/modules/auth/api/login.js.map +3 -3
- package/dist/modules/auth/lib/backendChrome.js +256 -0
- package/dist/modules/auth/lib/backendChrome.js.map +7 -0
- package/package.json +3 -3
- package/src/modules/auth/api/admin/nav.ts +112 -319
- package/src/modules/auth/api/login.ts +57 -11
- package/src/modules/auth/lib/backendChrome.tsx +359 -0
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
272
|
-
`nav:entities:${
|
|
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}:${
|
|
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
|
|
302
|
-
description: "Returns the backend
|
|
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: "
|
|
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": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,SAAS;AAClB,SAAS,
|
|
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
|
|
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
|
|
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
|
|
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;
|