@tetherto/dev-websites-core 0.3.5 → 0.3.6

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,3 +1,3 @@
1
- import { $ as createAccessHelpers, A as downloadCtaBlock, B as seoFields, C as codeBlockLanguages, D as sectionHeadingBlock, E as leafBlocks, F as richTextBlock, G as STOCK_COLLECTION_SLUGS, H as BuildCmsConfigOptions, I as coerceBlogPostCategoriesOnRead, J as GlobalOverride, K as StockCollectionSlug, L as coerceBlogPostCategoriesOnWrite, M as imageBlock, N as codeTabsBlock, O as accordionBlock, P as codeBlock, Q as AccessOptions, R as setPublishedAt, S as showcaseContentBlocks, T as languageOptions, U as buildCmsConfig, V as draftsWithAutosave, W as Overrides, X as applyGlobalOverride, Y as applyCollectionOverride, Z as AccessHelpers, _ as requireEnvAtRuntime, a as createChangelog, b as blogContentBlocks, c as createPages, d as createAuthors, f as createMedia, g as createS3MediaPlugin, h as CreateS3MediaPluginOptions, i as createEventTags, j as videoBlock, k as stepsBlock, l as createBlogPosts, m as createUsers, n as createSiteSettings, o as createChangelogTags, p as createApiTokens, q as CollectionOverride, r as createEvents, s as createLegalPages, t as createAnnouncementBanner, u as createCategories, v as getRichTextEditorConfig, w as defaultCodeLanguage, x as pageContentBlocks, y as richTextEditor, z as slugify } from "../index.mjs";
1
+ import { $ as createAccessHelpers, A as downloadCtaBlock, B as seoFields, C as codeBlockLanguages, D as sectionHeadingBlock, E as leafBlocks, F as richTextBlock, G as STOCK_COLLECTION_SLUGS, H as BuildCmsConfigOptions, I as coerceBlogPostCategoriesOnRead, J as GlobalOverride, K as StockCollectionSlug, L as coerceBlogPostCategoriesOnWrite, M as imageBlock, N as codeTabsBlock, O as accordionBlock, P as codeBlock, Q as AccessOptions, R as setPublishedAt, S as showcaseContentBlocks, T as languageOptions, U as buildCmsConfig, V as draftsWithAutosave, W as Overrides, X as applyGlobalOverride, Y as applyCollectionOverride, Z as AccessHelpers, _ as requireEnvAtRuntime, _t as writeAccess, a as createChangelog, at as RoleRule, b as blogContentBlocks, c as createPages, ct as collectionRoleRule, d as createAuthors, dt as hiddenUnless, et as ADMIN_ROLE, f as createMedia, ft as isCmsAdmin, g as createS3MediaPlugin, gt as withRoles, h as CreateS3MediaPluginOptions, ht as userHasAnyRole, i as createEventTags, it as RoleOption, j as videoBlock, k as stepsBlock, l as createBlogPosts, lt as effectiveRoles, m as createUsers, mt as resolveRoleOptions, n as createSiteSettings, nt as DEFAULT_GLOBAL_ROLES, o as createChangelogTags, ot as RolesOptions, p as createApiTokens, pt as publishedOrRoleRead, q as CollectionOverride, r as createEvents, rt as DEFAULT_ROLES, s as createLegalPages, st as adminRoleAccess, t as createAnnouncementBanner, tt as DEFAULT_COLLECTION_ROLES, u as createCategories, ut as globalRoleRule, v as getRichTextEditorConfig, w as defaultCodeLanguage, x as pageContentBlocks, y as richTextEditor, z as slugify } from "../index.mjs";
2
2
  import { n as readPayloadMediaAsDataUrl, r as readPayloadMediaBuffer, t as PayloadMedia } from "../readMediaBuffer.mjs";
3
- export { type AccessHelpers, type AccessOptions, type BuildCmsConfigOptions, type CollectionOverride, type CreateS3MediaPluginOptions, type GlobalOverride, type Overrides, type PayloadMedia, STOCK_COLLECTION_SLUGS, type StockCollectionSlug, accordionBlock, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, coerceBlogPostCategoriesOnRead, coerceBlogPostCategoriesOnWrite, createAccessHelpers, createAnnouncementBanner, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, getRichTextEditorConfig, imageBlock, languageOptions, leafBlocks, pageContentBlocks, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, videoBlock };
3
+ export { ADMIN_ROLE, type AccessHelpers, type AccessOptions, type BuildCmsConfigOptions, type CollectionOverride, type CreateS3MediaPluginOptions, DEFAULT_COLLECTION_ROLES, DEFAULT_GLOBAL_ROLES, DEFAULT_ROLES, type GlobalOverride, type Overrides, type PayloadMedia, type RoleOption, type RoleRule, type RolesOptions, STOCK_COLLECTION_SLUGS, type StockCollectionSlug, accordionBlock, adminRoleAccess, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, coerceBlogPostCategoriesOnRead, coerceBlogPostCategoriesOnWrite, collectionRoleRule, createAccessHelpers, createAnnouncementBanner, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, effectiveRoles, getRichTextEditorConfig, globalRoleRule, hiddenUnless, imageBlock, isCmsAdmin, languageOptions, leafBlocks, pageContentBlocks, publishedOrRoleRead, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, resolveRoleOptions, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, userHasAnyRole, videoBlock, withRoles, writeAccess };
@@ -1,3 +1,3 @@
1
- import { A as richTextBlock, B as languageOptions, C as blogContentBlocks, D as accordionBlock, E as sectionHeadingBlock, F as downloadCtaBlock, G as createAccessHelpers, H as seoFields, I as codeTabsBlock, L as codeBlock, M as richTextEditor, N as videoBlock, O as leafBlocks, P as imageBlock, R as codeBlockLanguages, S as coerceBlogPostCategoriesOnWrite, T as showcaseContentBlocks, U as slugify, V as createAuthors, W as createApiTokens, _ as createCategories, a as STOCK_COLLECTION_SLUGS, b as setPublishedAt, c as createSiteSettings, d as createPages, f as createMedia, g as createChangelog, h as createChangelogTags, i as buildCmsConfig, j as getRichTextEditorConfig, k as stepsBlock, l as createUsers, m as createEventTags, n as createS3MediaPlugin, o as applyCollectionOverride, p as createEvents, r as requireEnvAtRuntime, s as applyGlobalOverride, t as createAnnouncementBanner, u as createLegalPages, v as createBlogPosts, w as pageContentBlocks, x as coerceBlogPostCategoriesOnRead, y as draftsWithAutosave, z as defaultCodeLanguage } from "../cms.mjs";
1
+ import { $ as hiddenUnless, A as richTextBlock, B as languageOptions, C as blogContentBlocks, D as accordionBlock, E as sectionHeadingBlock, F as downloadCtaBlock, G as ADMIN_ROLE, H as seoFields, I as codeTabsBlock, J as DEFAULT_ROLES, K as DEFAULT_COLLECTION_ROLES, L as codeBlock, M as richTextEditor, N as videoBlock, O as leafBlocks, P as imageBlock, Q as globalRoleRule, R as codeBlockLanguages, S as coerceBlogPostCategoriesOnWrite, T as showcaseContentBlocks, U as slugify, V as createAuthors, W as createApiTokens, X as collectionRoleRule, Y as adminRoleAccess, Z as effectiveRoles, _ as createCategories, a as STOCK_COLLECTION_SLUGS, at as writeAccess, b as setPublishedAt, c as createSiteSettings, d as createPages, et as isCmsAdmin, f as createMedia, g as createChangelog, h as createChangelogTags, i as buildCmsConfig, it as withRoles, j as getRichTextEditorConfig, k as stepsBlock, l as createUsers, m as createEventTags, n as createS3MediaPlugin, nt as resolveRoleOptions, o as applyCollectionOverride, ot as createAccessHelpers, p as createEvents, q as DEFAULT_GLOBAL_ROLES, r as requireEnvAtRuntime, rt as userHasAnyRole, s as applyGlobalOverride, t as createAnnouncementBanner, tt as publishedOrRoleRead, u as createLegalPages, v as createBlogPosts, w as pageContentBlocks, x as coerceBlogPostCategoriesOnRead, y as draftsWithAutosave, z as defaultCodeLanguage } from "../cms.mjs";
2
2
  import { n as readPayloadMediaBuffer, t as readPayloadMediaAsDataUrl } from "../readMediaBuffer.mjs";
3
- export { STOCK_COLLECTION_SLUGS, accordionBlock, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, coerceBlogPostCategoriesOnRead, coerceBlogPostCategoriesOnWrite, createAccessHelpers, createAnnouncementBanner, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, getRichTextEditorConfig, imageBlock, languageOptions, leafBlocks, pageContentBlocks, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, videoBlock };
3
+ export { ADMIN_ROLE, DEFAULT_COLLECTION_ROLES, DEFAULT_GLOBAL_ROLES, DEFAULT_ROLES, STOCK_COLLECTION_SLUGS, accordionBlock, adminRoleAccess, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, coerceBlogPostCategoriesOnRead, coerceBlogPostCategoriesOnWrite, collectionRoleRule, createAccessHelpers, createAnnouncementBanner, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, effectiveRoles, getRichTextEditorConfig, globalRoleRule, hiddenUnless, imageBlock, isCmsAdmin, languageOptions, leafBlocks, pageContentBlocks, publishedOrRoleRead, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, resolveRoleOptions, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, userHasAnyRole, videoBlock, withRoles, writeAccess };
package/dist/cms.mjs CHANGED
@@ -30,6 +30,370 @@ function createAccessHelpers(options = {}) {
30
30
  };
31
31
  }
32
32
  //#endregion
33
+ //#region src/cms/shared/roles.ts
34
+ const ADMIN_ROLE = "admin";
35
+ const CMS_ROLES_SLUG = "cms-roles";
36
+ const moderator = ["moderator"];
37
+ const developer = ["developer"];
38
+ const moderatorAndDeveloper = ["moderator", "developer"];
39
+ const DEFAULT_ROLES = [
40
+ {
41
+ label: "Admin",
42
+ value: ADMIN_ROLE
43
+ },
44
+ {
45
+ label: "Moderator",
46
+ value: "moderator"
47
+ },
48
+ {
49
+ label: "Developer",
50
+ value: "developer"
51
+ }
52
+ ];
53
+ const DEFAULT_COLLECTION_ROLES = {
54
+ "api-tokens": false,
55
+ "media": { roles: moderatorAndDeveloper },
56
+ "authors": { roles: moderator },
57
+ "categories": { roles: moderator },
58
+ "blog-posts": {
59
+ roles: moderator,
60
+ drafts: true
61
+ },
62
+ "pages": {
63
+ roles: moderator,
64
+ drafts: true
65
+ },
66
+ "legal-pages": {
67
+ roles: moderator,
68
+ drafts: true
69
+ },
70
+ "changelog-tags": { roles: developer },
71
+ "changelog": {
72
+ roles: developer,
73
+ drafts: true
74
+ },
75
+ "event-tags": { roles: moderator },
76
+ "events": {
77
+ roles: moderator,
78
+ drafts: true
79
+ }
80
+ };
81
+ const DEFAULT_GLOBAL_ROLES = {
82
+ "site-settings": false,
83
+ "announcement-banner": { roles: moderator }
84
+ };
85
+ let roleCache = null;
86
+ function asCmsUser(user) {
87
+ if (!user || typeof user !== "object") return null;
88
+ const record = user;
89
+ if (record.collection !== "users") return null;
90
+ return record;
91
+ }
92
+ function roleRefs(roles) {
93
+ if (!Array.isArray(roles)) return {
94
+ ids: [],
95
+ slugs: []
96
+ };
97
+ const ids = [];
98
+ const slugs = [];
99
+ roles.forEach((role) => {
100
+ if (typeof role === "string" || typeof role === "number") {
101
+ const value = String(role);
102
+ if (/^[a-f0-9]{24}$/i.test(value)) ids.push(value);
103
+ else if (value) slugs.push(value);
104
+ return;
105
+ }
106
+ if (role && typeof role === "object" && "id" in role && role.id != null) ids.push(String(role.id));
107
+ });
108
+ return {
109
+ ids,
110
+ slugs
111
+ };
112
+ }
113
+ function readPermissions(value) {
114
+ if (!Array.isArray(value)) return [];
115
+ return value.flatMap((entry) => {
116
+ if (!entry || typeof entry !== "object" || !("target" in entry)) return [];
117
+ const row = entry;
118
+ if (!row.target) return [];
119
+ return [{
120
+ target: row.target,
121
+ create: Boolean(row.create),
122
+ update: Boolean(row.update),
123
+ delete: Boolean(row.delete),
124
+ drafts: Boolean(row.drafts)
125
+ }];
126
+ });
127
+ }
128
+ async function refreshRoleCache(payload) {
129
+ const found = await payload.find({
130
+ collection: CMS_ROLES_SLUG,
131
+ depth: 0,
132
+ limit: 500,
133
+ pagination: false,
134
+ overrideAccess: true
135
+ });
136
+ const byId = /* @__PURE__ */ new Map();
137
+ const bySlug = /* @__PURE__ */ new Map();
138
+ found.docs.forEach((doc) => {
139
+ const record = doc;
140
+ if (!record.slug) return;
141
+ const role = {
142
+ id: String(record.id),
143
+ name: record.name ?? record.slug,
144
+ slug: record.slug,
145
+ fullAccess: Boolean(record.fullAccess) || record.slug === "admin",
146
+ permissions: readPermissions(record.permissions)
147
+ };
148
+ byId.set(role.id, role);
149
+ bySlug.set(role.slug, role);
150
+ });
151
+ roleCache = {
152
+ byId,
153
+ bySlug
154
+ };
155
+ }
156
+ async function ensureRoleCache(payload) {
157
+ if (!roleCache) await refreshRoleCache(payload);
158
+ return roleCache ?? {
159
+ byId: /* @__PURE__ */ new Map(),
160
+ bySlug: /* @__PURE__ */ new Map()
161
+ };
162
+ }
163
+ function lookupRoles(refs, cache) {
164
+ return [...refs.ids.map((id) => cache.byId.get(id)), ...refs.slugs.map((slug) => cache.bySlug.get(slug))].filter((role) => Boolean(role));
165
+ }
166
+ function syncAssignment(user) {
167
+ const record = asCmsUser(user);
168
+ if (!record) return "guest";
169
+ const refs = roleRefs(record.roles);
170
+ if (refs.ids.length === 0 && refs.slugs.length === 0) return "admin";
171
+ if (!roleCache) return "unknown";
172
+ return lookupRoles(refs, roleCache);
173
+ }
174
+ async function loadAssignment(req) {
175
+ const record = asCmsUser(req.user);
176
+ if (!record) return "guest";
177
+ const refs = roleRefs(record.roles);
178
+ if (refs.ids.length === 0 && refs.slugs.length === 0) return "admin";
179
+ return lookupRoles(refs, await ensureRoleCache(req.payload));
180
+ }
181
+ function hasPermission(roles, target, action) {
182
+ if (roles.some((role) => role.fullAccess)) return true;
183
+ return roles.some((role) => role.permissions.some((permission) => permission.target === target && permission[action]));
184
+ }
185
+ /**
186
+ * `null` means this user has no roles yet and is treated as admin.
187
+ * `true` / `false` is the cached decision. A cold cache returns `null` so the
188
+ * nav stays visible until startup finishes loading roles.
189
+ */
190
+ function cachedPermission(user, target, action) {
191
+ const assigned = syncAssignment(user);
192
+ if (assigned === "guest") return false;
193
+ if (assigned === "admin") return true;
194
+ if (assigned === "unknown") return null;
195
+ return hasPermission(assigned, target, action);
196
+ }
197
+ async function userIsFullAdmin(req) {
198
+ const assigned = await loadAssignment(req);
199
+ if (assigned === "guest") return false;
200
+ if (assigned === "admin") return true;
201
+ return assigned.some((role) => role.fullAccess);
202
+ }
203
+ async function userCan(req, target, action) {
204
+ const assigned = await loadAssignment(req);
205
+ if (assigned === "guest") return false;
206
+ if (assigned === "admin") return true;
207
+ return hasPermission(assigned, target, action);
208
+ }
209
+ /**
210
+ * Users created before the roles field existed have none stored.
211
+ * Treat that as admin so turning roles on does not lock the panel.
212
+ */
213
+ function effectiveRoles(user) {
214
+ const assigned = syncAssignment(user);
215
+ if (assigned === "admin") return [ADMIN_ROLE];
216
+ if (assigned === "guest" || assigned === "unknown") return [];
217
+ return assigned.map((role) => role.slug);
218
+ }
219
+ function userHasAnyRole(user, roles) {
220
+ const assigned = effectiveRoles(user);
221
+ if (assigned.includes("admin")) return true;
222
+ return roles.some((role) => assigned.includes(role));
223
+ }
224
+ function isCmsAdmin(user) {
225
+ const assigned = syncAssignment(user);
226
+ if (assigned === "admin") return true;
227
+ if (assigned === "guest" || assigned === "unknown") return false;
228
+ return assigned.some((role) => role.fullAccess);
229
+ }
230
+ const adminRoleAccess = ({ req }) => userIsFullAdmin(req);
231
+ function hiddenUnless(roles) {
232
+ return ({ user }) => !userHasAnyRole(user, roles);
233
+ }
234
+ function writeAccess(roles) {
235
+ return ({ req }) => userHasAnyRole(req.user, roles);
236
+ }
237
+ function publishedOrRoleRead(roles) {
238
+ return ({ req }) => {
239
+ if (userHasAnyRole(req.user, roles)) return true;
240
+ return { _status: { equals: "published" } };
241
+ };
242
+ }
243
+ const selfOrAdminAccess = async ({ req }) => {
244
+ if (await userIsFullAdmin(req)) return true;
245
+ const record = asCmsUser(req.user);
246
+ if (record?.id == null) return false;
247
+ return { id: { equals: record.id } };
248
+ };
249
+ const manageRolesAccess = ({ req }) => userIsFullAdmin(req);
250
+ function hiddenFor(target) {
251
+ return ({ user }) => {
252
+ const create = cachedPermission(user, target, "create");
253
+ const update = cachedPermission(user, target, "update");
254
+ const drafts = cachedPermission(user, target, "drafts");
255
+ if (create == null || update == null || drafts == null) return false;
256
+ return !(create || update || drafts);
257
+ };
258
+ }
259
+ function hiddenUnlessFullAdmin() {
260
+ return ({ user }) => {
261
+ const assigned = syncAssignment(user);
262
+ if (assigned === "unknown" || assigned === "admin") return false;
263
+ if (assigned === "guest") return true;
264
+ return !assigned.some((role) => role.fullAccess);
265
+ };
266
+ }
267
+ function resolveRoleOptions(options = {}) {
268
+ const custom = options.roles?.filter((role) => role.value !== "admin") ?? DEFAULT_ROLES.filter((role) => role.value !== "admin");
269
+ const merged = /* @__PURE__ */ new Map([[ADMIN_ROLE, {
270
+ label: "Admin",
271
+ value: ADMIN_ROLE
272
+ }]]);
273
+ [...custom, ...options.extraRoles ?? []].forEach((role) => {
274
+ if (!role.value || role.value === "admin") return;
275
+ merged.set(role.value, {
276
+ label: role.label,
277
+ value: role.value
278
+ });
279
+ });
280
+ return [...merged.values()];
281
+ }
282
+ function collectionRoleRule(options, slug, fallback) {
283
+ return roleRule(options, "collections", slug, fallback);
284
+ }
285
+ function globalRoleRule(options, slug, fallback) {
286
+ return roleRule(options, "globals", slug, fallback);
287
+ }
288
+ function roleRule(options, kind, slug, fallback) {
289
+ const overrides = (options === true || options == null ? {} : options)[kind];
290
+ if (overrides && Object.prototype.hasOwnProperty.call(overrides, slug)) return overrides[slug];
291
+ const defaults = kind === "collections" ? DEFAULT_COLLECTION_ROLES : DEFAULT_GLOBAL_ROLES;
292
+ if (Object.prototype.hasOwnProperty.call(defaults, slug)) return defaults[slug];
293
+ return fallback;
294
+ }
295
+ function rolesField() {
296
+ return {
297
+ name: "roles",
298
+ type: "relationship",
299
+ relationTo: CMS_ROLES_SLUG,
300
+ hasMany: true,
301
+ saveToJWT: true,
302
+ access: {
303
+ create: manageRolesAccess,
304
+ update: manageRolesAccess
305
+ },
306
+ admin: {
307
+ position: "sidebar",
308
+ description: "Only an admin can change roles. Several roles can be combined. Empty means Admin until a role is saved. The user must sign in again before a change applies."
309
+ }
310
+ };
311
+ }
312
+ function permissionFor(target, rule, role, collection) {
313
+ if (rule == null || rule === false || !rule.roles.includes(role)) return null;
314
+ return {
315
+ target,
316
+ create: collection,
317
+ update: true,
318
+ delete: false,
319
+ drafts: collection && rule.drafts === true
320
+ };
321
+ }
322
+ /** Targets shown in the Roles editor: stock defaults plus anything the app configures. */
323
+ function roleTargetSlugs(options = {}) {
324
+ const targets = /* @__PURE__ */ new Set([
325
+ ...Object.keys(DEFAULT_COLLECTION_ROLES),
326
+ ...Object.keys(DEFAULT_GLOBAL_ROLES),
327
+ ...Object.keys(options.collections ?? {}),
328
+ ...Object.keys(options.globals ?? {})
329
+ ]);
330
+ targets.delete("users");
331
+ targets.delete(CMS_ROLES_SLUG);
332
+ return [...targets].sort();
333
+ }
334
+ /**
335
+ * Default role documents. Existing documents with the same slug are left alone,
336
+ * so edits made in the admin survive a restart.
337
+ */
338
+ function buildRoleSeeds(options = {}) {
339
+ const roles = resolveRoleOptions(options);
340
+ const targets = roleTargetSlugs(options);
341
+ return roles.map((role) => {
342
+ if (role.value === "admin") return {
343
+ name: role.label,
344
+ slug: role.value,
345
+ fullAccess: true,
346
+ permissions: []
347
+ };
348
+ const permissions = targets.flatMap((target) => {
349
+ const collection = collectionRoleRule(options, target);
350
+ const global = globalRoleRule(options, target);
351
+ const row = permissionFor(target, collection !== void 0 ? collection : global, role.value, collection !== void 0);
352
+ return row ? [row] : [];
353
+ });
354
+ return {
355
+ name: role.label,
356
+ slug: role.value,
357
+ fullAccess: false,
358
+ permissions
359
+ };
360
+ });
361
+ }
362
+ function isCollectionConfig(config) {
363
+ if ("auth" in config || "upload" in config || "labels" in config || "defaultSort" in config) return true;
364
+ return Boolean(config.access && ("create" in config.access || "delete" in config.access));
365
+ }
366
+ /** Limits create, update, and delete from the Roles collection. Admin is always allowed. */
367
+ function withRoles(config) {
368
+ const collection = isCollectionConfig(config);
369
+ const users = collection && Boolean(config.auth);
370
+ const slug = config.slug;
371
+ const versioned = "versions" in config && Boolean(config.versions);
372
+ return {
373
+ ...config,
374
+ admin: {
375
+ ...config.admin,
376
+ ...users ? { hidden: hiddenUnlessFullAdmin() } : { hidden: hiddenFor(slug) }
377
+ },
378
+ access: {
379
+ ...config.access,
380
+ ...collection ? {
381
+ create: users ? adminRoleAccess : ({ req }) => userCan(req, slug, "create"),
382
+ delete: users ? adminRoleAccess : ({ req }) => userCan(req, slug, "delete")
383
+ } : {},
384
+ ...users ? {
385
+ read: selfOrAdminAccess,
386
+ update: selfOrAdminAccess
387
+ } : { update: ({ req }) => userCan(req, slug, "update") },
388
+ ...versioned && !users ? { read: async ({ req }) => {
389
+ if (await userCan(req, slug, "update") || await userCan(req, slug, "drafts")) return true;
390
+ return { _status: { equals: "published" } };
391
+ } } : {}
392
+ },
393
+ ...users && !(config.fields ?? []).some((field) => "name" in field && field.name === "roles") ? { fields: [...config.fields ?? [], rolesField()] } : {}
394
+ };
395
+ }
396
+ //#endregion
33
397
  //#region src/cms/collections/createApiTokens.ts
34
398
  const adminOrSelf = ({ req }) => {
35
399
  if (req.user?.collection === "users") return true;
@@ -96,6 +460,133 @@ function createApiTokens(access) {
96
460
  //#region src/cms/hooks/slugify.ts
97
461
  const slugify = ({ value }) => typeof value === "string" ? value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") : value;
98
462
  //#endregion
463
+ //#region src/cms/collections/createCmsRoles.ts
464
+ const refresh = ({ req }) => refreshRoleCache(req.payload);
465
+ const refreshAfterDelete = ({ req }) => refreshRoleCache(req.payload);
466
+ const keepAdminRole = ({ data, originalDoc }) => {
467
+ if (originalDoc?.slug === "admin" || data?.slug === "admin") return {
468
+ ...data,
469
+ slug: ADMIN_ROLE,
470
+ fullAccess: true
471
+ };
472
+ return data;
473
+ };
474
+ const blockAdminDelete = async ({ id, req }) => {
475
+ if ((await req.payload.findByID({
476
+ collection: "cms-roles",
477
+ id,
478
+ depth: 0,
479
+ overrideAccess: true
480
+ })).slug === "admin") throw new Error("The Admin role cannot be deleted.");
481
+ };
482
+ const fullAdmin = ({ req }) => userIsFullAdmin(req);
483
+ function createCmsRoles(targets) {
484
+ return {
485
+ slug: CMS_ROLES_SLUG,
486
+ labels: {
487
+ singular: "Role",
488
+ plural: "Roles"
489
+ },
490
+ admin: {
491
+ useAsTitle: "name",
492
+ hidden: hiddenUnlessFullAdmin(),
493
+ description: "Who can edit each collection and global. Only an admin can change this."
494
+ },
495
+ access: {
496
+ read: fullAdmin,
497
+ create: fullAdmin,
498
+ update: fullAdmin,
499
+ delete: fullAdmin
500
+ },
501
+ hooks: {
502
+ beforeChange: [keepAdminRole],
503
+ beforeDelete: [blockAdminDelete],
504
+ afterChange: [refresh],
505
+ afterDelete: [refreshAfterDelete]
506
+ },
507
+ fields: [
508
+ {
509
+ name: "name",
510
+ type: "text",
511
+ required: true
512
+ },
513
+ {
514
+ name: "slug",
515
+ type: "text",
516
+ required: true,
517
+ unique: true,
518
+ index: true,
519
+ hooks: { beforeValidate: [slugify] },
520
+ admin: { description: "Stable id. Do not change it after users are assigned this role." }
521
+ },
522
+ {
523
+ name: "fullAccess",
524
+ type: "checkbox",
525
+ defaultValue: false,
526
+ admin: { description: "Can edit every collection, including Roles and Users." }
527
+ },
528
+ {
529
+ name: "permissions",
530
+ type: "array",
531
+ admin: {
532
+ condition: (data) => !data?.fullAccess,
533
+ description: "One row per collection or global. No row means no access."
534
+ },
535
+ fields: [
536
+ {
537
+ name: "target",
538
+ type: "select",
539
+ required: true,
540
+ options: targets.map((slug) => ({
541
+ label: slug,
542
+ value: slug
543
+ }))
544
+ },
545
+ {
546
+ name: "create",
547
+ type: "checkbox",
548
+ defaultValue: false
549
+ },
550
+ {
551
+ name: "update",
552
+ type: "checkbox",
553
+ defaultValue: false
554
+ },
555
+ {
556
+ name: "delete",
557
+ type: "checkbox",
558
+ defaultValue: false
559
+ },
560
+ {
561
+ name: "drafts",
562
+ type: "checkbox",
563
+ defaultValue: false,
564
+ admin: { description: "Can open unpublished documents. Update also allows this." }
565
+ }
566
+ ]
567
+ }
568
+ ]
569
+ };
570
+ }
571
+ /** Creates each default role once. Documents that already exist are left as edited. */
572
+ async function seedDefaultCmsRoles(payload, options = {}) {
573
+ const seeds = buildRoleSeeds(options);
574
+ await Promise.all(seeds.map(async (role) => {
575
+ if ((await payload.find({
576
+ collection: "cms-roles",
577
+ where: { slug: { equals: role.slug } },
578
+ limit: 1,
579
+ depth: 0,
580
+ overrideAccess: true
581
+ })).docs.length > 0) return;
582
+ await payload.create({
583
+ collection: CMS_ROLES_SLUG,
584
+ data: role,
585
+ overrideAccess: true
586
+ });
587
+ }));
588
+ }
589
+ //#endregion
99
590
  //#region src/cms/shared/seo-fields.ts
100
591
  const MAX_META_TITLE = 60;
101
592
  const MAX_META_DESCRIPTION = 160;
@@ -1693,12 +2184,31 @@ function buildCmsConfig(options) {
1693
2184
  "event-tags": createEventTags(access),
1694
2185
  "events": createEvents(access)
1695
2186
  };
1696
- const collections = [...STOCK_COLLECTION_SLUGS.map((slug) => applyCollectionOverride(stock[slug], overrides[slug])).filter((c) => c !== null), ...options.extraCollections ?? []];
1697
- const siteSettings = applyGlobalOverride(createSiteSettings(access), overrides["site-settings"]);
2187
+ const rolesSetting = options.access?.roles;
2188
+ const rolesEnabled = rolesSetting != null;
2189
+ const rolesConfig = rolesSetting === true || rolesSetting == null ? {} : rolesSetting;
2190
+ if (rolesEnabled) Object.keys(stock).forEach((slug) => {
2191
+ stock[slug] = withRoles(stock[slug]);
2192
+ });
2193
+ const collections = [
2194
+ ...rolesEnabled ? [createCmsRoles(roleTargetSlugs(rolesConfig))] : [],
2195
+ ...STOCK_COLLECTION_SLUGS.map((slug) => applyCollectionOverride(stock[slug], overrides[slug])).filter((c) => c !== null),
2196
+ ...options.extraCollections ?? []
2197
+ ];
2198
+ const siteSettingsBase = createSiteSettings(access);
2199
+ const siteSettingsWithRoles = rolesEnabled ? withRoles(siteSettingsBase) : siteSettingsBase;
2200
+ const siteSettings = applyGlobalOverride(siteSettingsWithRoles, overrides["site-settings"]);
1698
2201
  const globals = [...siteSettings ? [siteSettings] : [], ...options.extraGlobals ?? []];
1699
- const { jobs: jobsOverride, ...payloadRest } = options.payload ?? {};
2202
+ const { jobs: jobsOverride, onInit: callerOnInit, ...payloadRest } = options.payload ?? {};
1700
2203
  return buildConfig({
1701
2204
  ...payloadRest,
2205
+ onInit: async (payload) => {
2206
+ if (rolesEnabled) {
2207
+ await seedDefaultCmsRoles(payload, rolesConfig);
2208
+ await refreshRoleCache(payload);
2209
+ }
2210
+ if (callerOnInit) await callerOnInit(payload);
2211
+ },
1702
2212
  jobs: {
1703
2213
  autoRun: [{ cron: "* * * * *" }],
1704
2214
  ...jobsOverride
@@ -1843,6 +2353,6 @@ function createAnnouncementBanner(access) {
1843
2353
  };
1844
2354
  }
1845
2355
  //#endregion
1846
- export { richTextBlock as A, languageOptions as B, blogContentBlocks as C, accordionBlock as D, sectionHeadingBlock as E, downloadCtaBlock as F, createAccessHelpers as G, seoFields as H, codeTabsBlock as I, codeBlock as L, richTextEditor as M, videoBlock as N, leafBlocks as O, imageBlock as P, codeBlockLanguages as R, coerceBlogPostCategoriesOnWrite as S, showcaseContentBlocks as T, slugify as U, createAuthors as V, createApiTokens as W, createCategories as _, STOCK_COLLECTION_SLUGS as a, setPublishedAt as b, createSiteSettings as c, createPages as d, createMedia as f, createChangelog as g, createChangelogTags as h, buildCmsConfig as i, getRichTextEditorConfig as j, stepsBlock as k, createUsers as l, createEventTags as m, createS3MediaPlugin as n, applyCollectionOverride as o, createEvents as p, requireEnvAtRuntime as r, applyGlobalOverride as s, createAnnouncementBanner as t, createLegalPages as u, createBlogPosts as v, pageContentBlocks as w, coerceBlogPostCategoriesOnRead as x, draftsWithAutosave as y, defaultCodeLanguage as z };
2356
+ export { hiddenUnless as $, richTextBlock as A, languageOptions as B, blogContentBlocks as C, accordionBlock as D, sectionHeadingBlock as E, downloadCtaBlock as F, ADMIN_ROLE as G, seoFields as H, codeTabsBlock as I, DEFAULT_ROLES as J, DEFAULT_COLLECTION_ROLES as K, codeBlock as L, richTextEditor as M, videoBlock as N, leafBlocks as O, imageBlock as P, globalRoleRule as Q, codeBlockLanguages as R, coerceBlogPostCategoriesOnWrite as S, showcaseContentBlocks as T, slugify as U, createAuthors as V, createApiTokens as W, collectionRoleRule as X, adminRoleAccess as Y, effectiveRoles as Z, createCategories as _, STOCK_COLLECTION_SLUGS as a, writeAccess as at, setPublishedAt as b, createSiteSettings as c, createPages as d, isCmsAdmin as et, createMedia as f, createChangelog as g, createChangelogTags as h, buildCmsConfig as i, withRoles as it, getRichTextEditorConfig as j, stepsBlock as k, createUsers as l, createEventTags as m, createS3MediaPlugin as n, resolveRoleOptions as nt, applyCollectionOverride as o, createAccessHelpers as ot, createEvents as p, DEFAULT_GLOBAL_ROLES as q, requireEnvAtRuntime as r, userHasAnyRole as rt, applyGlobalOverride as s, createAnnouncementBanner as t, publishedOrRoleRead as tt, createLegalPages as u, createBlogPosts as v, pageContentBlocks as w, coerceBlogPostCategoriesOnRead as x, draftsWithAutosave as y, defaultCodeLanguage as z };
1847
2357
 
1848
2358
  //# sourceMappingURL=cms.mjs.map