@bison-lab/payload-core 3.11.0 → 3.12.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 +41 -16
- package/dist/admin.d.mts +16 -1
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +236 -3
- package/dist/admin.mjs.map +1 -1
- package/dist/identity-DZOb3_Gk.mjs.map +1 -1
- package/dist/index.d.mts +251 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +658 -3
- package/dist/index.mjs.map +1 -1
- package/dist/looks-BbL309kO.mjs.map +1 -1
- package/dist/{looks-DsizRfFV.d.mts → looks-CLxXwASa.d.mts} +3 -4
- package/dist/{looks-DsizRfFV.d.mts.map → looks-CLxXwASa.d.mts.map} +1 -1
- package/dist/theme.d.mts +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { A as THEME_IDENTITY_FALLBACK, C as THEME_APPEARANCE_FIELD, D as THEME_D
|
|
|
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";
|
|
6
|
+
import { APIError, ValidationError, definePlugin, validations } from "payload";
|
|
7
|
+
import { select, text } from "payload/shared";
|
|
6
8
|
//#region src/seo/fields.ts
|
|
7
9
|
/**
|
|
8
10
|
* The index switch, last in the SEO tab. Off by default: a page is public
|
|
@@ -239,7 +241,7 @@ const LIBRARY_SUCCESS_HEX = "#22c55e";
|
|
|
239
241
|
function requireAccess$1(options) {
|
|
240
242
|
const read = options.access?.read;
|
|
241
243
|
const update = options.access?.update;
|
|
242
|
-
if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass
|
|
244
|
+
if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass canManageBrand from this package as update.");
|
|
243
245
|
return {
|
|
244
246
|
read,
|
|
245
247
|
update
|
|
@@ -674,7 +676,7 @@ const BRAND_ASSETS_MIME_TYPES = [
|
|
|
674
676
|
function requireAccess(options) {
|
|
675
677
|
const read = options.access?.read;
|
|
676
678
|
const update = options.access?.update;
|
|
677
|
-
if (!read || !update) throw new Error("createBrandAssets requires access.read and access.update. Pass
|
|
679
|
+
if (!read || !update) throw new Error("createBrandAssets requires access.read and access.update. Pass canManageBrand from this package as update.");
|
|
678
680
|
return {
|
|
679
681
|
read,
|
|
680
682
|
update
|
|
@@ -777,6 +779,659 @@ const THEME_PREVIEW_BREAKPOINTS = [
|
|
|
777
779
|
}
|
|
778
780
|
];
|
|
779
781
|
//#endregion
|
|
780
|
-
|
|
782
|
+
//#region src/roles/types.ts
|
|
783
|
+
/**
|
|
784
|
+
* The Roles Global a site's generated types will describe. Optional and
|
|
785
|
+
* nullable, no index signature: a generated Global is assignable to this,
|
|
786
|
+
* never the reverse.
|
|
787
|
+
*/
|
|
788
|
+
const ROLES = [
|
|
789
|
+
"developer",
|
|
790
|
+
"admin",
|
|
791
|
+
"designer",
|
|
792
|
+
"author"
|
|
793
|
+
];
|
|
794
|
+
const ROLE_LABELS = {
|
|
795
|
+
developer: "Developer",
|
|
796
|
+
admin: "Admin",
|
|
797
|
+
designer: "Designer",
|
|
798
|
+
author: "Author"
|
|
799
|
+
};
|
|
800
|
+
const CAPABILITIES = [
|
|
801
|
+
"content",
|
|
802
|
+
"brand",
|
|
803
|
+
"publish",
|
|
804
|
+
"users"
|
|
805
|
+
];
|
|
806
|
+
function isRole(value) {
|
|
807
|
+
return typeof value === "string" && ROLES.includes(value);
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/roles/matrix.ts
|
|
811
|
+
const ROLES_SLUG = "roles";
|
|
812
|
+
/**
|
|
813
|
+
* Default rank and ticks. Brand is Designer only. The API tab is not a
|
|
814
|
+
* column — it is locked to Developer in `isDeveloper`.
|
|
815
|
+
*/
|
|
816
|
+
const DEFAULT_ROLE_MATRIX = [
|
|
817
|
+
{
|
|
818
|
+
role: "developer",
|
|
819
|
+
content: true,
|
|
820
|
+
brand: true,
|
|
821
|
+
publish: true,
|
|
822
|
+
users: true
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
role: "admin",
|
|
826
|
+
content: true,
|
|
827
|
+
brand: false,
|
|
828
|
+
publish: true,
|
|
829
|
+
users: true
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
role: "designer",
|
|
833
|
+
content: false,
|
|
834
|
+
brand: true,
|
|
835
|
+
publish: false,
|
|
836
|
+
users: false
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
role: "author",
|
|
840
|
+
content: true,
|
|
841
|
+
brand: false,
|
|
842
|
+
publish: false,
|
|
843
|
+
users: false
|
|
844
|
+
}
|
|
845
|
+
];
|
|
846
|
+
const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
|
|
847
|
+
const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
|
|
848
|
+
const CAPABILITY_LABELS = {
|
|
849
|
+
content: "Content",
|
|
850
|
+
brand: "Brand",
|
|
851
|
+
publish: "Publish",
|
|
852
|
+
users: "Users"
|
|
853
|
+
};
|
|
854
|
+
function defaultRolesFieldValue() {
|
|
855
|
+
return DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
856
|
+
id: row.role,
|
|
857
|
+
...row
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
function parseRolesMatrix(value) {
|
|
861
|
+
if (!Array.isArray(value)) return {
|
|
862
|
+
ok: false,
|
|
863
|
+
message: "Roles must be Developer, Admin, Designer, and Author."
|
|
864
|
+
};
|
|
865
|
+
const rows = [];
|
|
866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
867
|
+
for (const item of value) {
|
|
868
|
+
if (!item || typeof item !== "object") continue;
|
|
869
|
+
const role = "role" in item ? item.role : void 0;
|
|
870
|
+
if (!isRole(role) || seen.has(role)) continue;
|
|
871
|
+
seen.add(role);
|
|
872
|
+
rows.push({
|
|
873
|
+
id: role,
|
|
874
|
+
role,
|
|
875
|
+
content: Boolean("content" in item && item.content),
|
|
876
|
+
brand: Boolean("brand" in item && item.brand),
|
|
877
|
+
publish: Boolean("publish" in item && item.publish),
|
|
878
|
+
users: Boolean("users" in item && item.users)
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (rows.length !== ROLES.length || ROLES.some((role) => !seen.has(role))) return {
|
|
882
|
+
ok: false,
|
|
883
|
+
message: "Roles must be Developer, Admin, Designer, and Author."
|
|
884
|
+
};
|
|
885
|
+
return {
|
|
886
|
+
ok: true,
|
|
887
|
+
rows
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function hasPrivilegedUsersTick(rows) {
|
|
891
|
+
return rows.some((row) => (row.role === "admin" || row.role === "developer") && row.users);
|
|
892
|
+
}
|
|
893
|
+
function validateRolesMatrix(value) {
|
|
894
|
+
const parsed = parseRolesMatrix(value);
|
|
895
|
+
if (!parsed.ok) return parsed.message;
|
|
896
|
+
if (!hasPrivilegedUsersTick(parsed.rows)) return LAST_USERS_TICK_MESSAGE;
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Developer is exclusive. Admin does not swallow Designer: both store.
|
|
901
|
+
*/
|
|
902
|
+
function normalizeStoredRoles(value) {
|
|
903
|
+
if (!Array.isArray(value)) return [];
|
|
904
|
+
const roles = [...new Set(value.filter(isRole))];
|
|
905
|
+
if (roles.includes("developer")) return ["developer"];
|
|
906
|
+
return roles;
|
|
907
|
+
}
|
|
908
|
+
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
909
|
+
return matrix.map((row) => ({
|
|
910
|
+
label: ROLE_LABELS[row.role],
|
|
911
|
+
value: row.role
|
|
912
|
+
}));
|
|
913
|
+
}
|
|
914
|
+
/** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
|
|
915
|
+
function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
916
|
+
if (role === "developer") return DEVELOPER_DESCRIPTION;
|
|
917
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
918
|
+
if (!row) return "No capabilities";
|
|
919
|
+
return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
|
|
920
|
+
}
|
|
921
|
+
//#endregion
|
|
922
|
+
//#region src/roles/access.ts
|
|
923
|
+
function isAccessArgs(value) {
|
|
924
|
+
return typeof value === "object" && value !== null && "req" in value;
|
|
925
|
+
}
|
|
926
|
+
function storedRoles(user) {
|
|
927
|
+
return Array.isArray(user?.roles) ? user.roles : [];
|
|
928
|
+
}
|
|
929
|
+
function hasRole(user, role) {
|
|
930
|
+
return storedRoles(user).includes(role);
|
|
931
|
+
}
|
|
932
|
+
/** Not a tick. True only when `developer` is stored on the row. */
|
|
933
|
+
function isDeveloper(user) {
|
|
934
|
+
return hasRole(user, "developer");
|
|
935
|
+
}
|
|
936
|
+
function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
|
|
937
|
+
return storedRoles(user).some((role) => {
|
|
938
|
+
return matrix.find((entry) => entry.role === role)?.[capability] === true;
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Reads the saved Roles Global, falling back to the seed when the row is
|
|
943
|
+
* empty, missing, or unreadable. Always override-access so a Designer
|
|
944
|
+
* evaluating Theme does not have to read Settings → Roles.
|
|
945
|
+
*/
|
|
946
|
+
async function getRolesMatrix(req = {}) {
|
|
947
|
+
const findGlobal = req.payload?.findGlobal;
|
|
948
|
+
if (typeof findGlobal !== "function") return [...DEFAULT_ROLE_MATRIX];
|
|
949
|
+
try {
|
|
950
|
+
const doc = await findGlobal({
|
|
951
|
+
slug: ROLES_SLUG,
|
|
952
|
+
overrideAccess: true,
|
|
953
|
+
req
|
|
954
|
+
});
|
|
955
|
+
const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
|
|
956
|
+
return parsed.ok ? parsed.rows : [...DEFAULT_ROLE_MATRIX];
|
|
957
|
+
} catch {
|
|
958
|
+
return [...DEFAULT_ROLE_MATRIX];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function capabilityPredicate(capability) {
|
|
962
|
+
function predicate(userOrArgs, matrix) {
|
|
963
|
+
if (isAccessArgs(userOrArgs)) {
|
|
964
|
+
const user = userOrArgs.req.user;
|
|
965
|
+
if (matrix) return hasCapability(user, capability, matrix);
|
|
966
|
+
return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
|
|
967
|
+
}
|
|
968
|
+
return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
|
|
969
|
+
}
|
|
970
|
+
return predicate;
|
|
971
|
+
}
|
|
972
|
+
const canManageContent = capabilityPredicate("content");
|
|
973
|
+
const canManageBrand = capabilityPredicate("brand");
|
|
974
|
+
const canPublish = capabilityPredicate("publish");
|
|
975
|
+
/** Users tick on any held role. */
|
|
976
|
+
const isAdmin = capabilityPredicate("users");
|
|
977
|
+
/** This role id currently has the Users tick. */
|
|
978
|
+
function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
979
|
+
return isRole(role) && matrix.some((row) => row.role === role && row.users);
|
|
980
|
+
}
|
|
981
|
+
function isAuthenticated(userOrArgs) {
|
|
982
|
+
if (isAccessArgs(userOrArgs)) return Boolean(userOrArgs.req.user);
|
|
983
|
+
return Boolean(userOrArgs);
|
|
984
|
+
}
|
|
985
|
+
const isAdminOrSelf = async ({ req }) => {
|
|
986
|
+
if (!req.user) return false;
|
|
987
|
+
if (await isAdmin({ req })) return true;
|
|
988
|
+
return { id: { equals: req.user.id } };
|
|
989
|
+
};
|
|
990
|
+
const authenticatedOrPublished = ({ req: { user } }) => {
|
|
991
|
+
if (isAuthenticated(user)) return true;
|
|
992
|
+
return { _status: { equals: "published" } };
|
|
993
|
+
};
|
|
994
|
+
/** API tab condition: Developer only, not the Users tick. */
|
|
995
|
+
function isDeveloperTab({ req }) {
|
|
996
|
+
return isDeveloper(req.user);
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/roles/fields.ts
|
|
1000
|
+
const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
|
|
1001
|
+
const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/roles/global.ts
|
|
1004
|
+
/**
|
|
1005
|
+
* Settings → Roles. Rank is the array order (Payload's drag handle). Ticks
|
|
1006
|
+
* are Content, Brand, Publish, Users. The API tab is not a column.
|
|
1007
|
+
*/
|
|
1008
|
+
function createRoles() {
|
|
1009
|
+
return {
|
|
1010
|
+
slug: ROLES_SLUG,
|
|
1011
|
+
label: "Roles",
|
|
1012
|
+
admin: {
|
|
1013
|
+
group: "Settings",
|
|
1014
|
+
hidden: ({ user }) => !isAdmin(user),
|
|
1015
|
+
description: "Who may do what. Drag to change rank. The API tab is locked to Developer."
|
|
1016
|
+
},
|
|
1017
|
+
access: {
|
|
1018
|
+
read: (args) => isAdmin(args),
|
|
1019
|
+
update: (args) => isAdmin(args)
|
|
1020
|
+
},
|
|
1021
|
+
fields: [{
|
|
1022
|
+
name: "roles",
|
|
1023
|
+
type: "array",
|
|
1024
|
+
label: "Roles",
|
|
1025
|
+
labels: {
|
|
1026
|
+
singular: "Role",
|
|
1027
|
+
plural: "Roles"
|
|
1028
|
+
},
|
|
1029
|
+
minRows: 4,
|
|
1030
|
+
maxRows: 4,
|
|
1031
|
+
required: true,
|
|
1032
|
+
defaultValue: defaultRolesFieldValue(),
|
|
1033
|
+
validate: validateRolesMatrix,
|
|
1034
|
+
admin: {
|
|
1035
|
+
components: { Field: ROLES_MATRIX_FIELD },
|
|
1036
|
+
description: "Drag to change rank. Ticks are Content, Brand, Publish, and Users. The API tab is locked to Developer.",
|
|
1037
|
+
initCollapsed: false
|
|
1038
|
+
},
|
|
1039
|
+
fields: [
|
|
1040
|
+
{
|
|
1041
|
+
name: "role",
|
|
1042
|
+
type: "select",
|
|
1043
|
+
label: "Role",
|
|
1044
|
+
required: true,
|
|
1045
|
+
options: ROLES.map((value) => ({
|
|
1046
|
+
label: ROLE_LABELS[value],
|
|
1047
|
+
value
|
|
1048
|
+
})),
|
|
1049
|
+
admin: { readOnly: true }
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "content",
|
|
1053
|
+
type: "checkbox",
|
|
1054
|
+
label: "Content"
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
name: "brand",
|
|
1058
|
+
type: "checkbox",
|
|
1059
|
+
label: "Brand"
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "publish",
|
|
1063
|
+
type: "checkbox",
|
|
1064
|
+
label: "Publish"
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: "users",
|
|
1068
|
+
type: "checkbox",
|
|
1069
|
+
label: "Users"
|
|
1070
|
+
}
|
|
1071
|
+
]
|
|
1072
|
+
}]
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/roles/seed.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* Writes the default matrix. For a site's migration `up()`. Never writes a
|
|
1079
|
+
* user row — seeding `developer` on a person stays a site concern.
|
|
1080
|
+
*/
|
|
1081
|
+
async function seedRoles(payload) {
|
|
1082
|
+
return payload.updateGlobal({
|
|
1083
|
+
slug: ROLES_SLUG,
|
|
1084
|
+
data: { roles: defaultRolesFieldValue() }
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/fields/slug.ts
|
|
1089
|
+
/**
|
|
1090
|
+
* A stored slug from whatever was typed: lowercased, with any run of
|
|
1091
|
+
* whitespace and slashes stripped from either end in one pass, so `/ about-us /`
|
|
1092
|
+
* does not keep the inner spaces a trim-then-strip would leave.
|
|
1093
|
+
*/
|
|
1094
|
+
function normalizeSlug(value) {
|
|
1095
|
+
return value.replace(/^[\s/]+|[\s/]+$/g, "").toLowerCase();
|
|
1096
|
+
}
|
|
1097
|
+
async function slugProblem(slug, { collection, id, isReserved, req }) {
|
|
1098
|
+
if (!slug) return void 0;
|
|
1099
|
+
if (isReserved(slug)) return `"/${slug}" is a built-in page on this site, so a page here cannot use it. Please choose a different address.`;
|
|
1100
|
+
if (typeof req?.payload?.find !== "function") return void 0;
|
|
1101
|
+
const where = { slug: { equals: slug } };
|
|
1102
|
+
if (id !== void 0) where.id = { not_equals: id };
|
|
1103
|
+
const query = {
|
|
1104
|
+
collection,
|
|
1105
|
+
depth: 0,
|
|
1106
|
+
limit: 1,
|
|
1107
|
+
overrideAccess: true,
|
|
1108
|
+
pagination: false,
|
|
1109
|
+
req,
|
|
1110
|
+
where
|
|
1111
|
+
};
|
|
1112
|
+
return (await req.payload.find(query)).docs.length > 0 || (await req.payload.find({
|
|
1113
|
+
...query,
|
|
1114
|
+
draft: true
|
|
1115
|
+
})).docs.length > 0 ? `Another page is already using "/${slug}". Please choose a different address.` : void 0;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* The path a document is published at, normalised on the way in. Unique
|
|
1119
|
+
* across the collection. `validate` is the message under the field;
|
|
1120
|
+
* `beforeChange` is the enforcement on a draft save, where Payload skips
|
|
1121
|
+
* field validation.
|
|
1122
|
+
*/
|
|
1123
|
+
function slugField({ collection, isReserved }) {
|
|
1124
|
+
const validate = async (value, options) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const builtIn = await validations.text(value, options);
|
|
1127
|
+
if (builtIn !== true) return builtIn;
|
|
1128
|
+
} catch {}
|
|
1129
|
+
return await slugProblem(typeof value === "string" ? value : "", {
|
|
1130
|
+
collection,
|
|
1131
|
+
id: options.id,
|
|
1132
|
+
isReserved,
|
|
1133
|
+
req: options.req
|
|
1134
|
+
}) ?? true;
|
|
1135
|
+
};
|
|
1136
|
+
return {
|
|
1137
|
+
name: "slug",
|
|
1138
|
+
type: "text",
|
|
1139
|
+
required: true,
|
|
1140
|
+
unique: true,
|
|
1141
|
+
admin: { description: "Path under the site root, no leading slash: \"about-us\" or \"patients/stories\"." },
|
|
1142
|
+
hooks: {
|
|
1143
|
+
beforeValidate: [({ value }) => typeof value === "string" ? normalizeSlug(value) : value],
|
|
1144
|
+
beforeChange: [async ({ data, originalDoc, req, value }) => {
|
|
1145
|
+
if (typeof value !== "string") return value;
|
|
1146
|
+
const problem = await slugProblem(value, {
|
|
1147
|
+
collection,
|
|
1148
|
+
id: originalDoc?.id ?? data?.id,
|
|
1149
|
+
isReserved,
|
|
1150
|
+
req
|
|
1151
|
+
});
|
|
1152
|
+
if (problem) throw new ValidationError({
|
|
1153
|
+
collection,
|
|
1154
|
+
errors: [{
|
|
1155
|
+
message: problem,
|
|
1156
|
+
path: "slug"
|
|
1157
|
+
}],
|
|
1158
|
+
req
|
|
1159
|
+
}, req?.t);
|
|
1160
|
+
return value;
|
|
1161
|
+
}]
|
|
1162
|
+
},
|
|
1163
|
+
validate
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
//#endregion
|
|
1167
|
+
//#region src/collections/pages.ts
|
|
1168
|
+
/**
|
|
1169
|
+
* CMS-managed pages: one required hero, then a reorderable body of sections.
|
|
1170
|
+
* The CMS owns copy and the order of sections; what a section looks like is
|
|
1171
|
+
* code-owned, which is why the blocks are an argument.
|
|
1172
|
+
*/
|
|
1173
|
+
function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, previewButton }) {
|
|
1174
|
+
const [defaultHero] = heroBlocks;
|
|
1175
|
+
if (!defaultHero) throw new Error("createPages needs at least one hero block");
|
|
1176
|
+
return {
|
|
1177
|
+
slug: "pages",
|
|
1178
|
+
admin: {
|
|
1179
|
+
useAsTitle: "title",
|
|
1180
|
+
group: "Content",
|
|
1181
|
+
defaultColumns: [
|
|
1182
|
+
"title",
|
|
1183
|
+
"slug",
|
|
1184
|
+
"_status",
|
|
1185
|
+
"updatedAt"
|
|
1186
|
+
],
|
|
1187
|
+
preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
|
|
1188
|
+
...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
|
|
1189
|
+
},
|
|
1190
|
+
access: {
|
|
1191
|
+
read: authenticatedOrPublished,
|
|
1192
|
+
readVersions: isAuthenticated,
|
|
1193
|
+
create: canManageContent,
|
|
1194
|
+
update: canManageContent,
|
|
1195
|
+
delete: isAdmin
|
|
1196
|
+
},
|
|
1197
|
+
versions: {
|
|
1198
|
+
drafts: { autosave: { interval: 375 } },
|
|
1199
|
+
maxPerDoc: 50
|
|
1200
|
+
},
|
|
1201
|
+
fields: [
|
|
1202
|
+
{
|
|
1203
|
+
name: "title",
|
|
1204
|
+
type: "text",
|
|
1205
|
+
required: true
|
|
1206
|
+
},
|
|
1207
|
+
slugField({
|
|
1208
|
+
collection: "pages",
|
|
1209
|
+
isReserved: isReservedSlug
|
|
1210
|
+
}),
|
|
1211
|
+
{
|
|
1212
|
+
name: "hero",
|
|
1213
|
+
type: "blocks",
|
|
1214
|
+
required: true,
|
|
1215
|
+
minRows: 1,
|
|
1216
|
+
maxRows: 1,
|
|
1217
|
+
blocks: heroBlocks,
|
|
1218
|
+
defaultValue: [{ blockType: defaultHero.slug }],
|
|
1219
|
+
admin: { description: "Every page opens with one hero. A page cannot be published without one." }
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
name: "layout",
|
|
1223
|
+
type: "blocks",
|
|
1224
|
+
required: true,
|
|
1225
|
+
minRows: 1,
|
|
1226
|
+
blocks: layoutBlocks,
|
|
1227
|
+
labels: {
|
|
1228
|
+
singular: "Section",
|
|
1229
|
+
plural: "Sections"
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
]
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/collections/users.ts
|
|
1237
|
+
const nameValidate = (value, options) => {
|
|
1238
|
+
if (options.operation === "update" && value === "" && options.previousValue === "") return true;
|
|
1239
|
+
return text(value, options);
|
|
1240
|
+
};
|
|
1241
|
+
function nameField(name) {
|
|
1242
|
+
return {
|
|
1243
|
+
name,
|
|
1244
|
+
type: "text",
|
|
1245
|
+
required: true,
|
|
1246
|
+
validate: nameValidate
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/** Refuses the save that would remove Users from the only privileged user. */
|
|
1250
|
+
const LAST_ADMIN_DEMOTE_MESSAGE = "Make another user an admin before removing it from this one.";
|
|
1251
|
+
/** Refuses the delete that would remove the only privileged user. */
|
|
1252
|
+
const LAST_ADMIN_DELETE_MESSAGE = "Make another user an admin before deleting this one.";
|
|
1253
|
+
const LAST_ADMIN_LOCK = "select pg_advisory_xact_lock(hashtext('users:last-admin'))";
|
|
1254
|
+
async function lockLastAdminDecision(req) {
|
|
1255
|
+
const { execute, sessions } = req.payload.db;
|
|
1256
|
+
const id = req.transactionID;
|
|
1257
|
+
const session = typeof id === "string" || typeof id === "number" ? sessions?.[id] : void 0;
|
|
1258
|
+
if (!session || typeof execute !== "function") return;
|
|
1259
|
+
await execute({
|
|
1260
|
+
db: session.db,
|
|
1261
|
+
raw: LAST_ADMIN_LOCK
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
function privilegedRoles(matrix) {
|
|
1265
|
+
return matrix.filter((row) => row.users).map((row) => row.role);
|
|
1266
|
+
}
|
|
1267
|
+
function holdsPrivilegedRole(roles, matrix) {
|
|
1268
|
+
return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
|
|
1269
|
+
}
|
|
1270
|
+
function privilegedWhere(matrix) {
|
|
1271
|
+
const roles = privilegedRoles(matrix);
|
|
1272
|
+
if (roles.length === 0) return { id: { equals: "__none__" } };
|
|
1273
|
+
return { or: roles.map((role) => ({ roles: { contains: role } })) };
|
|
1274
|
+
}
|
|
1275
|
+
async function adminCount(req, excluding) {
|
|
1276
|
+
await lockLastAdminDecision(req);
|
|
1277
|
+
const holdsPrivileged = privilegedWhere(await getRolesMatrix(req));
|
|
1278
|
+
const { totalDocs } = await req.payload.count({
|
|
1279
|
+
collection: "users",
|
|
1280
|
+
overrideAccess: true,
|
|
1281
|
+
req,
|
|
1282
|
+
where: excluding === void 0 ? holdsPrivileged : { and: [holdsPrivileged, { id: { not_equals: excluding } }] }
|
|
1283
|
+
});
|
|
1284
|
+
return totalDocs;
|
|
1285
|
+
}
|
|
1286
|
+
const rolesValidate = async (value, options) => {
|
|
1287
|
+
const builtIn = select(value, options);
|
|
1288
|
+
if (builtIn !== true) return builtIn;
|
|
1289
|
+
if (options.operation !== "update" || options.id === void 0) return true;
|
|
1290
|
+
const matrix = await getRolesMatrix(options.req);
|
|
1291
|
+
if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
|
|
1292
|
+
return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
|
|
1293
|
+
};
|
|
1294
|
+
function createUsers({ secureCookies, rolesField }) {
|
|
1295
|
+
return {
|
|
1296
|
+
slug: "users",
|
|
1297
|
+
auth: {
|
|
1298
|
+
maxLoginAttempts: 5,
|
|
1299
|
+
lockTime: 600 * 1e3,
|
|
1300
|
+
tokenExpiration: 7200,
|
|
1301
|
+
cookies: {
|
|
1302
|
+
sameSite: "Lax",
|
|
1303
|
+
secure: secureCookies
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
admin: {
|
|
1307
|
+
useAsTitle: "email",
|
|
1308
|
+
defaultColumns: [
|
|
1309
|
+
"email",
|
|
1310
|
+
"firstName",
|
|
1311
|
+
"lastName",
|
|
1312
|
+
"roles"
|
|
1313
|
+
],
|
|
1314
|
+
hidden: ({ user }) => !isAdmin(user)
|
|
1315
|
+
},
|
|
1316
|
+
access: {
|
|
1317
|
+
create: isAdmin,
|
|
1318
|
+
delete: isAdmin,
|
|
1319
|
+
unlock: isAdmin,
|
|
1320
|
+
read: isAdminOrSelf,
|
|
1321
|
+
update: isAdminOrSelf
|
|
1322
|
+
},
|
|
1323
|
+
hooks: {
|
|
1324
|
+
beforeDelete: [async ({ id, req }) => {
|
|
1325
|
+
if (!isAdmin(await req.payload.findByID({
|
|
1326
|
+
collection: "users",
|
|
1327
|
+
id,
|
|
1328
|
+
depth: 0,
|
|
1329
|
+
disableErrors: true,
|
|
1330
|
+
overrideAccess: true,
|
|
1331
|
+
req
|
|
1332
|
+
})) || await adminCount(req, id) > 0) return;
|
|
1333
|
+
throw new APIError(LAST_ADMIN_DELETE_MESSAGE, 400);
|
|
1334
|
+
}],
|
|
1335
|
+
afterOperation: [async (arg) => {
|
|
1336
|
+
const { operation, req } = arg;
|
|
1337
|
+
const touchesRoles = (operation === "update" || operation === "updateByID") && arg.args.data?.roles !== void 0;
|
|
1338
|
+
const deletes = operation === "delete" || operation === "deleteByID";
|
|
1339
|
+
if ((touchesRoles || deletes) && await adminCount(req) === 0) throw new APIError(deletes ? LAST_ADMIN_DELETE_MESSAGE : LAST_ADMIN_DEMOTE_MESSAGE, 400);
|
|
1340
|
+
return arg.result;
|
|
1341
|
+
}]
|
|
1342
|
+
},
|
|
1343
|
+
fields: [{
|
|
1344
|
+
type: "row",
|
|
1345
|
+
fields: [nameField("firstName"), nameField("lastName")]
|
|
1346
|
+
}, {
|
|
1347
|
+
name: "roles",
|
|
1348
|
+
type: "select",
|
|
1349
|
+
hasMany: true,
|
|
1350
|
+
required: true,
|
|
1351
|
+
defaultValue: ["author"],
|
|
1352
|
+
options: roleSelectOptions(),
|
|
1353
|
+
admin: {
|
|
1354
|
+
components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
|
|
1355
|
+
description: "One person can hold several, e.g. Author + Designer."
|
|
1356
|
+
},
|
|
1357
|
+
access: { update: isAdmin },
|
|
1358
|
+
validate: rolesValidate,
|
|
1359
|
+
hooks: { beforeValidate: [({ value }) => Array.isArray(value) ? normalizeStoredRoles(value) : value] }
|
|
1360
|
+
}]
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
//#endregion
|
|
1364
|
+
//#region src/collections/media.ts
|
|
1365
|
+
/**
|
|
1366
|
+
* Uploads, readable by anyone: the public site serves these files. Writing is
|
|
1367
|
+
* an editorial action, deleting is not — removing an asset a live page
|
|
1368
|
+
* references breaks that page.
|
|
1369
|
+
*/
|
|
1370
|
+
function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes } = {}) {
|
|
1371
|
+
return {
|
|
1372
|
+
slug: "media",
|
|
1373
|
+
access: {
|
|
1374
|
+
read: () => true,
|
|
1375
|
+
create: canManageContent,
|
|
1376
|
+
update: canManageContent,
|
|
1377
|
+
delete: isAdmin
|
|
1378
|
+
},
|
|
1379
|
+
upload: {
|
|
1380
|
+
staticDir,
|
|
1381
|
+
mimeTypes,
|
|
1382
|
+
imageSizes
|
|
1383
|
+
},
|
|
1384
|
+
fields: [{
|
|
1385
|
+
name: "alt",
|
|
1386
|
+
type: "text",
|
|
1387
|
+
required: true
|
|
1388
|
+
}]
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/plugins/admin-only-api-tab.ts
|
|
1393
|
+
function gateApiTab(entity) {
|
|
1394
|
+
const components = entity.admin?.components;
|
|
1395
|
+
const edit = components?.views?.edit;
|
|
1396
|
+
const api = edit && "api" in edit ? edit.api : void 0;
|
|
1397
|
+
return {
|
|
1398
|
+
...entity,
|
|
1399
|
+
admin: {
|
|
1400
|
+
...entity.admin,
|
|
1401
|
+
components: {
|
|
1402
|
+
...components,
|
|
1403
|
+
views: {
|
|
1404
|
+
...components?.views,
|
|
1405
|
+
edit: {
|
|
1406
|
+
...edit,
|
|
1407
|
+
api: {
|
|
1408
|
+
...api,
|
|
1409
|
+
tab: {
|
|
1410
|
+
...api?.tab,
|
|
1411
|
+
condition: isDeveloperTab
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Gates the document API tab to Developer, config-wide. After plugins that
|
|
1422
|
+
* use `definePlugin` order (MCP is 10), so a collection those plugins
|
|
1423
|
+
* register is still covered.
|
|
1424
|
+
*/
|
|
1425
|
+
const adminOnlyApiTab = definePlugin({
|
|
1426
|
+
slug: "admin-only-api-tab",
|
|
1427
|
+
order: 1e3,
|
|
1428
|
+
plugin: ({ config }) => ({
|
|
1429
|
+
...config,
|
|
1430
|
+
collections: config.collections?.map(gateApiTab),
|
|
1431
|
+
globals: config.globals?.map(gateApiTab)
|
|
1432
|
+
})
|
|
1433
|
+
});
|
|
1434
|
+
//#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 };
|
|
781
1436
|
|
|
782
1437
|
//# sourceMappingURL=index.mjs.map
|