@dyrected/core 2.24.3 → 2.25.0
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/{app-Bg34iyUz.d.cts → app-Ti5woUOq.d.cts} +113 -3
- package/dist/{app-Bg34iyUz.d.ts → app-Ti5woUOq.d.ts} +113 -3
- package/dist/index.cjs +130 -21
- package/dist/index.d.cts +64 -4
- package/dist/index.d.ts +64 -4
- package/dist/index.js +123 -21
- package/dist/server.cjs +215 -51
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +215 -51
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1959,10 +1959,11 @@ var init_readonly_fix = __esm({
|
|
|
1959
1959
|
});
|
|
1960
1960
|
|
|
1961
1961
|
// src/types/navigation.ts
|
|
1962
|
-
var CURRENT_NAV_PREFERENCES_VERSION, DEFAULT_USER_NAV_PREFERENCES;
|
|
1962
|
+
var NAVIGATION_POLICY_KEY, CURRENT_NAV_PREFERENCES_VERSION, DEFAULT_USER_NAV_PREFERENCES;
|
|
1963
1963
|
var init_navigation = __esm({
|
|
1964
1964
|
"src/types/navigation.ts"() {
|
|
1965
1965
|
"use strict";
|
|
1966
|
+
NAVIGATION_POLICY_KEY = "admin:navigation-policy";
|
|
1966
1967
|
CURRENT_NAV_PREFERENCES_VERSION = 1;
|
|
1967
1968
|
DEFAULT_USER_NAV_PREFERENCES = {
|
|
1968
1969
|
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
@@ -7113,6 +7114,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7113
7114
|
};
|
|
7114
7115
|
}
|
|
7115
7116
|
const includeHidden = options?.includeHidden ?? false;
|
|
7117
|
+
const allowUserItems = options?.allowUserItems ?? true;
|
|
7116
7118
|
const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
|
|
7117
7119
|
const hiddenSet = new Set(effectivePrefs.hidden || []);
|
|
7118
7120
|
const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
|
|
@@ -7144,8 +7146,14 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7144
7146
|
});
|
|
7145
7147
|
}
|
|
7146
7148
|
}
|
|
7149
|
+
const ungroupedKeys = /* @__PURE__ */ new Set();
|
|
7150
|
+
for (const item of baseTree.ungrouped || []) {
|
|
7151
|
+
ungroupedKeys.add(item.id);
|
|
7152
|
+
if (item.slug) ungroupedKeys.add(item.slug);
|
|
7153
|
+
}
|
|
7147
7154
|
for (const itemOpt of effectivePrefs.items || []) {
|
|
7148
7155
|
const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
|
|
7156
|
+
if (ungroupedKeys.has(itemId) || itemOpt.slug && ungroupedKeys.has(itemOpt.slug)) continue;
|
|
7149
7157
|
let existingItem;
|
|
7150
7158
|
let existingParentGroup;
|
|
7151
7159
|
for (const g of groupsList) {
|
|
@@ -7156,6 +7164,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7156
7164
|
break;
|
|
7157
7165
|
}
|
|
7158
7166
|
}
|
|
7167
|
+
if (!existingItem && !allowUserItems) continue;
|
|
7159
7168
|
const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group || existingParentGroup?.name || "Custom";
|
|
7160
7169
|
const groupKey = targetGroup;
|
|
7161
7170
|
let group = findGroup(groupKey);
|
|
@@ -7185,7 +7194,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7185
7194
|
const baseViews = existingItem?.views || [];
|
|
7186
7195
|
const userViews = (itemOpt.views || []).map((uv) => {
|
|
7187
7196
|
const bv = baseViews.find((b) => b.slug === uv.slug);
|
|
7188
|
-
if (!bv) return uv;
|
|
7197
|
+
if (!bv) return { ...uv, access: void 0 };
|
|
7189
7198
|
const metricsScope = uv.metricsScope ?? bv.metricsScope;
|
|
7190
7199
|
let metrics = uv.metrics;
|
|
7191
7200
|
if (Array.isArray(uv.metrics) && Array.isArray(bv.metrics)) {
|
|
@@ -7206,6 +7215,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7206
7215
|
return {
|
|
7207
7216
|
...bv,
|
|
7208
7217
|
...uv,
|
|
7218
|
+
access: bv.access,
|
|
7209
7219
|
metricsScope,
|
|
7210
7220
|
metrics
|
|
7211
7221
|
};
|
|
@@ -7242,10 +7252,12 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7242
7252
|
group: group.name,
|
|
7243
7253
|
order: itemOpt.order ?? existingItem?.order ?? 100,
|
|
7244
7254
|
views: mergedViews,
|
|
7245
|
-
collection:
|
|
7246
|
-
global:
|
|
7247
|
-
|
|
7248
|
-
|
|
7255
|
+
collection: existingItem?.collection || itemOpt.collection,
|
|
7256
|
+
global: existingItem?.global || itemOpt.global,
|
|
7257
|
+
// Access and badges are policy/server-computed: only the server tree may set them.
|
|
7258
|
+
badge: existingItem?.badge,
|
|
7259
|
+
access: existingItem?.access,
|
|
7260
|
+
...existingItem ? existingItem.userDefined ? { userDefined: true } : {} : { userDefined: true }
|
|
7249
7261
|
};
|
|
7250
7262
|
if (existingIndex >= 0) {
|
|
7251
7263
|
group.items[existingIndex] = compiledItem;
|
|
@@ -7344,7 +7356,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7344
7356
|
label: pin.label || match.label,
|
|
7345
7357
|
icon: pin.icon || match.icon
|
|
7346
7358
|
});
|
|
7347
|
-
} else {
|
|
7359
|
+
} else if (allowUserItems) {
|
|
7348
7360
|
pinnedItems.push({
|
|
7349
7361
|
id: `pin_${pin.slug}`,
|
|
7350
7362
|
type: pin.type,
|
|
@@ -7363,6 +7375,62 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7363
7375
|
};
|
|
7364
7376
|
}
|
|
7365
7377
|
|
|
7378
|
+
// src/utils/navigation-policy.ts
|
|
7379
|
+
init_navigation();
|
|
7380
|
+
var isStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
7381
|
+
var itemKeys = (item) => [item.id, ...item.slug ? [item.slug] : []];
|
|
7382
|
+
var allTreeItems = (tree) => [
|
|
7383
|
+
...tree.groups.flatMap((g) => g.items),
|
|
7384
|
+
...tree.ungrouped
|
|
7385
|
+
];
|
|
7386
|
+
function sanitizeNavigationLayout(prefs, tree) {
|
|
7387
|
+
const known = new Set(allTreeItems(tree).flatMap(itemKeys));
|
|
7388
|
+
return {
|
|
7389
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
7390
|
+
hidden: isStringArray(prefs?.hidden) ? prefs.hidden : [],
|
|
7391
|
+
groupOrder: isStringArray(prefs?.groupOrder) ? prefs.groupOrder : [],
|
|
7392
|
+
itemOrder: Object.fromEntries(
|
|
7393
|
+
Object.entries(prefs?.itemOrder ?? {}).filter(([, order]) => isStringArray(order))
|
|
7394
|
+
),
|
|
7395
|
+
groups: Array.isArray(prefs?.groups) ? prefs.groups : [],
|
|
7396
|
+
items: (Array.isArray(prefs?.items) ? prefs.items : []).filter((item) => {
|
|
7397
|
+
if (item.slug) return known.has(item.slug) || known.has(`workspace_${item.slug}`);
|
|
7398
|
+
if (item.collection) return known.has(`collection_${item.collection}`);
|
|
7399
|
+
if (item.global) return known.has(`global_${item.global}`);
|
|
7400
|
+
return false;
|
|
7401
|
+
})
|
|
7402
|
+
};
|
|
7403
|
+
}
|
|
7404
|
+
function parseNavigationRolePolicy(value, tree) {
|
|
7405
|
+
if (!value || typeof value !== "object") return null;
|
|
7406
|
+
const raw = value;
|
|
7407
|
+
if (typeof raw.enforced !== "boolean" || !isStringArray(raw.allowed)) return null;
|
|
7408
|
+
const layout2 = sanitizeNavigationLayout(raw.layout, tree);
|
|
7409
|
+
return {
|
|
7410
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
7411
|
+
enforced: raw.enforced,
|
|
7412
|
+
allowed: raw.allowed,
|
|
7413
|
+
layout: {
|
|
7414
|
+
_version: layout2._version,
|
|
7415
|
+
hidden: layout2.hidden,
|
|
7416
|
+
groupOrder: layout2.groupOrder,
|
|
7417
|
+
itemOrder: layout2.itemOrder,
|
|
7418
|
+
groups: layout2.groups,
|
|
7419
|
+
items: layout2.items
|
|
7420
|
+
}
|
|
7421
|
+
};
|
|
7422
|
+
}
|
|
7423
|
+
function applyNavigationRolePolicies(tree, policies) {
|
|
7424
|
+
const enforced = policies.filter((p) => !!p?.enforced);
|
|
7425
|
+
if (policies.length === 0 || enforced.length !== policies.length) return tree;
|
|
7426
|
+
const allowed = new Set(enforced.flatMap((p) => p.allowed));
|
|
7427
|
+
const isAllowed = (item) => itemKeys(item).some((key) => allowed.has(key));
|
|
7428
|
+
const groups = tree.groups.map((g) => ({ ...g, items: g.items.filter(isAllowed) })).filter((g) => g.items.length > 0);
|
|
7429
|
+
const filtered = { groups, ungrouped: tree.ungrouped.filter(isAllowed) };
|
|
7430
|
+
const reconciled = reconcileNavigation(filtered, enforced[0].layout, void 0, { allowUserItems: false });
|
|
7431
|
+
return { groups: reconciled.groups, ungrouped: reconciled.ungrouped, locked: true };
|
|
7432
|
+
}
|
|
7433
|
+
|
|
7366
7434
|
// src/utils/navigation.ts
|
|
7367
7435
|
var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
7368
7436
|
"collections",
|
|
@@ -11982,7 +12050,7 @@ function layout({ preheader, title, content, footer, siteName }) {
|
|
|
11982
12050
|
${table(
|
|
11983
12051
|
row(
|
|
11984
12052
|
table(
|
|
11985
|
-
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${sectionLabel(siteName
|
|
12053
|
+
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${siteName?.trim() ? sectionLabel(siteName) : ""}${heading(title)}`, "padding:30px 32px 24px") + row(resolvedContent, "padding:0 32px 32px") + row(
|
|
11986
12054
|
`${divider()}${paragraph(resolvedFooter, "20px 0 6px")}${paragraph("Privacy: this message contains account-related information; please avoid forwarding it.", "10px 0px 0px 0px", "12px")}`,
|
|
11987
12055
|
"padding:0 32px 28px"
|
|
11988
12056
|
),
|
|
@@ -11997,14 +12065,15 @@ function layout({ preheader, title, content, footer, siteName }) {
|
|
|
11997
12065
|
</html>`;
|
|
11998
12066
|
}
|
|
11999
12067
|
function getDefaultEmailTemplate(purpose, options) {
|
|
12000
|
-
const siteName = options?.siteName ?? "
|
|
12068
|
+
const siteName = options?.siteName?.trim() ?? "";
|
|
12069
|
+
const account = siteName ? `${siteName} account` : "account";
|
|
12001
12070
|
switch (purpose) {
|
|
12002
12071
|
case "invite":
|
|
12003
12072
|
return {
|
|
12004
|
-
subject: `You've been invited to ${siteName}
|
|
12073
|
+
subject: siteName ? `You've been invited to ${siteName}` : "You've been invited",
|
|
12005
12074
|
rawHtml: layout({
|
|
12006
12075
|
siteName,
|
|
12007
|
-
preheader: `You've been invited to join ${siteName}
|
|
12076
|
+
preheader: siteName ? `You've been invited to join ${siteName}.` : "You've been invited to join.",
|
|
12008
12077
|
title: "You've been invited",
|
|
12009
12078
|
content: [
|
|
12010
12079
|
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
@@ -12021,7 +12090,7 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12021
12090
|
subject: "Reset your password",
|
|
12022
12091
|
rawHtml: layout({
|
|
12023
12092
|
siteName,
|
|
12024
|
-
preheader: `Reset your ${siteName} password
|
|
12093
|
+
preheader: siteName ? `Reset your ${siteName} password.` : "Reset your password.",
|
|
12025
12094
|
title: "Reset your password",
|
|
12026
12095
|
content: [
|
|
12027
12096
|
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
@@ -12035,10 +12104,10 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12035
12104
|
};
|
|
12036
12105
|
case "welcome":
|
|
12037
12106
|
return {
|
|
12038
|
-
subject: `Welcome to ${siteName} \u2014 your account is ready
|
|
12107
|
+
subject: siteName ? `Welcome to ${siteName} \u2014 your account is ready` : "Welcome \u2014 your account is ready",
|
|
12039
12108
|
rawHtml: layout({
|
|
12040
12109
|
siteName,
|
|
12041
|
-
preheader: `Your ${
|
|
12110
|
+
preheader: `Your ${account} is ready.`,
|
|
12042
12111
|
title: "Welcome \u2014 your account is ready",
|
|
12043
12112
|
content: [paragraph("Your account has been created. You can now log in with:"), detailBox("{{email}}")],
|
|
12044
12113
|
footer: "If you didn't create this account, you can safely ignore this email."
|
|
@@ -12049,7 +12118,7 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12049
12118
|
subject: "Your password has been changed",
|
|
12050
12119
|
rawHtml: layout({
|
|
12051
12120
|
siteName,
|
|
12052
|
-
preheader: `Your ${siteName} password was changed
|
|
12121
|
+
preheader: siteName ? `Your ${siteName} password was changed.` : "Your password was changed.",
|
|
12053
12122
|
title: "Password changed",
|
|
12054
12123
|
content: [
|
|
12055
12124
|
paragraph("The password for your account has been changed."),
|
|
@@ -12060,18 +12129,33 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12060
12129
|
};
|
|
12061
12130
|
default:
|
|
12062
12131
|
return {
|
|
12063
|
-
subject: `Notification from ${siteName}
|
|
12132
|
+
subject: siteName ? `Notification from ${siteName}` : "Account notification",
|
|
12064
12133
|
rawHtml: layout({
|
|
12065
12134
|
siteName,
|
|
12066
|
-
preheader: `Account notification from ${siteName}
|
|
12135
|
+
preheader: siteName ? `Account notification from ${siteName}.` : "Account notification.",
|
|
12067
12136
|
title: "Account Notification",
|
|
12068
|
-
content: [paragraph(`This is an account notification from ${siteName}.`)],
|
|
12137
|
+
content: [paragraph(siteName ? `This is an account notification from ${siteName}.` : "This is an account notification.")],
|
|
12069
12138
|
footer: "If you have any questions, please reach out to our team."
|
|
12070
12139
|
})
|
|
12071
12140
|
};
|
|
12072
12141
|
}
|
|
12073
12142
|
}
|
|
12074
12143
|
|
|
12144
|
+
// src/utils/app-name.ts
|
|
12145
|
+
var SUFFIX_SEPARATOR = /^[\s\-\u2013\u2014|\u00b7:]+/;
|
|
12146
|
+
function resolveAppName(admin) {
|
|
12147
|
+
const candidates = [
|
|
12148
|
+
admin?.branding?.appName,
|
|
12149
|
+
admin?.branding?.logoText,
|
|
12150
|
+
admin?.meta?.titleSuffix?.replace(SUFFIX_SEPARATOR, "")
|
|
12151
|
+
];
|
|
12152
|
+
for (const candidate of candidates) {
|
|
12153
|
+
const value = typeof candidate === "string" ? candidate.trim() : "";
|
|
12154
|
+
if (value) return value;
|
|
12155
|
+
}
|
|
12156
|
+
return "";
|
|
12157
|
+
}
|
|
12158
|
+
|
|
12075
12159
|
// src/services/email.service.ts
|
|
12076
12160
|
var _devSend = null;
|
|
12077
12161
|
var _devSendPromise = null;
|
|
@@ -12255,7 +12339,7 @@ function enrichArgs(config, collection, args) {
|
|
|
12255
12339
|
collection: slug,
|
|
12256
12340
|
collectionLabel: collection?.labels?.singular || collection?.labels?.plural || fallbackLabel,
|
|
12257
12341
|
collectionLabels: collection?.labels,
|
|
12258
|
-
siteName: config.admin
|
|
12342
|
+
siteName: resolveAppName(config.admin)
|
|
12259
12343
|
};
|
|
12260
12344
|
}
|
|
12261
12345
|
function toOutboundEmail(to, result, metadata) {
|
|
@@ -12291,7 +12375,7 @@ function buildWelcomeEmail(config, args, collection, dbOverride) {
|
|
|
12291
12375
|
subject: "Welcome \u2014 your account is ready",
|
|
12292
12376
|
html: layout({
|
|
12293
12377
|
siteName: enriched.siteName,
|
|
12294
|
-
preheader: `Your ${enriched.siteName} account is ready
|
|
12378
|
+
preheader: enriched.siteName ? `Your ${enriched.siteName} account is ready.` : "Your account is ready.",
|
|
12295
12379
|
title: "Welcome \u2014 your account is ready",
|
|
12296
12380
|
content: [
|
|
12297
12381
|
paragraph("Your account has been created. You can now log in with:"),
|
|
@@ -12316,7 +12400,7 @@ function buildInviteEmail(config, args, collection, dbOverride) {
|
|
|
12316
12400
|
subject: "You've been invited",
|
|
12317
12401
|
html: layout({
|
|
12318
12402
|
siteName: enriched.siteName,
|
|
12319
|
-
preheader: `You've been invited to join ${enriched.siteName}
|
|
12403
|
+
preheader: enriched.siteName ? `You've been invited to join ${enriched.siteName}.` : "You've been invited to join.",
|
|
12320
12404
|
title: "You've been invited",
|
|
12321
12405
|
content: [
|
|
12322
12406
|
args.invitedByEmail && paragraph(`You were invited by ${args.invitedByEmail}.`),
|
|
@@ -12351,7 +12435,7 @@ function buildResetPasswordEmail(config, args, collection, dbOverride) {
|
|
|
12351
12435
|
subject: "Reset your password",
|
|
12352
12436
|
html: layout({
|
|
12353
12437
|
siteName: enriched.siteName,
|
|
12354
|
-
preheader: `Reset your ${enriched.siteName} password
|
|
12438
|
+
preheader: enriched.siteName ? `Reset your ${enriched.siteName} password.` : "Reset your password.",
|
|
12355
12439
|
title: "Reset your password",
|
|
12356
12440
|
content: [
|
|
12357
12441
|
resetLink ? [
|
|
@@ -12383,7 +12467,7 @@ function buildPasswordChangedEmail(config, args, collection, dbOverride) {
|
|
|
12383
12467
|
subject: "Your password has been changed",
|
|
12384
12468
|
html: layout({
|
|
12385
12469
|
siteName: enriched.siteName,
|
|
12386
|
-
preheader: `Your ${enriched.siteName} password was changed
|
|
12470
|
+
preheader: enriched.siteName ? `Your ${enriched.siteName} password was changed.` : "Your password was changed.",
|
|
12387
12471
|
title: "Password changed",
|
|
12388
12472
|
content: [
|
|
12389
12473
|
paragraph("The password for this account was just changed:"),
|
|
@@ -14250,10 +14334,10 @@ function buildDyrectedSystemPrompt(context) {
|
|
|
14250
14334
|
return summary;
|
|
14251
14335
|
}).join("\n");
|
|
14252
14336
|
return `
|
|
14253
|
-
You are the
|
|
14337
|
+
You are the ${context.project?.name ? `${context.project.name} ` : ""}AI Assistant \u2014 an elite conversion copywriter, content strategist, and proactive editorial partner embedded natively inside this headless CMS.
|
|
14254
14338
|
|
|
14255
14339
|
### 1. CMS CONTEXT & PROJECT
|
|
14256
|
-
- Project: "${project.name}" (ID: ${project.id})
|
|
14340
|
+
- Project: "${project.name || project.id}" (ID: ${project.id})
|
|
14257
14341
|
- User: ${user.name || "Editor"} (Role: ${user.role || "editor"})
|
|
14258
14342
|
${globalPrompt ? `
|
|
14259
14343
|
### BRAND VOICE & DIRECTIVES
|
|
@@ -14348,7 +14432,7 @@ var AIAgent = class {
|
|
|
14348
14432
|
prompt: g.ai?.prompt
|
|
14349
14433
|
}));
|
|
14350
14434
|
const project = {
|
|
14351
|
-
name: this.config.admin
|
|
14435
|
+
name: resolveAppName(this.config.admin),
|
|
14352
14436
|
id: this.projectId
|
|
14353
14437
|
};
|
|
14354
14438
|
this.contextCache = {
|
|
@@ -15512,12 +15596,13 @@ function getGlobalTag(global) {
|
|
|
15512
15596
|
return `Global: ${global.label || global.slug}`;
|
|
15513
15597
|
}
|
|
15514
15598
|
function generateOpenApi(config) {
|
|
15599
|
+
const appName = resolveAppName(config.admin);
|
|
15515
15600
|
const spec = {
|
|
15516
15601
|
openapi: "3.0.0",
|
|
15517
15602
|
info: {
|
|
15518
|
-
title:
|
|
15603
|
+
title: appName ? `${appName} API` : "API",
|
|
15519
15604
|
version: "1.0.0",
|
|
15520
|
-
description: "Automatically generated OpenAPI specification for
|
|
15605
|
+
description: appName ? `Automatically generated OpenAPI specification for ${appName}.` : "Automatically generated OpenAPI specification for this project."
|
|
15521
15606
|
},
|
|
15522
15607
|
components: {
|
|
15523
15608
|
schemas: {
|
|
@@ -15628,7 +15713,7 @@ function generateOpenApi(config) {
|
|
|
15628
15713
|
spec.paths["/api/schemas"] = {
|
|
15629
15714
|
get: {
|
|
15630
15715
|
tags: ["System"],
|
|
15631
|
-
summary: "Get the serialized
|
|
15716
|
+
summary: "Get the serialized schema",
|
|
15632
15717
|
security: [],
|
|
15633
15718
|
responses: {
|
|
15634
15719
|
200: { description: "Collection and global schema definitions" }
|
|
@@ -16820,7 +16905,9 @@ function fieldToSchema(field) {
|
|
|
16820
16905
|
}
|
|
16821
16906
|
|
|
16822
16907
|
// src/utils/swagger.ts
|
|
16823
|
-
|
|
16908
|
+
var escapeTitle = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
16909
|
+
function getSwaggerHtml(specUrl, options) {
|
|
16910
|
+
const title = [options?.appName?.trim(), "API Documentation"].filter(Boolean).join(" ");
|
|
16824
16911
|
return `
|
|
16825
16912
|
<!DOCTYPE html>
|
|
16826
16913
|
<html lang="en">
|
|
@@ -16828,7 +16915,7 @@ function getSwaggerHtml(specUrl) {
|
|
|
16828
16915
|
<meta charset="utf-8" />
|
|
16829
16916
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
16830
16917
|
<meta name="description" content="SwaggerUI" />
|
|
16831
|
-
<title
|
|
16918
|
+
<title>${escapeTitle(title)}</title>
|
|
16832
16919
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
16833
16920
|
</head>
|
|
16834
16921
|
<body>
|
|
@@ -16874,6 +16961,7 @@ function getSwaggerHtml(specUrl) {
|
|
|
16874
16961
|
}
|
|
16875
16962
|
|
|
16876
16963
|
// src/router.ts
|
|
16964
|
+
init_navigation();
|
|
16877
16965
|
init_access2();
|
|
16878
16966
|
init_access_control();
|
|
16879
16967
|
init_trash();
|
|
@@ -17428,7 +17516,7 @@ function registerRoutes(app, config) {
|
|
|
17428
17516
|
return c.json(generateOpenApi(config));
|
|
17429
17517
|
});
|
|
17430
17518
|
app.get("/api/docs", (c) => {
|
|
17431
|
-
return c.html(getSwaggerHtml());
|
|
17519
|
+
return c.html(getSwaggerHtml(void 0, { appName: resolveAppName(config.admin) }));
|
|
17432
17520
|
});
|
|
17433
17521
|
app.get("/api/preferences/:key", optionalAuth(config), async (c) => {
|
|
17434
17522
|
const db = config.db;
|
|
@@ -17504,6 +17592,24 @@ function registerRoutes(app, config) {
|
|
|
17504
17592
|
if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
17505
17593
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
17506
17594
|
const body = await c.req.json().catch(() => ({}));
|
|
17595
|
+
if (key === NAVIGATION_POLICY_KEY) {
|
|
17596
|
+
if (scope !== "role") {
|
|
17597
|
+
return c.json(
|
|
17598
|
+
{ error: true, message: "Navigation policies are stored per role (scope=role)." },
|
|
17599
|
+
400
|
|
17600
|
+
);
|
|
17601
|
+
}
|
|
17602
|
+
const policy = parseNavigationRolePolicy(body.value, compileNavigation(config));
|
|
17603
|
+
if (!policy) {
|
|
17604
|
+
return c.json(
|
|
17605
|
+
{ error: true, message: "Invalid navigation policy: expected { enforced: boolean, allowed: string[] }." },
|
|
17606
|
+
400
|
|
17607
|
+
);
|
|
17608
|
+
}
|
|
17609
|
+
body.value = policy;
|
|
17610
|
+
} else if (key === "admin:navigation" && (scope === "role" || scope === "global")) {
|
|
17611
|
+
body.value = sanitizeNavigationLayout(body.value, compileNavigation(config));
|
|
17612
|
+
}
|
|
17507
17613
|
if (scope === "global") {
|
|
17508
17614
|
const userCol = config.collections?.find((col) => col.slug === user?.collection);
|
|
17509
17615
|
const isAdminUser = isUserAdmin(user, userCol);
|
|
@@ -17644,8 +17750,8 @@ function registerRoutes(app, config) {
|
|
|
17644
17750
|
});
|
|
17645
17751
|
return c.json({ success: true });
|
|
17646
17752
|
});
|
|
17647
|
-
const createNavAccessChecker = (c) => {
|
|
17648
|
-
const user = c.get("user");
|
|
17753
|
+
const createNavAccessChecker = (c, userOverride) => {
|
|
17754
|
+
const user = userOverride ?? c.get("user");
|
|
17649
17755
|
const roles = /* @__PURE__ */ new Set([...user?.role ? [user.role] : [], ...Array.isArray(user?.roles) ? user.roles : []]);
|
|
17650
17756
|
const args = { user, req: toHookRequestContext(c.req) };
|
|
17651
17757
|
const allows = async (rule) => {
|
|
@@ -17663,11 +17769,47 @@ function registerRoutes(app, config) {
|
|
|
17663
17769
|
canAccessView: (view) => allows(view.access?.read)
|
|
17664
17770
|
};
|
|
17665
17771
|
};
|
|
17666
|
-
|
|
17772
|
+
const loadNavigationPolicies = async (user, compiled) => {
|
|
17773
|
+
const db = config.db;
|
|
17774
|
+
const roles = [
|
|
17775
|
+
.../* @__PURE__ */ new Set([
|
|
17776
|
+
...typeof user?.role === "string" ? [user.role] : [],
|
|
17777
|
+
...Array.isArray(user?.roles) ? user.roles : []
|
|
17778
|
+
])
|
|
17779
|
+
];
|
|
17780
|
+
if (!db) return [];
|
|
17781
|
+
return Promise.all(
|
|
17782
|
+
roles.map(async (role) => {
|
|
17783
|
+
try {
|
|
17784
|
+
const doc = await db.findOne({
|
|
17785
|
+
collection: "__role_preferences",
|
|
17786
|
+
id: `${role}_${NAVIGATION_POLICY_KEY}`
|
|
17787
|
+
});
|
|
17788
|
+
return doc ? parseNavigationRolePolicy(doc.value, compiled) : null;
|
|
17789
|
+
} catch {
|
|
17790
|
+
return null;
|
|
17791
|
+
}
|
|
17792
|
+
})
|
|
17793
|
+
);
|
|
17794
|
+
};
|
|
17795
|
+
const resolveNavigationForUser = async (c, user, options) => {
|
|
17667
17796
|
const compiled = compileNavigation(config);
|
|
17668
|
-
const { canAccess, canAccessView } = createNavAccessChecker(c);
|
|
17797
|
+
const { canAccess, canAccessView } = createNavAccessChecker(c, user);
|
|
17669
17798
|
const pruned = await pruneNavigationByAccess(compiled, canAccess, canAccessView);
|
|
17670
|
-
return
|
|
17799
|
+
if (options?.ignorePolicy) return pruned;
|
|
17800
|
+
return applyNavigationRolePolicies(pruned, await loadNavigationPolicies(user ?? c.get("user"), compiled));
|
|
17801
|
+
};
|
|
17802
|
+
app.get("/api/admin/navigation", optionalAuth(config), async (c) => {
|
|
17803
|
+
const asRole = c.req.query("asRole");
|
|
17804
|
+
if (!asRole) return c.json(await resolveNavigationForUser(c));
|
|
17805
|
+
const user = c.get("user");
|
|
17806
|
+
if (!user) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
17807
|
+
const userCol = config.collections?.find((col) => col.slug === user.collection);
|
|
17808
|
+
if (!isUserAdmin(user, userCol)) {
|
|
17809
|
+
return c.json({ error: true, message: "Only administrators can view navigation as another role." }, 403);
|
|
17810
|
+
}
|
|
17811
|
+
const asUser = { ...user, role: asRole, roles: [asRole] };
|
|
17812
|
+
return c.json(await resolveNavigationForUser(c, asUser, { ignorePolicy: c.req.query("raw") === "true" }));
|
|
17671
17813
|
});
|
|
17672
17814
|
app.on(["GET", "POST"], "/api/admin/navigation/badges", optionalAuth(config), async (c) => {
|
|
17673
17815
|
const db = config.db;
|
|
@@ -17698,6 +17840,25 @@ function registerRoutes(app, config) {
|
|
|
17698
17840
|
} catch {
|
|
17699
17841
|
}
|
|
17700
17842
|
}
|
|
17843
|
+
if (!userNavPrefs) {
|
|
17844
|
+
const userRoles = [
|
|
17845
|
+
...typeof user?.role === "string" ? [user.role] : [],
|
|
17846
|
+
...Array.isArray(user?.roles) ? user.roles : []
|
|
17847
|
+
];
|
|
17848
|
+
for (const role of userRoles) {
|
|
17849
|
+
try {
|
|
17850
|
+
const roleDoc = await db.findOne({
|
|
17851
|
+
collection: "__role_preferences",
|
|
17852
|
+
id: `${role}_admin:navigation`
|
|
17853
|
+
});
|
|
17854
|
+
if (roleDoc?.value) {
|
|
17855
|
+
userNavPrefs = roleDoc.value;
|
|
17856
|
+
break;
|
|
17857
|
+
}
|
|
17858
|
+
} catch {
|
|
17859
|
+
}
|
|
17860
|
+
}
|
|
17861
|
+
}
|
|
17701
17862
|
if (!userNavPrefs) {
|
|
17702
17863
|
try {
|
|
17703
17864
|
const globalDoc = await db.findOne({
|
|
@@ -17710,28 +17871,31 @@ function registerRoutes(app, config) {
|
|
|
17710
17871
|
} catch {
|
|
17711
17872
|
}
|
|
17712
17873
|
}
|
|
17874
|
+
const baseTree = await resolveNavigationForUser(c);
|
|
17713
17875
|
let allItems;
|
|
17714
17876
|
if (userNavPrefs) {
|
|
17715
|
-
const reconciled = reconcileNavigation(
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17877
|
+
const reconciled = reconcileNavigation(
|
|
17878
|
+
baseTree,
|
|
17879
|
+
userNavPrefs,
|
|
17880
|
+
{
|
|
17881
|
+
collections: config.collections,
|
|
17882
|
+
globals: config.globals
|
|
17883
|
+
},
|
|
17884
|
+
{ allowUserItems: !baseTree.locked }
|
|
17885
|
+
);
|
|
17719
17886
|
allItems = [
|
|
17720
17887
|
...reconciled.ungrouped || [],
|
|
17721
17888
|
...reconciled.pinnedItems || [],
|
|
17722
17889
|
...(reconciled.groups || []).flatMap((g) => g.items || [])
|
|
17723
17890
|
];
|
|
17724
|
-
} else {
|
|
17725
|
-
const pruned = await pruneNavigationByAccess(compiled, createNavAccessChecker(c).canAccess);
|
|
17726
|
-
allItems = [
|
|
17727
|
-
...pruned.ungrouped,
|
|
17728
|
-
...pruned.groups.flatMap((g) => g.items)
|
|
17729
|
-
];
|
|
17730
|
-
}
|
|
17731
|
-
if (userNavPrefs) {
|
|
17732
17891
|
const { canAccess } = createNavAccessChecker(c);
|
|
17733
17892
|
const allowed = await Promise.all(allItems.map((item) => canAccess(item)));
|
|
17734
17893
|
allItems = allItems.filter((_, i) => allowed[i]);
|
|
17894
|
+
} else {
|
|
17895
|
+
allItems = [
|
|
17896
|
+
...baseTree.ungrouped,
|
|
17897
|
+
...baseTree.groups.flatMap((g) => g.items)
|
|
17898
|
+
];
|
|
17735
17899
|
}
|
|
17736
17900
|
const badges = {};
|
|
17737
17901
|
const requestsByCollection = /* @__PURE__ */ new Map();
|
|
@@ -18016,7 +18180,7 @@ function registerRoutes(app, config) {
|
|
|
18016
18180
|
if (!to || typeof to !== "string" || !to.includes("@")) {
|
|
18017
18181
|
return c.json({ error: true, message: "A valid recipient email address is required" }, 400);
|
|
18018
18182
|
}
|
|
18019
|
-
const siteName = config.admin
|
|
18183
|
+
const siteName = resolveAppName(config.admin);
|
|
18020
18184
|
let appUrl = config.admin?.siteUrl;
|
|
18021
18185
|
if (!appUrl) {
|
|
18022
18186
|
try {
|
|
@@ -18643,7 +18807,7 @@ var EMAIL_TEMPLATES_COLLECTION_CONFIG = {
|
|
|
18643
18807
|
beforeChange: [
|
|
18644
18808
|
async ({ data, req }) => {
|
|
18645
18809
|
if (data && data.format === "html") {
|
|
18646
|
-
const siteName = req?.config?.admin
|
|
18810
|
+
const siteName = resolveAppName(req?.config?.admin);
|
|
18647
18811
|
const defaults = getDefaultEmailTemplate(data.purpose || "invite", { siteName });
|
|
18648
18812
|
if (!data.subject) {
|
|
18649
18813
|
data.subject = defaults.subject;
|