@bison-lab/payload-core 3.12.0 → 3.13.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/README.md +63 -16
- package/dist/admin.d.mts +28 -9
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +448 -25
- package/dist/admin.mjs.map +1 -1
- package/dist/{identity-DZOb3_Gk.mjs → identity-LAzSeorC.mjs} +48 -2
- package/dist/identity-LAzSeorC.mjs.map +1 -0
- package/dist/index.d.mts +272 -54
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +625 -85
- package/dist/index.mjs.map +1 -1
- package/dist/looks-BbL309kO.mjs.map +1 -1
- package/dist/{looks-CLxXwASa.d.mts → looks-C6kTjvk3.d.mts} +21 -2
- package/dist/looks-C6kTjvk3.d.mts.map +1 -0
- package/dist/theme.d.mts +2 -2
- package/dist/theme.mjs +2 -2
- package/package.json +1 -1
- package/dist/identity-DZOb3_Gk.mjs.map +0 -1
- package/dist/looks-CLxXwASa.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as THEME_FONT_FIELD, C as validateThemeHex, D as THEME_COLOR_SCALE_FIELD, E as THEME_COLOR_FIELD, F as THEME_PUBLISH_FIELD, I as THEME_SAVE_BUTTON, L as THEME_SECTION_HEADING, M as THEME_IDENTITY_FALLBACK, N as THEME_LIBRARY_FIELD, O as THEME_CONTRAST_REPORT, P as THEME_PAIRING_FIELD, R as headingSelectValue, S as validateSourceIncluded, T as THEME_APPEARANCE_FIELD, _ as THEME_SLUG, a as pageEditorLooks, c as findColorTokens, d as rewriteColorToken, f as themeColorKeys, g as THEME_IDENTITY_SLUG, h as THEME_COLORS_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_GREY_SCALE_FIELD, k as THEME_DOCUMENT_CONTROLS, l as rewriteColorTokens, m as THEME_APPEARANCE_SLUG, n as colorTokenField, o as pageEditorTokens, p as SYSTEM_COLOR_KEYS, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as deleteLibraryColor, v as THEME_TYPOGRAPHY_SLUG, w as LOOK_FIELD, x as themeConfigFromDoc, y as docFromConfig } from "./identity-LAzSeorC.mjs";
|
|
3
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
4
4
|
import { catalog } from "@bison-lab/fonts";
|
|
5
5
|
import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
|
|
@@ -164,6 +164,145 @@ const APPEARANCE_CHOICES = {
|
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
//#endregion
|
|
167
|
+
//#region src/theme/color-usages.ts
|
|
168
|
+
/** Theme-store REST path. LibraryField calls `/api/globals/theme` + this. */
|
|
169
|
+
const COLOR_USAGES_PATH = "/color-usages";
|
|
170
|
+
/**
|
|
171
|
+
* Walk every current document in the named collections and globals
|
|
172
|
+
* (drafts included). A color only on an unpublished page is still in use.
|
|
173
|
+
*/
|
|
174
|
+
async function findColorUsages(payload, scopes, key) {
|
|
175
|
+
const usages = [];
|
|
176
|
+
for (const collection of scopes?.collections ?? []) for await (const doc of eachCollectionDoc(payload, collection)) for (const hit of findColorTokens(doc, key)) usages.push({
|
|
177
|
+
collection,
|
|
178
|
+
id: idOf(doc),
|
|
179
|
+
path: hit.path,
|
|
180
|
+
token: hit.token
|
|
181
|
+
});
|
|
182
|
+
for (const slug of scopes?.globals ?? []) {
|
|
183
|
+
const doc = await payload.findGlobal({
|
|
184
|
+
slug,
|
|
185
|
+
draft: true,
|
|
186
|
+
depth: 0,
|
|
187
|
+
overrideAccess: true
|
|
188
|
+
});
|
|
189
|
+
if (!doc) continue;
|
|
190
|
+
for (const hit of findColorTokens(doc, key)) usages.push({
|
|
191
|
+
global: slug,
|
|
192
|
+
path: hit.path,
|
|
193
|
+
token: hit.token
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return usages;
|
|
197
|
+
}
|
|
198
|
+
async function rewriteColorUsages(payload, scopes, fromKey, toKey) {
|
|
199
|
+
for (const collection of scopes?.collections ?? []) for await (const doc of eachCollectionDoc(payload, collection)) {
|
|
200
|
+
if (findColorTokens(doc, fromKey).length === 0) continue;
|
|
201
|
+
const { id: _id, ...data } = rewriteColorTokens(doc, fromKey, toKey);
|
|
202
|
+
const id = idOf(doc);
|
|
203
|
+
if (id == null) continue;
|
|
204
|
+
await payload.update({
|
|
205
|
+
collection,
|
|
206
|
+
id,
|
|
207
|
+
data,
|
|
208
|
+
draft: true,
|
|
209
|
+
overrideAccess: true
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
for (const slug of scopes?.globals ?? []) {
|
|
213
|
+
const doc = await payload.findGlobal({
|
|
214
|
+
slug,
|
|
215
|
+
draft: true,
|
|
216
|
+
depth: 0,
|
|
217
|
+
overrideAccess: true
|
|
218
|
+
});
|
|
219
|
+
if (!doc || findColorTokens(doc, fromKey).length === 0) continue;
|
|
220
|
+
await payload.updateGlobal({
|
|
221
|
+
slug,
|
|
222
|
+
data: rewriteColorTokens(doc, fromKey, toKey),
|
|
223
|
+
draft: true,
|
|
224
|
+
overrideAccess: true
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function replacementKeys(payload, except) {
|
|
229
|
+
return pageEditorLooks(await payload.findGlobal({
|
|
230
|
+
slug: THEME_SLUG,
|
|
231
|
+
depth: 0,
|
|
232
|
+
overrideAccess: true
|
|
233
|
+
})).map((look) => look.value).filter((key) => key !== except);
|
|
234
|
+
}
|
|
235
|
+
async function* eachCollectionDoc(payload, collection) {
|
|
236
|
+
let page = 1;
|
|
237
|
+
for (;;) {
|
|
238
|
+
const result = await payload.find({
|
|
239
|
+
collection,
|
|
240
|
+
draft: true,
|
|
241
|
+
depth: 0,
|
|
242
|
+
limit: 100,
|
|
243
|
+
page,
|
|
244
|
+
overrideAccess: true
|
|
245
|
+
});
|
|
246
|
+
for (const doc of result.docs) yield doc;
|
|
247
|
+
const totalPages = result.totalPages ?? (result.docs.length < 100 ? page : page + 1);
|
|
248
|
+
if (page >= totalPages || result.docs.length === 0) break;
|
|
249
|
+
page += 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function idOf(doc) {
|
|
253
|
+
const id = doc.id;
|
|
254
|
+
return typeof id === "string" || typeof id === "number" ? id : void 0;
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/theme/color-usage-endpoints.ts
|
|
258
|
+
function colorUsageEndpoints(access, scopes) {
|
|
259
|
+
return [{
|
|
260
|
+
path: COLOR_USAGES_PATH,
|
|
261
|
+
method: "get",
|
|
262
|
+
handler: async (req) => {
|
|
263
|
+
const denied = await denyUnlessUpdate(req, access.update);
|
|
264
|
+
if (denied) return denied;
|
|
265
|
+
const key = queryKey(req);
|
|
266
|
+
if (!key) return Response.json({ error: "key is required" }, { status: 400 });
|
|
267
|
+
const usages = await findColorUsages(req.payload, scopes, key);
|
|
268
|
+
return Response.json({ usages });
|
|
269
|
+
}
|
|
270
|
+
}, {
|
|
271
|
+
path: COLOR_USAGES_PATH,
|
|
272
|
+
method: "post",
|
|
273
|
+
handler: async (req) => {
|
|
274
|
+
const denied = await denyUnlessUpdate(req, access.update);
|
|
275
|
+
if (denied) return denied;
|
|
276
|
+
const body = await readJson(req);
|
|
277
|
+
const key = typeof body?.key === "string" ? body.key : "";
|
|
278
|
+
const replacement = typeof body?.replacement === "string" ? body.replacement : "";
|
|
279
|
+
if (!key || !replacement) return Response.json({ error: "key and replacement are required" }, { status: 400 });
|
|
280
|
+
if (!(await replacementKeys(req.payload, key)).includes(replacement)) return Response.json({ error: "Replacement must be a different live system or remaining custom key" }, { status: 400 });
|
|
281
|
+
await rewriteColorUsages(req.payload, scopes, key, replacement);
|
|
282
|
+
return Response.json({ ok: true });
|
|
283
|
+
}
|
|
284
|
+
}];
|
|
285
|
+
}
|
|
286
|
+
async function denyUnlessUpdate(req, update) {
|
|
287
|
+
if (await update({ req })) return null;
|
|
288
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
289
|
+
}
|
|
290
|
+
function queryKey(req) {
|
|
291
|
+
const fromQuery = req.query?.key;
|
|
292
|
+
if (typeof fromQuery === "string") return fromQuery;
|
|
293
|
+
if (Array.isArray(fromQuery) && typeof fromQuery[0] === "string") return fromQuery[0];
|
|
294
|
+
if (req.url) try {
|
|
295
|
+
return new URL(req.url, "http://local").searchParams.get("key") ?? "";
|
|
296
|
+
} catch {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
async function readJson(req) {
|
|
302
|
+
if (typeof req.json === "function") return req.json();
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
167
306
|
//#region src/theme/publish.ts
|
|
168
307
|
/**
|
|
169
308
|
* Publishing a child writes that slice only. Other responsibilities stay
|
|
@@ -570,7 +709,7 @@ function pageHooks(child) {
|
|
|
570
709
|
function createTheme(options) {
|
|
571
710
|
const access = requireAccess$1(options);
|
|
572
711
|
const destructive = requireDestructive(options);
|
|
573
|
-
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish } = options;
|
|
712
|
+
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish, colorUsages } = options;
|
|
574
713
|
const adminCustom = {
|
|
575
714
|
contrastTarget,
|
|
576
715
|
fontsBaseUrl,
|
|
@@ -599,6 +738,7 @@ function createTheme(options) {
|
|
|
599
738
|
...lookFields,
|
|
600
739
|
...markFields
|
|
601
740
|
],
|
|
741
|
+
endpoints: colorUsageEndpoints(access, colorUsages),
|
|
602
742
|
hooks: { afterChange: [async ({ doc }) => {
|
|
603
743
|
if (!onPublish) return;
|
|
604
744
|
const themeDoc = doc;
|
|
@@ -806,9 +946,20 @@ const CAPABILITIES = [
|
|
|
806
946
|
function isRole(value) {
|
|
807
947
|
return typeof value === "string" && ROLES.includes(value);
|
|
808
948
|
}
|
|
949
|
+
/** Lowercase slug: `editor`, `site-editor`. Empty and punctuation are out. */
|
|
950
|
+
function isRoleSlug(value) {
|
|
951
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
952
|
+
}
|
|
953
|
+
function roleLabel(role, label) {
|
|
954
|
+
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
955
|
+
if (trimmed) return trimmed;
|
|
956
|
+
return isRole(role) ? ROLE_LABELS[role] : role;
|
|
957
|
+
}
|
|
809
958
|
//#endregion
|
|
810
959
|
//#region src/roles/matrix.ts
|
|
811
960
|
const ROLES_SLUG = "roles";
|
|
961
|
+
const ROLES_GLOBAL_DESCRIPTION = "Who may do what. Drag to change rank.";
|
|
962
|
+
const ROLES_FIELD_DESCRIPTION = "Drag to change rank. Ticks are Content, Brand, Publish, and Users.";
|
|
812
963
|
/**
|
|
813
964
|
* Default rank and ticks. Brand is Designer only. The API tab is not a
|
|
814
965
|
* column — it is locked to Developer in `isDeveloper`.
|
|
@@ -845,42 +996,64 @@ const DEFAULT_ROLE_MATRIX = [
|
|
|
845
996
|
];
|
|
846
997
|
const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
|
|
847
998
|
const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
|
|
999
|
+
const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
|
|
1000
|
+
const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
|
|
848
1001
|
const CAPABILITY_LABELS = {
|
|
849
1002
|
content: "Content",
|
|
850
1003
|
brand: "Brand",
|
|
851
1004
|
publish: "Publish",
|
|
852
1005
|
users: "Users"
|
|
853
1006
|
};
|
|
854
|
-
function
|
|
855
|
-
return DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
1007
|
+
function seedRoleRows(extras = []) {
|
|
1008
|
+
return [...DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
1009
|
+
...row,
|
|
1010
|
+
label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
|
|
1011
|
+
})), ...extras.map((extra) => ({ ...extra }))];
|
|
1012
|
+
}
|
|
1013
|
+
function defaultRolesFieldValue(extras = []) {
|
|
1014
|
+
return seedRoleRows(extras).map((row) => ({
|
|
856
1015
|
id: row.role,
|
|
857
1016
|
...row
|
|
858
1017
|
}));
|
|
859
1018
|
}
|
|
1019
|
+
function readRoleRow(item) {
|
|
1020
|
+
const role = "role" in item ? item.role : void 0;
|
|
1021
|
+
if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
1022
|
+
if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
1023
|
+
return {
|
|
1024
|
+
id: role,
|
|
1025
|
+
role,
|
|
1026
|
+
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
1027
|
+
content: Boolean("content" in item && item.content),
|
|
1028
|
+
brand: Boolean("brand" in item && item.brand),
|
|
1029
|
+
publish: Boolean("publish" in item && item.publish),
|
|
1030
|
+
users: Boolean("users" in item && item.users)
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
860
1033
|
function parseRolesMatrix(value) {
|
|
861
1034
|
if (!Array.isArray(value)) return {
|
|
862
1035
|
ok: false,
|
|
863
|
-
message:
|
|
1036
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
864
1037
|
};
|
|
865
1038
|
const rows = [];
|
|
866
1039
|
const seen = /* @__PURE__ */ new Set();
|
|
867
1040
|
for (const item of value) {
|
|
868
1041
|
if (!item || typeof item !== "object") continue;
|
|
869
|
-
const
|
|
870
|
-
if (
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1042
|
+
const parsed = readRoleRow(item);
|
|
1043
|
+
if ("error" in parsed) return {
|
|
1044
|
+
ok: false,
|
|
1045
|
+
message: parsed.error
|
|
1046
|
+
};
|
|
1047
|
+
if (seen.has(parsed.role)) return {
|
|
1048
|
+
ok: false,
|
|
1049
|
+
message: DUPLICATE_ROLE_SLUG_MESSAGE
|
|
1050
|
+
};
|
|
1051
|
+
seen.add(parsed.role);
|
|
1052
|
+
rows.push(parsed);
|
|
880
1053
|
}
|
|
881
|
-
if (
|
|
1054
|
+
if (ROLES.some((role) => !seen.has(role))) return {
|
|
882
1055
|
ok: false,
|
|
883
|
-
message:
|
|
1056
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
884
1057
|
};
|
|
885
1058
|
return {
|
|
886
1059
|
ok: true,
|
|
@@ -898,16 +1071,19 @@ function validateRolesMatrix(value) {
|
|
|
898
1071
|
}
|
|
899
1072
|
/**
|
|
900
1073
|
* Developer is exclusive. Admin does not swallow Designer: both store.
|
|
1074
|
+
* Unknown slugs (dropped catalogue keys such as `approver`) fall away unless
|
|
1075
|
+
* they appear on the matrix.
|
|
901
1076
|
*/
|
|
902
|
-
function normalizeStoredRoles(value) {
|
|
1077
|
+
function normalizeStoredRoles(value, matrix = DEFAULT_ROLE_MATRIX) {
|
|
903
1078
|
if (!Array.isArray(value)) return [];
|
|
904
|
-
const
|
|
1079
|
+
const allowed = new Set(matrix.map((row) => row.role));
|
|
1080
|
+
const roles = [...new Set(value.filter((entry) => typeof entry === "string" && allowed.has(entry)))];
|
|
905
1081
|
if (roles.includes("developer")) return ["developer"];
|
|
906
1082
|
return roles;
|
|
907
1083
|
}
|
|
908
1084
|
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
909
1085
|
return matrix.map((row) => ({
|
|
910
|
-
label:
|
|
1086
|
+
label: roleLabel(row.role, row.label),
|
|
911
1087
|
value: row.role
|
|
912
1088
|
}));
|
|
913
1089
|
}
|
|
@@ -918,9 +1094,22 @@ function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
|
918
1094
|
if (!row) return "No capabilities";
|
|
919
1095
|
return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
|
|
920
1096
|
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Seed ticks and package labels come back; extra rows stay as they are.
|
|
1099
|
+
*/
|
|
1100
|
+
function resetRolesMatrix(current) {
|
|
1101
|
+
const extras = [];
|
|
1102
|
+
if (Array.isArray(current)) for (const item of current) {
|
|
1103
|
+
if (!item || typeof item !== "object") continue;
|
|
1104
|
+
const parsed = readRoleRow(item);
|
|
1105
|
+
if ("error" in parsed || isRole(parsed.role)) continue;
|
|
1106
|
+
extras.push(parsed);
|
|
1107
|
+
}
|
|
1108
|
+
return [...defaultRolesFieldValue(), ...extras];
|
|
1109
|
+
}
|
|
921
1110
|
//#endregion
|
|
922
1111
|
//#region src/roles/access.ts
|
|
923
|
-
function isAccessArgs(value) {
|
|
1112
|
+
function isAccessArgs$1(value) {
|
|
924
1113
|
return typeof value === "object" && value !== null && "req" in value;
|
|
925
1114
|
}
|
|
926
1115
|
function storedRoles(user) {
|
|
@@ -943,9 +1132,10 @@ function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
|
|
|
943
1132
|
* empty, missing, or unreadable. Always override-access so a Designer
|
|
944
1133
|
* evaluating Theme does not have to read Settings → Roles.
|
|
945
1134
|
*/
|
|
946
|
-
async function getRolesMatrix(req = {}) {
|
|
1135
|
+
async function getRolesMatrix(req = {}, extras = []) {
|
|
1136
|
+
const fallback = seedRoleRows(extras);
|
|
947
1137
|
const findGlobal = req.payload?.findGlobal;
|
|
948
|
-
if (typeof findGlobal !== "function") return
|
|
1138
|
+
if (typeof findGlobal !== "function") return fallback;
|
|
949
1139
|
try {
|
|
950
1140
|
const doc = await findGlobal({
|
|
951
1141
|
slug: ROLES_SLUG,
|
|
@@ -953,14 +1143,14 @@ async function getRolesMatrix(req = {}) {
|
|
|
953
1143
|
req
|
|
954
1144
|
});
|
|
955
1145
|
const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
|
|
956
|
-
return parsed.ok ? parsed.rows :
|
|
1146
|
+
return parsed.ok ? parsed.rows : fallback;
|
|
957
1147
|
} catch {
|
|
958
|
-
return
|
|
1148
|
+
return fallback;
|
|
959
1149
|
}
|
|
960
1150
|
}
|
|
961
1151
|
function capabilityPredicate(capability) {
|
|
962
1152
|
function predicate(userOrArgs, matrix) {
|
|
963
|
-
if (isAccessArgs(userOrArgs)) {
|
|
1153
|
+
if (isAccessArgs$1(userOrArgs)) {
|
|
964
1154
|
const user = userOrArgs.req.user;
|
|
965
1155
|
if (matrix) return hasCapability(user, capability, matrix);
|
|
966
1156
|
return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
|
|
@@ -976,10 +1166,10 @@ const canPublish = capabilityPredicate("publish");
|
|
|
976
1166
|
const isAdmin = capabilityPredicate("users");
|
|
977
1167
|
/** This role id currently has the Users tick. */
|
|
978
1168
|
function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
979
|
-
return
|
|
1169
|
+
return typeof role === "string" && matrix.some((row) => row.role === role && row.users);
|
|
980
1170
|
}
|
|
981
1171
|
function isAuthenticated(userOrArgs) {
|
|
982
|
-
if (isAccessArgs(userOrArgs)) return Boolean(userOrArgs.req.user);
|
|
1172
|
+
if (isAccessArgs$1(userOrArgs)) return Boolean(userOrArgs.req.user);
|
|
983
1173
|
return Boolean(userOrArgs);
|
|
984
1174
|
}
|
|
985
1175
|
const isAdminOrSelf = async ({ req }) => {
|
|
@@ -998,93 +1188,431 @@ function isDeveloperTab({ req }) {
|
|
|
998
1188
|
//#endregion
|
|
999
1189
|
//#region src/roles/fields.ts
|
|
1000
1190
|
const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
|
|
1191
|
+
const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
|
|
1192
|
+
const ROLE_SLUG_FIELD = "@bison-lab/payload-core/admin#RoleSlugField";
|
|
1001
1193
|
const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
|
|
1002
1194
|
//#endregion
|
|
1003
1195
|
//#region src/roles/global.ts
|
|
1196
|
+
const roleSlugValidate = (value, options) => {
|
|
1197
|
+
if (options.operation === "update" && isRole(options.previousValue)) return value === options.previousValue || "Each role needs a unique slug.";
|
|
1198
|
+
if (typeof value !== "string" || value === "") return DUPLICATE_ROLE_SLUG_MESSAGE;
|
|
1199
|
+
return isRoleSlug(value) || "Each role needs a unique slug.";
|
|
1200
|
+
};
|
|
1004
1201
|
/**
|
|
1005
1202
|
* Settings → Roles. Rank is the array order (Payload's drag handle). Ticks
|
|
1006
1203
|
* are Content, Brand, Publish, Users. The API tab is not a column.
|
|
1007
1204
|
*/
|
|
1008
|
-
function createRoles() {
|
|
1205
|
+
function createRoles({ extras = [] } = {}) {
|
|
1206
|
+
const rolesField = {
|
|
1207
|
+
name: "roles",
|
|
1208
|
+
type: "array",
|
|
1209
|
+
label: "Roles",
|
|
1210
|
+
labels: {
|
|
1211
|
+
singular: "Role",
|
|
1212
|
+
plural: "Roles"
|
|
1213
|
+
},
|
|
1214
|
+
minRows: 4,
|
|
1215
|
+
required: true,
|
|
1216
|
+
defaultValue: defaultRolesFieldValue(extras),
|
|
1217
|
+
validate: validateRolesMatrix,
|
|
1218
|
+
admin: {
|
|
1219
|
+
components: {
|
|
1220
|
+
Field: ROLES_MATRIX_FIELD,
|
|
1221
|
+
RowLabel: ROLES_ROW_LABEL
|
|
1222
|
+
},
|
|
1223
|
+
description: ROLES_FIELD_DESCRIPTION,
|
|
1224
|
+
initCollapsed: false
|
|
1225
|
+
},
|
|
1226
|
+
fields: [
|
|
1227
|
+
{
|
|
1228
|
+
name: "role",
|
|
1229
|
+
type: "text",
|
|
1230
|
+
label: "Slug",
|
|
1231
|
+
required: true,
|
|
1232
|
+
validate: roleSlugValidate,
|
|
1233
|
+
hooks: { beforeChange: [({ value, previousValue }) => isRole(previousValue) ? previousValue : value] },
|
|
1234
|
+
admin: { components: { Field: ROLE_SLUG_FIELD } }
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
name: "label",
|
|
1238
|
+
type: "text",
|
|
1239
|
+
label: "Name",
|
|
1240
|
+
required: true
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
name: "content",
|
|
1244
|
+
type: "checkbox",
|
|
1245
|
+
label: "Content"
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
name: "brand",
|
|
1249
|
+
type: "checkbox",
|
|
1250
|
+
label: "Brand"
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: "publish",
|
|
1254
|
+
type: "checkbox",
|
|
1255
|
+
label: "Publish"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
name: "users",
|
|
1259
|
+
type: "checkbox",
|
|
1260
|
+
label: "Users"
|
|
1261
|
+
}
|
|
1262
|
+
]
|
|
1263
|
+
};
|
|
1009
1264
|
return {
|
|
1010
1265
|
slug: ROLES_SLUG,
|
|
1011
1266
|
label: "Roles",
|
|
1012
1267
|
admin: {
|
|
1013
1268
|
group: "Settings",
|
|
1014
1269
|
hidden: ({ user }) => !isAdmin(user),
|
|
1015
|
-
description:
|
|
1270
|
+
description: ROLES_GLOBAL_DESCRIPTION
|
|
1271
|
+
},
|
|
1272
|
+
access: {
|
|
1273
|
+
read: (args) => isAdmin(args),
|
|
1274
|
+
update: (args) => isAdmin(args)
|
|
1275
|
+
},
|
|
1276
|
+
fields: [rolesField]
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/roles/seed.ts
|
|
1281
|
+
/**
|
|
1282
|
+
* Writes the default matrix (seed four plus any site extras). For a site's
|
|
1283
|
+
* migration `up()`. Never writes a user row — seeding `developer` on a
|
|
1284
|
+
* person stays a site concern.
|
|
1285
|
+
*/
|
|
1286
|
+
async function seedRoles(payload, extras = []) {
|
|
1287
|
+
return payload.updateGlobal({
|
|
1288
|
+
slug: ROLES_SLUG,
|
|
1289
|
+
data: { roles: defaultRolesFieldValue(extras) }
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
//#endregion
|
|
1293
|
+
//#region src/features/fields.ts
|
|
1294
|
+
const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField";
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region src/features/types.ts
|
|
1297
|
+
const FEATURES_SLUG = "features";
|
|
1298
|
+
const PACKAGE_FEATURE_SLUGS = [
|
|
1299
|
+
"pages",
|
|
1300
|
+
"media",
|
|
1301
|
+
"theme",
|
|
1302
|
+
"brand-assets",
|
|
1303
|
+
"users",
|
|
1304
|
+
"roles",
|
|
1305
|
+
"features"
|
|
1306
|
+
];
|
|
1307
|
+
/**
|
|
1308
|
+
* Package catalogue. Locked rows cannot be turned off. Empty Global falls
|
|
1309
|
+
* back to the named capability (Content → Pages/Media, Brand → Theme /
|
|
1310
|
+
* Brand assets, Users → Users/Roles/Features).
|
|
1311
|
+
*/
|
|
1312
|
+
const PACKAGE_FEATURES = [
|
|
1313
|
+
{
|
|
1314
|
+
slug: "pages",
|
|
1315
|
+
label: "Pages",
|
|
1316
|
+
fallback: "content",
|
|
1317
|
+
locked: false
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
slug: "media",
|
|
1321
|
+
label: "Media",
|
|
1322
|
+
fallback: "content",
|
|
1323
|
+
locked: false
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
slug: "theme",
|
|
1327
|
+
label: "Theme",
|
|
1328
|
+
fallback: "brand",
|
|
1329
|
+
locked: false
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
slug: "brand-assets",
|
|
1333
|
+
label: "Brand assets",
|
|
1334
|
+
fallback: "brand",
|
|
1335
|
+
locked: false
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
slug: "users",
|
|
1339
|
+
label: "Users",
|
|
1340
|
+
fallback: "users",
|
|
1341
|
+
locked: true
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
slug: "roles",
|
|
1345
|
+
label: "Roles",
|
|
1346
|
+
fallback: "users",
|
|
1347
|
+
locked: true
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
slug: "features",
|
|
1351
|
+
label: "Features",
|
|
1352
|
+
fallback: "users",
|
|
1353
|
+
locked: true
|
|
1354
|
+
}
|
|
1355
|
+
];
|
|
1356
|
+
function isPackageFeatureSlug(value) {
|
|
1357
|
+
return typeof value === "string" && PACKAGE_FEATURE_SLUGS.includes(value);
|
|
1358
|
+
}
|
|
1359
|
+
function isFeatureSlug(value) {
|
|
1360
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
1361
|
+
}
|
|
1362
|
+
function isLockedFeature(slug, locked) {
|
|
1363
|
+
if (locked === true) return true;
|
|
1364
|
+
return PACKAGE_FEATURES.find((feature) => feature.slug === slug)?.locked === true;
|
|
1365
|
+
}
|
|
1366
|
+
//#endregion
|
|
1367
|
+
//#region src/features/matrix.ts
|
|
1368
|
+
const MISSING_PACKAGE_FEATURES_MESSAGE = "Features must include Pages, Media, Theme, Brand assets, Users, Roles, and Features.";
|
|
1369
|
+
const LOCKED_FEATURE_MESSAGE = "Users, Roles, and Features cannot be turned off. Developer stays allowed on every row.";
|
|
1370
|
+
function featureCatalogue(extras = []) {
|
|
1371
|
+
return [...PACKAGE_FEATURES.map((feature) => ({ ...feature })), ...extras.map((extra) => ({
|
|
1372
|
+
slug: extra.slug,
|
|
1373
|
+
label: extra.label,
|
|
1374
|
+
fallback: null,
|
|
1375
|
+
locked: false
|
|
1376
|
+
}))];
|
|
1377
|
+
}
|
|
1378
|
+
function rolesForFeature(feature, matrix) {
|
|
1379
|
+
const allowed = new Set(["developer"]);
|
|
1380
|
+
if (feature.locked) {
|
|
1381
|
+
for (const row of matrix) allowed.add(row.role);
|
|
1382
|
+
return [...allowed];
|
|
1383
|
+
}
|
|
1384
|
+
if (feature.fallback) {
|
|
1385
|
+
for (const row of matrix) if (row[feature.fallback]) allowed.add(row.role);
|
|
1386
|
+
}
|
|
1387
|
+
return [...allowed];
|
|
1388
|
+
}
|
|
1389
|
+
function defaultFeaturesFieldValue(extras = [], matrix = DEFAULT_ROLE_MATRIX) {
|
|
1390
|
+
return featureCatalogue(extras).map((feature) => ({
|
|
1391
|
+
id: feature.slug,
|
|
1392
|
+
slug: feature.slug,
|
|
1393
|
+
label: feature.label,
|
|
1394
|
+
locked: feature.locked,
|
|
1395
|
+
roles: rolesForFeature(feature, matrix)
|
|
1396
|
+
}));
|
|
1397
|
+
}
|
|
1398
|
+
function parseFeaturesMatrix(value) {
|
|
1399
|
+
if (!Array.isArray(value) || value.length === 0) return {
|
|
1400
|
+
ok: false,
|
|
1401
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
1402
|
+
};
|
|
1403
|
+
const rows = [];
|
|
1404
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1405
|
+
for (const item of value) {
|
|
1406
|
+
if (!item || typeof item !== "object") continue;
|
|
1407
|
+
const slug = "slug" in item ? item.slug : void 0;
|
|
1408
|
+
if (!isFeatureSlug(slug) || seen.has(slug)) continue;
|
|
1409
|
+
seen.add(slug);
|
|
1410
|
+
const rawRoles = "roles" in item && Array.isArray(item.roles) ? item.roles : [];
|
|
1411
|
+
const roles = [];
|
|
1412
|
+
for (const role of rawRoles) if (typeof role === "string" && !roles.includes(role)) roles.push(role);
|
|
1413
|
+
const label = "label" in item && typeof item.label === "string" ? item.label : slug;
|
|
1414
|
+
rows.push({
|
|
1415
|
+
id: slug,
|
|
1416
|
+
slug,
|
|
1417
|
+
label,
|
|
1418
|
+
locked: isLockedFeature(slug, "locked" in item ? Boolean(item.locked) : void 0),
|
|
1419
|
+
roles
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (PACKAGE_FEATURES.some((feature) => !seen.has(feature.slug))) return {
|
|
1423
|
+
ok: false,
|
|
1424
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
1425
|
+
};
|
|
1426
|
+
return {
|
|
1427
|
+
ok: true,
|
|
1428
|
+
rows
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function enforceFeatureLocks(rows, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1432
|
+
const roleSlugs = matrix.map((row) => row.role);
|
|
1433
|
+
return rows.map((row) => {
|
|
1434
|
+
const roles = new Set(row.roles ?? []);
|
|
1435
|
+
roles.add("developer");
|
|
1436
|
+
if (isLockedFeature(row.slug, row.locked)) for (const role of roleSlugs) roles.add(role);
|
|
1437
|
+
return {
|
|
1438
|
+
...row,
|
|
1439
|
+
roles: [...roles]
|
|
1440
|
+
};
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
function validateFeaturesMatrix(value, extras = [], matrix = DEFAULT_ROLE_MATRIX) {
|
|
1444
|
+
const parsed = parseFeaturesMatrix(value);
|
|
1445
|
+
if (!parsed.ok) return parsed.message;
|
|
1446
|
+
const allowed = new Set(featureCatalogue(extras).map((feature) => feature.slug));
|
|
1447
|
+
if (parsed.rows.some((row) => !allowed.has(row.slug))) return MISSING_PACKAGE_FEATURES_MESSAGE;
|
|
1448
|
+
for (const row of parsed.rows) {
|
|
1449
|
+
if (!row.roles?.includes("developer")) return LOCKED_FEATURE_MESSAGE;
|
|
1450
|
+
if (isLockedFeature(row.slug, row.locked) && matrix.some((role) => !row.roles?.includes(role.role))) return LOCKED_FEATURE_MESSAGE;
|
|
1451
|
+
}
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
//#endregion
|
|
1455
|
+
//#region src/features/global.ts
|
|
1456
|
+
/**
|
|
1457
|
+
* Settings → Features. Rows are the catalogue (code). Columns are the
|
|
1458
|
+
* Roles Global. Locked rows and Developer cannot be turned off.
|
|
1459
|
+
*/
|
|
1460
|
+
function createFeatures({ extras = [] } = {}) {
|
|
1461
|
+
const catalogue = featureCatalogue(extras);
|
|
1462
|
+
return {
|
|
1463
|
+
slug: FEATURES_SLUG,
|
|
1464
|
+
label: "Features",
|
|
1465
|
+
admin: {
|
|
1466
|
+
group: "Settings",
|
|
1467
|
+
hidden: ({ user }) => !isAdmin(user),
|
|
1468
|
+
description: "Who may use which collections and globals."
|
|
1016
1469
|
},
|
|
1017
1470
|
access: {
|
|
1018
1471
|
read: (args) => isAdmin(args),
|
|
1019
1472
|
update: (args) => isAdmin(args)
|
|
1020
1473
|
},
|
|
1021
1474
|
fields: [{
|
|
1022
|
-
name: "
|
|
1475
|
+
name: "features",
|
|
1023
1476
|
type: "array",
|
|
1024
|
-
label: "
|
|
1477
|
+
label: "Features",
|
|
1025
1478
|
labels: {
|
|
1026
|
-
singular: "
|
|
1027
|
-
plural: "
|
|
1479
|
+
singular: "Feature",
|
|
1480
|
+
plural: "Features"
|
|
1028
1481
|
},
|
|
1029
|
-
minRows:
|
|
1030
|
-
maxRows:
|
|
1482
|
+
minRows: catalogue.length,
|
|
1483
|
+
maxRows: catalogue.length,
|
|
1031
1484
|
required: true,
|
|
1032
|
-
defaultValue:
|
|
1033
|
-
validate:
|
|
1485
|
+
defaultValue: defaultFeaturesFieldValue(extras),
|
|
1486
|
+
validate: async (value, { req }) => {
|
|
1487
|
+
return validateFeaturesMatrix(value, extras, await getRolesMatrix(req));
|
|
1488
|
+
},
|
|
1489
|
+
hooks: { beforeChange: [async ({ value, req }) => {
|
|
1490
|
+
if (!Array.isArray(value)) return value;
|
|
1491
|
+
return enforceFeatureLocks(value, await getRolesMatrix(req));
|
|
1492
|
+
}] },
|
|
1034
1493
|
admin: {
|
|
1035
|
-
components: { Field:
|
|
1036
|
-
description: "
|
|
1494
|
+
components: { Field: FEATURES_MATRIX_FIELD },
|
|
1495
|
+
description: "Turn a feature on or off per role. Users, Roles, and Features stay on. Developer is always allowed.",
|
|
1037
1496
|
initCollapsed: false
|
|
1038
1497
|
},
|
|
1039
1498
|
fields: [
|
|
1040
1499
|
{
|
|
1041
|
-
name: "
|
|
1042
|
-
type: "
|
|
1043
|
-
label: "
|
|
1500
|
+
name: "slug",
|
|
1501
|
+
type: "text",
|
|
1502
|
+
label: "Slug",
|
|
1044
1503
|
required: true,
|
|
1045
|
-
options: ROLES.map((value) => ({
|
|
1046
|
-
label: ROLE_LABELS[value],
|
|
1047
|
-
value
|
|
1048
|
-
})),
|
|
1049
1504
|
admin: { readOnly: true }
|
|
1050
1505
|
},
|
|
1051
1506
|
{
|
|
1052
|
-
name: "
|
|
1053
|
-
type: "
|
|
1054
|
-
label: "
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
name: "brand",
|
|
1058
|
-
type: "checkbox",
|
|
1059
|
-
label: "Brand"
|
|
1507
|
+
name: "label",
|
|
1508
|
+
type: "text",
|
|
1509
|
+
label: "Name",
|
|
1510
|
+
required: true,
|
|
1511
|
+
admin: { readOnly: true }
|
|
1060
1512
|
},
|
|
1061
1513
|
{
|
|
1062
|
-
name: "
|
|
1514
|
+
name: "locked",
|
|
1063
1515
|
type: "checkbox",
|
|
1064
|
-
label: "
|
|
1516
|
+
label: "Locked",
|
|
1517
|
+
admin: { hidden: true }
|
|
1065
1518
|
},
|
|
1066
1519
|
{
|
|
1067
|
-
name: "
|
|
1068
|
-
type: "
|
|
1069
|
-
label: "
|
|
1520
|
+
name: "roles",
|
|
1521
|
+
type: "json",
|
|
1522
|
+
label: "Roles",
|
|
1523
|
+
required: true
|
|
1070
1524
|
}
|
|
1071
1525
|
]
|
|
1072
1526
|
}]
|
|
1073
1527
|
};
|
|
1074
1528
|
}
|
|
1075
1529
|
//#endregion
|
|
1076
|
-
//#region src/
|
|
1530
|
+
//#region src/features/seed.ts
|
|
1077
1531
|
/**
|
|
1078
|
-
* Writes the default
|
|
1079
|
-
*
|
|
1532
|
+
* Writes the default switchboard (package rows plus any site extras).
|
|
1533
|
+
* For a site's migration `up()`.
|
|
1080
1534
|
*/
|
|
1081
|
-
async function
|
|
1535
|
+
async function seedFeatures(payload, extras = []) {
|
|
1082
1536
|
return payload.updateGlobal({
|
|
1083
|
-
slug:
|
|
1084
|
-
data: {
|
|
1537
|
+
slug: FEATURES_SLUG,
|
|
1538
|
+
data: { features: defaultFeaturesFieldValue(extras) }
|
|
1085
1539
|
});
|
|
1086
1540
|
}
|
|
1087
1541
|
//#endregion
|
|
1542
|
+
//#region src/features/access.ts
|
|
1543
|
+
function isAccessArgs(value) {
|
|
1544
|
+
return typeof value === "object" && value !== null && "req" in value;
|
|
1545
|
+
}
|
|
1546
|
+
function fallbackFeature(user, slug, matrix) {
|
|
1547
|
+
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
1548
|
+
if (!pack) return false;
|
|
1549
|
+
return hasCapability(user, pack.fallback, matrix);
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* True when any stored role is on the feature row. An empty grid falls
|
|
1553
|
+
* back to the capability map. Developer is always allowed.
|
|
1554
|
+
*/
|
|
1555
|
+
function hasFeature(user, slug, features = null, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1556
|
+
if (isDeveloper(user)) return true;
|
|
1557
|
+
if (!user) return false;
|
|
1558
|
+
if (!features || features.length === 0) return fallbackFeature(user, slug, matrix);
|
|
1559
|
+
const row = features.find((entry) => entry.slug === slug);
|
|
1560
|
+
if (!row) return false;
|
|
1561
|
+
const allowed = row.roles ?? [];
|
|
1562
|
+
return storedRoles(user).some((role) => allowed.includes(role));
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Reads the saved Features Global. `null` means empty or unreadable — callers
|
|
1566
|
+
* fall back to the capability map. Always override-access.
|
|
1567
|
+
*/
|
|
1568
|
+
async function getFeaturesMatrix(req = {}) {
|
|
1569
|
+
const findGlobal = req.payload?.findGlobal;
|
|
1570
|
+
if (typeof findGlobal !== "function") return null;
|
|
1571
|
+
try {
|
|
1572
|
+
const doc = await findGlobal({
|
|
1573
|
+
slug: FEATURES_SLUG,
|
|
1574
|
+
overrideAccess: true,
|
|
1575
|
+
req
|
|
1576
|
+
});
|
|
1577
|
+
const parsed = parseFeaturesMatrix(doc && typeof doc === "object" && "features" in doc ? doc.features : void 0);
|
|
1578
|
+
if (!parsed.ok) return null;
|
|
1579
|
+
const matrix = await getRolesMatrix(req);
|
|
1580
|
+
return enforceFeatureLocks(parsed.rows, matrix);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Access / nav helper for one catalogue slug. Sync against a passed grid;
|
|
1587
|
+
* async when given `req` so it can read both Globals.
|
|
1588
|
+
*/
|
|
1589
|
+
function canUseFeature(slug) {
|
|
1590
|
+
function predicate(userOrArgs, features, roles) {
|
|
1591
|
+
if (isAccessArgs(userOrArgs)) {
|
|
1592
|
+
const user = userOrArgs.req.user;
|
|
1593
|
+
if (features !== void 0) return hasFeature(user, slug, features, roles ?? DEFAULT_ROLE_MATRIX);
|
|
1594
|
+
return Promise.all([getFeaturesMatrix(userOrArgs.req), getRolesMatrix(userOrArgs.req)]).then(([grid, matrix]) => hasFeature(user, slug, grid, matrix));
|
|
1595
|
+
}
|
|
1596
|
+
return hasFeature(userOrArgs, slug, features ?? null, roles ?? DEFAULT_ROLE_MATRIX);
|
|
1597
|
+
}
|
|
1598
|
+
return predicate;
|
|
1599
|
+
}
|
|
1600
|
+
/** `admin.hidden`: hide when the login cannot use the feature. */
|
|
1601
|
+
function hideUnlessFeature(slug) {
|
|
1602
|
+
return (args) => {
|
|
1603
|
+
const user = args.user;
|
|
1604
|
+
if (args.req) {
|
|
1605
|
+
const result = canUseFeature(slug)({ req: {
|
|
1606
|
+
...args.req,
|
|
1607
|
+
user
|
|
1608
|
+
} });
|
|
1609
|
+
if (result instanceof Promise) return result.then((ok) => !ok);
|
|
1610
|
+
return !result;
|
|
1611
|
+
}
|
|
1612
|
+
return !canUseFeature(slug)(user ?? null);
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1088
1616
|
//#region src/fields/slug.ts
|
|
1089
1617
|
/**
|
|
1090
1618
|
* A stored slug from whatever was typed: lowercased, with any run of
|
|
@@ -1184,14 +1712,15 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
|
|
|
1184
1712
|
"_status",
|
|
1185
1713
|
"updatedAt"
|
|
1186
1714
|
],
|
|
1715
|
+
hidden: hideUnlessFeature("pages"),
|
|
1187
1716
|
preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
|
|
1188
1717
|
...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
|
|
1189
1718
|
},
|
|
1190
1719
|
access: {
|
|
1191
1720
|
read: authenticatedOrPublished,
|
|
1192
1721
|
readVersions: isAuthenticated,
|
|
1193
|
-
create:
|
|
1194
|
-
update:
|
|
1722
|
+
create: canUseFeature("pages"),
|
|
1723
|
+
update: canUseFeature("pages"),
|
|
1195
1724
|
delete: isAdmin
|
|
1196
1725
|
},
|
|
1197
1726
|
versions: {
|
|
@@ -1283,15 +1812,21 @@ async function adminCount(req, excluding) {
|
|
|
1283
1812
|
});
|
|
1284
1813
|
return totalDocs;
|
|
1285
1814
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1815
|
+
function rolesValidate(extras) {
|
|
1816
|
+
return async (value, options) => {
|
|
1817
|
+
const matrix = await getRolesMatrix(options.req, extras);
|
|
1818
|
+
const builtIn = select(value, {
|
|
1819
|
+
...options,
|
|
1820
|
+
options: roleSelectOptions(matrix)
|
|
1821
|
+
});
|
|
1822
|
+
if (builtIn !== true) return builtIn;
|
|
1823
|
+
if (options.operation !== "update" || options.id === void 0) return true;
|
|
1824
|
+
if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
|
|
1825
|
+
return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function createUsers({ secureCookies, rolesField, extras = [] }) {
|
|
1829
|
+
const fallback = seedRoleRows(extras);
|
|
1295
1830
|
return {
|
|
1296
1831
|
slug: "users",
|
|
1297
1832
|
auth: {
|
|
@@ -1304,6 +1839,7 @@ function createUsers({ secureCookies, rolesField }) {
|
|
|
1304
1839
|
}
|
|
1305
1840
|
},
|
|
1306
1841
|
admin: {
|
|
1842
|
+
group: "Settings",
|
|
1307
1843
|
useAsTitle: "email",
|
|
1308
1844
|
defaultColumns: [
|
|
1309
1845
|
"email",
|
|
@@ -1329,7 +1865,7 @@ function createUsers({ secureCookies, rolesField }) {
|
|
|
1329
1865
|
disableErrors: true,
|
|
1330
1866
|
overrideAccess: true,
|
|
1331
1867
|
req
|
|
1332
|
-
})) || await adminCount(req, id) > 0) return;
|
|
1868
|
+
}), await getRolesMatrix(req, extras)) || await adminCount(req, id) > 0) return;
|
|
1333
1869
|
throw new APIError(LAST_ADMIN_DELETE_MESSAGE, 400);
|
|
1334
1870
|
}],
|
|
1335
1871
|
afterOperation: [async (arg) => {
|
|
@@ -1349,14 +1885,17 @@ function createUsers({ secureCookies, rolesField }) {
|
|
|
1349
1885
|
hasMany: true,
|
|
1350
1886
|
required: true,
|
|
1351
1887
|
defaultValue: ["author"],
|
|
1352
|
-
options: roleSelectOptions(),
|
|
1888
|
+
options: roleSelectOptions(fallback),
|
|
1353
1889
|
admin: {
|
|
1354
1890
|
components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
|
|
1355
1891
|
description: "One person can hold several, e.g. Author + Designer."
|
|
1356
1892
|
},
|
|
1357
1893
|
access: { update: isAdmin },
|
|
1358
|
-
validate: rolesValidate,
|
|
1359
|
-
hooks: { beforeValidate: [({ value }) =>
|
|
1894
|
+
validate: rolesValidate(extras),
|
|
1895
|
+
hooks: { beforeValidate: [async ({ value, req }) => {
|
|
1896
|
+
if (!Array.isArray(value)) return value;
|
|
1897
|
+
return normalizeStoredRoles(value, await getRolesMatrix(req, extras));
|
|
1898
|
+
}] }
|
|
1360
1899
|
}]
|
|
1361
1900
|
};
|
|
1362
1901
|
}
|
|
@@ -1370,10 +1909,11 @@ function createUsers({ secureCookies, rolesField }) {
|
|
|
1370
1909
|
function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes } = {}) {
|
|
1371
1910
|
return {
|
|
1372
1911
|
slug: "media",
|
|
1912
|
+
admin: { hidden: hideUnlessFeature("media") },
|
|
1373
1913
|
access: {
|
|
1374
1914
|
read: () => true,
|
|
1375
|
-
create:
|
|
1376
|
-
update:
|
|
1915
|
+
create: canUseFeature("media"),
|
|
1916
|
+
update: canUseFeature("media"),
|
|
1377
1917
|
delete: isAdmin
|
|
1378
1918
|
},
|
|
1379
1919
|
upload: {
|
|
@@ -1432,6 +1972,6 @@ const adminOnlyApiTab = definePlugin({
|
|
|
1432
1972
|
})
|
|
1433
1973
|
});
|
|
1434
1974
|
//#endregion
|
|
1435
|
-
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_MATRIX_FIELD, ROLES_SLUG, ROLE_LABELS, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_APPEARANCE_SLUG, THEME_COLORS_SLUG, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_DOCUMENT_CONTROLS, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_IDENTITY_SLUG, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, THEME_TYPOGRAPHY_SLUG, adminOnlyApiTab, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, colorTokenField, createBrandAssets, createMedia, createPages, createRoles, createTheme, createUsers, defaultRolesFieldValue, deleteLibraryColor, documentTitle, firstImageIn, getRolesMatrix, hasCapability, hasRole, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isPrivilegedRole, isRole, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseRolesMatrix, persistThemeChild, publishThemeChild, resolveThemeIdentity, rewriteColorToken, roleDescription, roleSelectOptions, sanitizeSvg, seedRoles, seedTheme, seoPlugin, slugField, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateRolesMatrix };
|
|
1975
|
+
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOCKED_FEATURE_MESSAGE, LOOK_FIELD, MISSING_PACKAGE_FEATURES_MESSAGE, MISSING_SEED_ROLES_MESSAGE, PACKAGE_FEATURES, PACKAGE_FEATURE_SLUGS, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_FIELD_DESCRIPTION, ROLES_GLOBAL_DESCRIPTION, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_SLUG_FIELD, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_APPEARANCE_SLUG, THEME_COLORS_SLUG, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_DOCUMENT_CONTROLS, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_IDENTITY_SLUG, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, THEME_TYPOGRAPHY_SLUG, adminOnlyApiTab, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, documentTitle, featureCatalogue, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasRole, hideUnlessFeature, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleSlug, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, resetRolesMatrix, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
|
|
1436
1976
|
|
|
1437
1977
|
//# sourceMappingURL=index.mjs.map
|