@easy-cms/core 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +82 -3
- package/dist/index.js +373 -56
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -744,6 +744,9 @@ type GlobalInput<C extends Config, S extends GlobalSlug<C>> = Partial<Simplify<F
|
|
|
744
744
|
/** What a role may do with a collection or global, ticked in Settings → Roles. */
|
|
745
745
|
declare const ROLE_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
|
|
746
746
|
type RoleOperation = (typeof ROLE_OPERATIONS)[number];
|
|
747
|
+
/** A field a role may only read, or not see at all; fields not listed follow the collection. */
|
|
748
|
+
type FieldRule = 'read' | 'hidden';
|
|
749
|
+
type FieldRules = Readonly<Record<string, Readonly<Record<string, FieldRule>>>>;
|
|
747
750
|
/**
|
|
748
751
|
* Stored in a role's `permissions`. Anything not listed is not allowed. A slug listed with no
|
|
749
752
|
* operations has been looked at and given nothing (it is not "new" in Settings → Roles).
|
|
@@ -753,6 +756,13 @@ interface RolePermissions {
|
|
|
753
756
|
readonly globals?: Readonly<Record<string, readonly RoleOperation[]>>;
|
|
754
757
|
/** Admin pages: `status`, `deliveries`, `page:<path>` (`admin.pages`), `widget:<tag>` (`admin.dashboard`). */
|
|
755
758
|
readonly admin?: readonly string[];
|
|
759
|
+
/** Operations of `collections` allowed only on the role's own documents (see `admin.ownerField`). */
|
|
760
|
+
readonly own?: Readonly<Record<string, readonly RoleOperation[]>>;
|
|
761
|
+
/** Top-level fields the role may only read or not see, by collection or global slug. */
|
|
762
|
+
readonly fields?: {
|
|
763
|
+
readonly collections?: FieldRules;
|
|
764
|
+
readonly globals?: FieldRules;
|
|
765
|
+
};
|
|
756
766
|
}
|
|
757
767
|
/** One role as Settings → Roles shows it (`GET <api>/admin/roles`). */
|
|
758
768
|
interface AdminRole {
|
|
@@ -767,6 +777,15 @@ interface AdminRole {
|
|
|
767
777
|
permissions: RolePermissions;
|
|
768
778
|
updatedAt: string;
|
|
769
779
|
}
|
|
780
|
+
/** A top-level field whose permission can be set per role. */
|
|
781
|
+
interface AdminRoleField {
|
|
782
|
+
name: string;
|
|
783
|
+
label?: Label;
|
|
784
|
+
/** Required without a default value: roles that create documents must be able to fill it. */
|
|
785
|
+
required: boolean;
|
|
786
|
+
/** The collection's owner field (`admin.ownerField`). */
|
|
787
|
+
owner?: boolean;
|
|
788
|
+
}
|
|
770
789
|
/** A row of the permissions table. */
|
|
771
790
|
interface AdminRoleTarget {
|
|
772
791
|
slug: string;
|
|
@@ -774,6 +793,7 @@ interface AdminRoleTarget {
|
|
|
774
793
|
ops: RoleOperation[];
|
|
775
794
|
/** No role has been given anything here yet (added to the config since). */
|
|
776
795
|
new: boolean;
|
|
796
|
+
fields: AdminRoleField[];
|
|
777
797
|
}
|
|
778
798
|
/** A page or dashboard panel that can be given to roles. */
|
|
779
799
|
interface AdminRoleView {
|
|
@@ -787,6 +807,8 @@ interface AdminRoles {
|
|
|
787
807
|
collections: (AdminRoleTarget & {
|
|
788
808
|
/** Collections its relationship and upload fields point to. */
|
|
789
809
|
references: string[];
|
|
810
|
+
/** Can be limited to own documents: the field that names the owner (`createdBy` by default). */
|
|
811
|
+
owner?: string;
|
|
790
812
|
})[];
|
|
791
813
|
globals: AdminRoleTarget[];
|
|
792
814
|
views: AdminRoleView[];
|
|
@@ -808,11 +830,12 @@ interface AdminRoleChange {
|
|
|
808
830
|
declare class Roles {
|
|
809
831
|
private readonly config;
|
|
810
832
|
private readonly db;
|
|
833
|
+
private readonly log?;
|
|
811
834
|
readonly enabled: boolean;
|
|
812
835
|
private readonly versions;
|
|
813
836
|
private cache;
|
|
814
837
|
private synced;
|
|
815
|
-
constructor(config: ResolvedConfig, db: Database);
|
|
838
|
+
constructor(config: ResolvedConfig, db: Database, log?: ((message: string) => void) | undefined);
|
|
816
839
|
/**
|
|
817
840
|
* Whether the user's role allows an operation: `true`, `false`, or for their own account
|
|
818
841
|
* (users without read or update on `users`) a constraint to it.
|
|
@@ -827,6 +850,25 @@ declare class Roles {
|
|
|
827
850
|
* it; without roles, nobody else (`status`, `deliveries`) or everybody (plugin pages).
|
|
828
851
|
*/
|
|
829
852
|
canView(user: AuthUser, id: string): Promise<boolean>;
|
|
853
|
+
/**
|
|
854
|
+
* The field that names a collection's owner, for "own documents only": `admin.ownerField`, or
|
|
855
|
+
* `createdBy`. `undefined` without roles, and for users and collections roles don't cover.
|
|
856
|
+
*/
|
|
857
|
+
ownerOf(slug: string): string | undefined;
|
|
858
|
+
/**
|
|
859
|
+
* The user's field rules (top-level fields their role may only read, or not see), for field
|
|
860
|
+
* access checks. Roles that may update only their own documents can't change the owner field.
|
|
861
|
+
*/
|
|
862
|
+
fieldRules(user: AuthUser | null): Promise<ReadonlyMap<Field, FieldRule> | undefined>;
|
|
863
|
+
/** A new document is its creator's: sets `createdBy`, and an empty owner field. */
|
|
864
|
+
fillOwner(slug: string, data: Record<string, unknown>, user: AuthUser | null): void;
|
|
865
|
+
/** Documents a user owns, by collection (`createdBy` and owner fields), for deleting them. */
|
|
866
|
+
owned(userId: ID): Promise<{
|
|
867
|
+
collection: string;
|
|
868
|
+
count: number;
|
|
869
|
+
}[]>;
|
|
870
|
+
/** Gives a deleted user's documents to another user, or to nobody (`null`). */
|
|
871
|
+
transfer(from: ID, to: ID | null): Promise<void>;
|
|
830
872
|
/** Forgets cached permissions, after a role changed. */
|
|
831
873
|
invalidate(): void;
|
|
832
874
|
/**
|
|
@@ -835,6 +877,12 @@ declare class Roles {
|
|
|
835
877
|
*/
|
|
836
878
|
sync(): Promise<void>;
|
|
837
879
|
private addMissing;
|
|
880
|
+
/**
|
|
881
|
+
* Documents from before roles have no `createdBy`: it is filled from who saved their first
|
|
882
|
+
* version, for collections with versions. Only while no document of the collection has one,
|
|
883
|
+
* so it runs once (and finds nothing to do afterwards).
|
|
884
|
+
*/
|
|
885
|
+
private backfillCreators;
|
|
838
886
|
/** Whether a role exists (users can only be given existing roles). */
|
|
839
887
|
exists(key: string): Promise<boolean>;
|
|
840
888
|
/** Roles a user can be given, those of `auth.roles` first; `name` may be empty. */
|
|
@@ -860,6 +908,15 @@ declare class Roles {
|
|
|
860
908
|
private everything;
|
|
861
909
|
/** Keeps only known collections, globals, operations and pages; `read` comes with the others. */
|
|
862
910
|
private clean;
|
|
911
|
+
/**
|
|
912
|
+
* Keeps rules for known top-level fields. A field that must be filled in (required, without a
|
|
913
|
+
* default) can't be read-only or hidden for a role that creates documents.
|
|
914
|
+
*/
|
|
915
|
+
private cleanRules;
|
|
916
|
+
/** Fields that name a collection's owner (`createdBy`, and `admin.ownerField`). */
|
|
917
|
+
private ownerColumns;
|
|
918
|
+
/** Sets one field of every matching document, keeping the rest as stored. */
|
|
919
|
+
private setWhere;
|
|
863
920
|
private cleanName;
|
|
864
921
|
private permissionsOf;
|
|
865
922
|
private load;
|
|
@@ -1107,7 +1164,13 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1107
1164
|
private previewURL;
|
|
1108
1165
|
private versionTarget;
|
|
1109
1166
|
private updateDocument;
|
|
1110
|
-
|
|
1167
|
+
/**
|
|
1168
|
+
* Deletes a document. Deleting a user with roles from the admin (`auth.rbac`): `transferTo`
|
|
1169
|
+
* gives the documents they own to another user; without it they have no owner.
|
|
1170
|
+
*/
|
|
1171
|
+
delete<S extends Slug<C>>(collection: S, id: ID, options?: AccessOptions & {
|
|
1172
|
+
transferTo?: ID | null;
|
|
1173
|
+
}): Promise<Doc<C, S>>;
|
|
1111
1174
|
findGlobal<S extends GSlug<C>>(slug: S, options?: ReadOptions): Promise<GDoc<C, S>>;
|
|
1112
1175
|
updateGlobal<S extends GSlug<C>>(slug: S, data: GInput<C, S>, options?: DepthOptions): Promise<GDoc<C, S>>;
|
|
1113
1176
|
/** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
|
|
@@ -1244,6 +1307,13 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1244
1307
|
private checkDocumentAccess;
|
|
1245
1308
|
private checkGlobalAccess;
|
|
1246
1309
|
private fieldChecker;
|
|
1310
|
+
/**
|
|
1311
|
+
* Refuses filtering or sorting by fields the caller may not read: the results would tell
|
|
1312
|
+
* their values. Checks each field along a dotted path, through groups, arrays and blocks.
|
|
1313
|
+
*/
|
|
1314
|
+
private checkQueryFields;
|
|
1315
|
+
/** `checkGrant` for one document: a role limited to its own documents must own it. */
|
|
1316
|
+
private checkDocumentGrant;
|
|
1247
1317
|
/** Runs afterRead hooks, populates relationships and removes what the caller may not see. */
|
|
1248
1318
|
private output;
|
|
1249
1319
|
/** Creates a document: access, input filtering, hooks, validation, then afterChange. */
|
|
@@ -1638,6 +1708,11 @@ interface CollectionAdmin extends ContainerAdmin {
|
|
|
1638
1708
|
*/
|
|
1639
1709
|
readonly group?: 'settings';
|
|
1640
1710
|
readonly list?: CollectionListAdmin;
|
|
1711
|
+
/**
|
|
1712
|
+
* With `auth.rbac`: the relationship field to `users` that names a document's owner, e.g.
|
|
1713
|
+
* `author`, for roles given "own documents only". Default: who created it (`createdBy`).
|
|
1714
|
+
*/
|
|
1715
|
+
readonly ownerField?: string;
|
|
1641
1716
|
}
|
|
1642
1717
|
interface CollectionListAdmin {
|
|
1643
1718
|
/**
|
|
@@ -2427,6 +2502,8 @@ interface AdminCollection {
|
|
|
2427
2502
|
delete: boolean;
|
|
2428
2503
|
publish: boolean;
|
|
2429
2504
|
};
|
|
2505
|
+
/** With `auth.rbac`: the field naming a document's owner (`createdBy` or `admin.ownerField`). */
|
|
2506
|
+
owner?: string;
|
|
2430
2507
|
}
|
|
2431
2508
|
interface AdminGlobal {
|
|
2432
2509
|
slug: string;
|
|
@@ -2491,6 +2568,8 @@ interface AdminSchema {
|
|
|
2491
2568
|
email: boolean;
|
|
2492
2569
|
roles: boolean;
|
|
2493
2570
|
};
|
|
2571
|
+
/** Roles from the admin are on (`auth.rbac`): documents have owners. */
|
|
2572
|
+
rbac: boolean;
|
|
2494
2573
|
/** Pages this user may open (`admin.pages`). */
|
|
2495
2574
|
pages: AdminPageRef[];
|
|
2496
2575
|
/** Dashboard panels this user may see (`admin.dashboard`). */
|
|
@@ -2589,4 +2668,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
|
|
|
2589
2668
|
/** The version of `@easy-cms/core`, set when the package is built. */
|
|
2590
2669
|
declare const VERSION: string;
|
|
2591
2670
|
|
|
2592
|
-
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBackup, type AdminBackups, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminRole, type AdminRoleChange, type AdminRoleTarget, type AdminRoleView, type AdminRoles, type AdminSchema, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BackupState, type BackupsConfig, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, ROLE_OPERATIONS, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };
|
|
2671
|
+
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBackup, type AdminBackups, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminRole, type AdminRoleChange, type AdminRoleField, type AdminRoleTarget, type AdminRoleView, type AdminRoles, type AdminSchema, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BackupState, type BackupsConfig, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldRule, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, ROLE_OPERATIONS, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };
|
package/dist/index.js
CHANGED
|
@@ -253,14 +253,19 @@ function andWhere(where, constraint) {
|
|
|
253
253
|
return where === void 0 ? constraint : { and: [where, constraint] };
|
|
254
254
|
}
|
|
255
255
|
var FieldAccessChecker = class {
|
|
256
|
-
|
|
256
|
+
/** `rules`: the user's role's rules for top-level fields (`auth.rbac`), on top of `access`. */
|
|
257
|
+
constructor(kind, args, rules) {
|
|
257
258
|
this.kind = kind;
|
|
258
259
|
this.args = args;
|
|
260
|
+
this.rules = rules;
|
|
259
261
|
}
|
|
260
262
|
kind;
|
|
261
263
|
args;
|
|
264
|
+
rules;
|
|
262
265
|
cache = /* @__PURE__ */ new Map();
|
|
263
266
|
async allows(field) {
|
|
267
|
+
const rule = this.rules?.get(field);
|
|
268
|
+
if (rule === "hidden" || rule === "read" && this.kind === "update") return false;
|
|
264
269
|
const fn = field.access?.[this.kind];
|
|
265
270
|
if (!fn) return true;
|
|
266
271
|
let allowed2 = this.cache.get(field);
|
|
@@ -338,6 +343,23 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
338
343
|
ROLES
|
|
339
344
|
]);
|
|
340
345
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
346
|
+
var CREATED_BY_FIELD = {
|
|
347
|
+
name: "createdBy",
|
|
348
|
+
type: "relationship",
|
|
349
|
+
to: USERS,
|
|
350
|
+
position: "sidebar",
|
|
351
|
+
access: { update: () => false },
|
|
352
|
+
label: { en: "Created by", th: "\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E42\u0E14\u0E22" }
|
|
353
|
+
};
|
|
354
|
+
function withCreatedBy(config) {
|
|
355
|
+
if (config.auth?.rbac !== true) return config;
|
|
356
|
+
return {
|
|
357
|
+
...config,
|
|
358
|
+
collections: (config.collections ?? []).map(
|
|
359
|
+
(c) => c.slug === USERS || c.fields?.some((f) => f.name === CREATED_BY_FIELD.name) ? c : { ...c, fields: [...c.fields, CREATED_BY_FIELD] }
|
|
360
|
+
)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
341
363
|
var adminOrSelf = ({ user }) => {
|
|
342
364
|
if (!user) return false;
|
|
343
365
|
if (user.role === "admin") return true;
|
|
@@ -1361,6 +1383,7 @@ async function runBackup(cms, record) {
|
|
|
1361
1383
|
const filename = filenameFor(cms, started);
|
|
1362
1384
|
const key2 = await storage.get(filename) ? filename.replace(/\.db\.gz$/, `-${row.id}.db.gz`) : filename;
|
|
1363
1385
|
await storage.put(key2, data, { contentType: "application/gzip" });
|
|
1386
|
+
await pruneBackups(cms, 1);
|
|
1364
1387
|
row = await update(cms, row, {
|
|
1365
1388
|
state: "done",
|
|
1366
1389
|
filename: key2,
|
|
@@ -1368,7 +1391,6 @@ async function runBackup(cms, record) {
|
|
|
1368
1391
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1369
1392
|
});
|
|
1370
1393
|
cms.logger.info(`Backup ${key2} written (${data.byteLength} bytes)`);
|
|
1371
|
-
await pruneBackups(cms);
|
|
1372
1394
|
} catch (error) {
|
|
1373
1395
|
await update(cms, row, {
|
|
1374
1396
|
state: "failed",
|
|
@@ -1380,8 +1402,8 @@ async function runBackup(cms, record) {
|
|
|
1380
1402
|
await rm2(dir, { recursive: true, force: true });
|
|
1381
1403
|
}
|
|
1382
1404
|
}
|
|
1383
|
-
async function pruneBackups(cms) {
|
|
1384
|
-
const keep = cms.config.backups?.keep ?? DEFAULT_BACKUP_KEEP;
|
|
1405
|
+
async function pruneBackups(cms, room = 0) {
|
|
1406
|
+
const keep = Math.max(0, (cms.config.backups?.keep ?? DEFAULT_BACKUP_KEEP) - room);
|
|
1385
1407
|
const storage = await backupStorage(cms);
|
|
1386
1408
|
const { docs } = await cms.db.find({
|
|
1387
1409
|
collection: DATABASE_BACKUPS,
|
|
@@ -2313,6 +2335,7 @@ var VersionStore = class {
|
|
|
2313
2335
|
// src/roles.ts
|
|
2314
2336
|
var ROLE_OPERATIONS = ["read", "create", "update", "delete", "publish"];
|
|
2315
2337
|
var GLOBAL_OPERATIONS = ["read", "update", "publish"];
|
|
2338
|
+
var OWN_OPERATIONS = ["read", "update", "delete", "publish"];
|
|
2316
2339
|
var KEY = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2317
2340
|
var HISTORY = 50;
|
|
2318
2341
|
var TTL = 3e4;
|
|
@@ -2328,6 +2351,9 @@ var rolesCollection = {
|
|
|
2328
2351
|
function governed(slug2) {
|
|
2329
2352
|
return !INTERNAL_COLLECTIONS.has(slug2) && slug2 !== API_KEYS;
|
|
2330
2353
|
}
|
|
2354
|
+
function ruleFields(fields) {
|
|
2355
|
+
return fields.filter((f) => !f.hidden && f.name !== "createdBy");
|
|
2356
|
+
}
|
|
2331
2357
|
function references(fields, out = /* @__PURE__ */ new Set()) {
|
|
2332
2358
|
for (const field of fields) {
|
|
2333
2359
|
if (field.type === "relationship") out.add(field.to);
|
|
@@ -2343,14 +2369,16 @@ var viewId = {
|
|
|
2343
2369
|
};
|
|
2344
2370
|
var tagOf = (component) => typeof component === "string" ? component : component.tag;
|
|
2345
2371
|
var Roles = class {
|
|
2346
|
-
constructor(config, db) {
|
|
2372
|
+
constructor(config, db, log) {
|
|
2347
2373
|
this.config = config;
|
|
2348
2374
|
this.db = db;
|
|
2375
|
+
this.log = log;
|
|
2349
2376
|
this.enabled = config.auth.rbac;
|
|
2350
2377
|
this.versions = new VersionStore(db);
|
|
2351
2378
|
}
|
|
2352
2379
|
config;
|
|
2353
2380
|
db;
|
|
2381
|
+
log;
|
|
2354
2382
|
enabled;
|
|
2355
2383
|
versions;
|
|
2356
2384
|
cache;
|
|
@@ -2364,7 +2392,13 @@ var Roles = class {
|
|
|
2364
2392
|
if ("collection" in target && !governed(target.collection)) return true;
|
|
2365
2393
|
const permissions = await this.permissionsOf(user.role);
|
|
2366
2394
|
const ops = "collection" in target ? permissions.collections?.[target.collection] : permissions.globals?.[target.global];
|
|
2367
|
-
if (ops?.includes(operation))
|
|
2395
|
+
if (ops?.includes(operation)) {
|
|
2396
|
+
if ("collection" in target && permissions.own?.[target.collection]?.includes(operation)) {
|
|
2397
|
+
const owner = this.ownerOf(target.collection);
|
|
2398
|
+
if (owner && operation !== "create") return { [owner]: { equals: user.id } };
|
|
2399
|
+
}
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2368
2402
|
if ("collection" in target && target.collection === USERS) {
|
|
2369
2403
|
if (operation === "read" || operation === "update") return { id: { equals: user.id } };
|
|
2370
2404
|
}
|
|
@@ -2380,6 +2414,76 @@ var Roles = class {
|
|
|
2380
2414
|
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
2381
2415
|
return (await this.permissionsOf(user.role)).admin?.includes(id) ?? false;
|
|
2382
2416
|
}
|
|
2417
|
+
/**
|
|
2418
|
+
* The field that names a collection's owner, for "own documents only": `admin.ownerField`, or
|
|
2419
|
+
* `createdBy`. `undefined` without roles, and for users and collections roles don't cover.
|
|
2420
|
+
*/
|
|
2421
|
+
ownerOf(slug2) {
|
|
2422
|
+
if (!this.enabled || slug2 === USERS || !governed(slug2)) return void 0;
|
|
2423
|
+
const config = this.config.collections.find((c) => c.slug === slug2);
|
|
2424
|
+
if (!config) return void 0;
|
|
2425
|
+
return config.admin?.ownerField ?? "createdBy";
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* The user's field rules (top-level fields their role may only read, or not see), for field
|
|
2429
|
+
* access checks. Roles that may update only their own documents can't change the owner field.
|
|
2430
|
+
*/
|
|
2431
|
+
async fieldRules(user) {
|
|
2432
|
+
if (!this.enabled || !user || user.role === "admin") return void 0;
|
|
2433
|
+
await this.load();
|
|
2434
|
+
const cache = this.cache;
|
|
2435
|
+
const cached = cache?.rules.get(user.role);
|
|
2436
|
+
if (cached) return cached;
|
|
2437
|
+
const permissions = await this.permissionsOf(user.role);
|
|
2438
|
+
const rules = /* @__PURE__ */ new Map();
|
|
2439
|
+
const add = (fields, set) => {
|
|
2440
|
+
for (const field of fields) {
|
|
2441
|
+
const rule = set?.[field.name];
|
|
2442
|
+
if (rule === "read" || rule === "hidden") rules.set(field, rule);
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
for (const c of this.config.collections) {
|
|
2446
|
+
if (!governed(c.slug)) continue;
|
|
2447
|
+
add(c.fields, permissions.fields?.collections?.[c.slug]);
|
|
2448
|
+
const ownerField = c.admin?.ownerField;
|
|
2449
|
+
if (ownerField && permissions.own?.[c.slug]?.includes("update")) {
|
|
2450
|
+
const field = c.fields.find((f) => f.name === ownerField);
|
|
2451
|
+
if (field && !rules.has(field)) rules.set(field, "read");
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
for (const g of this.config.globals) add(g.fields, permissions.fields?.globals?.[g.slug]);
|
|
2455
|
+
cache?.rules.set(user.role, rules);
|
|
2456
|
+
return rules;
|
|
2457
|
+
}
|
|
2458
|
+
/** A new document is its creator's: sets `createdBy`, and an empty owner field. */
|
|
2459
|
+
fillOwner(slug2, data, user) {
|
|
2460
|
+
if (!user || !this.ownerOf(slug2)) return;
|
|
2461
|
+
data.createdBy ??= user.id;
|
|
2462
|
+
const ownerField = this.config.collections.find((c) => c.slug === slug2)?.admin?.ownerField;
|
|
2463
|
+
if (ownerField && (data[ownerField] === void 0 || data[ownerField] === null))
|
|
2464
|
+
data[ownerField] = user.id;
|
|
2465
|
+
}
|
|
2466
|
+
/** Documents a user owns, by collection (`createdBy` and owner fields), for deleting them. */
|
|
2467
|
+
async owned(userId) {
|
|
2468
|
+
const out = [];
|
|
2469
|
+
for (const c of this.config.collections) {
|
|
2470
|
+
const columns = this.ownerColumns(c.slug);
|
|
2471
|
+
if (columns.length === 0) continue;
|
|
2472
|
+
const count = await this.db.count({
|
|
2473
|
+
collection: c.slug,
|
|
2474
|
+
where: { or: columns.map((column) => ({ [column]: { equals: userId } })) }
|
|
2475
|
+
});
|
|
2476
|
+
if (count > 0) out.push({ collection: c.slug, count });
|
|
2477
|
+
}
|
|
2478
|
+
return out;
|
|
2479
|
+
}
|
|
2480
|
+
/** Gives a deleted user's documents to another user, or to nobody (`null`). */
|
|
2481
|
+
async transfer(from, to) {
|
|
2482
|
+
for (const c of this.config.collections) {
|
|
2483
|
+
for (const column of this.ownerColumns(c.slug))
|
|
2484
|
+
await this.setWhere(c.slug, { [column]: { equals: from } }, column, to);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2383
2487
|
/** Forgets cached permissions, after a role changed. */
|
|
2384
2488
|
invalidate() {
|
|
2385
2489
|
this.cache = void 0;
|
|
@@ -2409,6 +2513,39 @@ var Roles = class {
|
|
|
2409
2513
|
}
|
|
2410
2514
|
}
|
|
2411
2515
|
this.invalidate();
|
|
2516
|
+
await this.backfillCreators();
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Documents from before roles have no `createdBy`: it is filled from who saved their first
|
|
2520
|
+
* version, for collections with versions. Only while no document of the collection has one,
|
|
2521
|
+
* so it runs once (and finds nothing to do afterwards).
|
|
2522
|
+
*/
|
|
2523
|
+
async backfillCreators() {
|
|
2524
|
+
for (const c of this.config.collections) {
|
|
2525
|
+
if (!c.versions || !this.ownerOf(c.slug)) continue;
|
|
2526
|
+
if (await this.db.count({ collection: c.slug, where: { createdBy: { exists: true } } }) > 0)
|
|
2527
|
+
continue;
|
|
2528
|
+
const versions = await this.db.find({
|
|
2529
|
+
collection: VERSIONS,
|
|
2530
|
+
where: { and: [{ parent: { equals: c.slug } }, { author: { exists: true } }] },
|
|
2531
|
+
sort: ["id"],
|
|
2532
|
+
limit: 0,
|
|
2533
|
+
page: 1
|
|
2534
|
+
});
|
|
2535
|
+
const first = /* @__PURE__ */ new Map();
|
|
2536
|
+
for (const v of versions.docs) {
|
|
2537
|
+
if (!first.has(v.doc) && v.author !== null) first.set(v.doc, v.author);
|
|
2538
|
+
}
|
|
2539
|
+
let filled = 0;
|
|
2540
|
+
for (const [doc, author] of first) {
|
|
2541
|
+
const row = await this.db.findById({ collection: c.slug, id: doc });
|
|
2542
|
+
if (!row || row.createdBy !== null && row.createdBy !== void 0) continue;
|
|
2543
|
+
const { id, ...rest } = row;
|
|
2544
|
+
await this.db.update({ collection: c.slug, id, data: { ...rest, createdBy: author } });
|
|
2545
|
+
filled++;
|
|
2546
|
+
}
|
|
2547
|
+
if (filled > 0) this.log?.(`Set who created ${filled} ${c.slug} from their history`);
|
|
2548
|
+
}
|
|
2412
2549
|
}
|
|
2413
2550
|
/** Whether a role exists (users can only be given existing roles). */
|
|
2414
2551
|
async exists(key2) {
|
|
@@ -2437,16 +2574,24 @@ var Roles = class {
|
|
|
2437
2574
|
const seen = (group, slug2) => decided.some((p) => p[group]?.[slug2] !== void 0);
|
|
2438
2575
|
return {
|
|
2439
2576
|
roles,
|
|
2440
|
-
collections: this.config.collections.filter((c) => governed(c.slug)).map((c) =>
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2577
|
+
collections: this.config.collections.filter((c) => governed(c.slug)).map((c) => {
|
|
2578
|
+
const owner = this.ownerOf(c.slug);
|
|
2579
|
+
return {
|
|
2580
|
+
slug: c.slug,
|
|
2581
|
+
ops: ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true),
|
|
2582
|
+
new: !seen("collections", c.slug),
|
|
2583
|
+
references: [...references(ruleFields(c.fields))].filter(
|
|
2584
|
+
(s) => s !== c.slug && governed(s)
|
|
2585
|
+
),
|
|
2586
|
+
fields: adminFields(c.fields, c.admin?.ownerField),
|
|
2587
|
+
...owner ? { owner } : {}
|
|
2588
|
+
};
|
|
2589
|
+
}),
|
|
2446
2590
|
globals: this.config.globals.map((g) => ({
|
|
2447
2591
|
slug: g.slug,
|
|
2448
2592
|
ops: GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true),
|
|
2449
|
-
new: !seen("globals", g.slug)
|
|
2593
|
+
new: !seen("globals", g.slug),
|
|
2594
|
+
fields: adminFields(g.fields, void 0)
|
|
2450
2595
|
})),
|
|
2451
2596
|
views: this.views()
|
|
2452
2597
|
};
|
|
@@ -2560,9 +2705,9 @@ var Roles = class {
|
|
|
2560
2705
|
for (const [slug2, ops] of Object.entries(group)) {
|
|
2561
2706
|
const valid = allowed2.get(slug2);
|
|
2562
2707
|
if (!valid || !Array.isArray(ops)) continue;
|
|
2563
|
-
const
|
|
2564
|
-
if (
|
|
2565
|
-
out[slug2] =
|
|
2708
|
+
const picked2 = valid.filter((op) => ops.includes(op));
|
|
2709
|
+
if (picked2.length > 0 && !picked2.includes("read")) picked2.unshift("read");
|
|
2710
|
+
out[slug2] = picked2;
|
|
2566
2711
|
}
|
|
2567
2712
|
return out;
|
|
2568
2713
|
};
|
|
@@ -2584,11 +2729,71 @@ var Roles = class {
|
|
|
2584
2729
|
input.admin.filter((id) => typeof id === "string" && views.has(id))
|
|
2585
2730
|
)
|
|
2586
2731
|
] : [];
|
|
2587
|
-
|
|
2732
|
+
const picked = {
|
|
2588
2733
|
collections: pick(input.collections, collections),
|
|
2589
|
-
globals: pick(input.globals, globals)
|
|
2590
|
-
admin
|
|
2734
|
+
globals: pick(input.globals, globals)
|
|
2591
2735
|
};
|
|
2736
|
+
const own = {};
|
|
2737
|
+
if (typeof input.own === "object" && input.own !== null) {
|
|
2738
|
+
for (const [slug2, ops] of Object.entries(input.own)) {
|
|
2739
|
+
const granted = picked.collections[slug2];
|
|
2740
|
+
if (!granted || !this.ownerOf(slug2) || !Array.isArray(ops)) continue;
|
|
2741
|
+
const valid = OWN_OPERATIONS.filter((op) => granted.includes(op) && ops.includes(op));
|
|
2742
|
+
if (valid.length > 0) own[slug2] = valid;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
const fields = {
|
|
2746
|
+
collections: this.cleanRules(
|
|
2747
|
+
input.fields?.collections,
|
|
2748
|
+
this.config.collections.filter((c) => governed(c.slug)),
|
|
2749
|
+
picked.collections
|
|
2750
|
+
),
|
|
2751
|
+
globals: this.cleanRules(
|
|
2752
|
+
input.fields?.globals,
|
|
2753
|
+
this.config.globals,
|
|
2754
|
+
void 0
|
|
2755
|
+
)
|
|
2756
|
+
};
|
|
2757
|
+
return { ...picked, admin, own, fields };
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Keeps rules for known top-level fields. A field that must be filled in (required, without a
|
|
2761
|
+
* default) can't be read-only or hidden for a role that creates documents.
|
|
2762
|
+
*/
|
|
2763
|
+
cleanRules(value, containers, granted) {
|
|
2764
|
+
const out = {};
|
|
2765
|
+
if (typeof value !== "object" || value === null) return out;
|
|
2766
|
+
for (const [slug2, rules] of Object.entries(value)) {
|
|
2767
|
+
const container = containers.find((c) => c.slug === slug2);
|
|
2768
|
+
if (!container || typeof rules !== "object" || rules === null) continue;
|
|
2769
|
+
const set = {};
|
|
2770
|
+
for (const field of ruleFields(container.fields)) {
|
|
2771
|
+
const rule = rules[field.name];
|
|
2772
|
+
if (rule !== "read" && rule !== "hidden") continue;
|
|
2773
|
+
if (granted?.[slug2]?.includes("create") && mustFill(field))
|
|
2774
|
+
throw new ValidationError(ROLES, [
|
|
2775
|
+
{
|
|
2776
|
+
field: `fields.${slug2}.${field.name}`,
|
|
2777
|
+
message: "is required: roles that create documents must be able to fill it"
|
|
2778
|
+
}
|
|
2779
|
+
]);
|
|
2780
|
+
set[field.name] = rule;
|
|
2781
|
+
}
|
|
2782
|
+
if (Object.keys(set).length > 0) out[slug2] = set;
|
|
2783
|
+
}
|
|
2784
|
+
return out;
|
|
2785
|
+
}
|
|
2786
|
+
/** Fields that name a collection's owner (`createdBy`, and `admin.ownerField`). */
|
|
2787
|
+
ownerColumns(slug2) {
|
|
2788
|
+
if (!this.ownerOf(slug2)) return [];
|
|
2789
|
+
const ownerField = this.config.collections.find((c) => c.slug === slug2)?.admin?.ownerField;
|
|
2790
|
+
return ownerField ? ["createdBy", ownerField] : ["createdBy"];
|
|
2791
|
+
}
|
|
2792
|
+
/** Sets one field of every matching document, keeping the rest as stored. */
|
|
2793
|
+
async setWhere(collection2, where, field, value) {
|
|
2794
|
+
const { docs } = await this.db.find({ collection: collection2, where, sort: ["id"], limit: 0, page: 1 });
|
|
2795
|
+
for (const { id, ...rest } of docs)
|
|
2796
|
+
await this.db.update({ collection: collection2, id, data: { ...rest, [field]: value } });
|
|
2592
2797
|
}
|
|
2593
2798
|
cleanName(value) {
|
|
2594
2799
|
if (value === void 0 || value === null) return "";
|
|
@@ -2607,7 +2812,7 @@ var Roles = class {
|
|
|
2607
2812
|
const byKey = /* @__PURE__ */ new Map();
|
|
2608
2813
|
for (const row of await this.rows())
|
|
2609
2814
|
byKey.set(String(row.key), row.permissions ?? {});
|
|
2610
|
-
this.cache = { at: Date.now(), byKey };
|
|
2815
|
+
this.cache = { at: Date.now(), byKey, rules: /* @__PURE__ */ new Map() };
|
|
2611
2816
|
return byKey;
|
|
2612
2817
|
}
|
|
2613
2818
|
async rows() {
|
|
@@ -2655,6 +2860,17 @@ var Roles = class {
|
|
|
2655
2860
|
};
|
|
2656
2861
|
}
|
|
2657
2862
|
};
|
|
2863
|
+
function mustFill(field) {
|
|
2864
|
+
return field.required === true && field.defaultValue === void 0;
|
|
2865
|
+
}
|
|
2866
|
+
function adminFields(fields, ownerField) {
|
|
2867
|
+
return ruleFields(fields).map((f) => ({
|
|
2868
|
+
name: f.name,
|
|
2869
|
+
...f.label !== void 0 ? { label: f.label } : {},
|
|
2870
|
+
required: mustFill(f),
|
|
2871
|
+
...f.name === ownerField ? { owner: true } : {}
|
|
2872
|
+
}));
|
|
2873
|
+
}
|
|
2658
2874
|
function rank(role, config) {
|
|
2659
2875
|
const index = config.auth.roles.indexOf(role.key);
|
|
2660
2876
|
return index === -1 ? config.auth.roles.length : index;
|
|
@@ -2908,7 +3124,10 @@ function validateConfig(config) {
|
|
|
2908
3124
|
if (apiKeys !== void 0 && typeof apiKeys !== "boolean") add("apiKeys", "must be true or false");
|
|
2909
3125
|
collections.forEach((collection2, i) => {
|
|
2910
3126
|
const path = `collections.${collection2.slug ?? `[${i}]`}`;
|
|
2911
|
-
validateContainer(collection2, path, collectionSlugs, add
|
|
3127
|
+
validateContainer(collection2, path, collectionSlugs, add, {
|
|
3128
|
+
collection: true,
|
|
3129
|
+
rbac: config.auth?.rbac === true
|
|
3130
|
+
});
|
|
2912
3131
|
validateUseAsTitle(collection2, path, add);
|
|
2913
3132
|
validateIcon(collection2, path, add);
|
|
2914
3133
|
const editIn = collection2.editIn;
|
|
@@ -2917,7 +3136,10 @@ function validateConfig(config) {
|
|
|
2917
3136
|
});
|
|
2918
3137
|
globals.forEach((global2, i) => {
|
|
2919
3138
|
const path = `globals.${global2.slug ?? `[${i}]`}`;
|
|
2920
|
-
validateContainer(global2, path, collectionSlugs, add
|
|
3139
|
+
validateContainer(global2, path, collectionSlugs, add, {
|
|
3140
|
+
collection: false,
|
|
3141
|
+
rbac: config.auth?.rbac === true
|
|
3142
|
+
});
|
|
2921
3143
|
validateIcon(global2, path, add);
|
|
2922
3144
|
});
|
|
2923
3145
|
const plugins = config.plugins;
|
|
@@ -3361,7 +3583,7 @@ function checkSlug(container, path, seen, add) {
|
|
|
3361
3583
|
seen.add(slug2);
|
|
3362
3584
|
return slug2;
|
|
3363
3585
|
}
|
|
3364
|
-
function validateContainer(container, path, collectionSlugs, add) {
|
|
3586
|
+
function validateContainer(container, path, collectionSlugs, add, { collection: collection2, rbac }) {
|
|
3365
3587
|
if (!Array.isArray(container.fields)) {
|
|
3366
3588
|
add(`${path}.fields`, "must be an array");
|
|
3367
3589
|
return;
|
|
@@ -3398,8 +3620,25 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
3398
3620
|
add(`${path}.admin.group`, `must be 'settings' (got ${JSON.stringify(group)})`);
|
|
3399
3621
|
const list = admin.list;
|
|
3400
3622
|
if (list !== void 0) validateList(list, container, `${path}.admin.list`, add);
|
|
3623
|
+
const owner = admin.ownerField;
|
|
3624
|
+
if (owner !== void 0) {
|
|
3625
|
+
const field = container.fields.find((f) => f.name === owner);
|
|
3626
|
+
if (!collection2 || container.slug === "users" || field?.type !== "relationship" || field.to !== "users" || field.hasMany)
|
|
3627
|
+
add(
|
|
3628
|
+
`${path}.admin.ownerField`,
|
|
3629
|
+
'must name a relationship field to "users" (not hasMany) of a collection',
|
|
3630
|
+
"e.g. { name: 'author', type: 'relationship', to: 'users' }"
|
|
3631
|
+
);
|
|
3632
|
+
}
|
|
3401
3633
|
}
|
|
3402
3634
|
}
|
|
3635
|
+
const createdBy = container.fields.find((f) => f.name === CREATED_BY_FIELD.name);
|
|
3636
|
+
if (rbac && createdBy && createdBy !== CREATED_BY_FIELD)
|
|
3637
|
+
add(
|
|
3638
|
+
`${path}.fields.createdBy`,
|
|
3639
|
+
'"createdBy" is added by Easy CMS with auth.rbac',
|
|
3640
|
+
"rename the field; to use it as the owner, set admin.ownerField"
|
|
3641
|
+
);
|
|
3403
3642
|
const reserved = new Set(SYSTEM_FIELD_NAMES);
|
|
3404
3643
|
if (container.drafts) reserved.add("status");
|
|
3405
3644
|
validateFields2(container.fields, `${path}.fields`, reserved, collectionSlugs, add);
|
|
@@ -3753,7 +3992,7 @@ async function resolveConfig(input) {
|
|
|
3753
3992
|
if (resolved.has(input)) return input;
|
|
3754
3993
|
let config = await applyPlugins(input);
|
|
3755
3994
|
if (typeof config === "object" && config !== null && Array.isArray(config.collections ?? [])) {
|
|
3756
|
-
config = withMedia(withUsers(config));
|
|
3995
|
+
config = withCreatedBy(withMedia(withUsers(config)));
|
|
3757
3996
|
}
|
|
3758
3997
|
const applied = applyFieldTypes(config);
|
|
3759
3998
|
config = applied.config;
|
|
@@ -4402,7 +4641,7 @@ var EasyCMS = class {
|
|
|
4402
4641
|
this.storage = storage;
|
|
4403
4642
|
this.cwd = cwd;
|
|
4404
4643
|
this.versions = new VersionStore(db);
|
|
4405
|
-
this.roles = new Roles(config, db);
|
|
4644
|
+
this.roles = new Roles(config, db, (message2) => logger.info(message2));
|
|
4406
4645
|
this.webhooks = new Webhooks(
|
|
4407
4646
|
config.webhooks ?? [],
|
|
4408
4647
|
logger,
|
|
@@ -4429,6 +4668,7 @@ var EasyCMS = class {
|
|
|
4429
4668
|
options
|
|
4430
4669
|
);
|
|
4431
4670
|
const requested = options.sort === void 0 ? ["-createdAt"] : [options.sort].flat();
|
|
4671
|
+
await this.checkQueryFields(config, guard, void 0, requested);
|
|
4432
4672
|
const locale = this.localeOf(options);
|
|
4433
4673
|
const sort = locale === void 0 ? requested : localizeSort(
|
|
4434
4674
|
config.fields,
|
|
@@ -4723,7 +4963,7 @@ var EasyCMS = class {
|
|
|
4723
4963
|
const filtered = await filterInput(
|
|
4724
4964
|
config.fields,
|
|
4725
4965
|
input,
|
|
4726
|
-
this.fieldChecker("update", guard, parsed, input)
|
|
4966
|
+
await this.fieldChecker("update", guard, parsed, input)
|
|
4727
4967
|
);
|
|
4728
4968
|
const localized = this.toMaps(config, filtered, base ?? {}, options);
|
|
4729
4969
|
const merged = base ? mergeForUpdate(config.fields, base, localized) : applyDefaults(config.fields, localized, this.config.localization);
|
|
@@ -4763,7 +5003,7 @@ var EasyCMS = class {
|
|
|
4763
5003
|
const input = await filterInput(
|
|
4764
5004
|
config.fields,
|
|
4765
5005
|
raw,
|
|
4766
|
-
this.fieldChecker("update", guard, void 0, raw)
|
|
5006
|
+
await this.fieldChecker("update", guard, void 0, raw)
|
|
4767
5007
|
);
|
|
4768
5008
|
const saved = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4769
5009
|
const current = await this.globalDraft(config, saved);
|
|
@@ -4828,7 +5068,7 @@ var EasyCMS = class {
|
|
|
4828
5068
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
4829
5069
|
await this.checkDocumentAccess(config, "update", guard, parsed, raw);
|
|
4830
5070
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4831
|
-
await this.
|
|
5071
|
+
await this.checkDocumentGrant(guard, collection2, "publish", parsed);
|
|
4832
5072
|
const [drafted] = await this.withDrafts(config, [existing]);
|
|
4833
5073
|
const live = options.live === true && mode === "save";
|
|
4834
5074
|
const current = live ? existing : drafted;
|
|
@@ -4840,7 +5080,7 @@ var EasyCMS = class {
|
|
|
4840
5080
|
const filtered = await filterInput(
|
|
4841
5081
|
config.fields,
|
|
4842
5082
|
input,
|
|
4843
|
-
this.fieldChecker("update", guard, parsed, input)
|
|
5083
|
+
await this.fieldChecker("update", guard, parsed, input)
|
|
4844
5084
|
);
|
|
4845
5085
|
const localized = mode === "restore" ? filtered : this.toMaps(config, filtered, current, options);
|
|
4846
5086
|
let merged = mergeForUpdate(config.fields, current, localized);
|
|
@@ -4908,6 +5148,10 @@ var EasyCMS = class {
|
|
|
4908
5148
|
const [out] = await this.output(config, [doc], guard, { ...options, draft: true });
|
|
4909
5149
|
return out;
|
|
4910
5150
|
}
|
|
5151
|
+
/**
|
|
5152
|
+
* Deletes a document. Deleting a user with roles from the admin (`auth.rbac`): `transferTo`
|
|
5153
|
+
* gives the documents they own to another user; without it they have no owner.
|
|
5154
|
+
*/
|
|
4911
5155
|
async delete(collection2, id, options = {}) {
|
|
4912
5156
|
const config = this.collection(collection2);
|
|
4913
5157
|
const guard = guardOf(options);
|
|
@@ -4919,7 +5163,19 @@ var EasyCMS = class {
|
|
|
4919
5163
|
await this.guardLastAdmin(parsed, existing, { ...existing, active: false });
|
|
4920
5164
|
const base = this.hookArgs(config, guard);
|
|
4921
5165
|
for (const hook of config.hooks?.beforeDelete ?? []) await hook({ ...base, id: parsed });
|
|
4922
|
-
if (config.slug === USERS)
|
|
5166
|
+
if (config.slug === USERS) {
|
|
5167
|
+
const to = options.transferTo;
|
|
5168
|
+
if (to !== void 0 && to !== null) {
|
|
5169
|
+
const target = parseId(to);
|
|
5170
|
+
const heir = target === void 0 ? null : await this.db.findById({ collection: USERS, id: target });
|
|
5171
|
+
if (!heir || String(heir.id) === String(parsed))
|
|
5172
|
+
throw new ValidationError(USERS, [
|
|
5173
|
+
{ field: "transferTo", message: "must be another existing user" }
|
|
5174
|
+
]);
|
|
5175
|
+
await this.roles.transfer(parsed, heir.id);
|
|
5176
|
+
} else await this.roles.transfer(parsed, null);
|
|
5177
|
+
await this.auth.revokeSessions(parsed);
|
|
5178
|
+
}
|
|
4923
5179
|
await this.db.delete({ collection: collection2, id: parsed });
|
|
4924
5180
|
if (versionLimit(config)) await this.versions.deleteAll(collectionParent(collection2), parsed);
|
|
4925
5181
|
if (config.schedule) {
|
|
@@ -5027,7 +5283,7 @@ var EasyCMS = class {
|
|
|
5027
5283
|
const input = await filterInput(
|
|
5028
5284
|
config.fields,
|
|
5029
5285
|
raw,
|
|
5030
|
-
this.fieldChecker("update", guard, void 0, raw)
|
|
5286
|
+
await this.fieldChecker("update", guard, void 0, raw)
|
|
5031
5287
|
);
|
|
5032
5288
|
const existing = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
5033
5289
|
const current = await this.globalDraft(config, existing);
|
|
@@ -5312,7 +5568,7 @@ var EasyCMS = class {
|
|
|
5312
5568
|
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
5313
5569
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
5314
5570
|
await this.checkDocumentAccess(config, "update", guardOf(options), parsed, void 0);
|
|
5315
|
-
await this.
|
|
5571
|
+
await this.checkDocumentGrant(guardOf(options), collection2, "publish", parsed);
|
|
5316
5572
|
return { config, parsed };
|
|
5317
5573
|
}
|
|
5318
5574
|
async globalScheduleTarget(slug2, options) {
|
|
@@ -5483,6 +5739,7 @@ var EasyCMS = class {
|
|
|
5483
5739
|
const own = await this.checkGrant(guard, { collection: config.slug }, "read");
|
|
5484
5740
|
const access = await evaluateAccess(config.access?.read, { user: guard.user });
|
|
5485
5741
|
if (access === false) throw deny(guard.user);
|
|
5742
|
+
await this.checkQueryFields(config, guard, where);
|
|
5486
5743
|
return andWhere(andWhere(where, access), own);
|
|
5487
5744
|
}
|
|
5488
5745
|
async checkDocumentAccess(config, operation, guard, id, data) {
|
|
@@ -5512,17 +5769,52 @@ var EasyCMS = class {
|
|
|
5512
5769
|
throw new QueryError(`${operation} access of global "${config.slug}" must return a boolean`);
|
|
5513
5770
|
if (!access) throw deny(guard.user);
|
|
5514
5771
|
}
|
|
5515
|
-
fieldChecker(kind, guard, id, data) {
|
|
5772
|
+
async fieldChecker(kind, guard, id, data) {
|
|
5516
5773
|
if (!guard.enforce) return void 0;
|
|
5517
|
-
return new FieldAccessChecker(
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5774
|
+
return new FieldAccessChecker(
|
|
5775
|
+
kind,
|
|
5776
|
+
{
|
|
5777
|
+
user: guard.user,
|
|
5778
|
+
...id !== void 0 ? { id } : {},
|
|
5779
|
+
...data ? { data } : {}
|
|
5780
|
+
},
|
|
5781
|
+
await this.roles.fieldRules(guard.user)
|
|
5782
|
+
);
|
|
5783
|
+
}
|
|
5784
|
+
/**
|
|
5785
|
+
* Refuses filtering or sorting by fields the caller may not read: the results would tell
|
|
5786
|
+
* their values. Checks each field along a dotted path, through groups, arrays and blocks.
|
|
5787
|
+
*/
|
|
5788
|
+
async checkQueryFields(config, guard, where, sort = []) {
|
|
5789
|
+
if (!guard.enforce) return;
|
|
5790
|
+
const paths = [...wherePaths(where), ...sort.map((s) => s.replace(/^-/, ""))];
|
|
5791
|
+
if (paths.length === 0) return;
|
|
5792
|
+
const read2 = await this.fieldChecker("read", guard, void 0, void 0);
|
|
5793
|
+
for (const path of paths) {
|
|
5794
|
+
let fields = config.fields;
|
|
5795
|
+
for (const name of path.split(".")) {
|
|
5796
|
+
if (!fields) break;
|
|
5797
|
+
const field = fields.find((f) => f.name === name);
|
|
5798
|
+
if (!field) break;
|
|
5799
|
+
if (field.hidden || read2 && !await read2.allows(field))
|
|
5800
|
+
throw new ForbiddenError(`You may not filter or sort by "${path}"`);
|
|
5801
|
+
fields = field.type === "group" || field.type === "array" ? field.fields : field.type === "blocks" ? field.blocks.flatMap((b) => b.fields) : void 0;
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
/** `checkGrant` for one document: a role limited to its own documents must own it. */
|
|
5806
|
+
async checkDocumentGrant(guard, collection2, operation, id) {
|
|
5807
|
+
const own = await this.checkGrant(guard, { collection: collection2 }, operation);
|
|
5808
|
+
if (own === true) return;
|
|
5809
|
+
const matches2 = await this.db.count({
|
|
5810
|
+
collection: collection2,
|
|
5811
|
+
where: andWhere(own, { id: { equals: id } })
|
|
5521
5812
|
});
|
|
5813
|
+
if (matches2 === 0) throw deny(guard.user);
|
|
5522
5814
|
}
|
|
5523
5815
|
/** Runs afterRead hooks, populates relationships and removes what the caller may not see. */
|
|
5524
5816
|
async output(config, docs, guard, options) {
|
|
5525
|
-
const read2 = this.fieldChecker("read", guard, void 0, void 0);
|
|
5817
|
+
const read2 = await this.fieldChecker("read", guard, void 0, void 0);
|
|
5526
5818
|
const finish = async (target, doc) => {
|
|
5527
5819
|
const hooked = await this.transform(
|
|
5528
5820
|
target.hooks?.afterRead,
|
|
@@ -5572,8 +5864,9 @@ var EasyCMS = class {
|
|
|
5572
5864
|
const filtered = await filterInput(
|
|
5573
5865
|
config.fields,
|
|
5574
5866
|
input,
|
|
5575
|
-
this.fieldChecker("update", guard, void 0, input)
|
|
5867
|
+
await this.fieldChecker("update", guard, void 0, input)
|
|
5576
5868
|
);
|
|
5869
|
+
this.roles.fillOwner(collection2, filtered, guard.user ?? hookGuard?.user ?? null);
|
|
5577
5870
|
const base = this.hookArgs(config, hookGuard ?? guard);
|
|
5578
5871
|
let data = applyDefaults(
|
|
5579
5872
|
config.fields,
|
|
@@ -5944,6 +6237,16 @@ function loadSharp() {
|
|
|
5944
6237
|
);
|
|
5945
6238
|
return sharpModule;
|
|
5946
6239
|
}
|
|
6240
|
+
function wherePaths(where) {
|
|
6241
|
+
if (!where) return [];
|
|
6242
|
+
const out = [];
|
|
6243
|
+
for (const [key2, value] of Object.entries(where)) {
|
|
6244
|
+
if (key2 === "and" || key2 === "or") {
|
|
6245
|
+
for (const sub of value ?? []) out.push(...wherePaths(sub));
|
|
6246
|
+
} else out.push(key2);
|
|
6247
|
+
}
|
|
6248
|
+
return out;
|
|
6249
|
+
}
|
|
5947
6250
|
function guardOf(options) {
|
|
5948
6251
|
return { enforce: options.overrideAccess === false, user: options.user ?? null };
|
|
5949
6252
|
}
|
|
@@ -6215,10 +6518,11 @@ function componentRef(component) {
|
|
|
6215
6518
|
async function allowed(access, user) {
|
|
6216
6519
|
return await evaluateAccess(access, { user }) !== false;
|
|
6217
6520
|
}
|
|
6218
|
-
async function serializeFields(fields,
|
|
6521
|
+
async function serializeFields(fields, checks, localized) {
|
|
6522
|
+
const { update: update2 } = checks;
|
|
6219
6523
|
const out = [];
|
|
6220
6524
|
for (const field of fields) {
|
|
6221
|
-
if (field.hidden) continue;
|
|
6525
|
+
if (field.hidden || !await checks.read.allows(field)) continue;
|
|
6222
6526
|
const f = { name: field.name, type: field.type };
|
|
6223
6527
|
if (field.label !== void 0) f.label = field.label;
|
|
6224
6528
|
if (field.required) f.required = true;
|
|
@@ -6271,7 +6575,7 @@ async function serializeFields(fields, update2, localized) {
|
|
|
6271
6575
|
case "array":
|
|
6272
6576
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
6273
6577
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
6274
|
-
f.fields = await serializeFields(field.fields,
|
|
6578
|
+
f.fields = await serializeFields(field.fields, checks, localized);
|
|
6275
6579
|
break;
|
|
6276
6580
|
case "blocks":
|
|
6277
6581
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
@@ -6280,28 +6584,31 @@ async function serializeFields(fields, update2, localized) {
|
|
|
6280
6584
|
field.blocks.map(async (block) => ({
|
|
6281
6585
|
slug: block.slug,
|
|
6282
6586
|
...block.labels ? { labels: block.labels } : {},
|
|
6283
|
-
fields: await serializeFields(block.fields,
|
|
6587
|
+
fields: await serializeFields(block.fields, checks, localized)
|
|
6284
6588
|
}))
|
|
6285
6589
|
);
|
|
6286
6590
|
break;
|
|
6287
6591
|
case "group":
|
|
6288
|
-
f.fields = await serializeFields(field.fields,
|
|
6592
|
+
f.fields = await serializeFields(field.fields, checks, localized);
|
|
6289
6593
|
break;
|
|
6290
6594
|
}
|
|
6291
6595
|
out.push(f);
|
|
6292
6596
|
}
|
|
6293
6597
|
return out;
|
|
6294
6598
|
}
|
|
6599
|
+
async function checkers(cms, user) {
|
|
6600
|
+
const rules = await cms.roles.fieldRules(user);
|
|
6601
|
+
return {
|
|
6602
|
+
update: new FieldAccessChecker("update", { user }, rules),
|
|
6603
|
+
read: new FieldAccessChecker("read", { user }, rules)
|
|
6604
|
+
};
|
|
6605
|
+
}
|
|
6295
6606
|
async function may(cms, access, user, target, operation) {
|
|
6296
6607
|
return await allowed(access, user) && await cms.roles.allows(user, target, operation) !== false;
|
|
6297
6608
|
}
|
|
6298
6609
|
async function collection(cms, config, user, localized, roles) {
|
|
6299
6610
|
const target = { collection: config.slug };
|
|
6300
|
-
const fields = await serializeFields(
|
|
6301
|
-
config.fields,
|
|
6302
|
-
new FieldAccessChecker("update", { user }),
|
|
6303
|
-
localized
|
|
6304
|
-
);
|
|
6611
|
+
const fields = await serializeFields(config.fields, await checkers(cms, user), localized);
|
|
6305
6612
|
if (config.slug === USERS && cms.roles.enabled) {
|
|
6306
6613
|
const role = fields.find((f) => f.name === "role");
|
|
6307
6614
|
if (role) {
|
|
@@ -6331,23 +6638,22 @@ async function collection(cms, config, user, localized, roles) {
|
|
|
6331
6638
|
if (config.admin?.group === "settings" || SETTINGS.has(config.slug)) result.group = "settings";
|
|
6332
6639
|
if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
|
|
6333
6640
|
if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
|
|
6641
|
+
const owner = cms.roles.ownerOf(config.slug);
|
|
6642
|
+
if (owner && fields.some((f) => f.name === owner)) result.owner = owner;
|
|
6334
6643
|
if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
|
|
6335
6644
|
result.editIn = "drawer";
|
|
6336
6645
|
return result;
|
|
6337
6646
|
}
|
|
6338
6647
|
async function global(cms, config, user, localized) {
|
|
6339
6648
|
const target = { global: config.slug };
|
|
6649
|
+
const checks = await checkers(cms, user);
|
|
6340
6650
|
const result = {
|
|
6341
6651
|
slug: config.slug,
|
|
6342
6652
|
drafts: config.drafts === true,
|
|
6343
6653
|
versions: Boolean(config.versions),
|
|
6344
6654
|
preview: typeof config.preview === "function",
|
|
6345
6655
|
schedule: config.schedule === true,
|
|
6346
|
-
fields: await serializeFields(
|
|
6347
|
-
config.fields,
|
|
6348
|
-
new FieldAccessChecker("update", { user }),
|
|
6349
|
-
localized
|
|
6350
|
-
),
|
|
6656
|
+
fields: await serializeFields(config.fields, checks, localized),
|
|
6351
6657
|
permissions: {
|
|
6352
6658
|
read: await may(cms, config.access?.read, user, target, "read"),
|
|
6353
6659
|
update: await may(cms, config.access?.update, user, target, "update"),
|
|
@@ -6387,6 +6693,7 @@ async function adminSchema(cms, user, origin) {
|
|
|
6387
6693
|
modules: adminModuleUrls(cms),
|
|
6388
6694
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
6389
6695
|
views,
|
|
6696
|
+
rbac: cms.roles.enabled,
|
|
6390
6697
|
...views.deliveries ? {
|
|
6391
6698
|
deliveries: {
|
|
6392
6699
|
webhook: cms.config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES),
|
|
@@ -6432,7 +6739,7 @@ async function widgets(cms, user) {
|
|
|
6432
6739
|
}
|
|
6433
6740
|
|
|
6434
6741
|
// src/version.ts
|
|
6435
|
-
var VERSION = true ? "0.
|
|
6742
|
+
var VERSION = true ? "0.34.0" : "0.0.0-dev";
|
|
6436
6743
|
|
|
6437
6744
|
// src/rest/admin-status.ts
|
|
6438
6745
|
var DAY = 864e5;
|
|
@@ -6923,6 +7230,14 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6923
7230
|
}
|
|
6924
7231
|
throw new HttpError("Not found", 404);
|
|
6925
7232
|
}
|
|
7233
|
+
if (first === "admin" && second === "owned" && third !== void 0 && segments.length === 3) {
|
|
7234
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
7235
|
+
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
7236
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
7237
|
+
const parsed = parseId(third);
|
|
7238
|
+
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
7239
|
+
return { body: await cms.roles.owned(parsed) };
|
|
7240
|
+
}
|
|
6926
7241
|
if (first === "admin" && second === "roles") {
|
|
6927
7242
|
if (!ctx.user) throw new UnauthorizedError();
|
|
6928
7243
|
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
@@ -7127,7 +7442,9 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
7127
7442
|
return { body: doc };
|
|
7128
7443
|
}
|
|
7129
7444
|
if (method === "DELETE") {
|
|
7130
|
-
|
|
7445
|
+
const to = collection2 === USERS ? ctx.url.searchParams.get("transferTo") : null;
|
|
7446
|
+
const transferTo = to === null ? {} : { transferTo: to === "none" ? null : parseId(to) ?? to };
|
|
7447
|
+
return { body: await cms.delete(collection2, id, { ...access, ...transferTo }) };
|
|
7131
7448
|
}
|
|
7132
7449
|
throw methodNotAllowed(ctx, "GET, PATCH, DELETE");
|
|
7133
7450
|
}
|