@easy-cms/core 0.43.0 → 0.44.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 +114 -7
- package/dist/index.js +307 -130
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,19 @@ interface AuthUser {
|
|
|
6
6
|
readonly id: ID;
|
|
7
7
|
readonly email: string;
|
|
8
8
|
readonly role: Role;
|
|
9
|
+
/**
|
|
10
|
+
* Set by `onRequest` (e.g. a multi-tenant plugin) when `role` holds only in part of the site,
|
|
11
|
+
* such as one tenant: an `admin` there administers that part, not the whole system (Settings,
|
|
12
|
+
* backups, roles, other users' API keys).
|
|
13
|
+
*/
|
|
14
|
+
readonly scoped?: boolean;
|
|
9
15
|
readonly [field: string]: unknown;
|
|
10
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* An admin of the whole system: role `admin`, signed in (not an API key), and not an admin of a
|
|
19
|
+
* part only (`scoped`).
|
|
20
|
+
*/
|
|
21
|
+
declare const isSystemAdmin: (user: AuthUser | null | undefined) => boolean;
|
|
11
22
|
interface WhereOperators {
|
|
12
23
|
readonly equals?: unknown;
|
|
13
24
|
readonly not_equals?: unknown;
|
|
@@ -26,9 +37,16 @@ type Where = {
|
|
|
26
37
|
} & {
|
|
27
38
|
readonly [field: string]: WhereOperators | readonly Where[] | undefined;
|
|
28
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* What a request is about besides its user, e.g. the site or tenant it works in: set by
|
|
42
|
+
* `onRequest` in the config (usually a plugin's), or passed to the Local API as `context`.
|
|
43
|
+
*/
|
|
44
|
+
type RequestContext = Readonly<Record<string, unknown>>;
|
|
29
45
|
interface AccessArgs {
|
|
30
46
|
/** `null` when the request is not logged in. */
|
|
31
47
|
readonly user: AuthUser | null;
|
|
48
|
+
/** The request's context (`onRequest`), `{}` when there is none. */
|
|
49
|
+
readonly context?: RequestContext;
|
|
32
50
|
readonly id?: ID;
|
|
33
51
|
readonly data?: Readonly<Record<string, unknown>>;
|
|
34
52
|
}
|
|
@@ -1191,6 +1209,11 @@ interface AccessOptions {
|
|
|
1191
1209
|
readonly overrideAccess?: boolean;
|
|
1192
1210
|
/** The user to check access for. `null` = not logged in. */
|
|
1193
1211
|
readonly user?: AuthUser | null;
|
|
1212
|
+
/**
|
|
1213
|
+
* What the call is about besides its user, e.g. the tenant (see `onRequest`): given to access
|
|
1214
|
+
* functions, hooks and `filterOptions`.
|
|
1215
|
+
*/
|
|
1216
|
+
readonly context?: RequestContext;
|
|
1194
1217
|
}
|
|
1195
1218
|
interface DepthOptions extends AccessOptions {
|
|
1196
1219
|
/** How many levels of relationships to populate. Default 1, maximum 3. */
|
|
@@ -1278,6 +1301,20 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1278
1301
|
private readonly webhooks;
|
|
1279
1302
|
private readonly mailer;
|
|
1280
1303
|
constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter, cwd?: string, privateStorage?: StorageAdapter | null);
|
|
1304
|
+
/**
|
|
1305
|
+
* The user and context of a request, as the REST API works them out: its session cookie or
|
|
1306
|
+
* Bearer token, then `onRequest`. For framework routes and server code that act for a visitor;
|
|
1307
|
+
* pass both to the Local API with `overrideAccess: false`.
|
|
1308
|
+
*/
|
|
1309
|
+
forRequest(request: Request | Headers): Promise<{
|
|
1310
|
+
user: AuthUser | null;
|
|
1311
|
+
context: RequestContext;
|
|
1312
|
+
}>;
|
|
1313
|
+
/** @internal `onRequest` of the config, for a request whose user is known. */
|
|
1314
|
+
applyOnRequest(headers: Headers, url: URL | undefined, user: AuthUser | null): Promise<{
|
|
1315
|
+
user: AuthUser | null;
|
|
1316
|
+
context: RequestContext;
|
|
1317
|
+
}>;
|
|
1281
1318
|
find<S extends Slug<C>>(collection: S, options?: FindOptions): Promise<PaginatedDocs<Doc<C, S>>>;
|
|
1282
1319
|
findById<S extends Slug<C>>(collection: S, id: ID, options?: ReadOptions): Promise<Doc<C, S> | null>;
|
|
1283
1320
|
count<S extends Slug<C>>(collection: S, options?: {
|
|
@@ -1442,6 +1479,16 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1442
1479
|
transferTo?: ID | null;
|
|
1443
1480
|
}): Promise<Doc<C, S>>;
|
|
1444
1481
|
findGlobal<S extends GSlug<C>>(slug: S, options?: ReadOptions): Promise<GDoc<C, S>>;
|
|
1482
|
+
/** A global as stored under `key` (see `globalKey`); empty when there is no key. */
|
|
1483
|
+
private readGlobal;
|
|
1484
|
+
/**
|
|
1485
|
+
* Where a global is kept: its slug, or `<slug>@<scope>` for globals with `scope` (e.g. one per
|
|
1486
|
+
* tenant). `null` when the call has no scope: reads give the global empty. `scope` is given by
|
|
1487
|
+
* scheduled jobs, which have no request.
|
|
1488
|
+
*/
|
|
1489
|
+
private globalKey;
|
|
1490
|
+
/** `globalKey` for changes, which need a scope. */
|
|
1491
|
+
private writeKey;
|
|
1445
1492
|
updateGlobal<S extends GSlug<C>>(slug: S, data: GInput<C, S>, options?: DepthOptions): Promise<GDoc<C, S>>;
|
|
1446
1493
|
/** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
|
|
1447
1494
|
unpublishGlobal<S extends GSlug<C>>(slug: S, options?: DepthOptions): Promise<GDoc<C, S>>;
|
|
@@ -1463,7 +1510,7 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1463
1510
|
* What `user` may do with one document. Resolves `where`-style access
|
|
1464
1511
|
* against the document, so the admin UI can hide actions precisely.
|
|
1465
1512
|
*/
|
|
1466
|
-
documentPermissions(collection: Slug<C>, id: ID, user: AuthUser | null): Promise<{
|
|
1513
|
+
documentPermissions(collection: Slug<C>, id: ID, user: AuthUser | null, context?: RequestContext): Promise<{
|
|
1467
1514
|
update: boolean;
|
|
1468
1515
|
delete: boolean;
|
|
1469
1516
|
}>;
|
|
@@ -1693,6 +1740,12 @@ interface BaseField<TType extends string, TValue> {
|
|
|
1693
1740
|
readonly label?: Label;
|
|
1694
1741
|
readonly required?: boolean;
|
|
1695
1742
|
readonly unique?: boolean;
|
|
1743
|
+
/**
|
|
1744
|
+
* With `unique` (or on a slug field): the name of a sibling field (e.g. `parent` or `tenant`)
|
|
1745
|
+
* whose documents only need to differ among themselves, so `/about/team` and `/careers/team`
|
|
1746
|
+
* can both be `team`. A single, unlocalized relationship, select, text or number field.
|
|
1747
|
+
*/
|
|
1748
|
+
readonly uniqueWithin?: string;
|
|
1696
1749
|
readonly index?: boolean;
|
|
1697
1750
|
readonly defaultValue?: TValue;
|
|
1698
1751
|
readonly validate?: FieldValidate<TValue>;
|
|
@@ -1743,11 +1796,6 @@ interface SelectField extends BaseField<'select', string | readonly string[]> {
|
|
|
1743
1796
|
interface SlugField extends BaseField<'slug', string> {
|
|
1744
1797
|
/** Name of a sibling `text` field to generate the slug from. */
|
|
1745
1798
|
readonly from?: string;
|
|
1746
|
-
/**
|
|
1747
|
-
* Name of a sibling field (e.g. `parent`): slugs then only need to differ among documents
|
|
1748
|
-
* with the same value there, so `/about/team` and `/careers/team` can both be `team`.
|
|
1749
|
-
*/
|
|
1750
|
-
readonly uniqueWithin?: string;
|
|
1751
1799
|
}
|
|
1752
1800
|
/** Tiptap / ProseMirror JSON document. */
|
|
1753
1801
|
interface RichTextDocument {
|
|
@@ -1783,6 +1831,8 @@ interface FilterOptionsArgs {
|
|
|
1783
1831
|
readonly id: ID | undefined;
|
|
1784
1832
|
readonly user: AuthUser | null;
|
|
1785
1833
|
readonly cms: EasyCMS;
|
|
1834
|
+
/** The request's context (`onRequest`), `{}` when there is none. */
|
|
1835
|
+
readonly context: RequestContext;
|
|
1786
1836
|
}
|
|
1787
1837
|
/**
|
|
1788
1838
|
* Which documents a relationship may point to: a `where` on the target collection, or `true`
|
|
@@ -2170,6 +2220,8 @@ interface HookBase {
|
|
|
2170
2220
|
readonly cms: EasyCMS;
|
|
2171
2221
|
/** Slug of the collection or global. */
|
|
2172
2222
|
readonly slug: string;
|
|
2223
|
+
/** The request's context (`onRequest`, or `context` of a Local API call); `{}` when there is none. */
|
|
2224
|
+
readonly context: RequestContext;
|
|
2173
2225
|
}
|
|
2174
2226
|
type BeforeValidateHook = (args: HookBase & {
|
|
2175
2227
|
data: Data$1;
|
|
@@ -2307,6 +2359,15 @@ interface GlobalConfig {
|
|
|
2307
2359
|
readonly preview?: PreviewURL;
|
|
2308
2360
|
readonly access?: GlobalAccess;
|
|
2309
2361
|
readonly hooks?: GlobalHooks;
|
|
2362
|
+
/**
|
|
2363
|
+
* One value per scope, e.g. per tenant (usually set by a plugin): the scope of a call, from its
|
|
2364
|
+
* context (`onRequest`). A string keeps a value of its own; `undefined` the shared value; `null`
|
|
2365
|
+
* means the call has none, so reads give the global empty and changes are refused.
|
|
2366
|
+
*/
|
|
2367
|
+
readonly scope?: (args: {
|
|
2368
|
+
readonly context: RequestContext;
|
|
2369
|
+
readonly user: AuthUser | null;
|
|
2370
|
+
}) => string | null | undefined;
|
|
2310
2371
|
/** Custom admin components. */
|
|
2311
2372
|
readonly admin?: ContainerAdmin;
|
|
2312
2373
|
}
|
|
@@ -2382,6 +2443,23 @@ interface AdminConfig {
|
|
|
2382
2443
|
readonly pages?: readonly AdminPage[];
|
|
2383
2444
|
/** Panels on the dashboard, after the built-in ones. */
|
|
2384
2445
|
readonly dashboard?: readonly DashboardWidget[];
|
|
2446
|
+
/**
|
|
2447
|
+
* A choice at the top of the menu that applies to everything in the admin, e.g. the tenant
|
|
2448
|
+
* (usually set by a plugin). The choice is kept in a cookie, which every request of the admin
|
|
2449
|
+
* sends; `onRequest` reads it.
|
|
2450
|
+
*/
|
|
2451
|
+
readonly switcher?: AdminSwitcher | null;
|
|
2452
|
+
}
|
|
2453
|
+
/** See `AdminConfig.switcher`. */
|
|
2454
|
+
interface AdminSwitcher {
|
|
2455
|
+
/** Cookie that keeps the choice, e.g. `ecms-tenant`. */
|
|
2456
|
+
readonly cookie: string;
|
|
2457
|
+
readonly label: Label;
|
|
2458
|
+
/**
|
|
2459
|
+
* Path under the API that lists the choices for the logged-in user:
|
|
2460
|
+
* `{ options: [{ value, label }], all?: Label }`, with `all` to offer "all of them" (value `*`).
|
|
2461
|
+
*/
|
|
2462
|
+
readonly options: string;
|
|
2385
2463
|
}
|
|
2386
2464
|
interface ImageSize {
|
|
2387
2465
|
/** Key in `media.sizes`, e.g. `thumbnail`. */
|
|
@@ -2497,6 +2575,24 @@ interface AuthConfig {
|
|
|
2497
2575
|
readonly passwordChanged?: PasswordEmailFn;
|
|
2498
2576
|
};
|
|
2499
2577
|
}
|
|
2578
|
+
/** What `onRequest` receives. */
|
|
2579
|
+
interface OnRequestArgs {
|
|
2580
|
+
readonly headers: Headers;
|
|
2581
|
+
/** The request's URL; `undefined` when only headers are known (`cms.forRequest(headers)`). */
|
|
2582
|
+
readonly url: URL | undefined;
|
|
2583
|
+
/** The user of the session or API key, before `onRequest` changes it; `null` when anonymous. */
|
|
2584
|
+
readonly user: AuthUser | null;
|
|
2585
|
+
readonly cms: EasyCMS;
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Runs on each API request (REST, plugin endpoints, `cms.forRequest`) after the user is known:
|
|
2589
|
+
* returns the request's `context` (e.g. the tenant it works in, from a header) and may change the
|
|
2590
|
+
* user (e.g. their role in that tenant, `scoped`). Plugins that set it call the one before.
|
|
2591
|
+
*/
|
|
2592
|
+
type OnRequest = (args: OnRequestArgs) => MaybePromise<{
|
|
2593
|
+
readonly user?: AuthUser | null;
|
|
2594
|
+
readonly context?: RequestContext;
|
|
2595
|
+
} | undefined>;
|
|
2500
2596
|
/** What an endpoint's handler receives. */
|
|
2501
2597
|
interface EndpointRequest {
|
|
2502
2598
|
readonly request: Request;
|
|
@@ -2505,6 +2601,8 @@ interface EndpointRequest {
|
|
|
2505
2601
|
readonly params: Readonly<Record<string, string>>;
|
|
2506
2602
|
/** The logged-in user (session cookie or Bearer token), or `null`. */
|
|
2507
2603
|
readonly user: AuthUser | null;
|
|
2604
|
+
/** The request's context (`onRequest`): pass it to the Local API with `user`. */
|
|
2605
|
+
readonly context: RequestContext;
|
|
2508
2606
|
/** The client's IP address, when the adapter knows it (e.g. to rate-limit a public form). */
|
|
2509
2607
|
readonly ip: string | undefined;
|
|
2510
2608
|
/** The Local API. Pass `{ user, overrideAccess: false }` to apply the user's access rules. */
|
|
@@ -2656,6 +2754,8 @@ interface Config {
|
|
|
2656
2754
|
readonly endpoints?: readonly Endpoint[];
|
|
2657
2755
|
/** Extra `easy-cms <name>` CLI commands, e.g. from plugins. */
|
|
2658
2756
|
readonly commands?: readonly CliCommand[];
|
|
2757
|
+
/** The request's context and user, worked out on each API request (usually by a plugin). */
|
|
2758
|
+
readonly onRequest?: OnRequest;
|
|
2659
2759
|
/**
|
|
2660
2760
|
* Field types from packages, e.g. `color` from `@easy-cms/fields`: fields then use
|
|
2661
2761
|
* `type: 'color'`. See `defineFieldType`.
|
|
@@ -2725,6 +2825,11 @@ interface ApiKeyPermissions {
|
|
|
2725
2825
|
* or missing: every folder.
|
|
2726
2826
|
*/
|
|
2727
2827
|
readonly folders?: readonly (string | number)[];
|
|
2828
|
+
/**
|
|
2829
|
+
* The context the key was created in (`onRequest`, e.g. its tenant), kept for good: requests
|
|
2830
|
+
* with the key get it as `user.apiKey.permissions.context`. Set by Easy CMS, not by input.
|
|
2831
|
+
*/
|
|
2832
|
+
readonly context?: RequestContext;
|
|
2728
2833
|
}
|
|
2729
2834
|
/** Set on `AuthUser.apiKey` when a request authenticated with an API key. */
|
|
2730
2835
|
interface ApiKeyContext {
|
|
@@ -3167,6 +3272,8 @@ interface AdminWidgetRef {
|
|
|
3167
3272
|
}
|
|
3168
3273
|
interface AdminSchema {
|
|
3169
3274
|
locale: AdminLocale;
|
|
3275
|
+
/** A choice for the whole admin, e.g. the tenant (`admin.switcher`). */
|
|
3276
|
+
switcher: AdminSwitcher | null;
|
|
3170
3277
|
/** Admins can email users links to set their password (needs `email` and `serverURL`). */
|
|
3171
3278
|
passwordLinks: boolean;
|
|
3172
3279
|
/** Menu order of collections (`admin.menu`); unlisted ones follow. */
|
|
@@ -3330,4 +3437,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
|
|
|
3330
3437
|
/** The version of `@easy-cms/core`, set when the package is built. */
|
|
3331
3438
|
declare const VERSION: string;
|
|
3332
3439
|
|
|
3333
|
-
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 AdminSso, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiHandlerOptions, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, type AuditChange, type AuditConfig, type AuditEntry, AuditLog, type AuditPage, type AuditVerification, Auth, type AuthConfig, type AuthProvider, type AuthProviderProfile, type AuthProviderRequest, 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 BuiltinMediaFoldersCollection, 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 DirectUpload, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, FOLDER_LEVELS, 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, type FolderLevel, type FolderPermissions, 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, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, MediaFolders, 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, SSO_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type SsoOutcome, type SsoProviderRef, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, type UserIdentity, 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, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };
|
|
3440
|
+
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 AdminSso, type AdminStatus, type AdminSwitcher, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiHandlerOptions, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, type AuditChange, type AuditConfig, type AuditEntry, AuditLog, type AuditPage, type AuditVerification, Auth, type AuthConfig, type AuthProvider, type AuthProviderProfile, type AuthProviderRequest, 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 BuiltinMediaFoldersCollection, 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 DirectUpload, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, FOLDER_LEVELS, 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, type FolderLevel, type FolderPermissions, 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, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, MediaFolders, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type OnRequest, type OnRequestArgs, 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 RequestContext, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, SSO_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type SsoOutcome, type SsoProviderRef, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, type UserIdentity, 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, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/access.ts
|
|
2
|
+
var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
|
|
2
3
|
var anyone = () => true;
|
|
3
4
|
var isLoggedIn = ({ user }) => user !== null;
|
|
4
5
|
var isAdmin = ({ user }) => user?.role === "admin";
|
|
@@ -41,8 +42,7 @@ function keyAllows(user, target, operation) {
|
|
|
41
42
|
const allowed2 = key2.permissions.globals?.[target.global];
|
|
42
43
|
return allowed2?.includes(operation) ?? false;
|
|
43
44
|
}
|
|
44
|
-
var
|
|
45
|
-
var ownKeys = ({ user }) => !user || user.apiKey ? false : isAdmin2(user) ? true : { user: { equals: user.id } };
|
|
45
|
+
var ownKeys = ({ user }) => !user || user.apiKey ? false : isSystemAdmin(user) ? true : { user: { equals: user.id } };
|
|
46
46
|
var system = { update: () => false };
|
|
47
47
|
var apiKeysCollection = {
|
|
48
48
|
slug: API_KEYS,
|
|
@@ -99,14 +99,17 @@ var apiKeysCollection = {
|
|
|
99
99
|
{ name: "keyHash", type: "text", hidden: true }
|
|
100
100
|
],
|
|
101
101
|
hooks: {
|
|
102
|
-
// Only real collections, globals and operations are kept.
|
|
102
|
+
// Only real collections, globals and operations are kept. A key keeps the context it was
|
|
103
|
+
// created in (`createApiKey`), whatever its permissions are changed to.
|
|
103
104
|
beforeChange: [
|
|
104
|
-
({ data, cms }) =>
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
({ data, cms, operation, originalDoc }) => {
|
|
106
|
+
if (data.permissions === void 0) return data;
|
|
107
|
+
const context = operation === "create" ? data.permissions.context : originalDoc?.permissions?.context;
|
|
108
|
+
const permissions = cleanPermissions(data.permissions, {
|
|
107
109
|
collections: cms.config.collections.map((c) => c.slug),
|
|
108
110
|
globals: cms.config.globals.map((g) => g.slug)
|
|
109
|
-
})
|
|
111
|
+
});
|
|
112
|
+
return { ...data, permissions: context ? { ...permissions, context } : permissions };
|
|
110
113
|
}
|
|
111
114
|
]
|
|
112
115
|
}
|
|
@@ -183,11 +186,11 @@ function withCreatedBy(config) {
|
|
|
183
186
|
}
|
|
184
187
|
var adminOrSelf = ({ user }) => {
|
|
185
188
|
if (!user) return false;
|
|
186
|
-
if (user
|
|
189
|
+
if (isSystemAdmin(user)) return true;
|
|
187
190
|
return { id: { equals: user.id } };
|
|
188
191
|
};
|
|
189
192
|
var adminOnly = {
|
|
190
|
-
update: ({ user }) => user
|
|
193
|
+
update: ({ user }) => isSystemAdmin(user)
|
|
191
194
|
};
|
|
192
195
|
function userFields(roles, rbac) {
|
|
193
196
|
const defaultRole = roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
@@ -239,9 +242,9 @@ function withUsers(config) {
|
|
|
239
242
|
fields: [...userFields(roles, config.auth?.rbac === true), ...custom?.fields ?? []],
|
|
240
243
|
access: {
|
|
241
244
|
read: isLoggedIn,
|
|
242
|
-
create:
|
|
245
|
+
create: ({ user }) => isSystemAdmin(user),
|
|
243
246
|
update: adminOrSelf,
|
|
244
|
-
delete:
|
|
247
|
+
delete: ({ user }) => isSystemAdmin(user),
|
|
245
248
|
...custom?.access
|
|
246
249
|
}
|
|
247
250
|
};
|
|
@@ -421,13 +424,28 @@ function withMedia(config) {
|
|
|
421
424
|
]
|
|
422
425
|
}
|
|
423
426
|
};
|
|
424
|
-
const
|
|
427
|
+
const customFolders = config.collections?.find((c) => c.slug === MEDIA_FOLDERS);
|
|
428
|
+
const mediaFolders = customFolders ? {
|
|
429
|
+
...mediaFoldersCollection,
|
|
430
|
+
fields: [...mediaFoldersCollection.fields, ...customFolders.fields],
|
|
431
|
+
access: { ...mediaFoldersCollection.access, ...customFolders.access },
|
|
432
|
+
hooks: {
|
|
433
|
+
...customFolders.hooks,
|
|
434
|
+
afterRead: [
|
|
435
|
+
...mediaFoldersCollection.hooks?.afterRead ?? [],
|
|
436
|
+
...customFolders.hooks?.afterRead ?? []
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
} : mediaFoldersCollection;
|
|
440
|
+
const others = (config.collections ?? []).filter(
|
|
441
|
+
(c) => c.slug !== MEDIA && c.slug !== MEDIA_FOLDERS
|
|
442
|
+
);
|
|
425
443
|
return {
|
|
426
444
|
...config,
|
|
427
445
|
collections: [
|
|
428
446
|
...others.slice(0, 1),
|
|
429
447
|
media,
|
|
430
|
-
...folders ? [
|
|
448
|
+
...folders ? [mediaFolders] : [],
|
|
431
449
|
...others.slice(1)
|
|
432
450
|
]
|
|
433
451
|
};
|
|
@@ -4079,7 +4097,7 @@ var Roles = class {
|
|
|
4079
4097
|
* it; without roles, nobody else (`status`, `deliveries`) or everybody (plugin pages).
|
|
4080
4098
|
*/
|
|
4081
4099
|
async canView(user, id) {
|
|
4082
|
-
if (user
|
|
4100
|
+
if (isSystemAdmin(user)) return true;
|
|
4083
4101
|
if (user.apiKey) return false;
|
|
4084
4102
|
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
4085
4103
|
return (await this.permissionsOf(user.role)).admin?.includes(id) ?? false;
|
|
@@ -4866,6 +4884,14 @@ function validateAdmin(config, add) {
|
|
|
4866
4884
|
"e.g. siteUrl: 'https://example.com'"
|
|
4867
4885
|
);
|
|
4868
4886
|
}
|
|
4887
|
+
const switcher = admin.switcher;
|
|
4888
|
+
if (switcher !== void 0 && switcher !== null) {
|
|
4889
|
+
const { cookie: cookie2, options } = switcher;
|
|
4890
|
+
if (typeof cookie2 !== "string" || !/^[A-Za-z0-9_-]+$/.test(cookie2))
|
|
4891
|
+
add("admin.switcher.cookie", "must be a cookie name (letters, digits, - and _)");
|
|
4892
|
+
if (typeof options !== "string" || !options.startsWith("/"))
|
|
4893
|
+
add("admin.switcher.options", 'must be a path under the API starting with "/"');
|
|
4894
|
+
}
|
|
4869
4895
|
const modules = admin.modules;
|
|
4870
4896
|
if (modules !== void 0) {
|
|
4871
4897
|
if (!Array.isArray(modules)) {
|
|
@@ -5501,6 +5527,19 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
5501
5527
|
if (cell !== void 0) validateComponent(cell, `${path}.admin.cell`, add);
|
|
5502
5528
|
}
|
|
5503
5529
|
}
|
|
5530
|
+
if (field.uniqueWithin !== void 0) {
|
|
5531
|
+
const scope = siblings.get(field.uniqueWithin);
|
|
5532
|
+
if (!field.unique && field.type !== "slug") {
|
|
5533
|
+
add(`${path}.uniqueWithin`, "needs `unique: true` (or a slug field)");
|
|
5534
|
+
} else if (!scope || scope === field) {
|
|
5535
|
+
add(`${path}.uniqueWithin`, `no sibling field named "${field.uniqueWithin}"`);
|
|
5536
|
+
} else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
|
|
5537
|
+
add(
|
|
5538
|
+
`${path}.uniqueWithin`,
|
|
5539
|
+
`"${field.uniqueWithin}" must be a single, unlocalized relationship, select, text or number field`
|
|
5540
|
+
);
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5504
5543
|
switch (field.type) {
|
|
5505
5544
|
case "text":
|
|
5506
5545
|
case "textarea":
|
|
@@ -5521,17 +5560,6 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
5521
5560
|
add(`${path}.from`, `"${field.from}" must be a text field (got ${source.type})`);
|
|
5522
5561
|
}
|
|
5523
5562
|
}
|
|
5524
|
-
if (field.uniqueWithin !== void 0) {
|
|
5525
|
-
const scope = siblings.get(field.uniqueWithin);
|
|
5526
|
-
if (!scope || scope === field) {
|
|
5527
|
-
add(`${path}.uniqueWithin`, `no sibling field named "${field.uniqueWithin}"`);
|
|
5528
|
-
} else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
|
|
5529
|
-
add(
|
|
5530
|
-
`${path}.uniqueWithin`,
|
|
5531
|
-
`"${field.uniqueWithin}" must be a single, unlocalized relationship, select, text or number field`
|
|
5532
|
-
);
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
5563
|
break;
|
|
5536
5564
|
case "relationship":
|
|
5537
5565
|
if (typeof field.to !== "string" || !collectionSlugs.has(field.to)) {
|
|
@@ -5822,7 +5850,8 @@ async function resolveConfig(input) {
|
|
|
5822
5850
|
menu: config.admin?.menu ?? [],
|
|
5823
5851
|
modules: config.admin?.modules ?? [],
|
|
5824
5852
|
pages: config.admin?.pages ?? [],
|
|
5825
|
-
dashboard: config.admin?.dashboard ?? []
|
|
5853
|
+
dashboard: config.admin?.dashboard ?? [],
|
|
5854
|
+
switcher: config.admin?.switcher ?? null
|
|
5826
5855
|
},
|
|
5827
5856
|
upload: {
|
|
5828
5857
|
dir: config.upload?.dir ?? "uploads",
|
|
@@ -5969,6 +5998,29 @@ var EasyCMS = class {
|
|
|
5969
5998
|
);
|
|
5970
5999
|
this.auth = new Auth(this);
|
|
5971
6000
|
}
|
|
6001
|
+
/**
|
|
6002
|
+
* The user and context of a request, as the REST API works them out: its session cookie or
|
|
6003
|
+
* Bearer token, then `onRequest`. For framework routes and server code that act for a visitor;
|
|
6004
|
+
* pass both to the Local API with `overrideAccess: false`.
|
|
6005
|
+
*/
|
|
6006
|
+
async forRequest(request) {
|
|
6007
|
+
const headers = request instanceof Headers ? request : request.headers;
|
|
6008
|
+
const url = request instanceof Headers ? void 0 : new URL(request.url);
|
|
6009
|
+
return this.applyOnRequest(headers, url, await this.auth.userFromHeaders(headers));
|
|
6010
|
+
}
|
|
6011
|
+
/** @internal `onRequest` of the config, for a request whose user is known. */
|
|
6012
|
+
async applyOnRequest(headers, url, user) {
|
|
6013
|
+
const result = await this.config.onRequest?.({
|
|
6014
|
+
headers,
|
|
6015
|
+
url,
|
|
6016
|
+
user,
|
|
6017
|
+
cms: this
|
|
6018
|
+
});
|
|
6019
|
+
return {
|
|
6020
|
+
user: result?.user !== void 0 ? result.user : user,
|
|
6021
|
+
context: result?.context ?? NO_CONTEXT
|
|
6022
|
+
};
|
|
6023
|
+
}
|
|
5972
6024
|
async find(collection2, options = {}) {
|
|
5973
6025
|
const config = this.collection(collection2);
|
|
5974
6026
|
const guard = guardOf(options);
|
|
@@ -6222,6 +6274,7 @@ var EasyCMS = class {
|
|
|
6222
6274
|
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
6223
6275
|
const allowed2 = await evaluateAccess(this.collection(MEDIA).access?.create, {
|
|
6224
6276
|
user: guard.user,
|
|
6277
|
+
context: guard.context,
|
|
6225
6278
|
data
|
|
6226
6279
|
});
|
|
6227
6280
|
if (allowed2 !== true) throw deny(guard.user);
|
|
@@ -6259,7 +6312,11 @@ var EasyCMS = class {
|
|
|
6259
6312
|
const fromURL = this.config.upload.fromURL;
|
|
6260
6313
|
if (guard.enforce) {
|
|
6261
6314
|
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
6262
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6315
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6316
|
+
user: guard.user,
|
|
6317
|
+
context: guard.context,
|
|
6318
|
+
data
|
|
6319
|
+
});
|
|
6263
6320
|
if (allowed2 !== true) throw deny(guard.user);
|
|
6264
6321
|
if (!fromURL)
|
|
6265
6322
|
throw new ValidationError(MEDIA, [
|
|
@@ -6291,7 +6348,10 @@ var EasyCMS = class {
|
|
|
6291
6348
|
const config = this.collection(API_KEYS);
|
|
6292
6349
|
const guard = guardOf(options);
|
|
6293
6350
|
if (guard.enforce) {
|
|
6294
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6351
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6352
|
+
user: guard.user,
|
|
6353
|
+
context: guard.context
|
|
6354
|
+
});
|
|
6295
6355
|
if (allowed2 !== true) throw deny(guard.user);
|
|
6296
6356
|
}
|
|
6297
6357
|
const owner = guard.enforce ? guard.user?.id : data.user;
|
|
@@ -6303,7 +6363,12 @@ var EasyCMS = class {
|
|
|
6303
6363
|
config,
|
|
6304
6364
|
{
|
|
6305
6365
|
name: data.name,
|
|
6306
|
-
|
|
6366
|
+
// The context it is created in (e.g. a tenant) stays with the key. Requests can't name
|
|
6367
|
+
// one; trusted calls may.
|
|
6368
|
+
permissions: {
|
|
6369
|
+
...withoutContext(data.permissions),
|
|
6370
|
+
...guard.enforce ? Object.keys(guard.context).length > 0 ? { context: guard.context } : {} : data.permissions?.context ? { context: data.permissions.context } : {}
|
|
6371
|
+
},
|
|
6307
6372
|
expiresAt: expiresAt ?? null,
|
|
6308
6373
|
prefix,
|
|
6309
6374
|
user: owner
|
|
@@ -6564,7 +6629,11 @@ var EasyCMS = class {
|
|
|
6564
6629
|
if (id === null) {
|
|
6565
6630
|
if (guard.enforce) {
|
|
6566
6631
|
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
6567
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6632
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6633
|
+
user: guard.user,
|
|
6634
|
+
context: guard.context,
|
|
6635
|
+
data: raw
|
|
6636
|
+
});
|
|
6568
6637
|
if (allowed2 !== true) throw deny(guard.user);
|
|
6569
6638
|
}
|
|
6570
6639
|
} else {
|
|
@@ -6620,8 +6689,9 @@ var EasyCMS = class {
|
|
|
6620
6689
|
raw,
|
|
6621
6690
|
await this.fieldChecker("update", guard, void 0, raw)
|
|
6622
6691
|
);
|
|
6623
|
-
const
|
|
6624
|
-
const
|
|
6692
|
+
const key2 = this.globalKey(config, guard);
|
|
6693
|
+
const saved = (key2 ? await this.db.findGlobal({ slug: key2 }) : null) ?? {};
|
|
6694
|
+
const current = await this.globalDraft(config, saved, key2);
|
|
6625
6695
|
const merged = generateSlugs(
|
|
6626
6696
|
config.fields,
|
|
6627
6697
|
applyDefaults(
|
|
@@ -6719,7 +6789,7 @@ var EasyCMS = class {
|
|
|
6719
6789
|
"update",
|
|
6720
6790
|
parsed,
|
|
6721
6791
|
options,
|
|
6722
|
-
guard
|
|
6792
|
+
guard
|
|
6723
6793
|
);
|
|
6724
6794
|
prepared = await this.transform(
|
|
6725
6795
|
config.hooks?.beforeChange,
|
|
@@ -6853,8 +6923,12 @@ var EasyCMS = class {
|
|
|
6853
6923
|
const config = this.global(slug2);
|
|
6854
6924
|
const guard = guardOf(options);
|
|
6855
6925
|
await this.checkGlobalAccess(config, "read", guard);
|
|
6856
|
-
|
|
6857
|
-
|
|
6926
|
+
return this.readGlobal(config, this.globalKey(config, guard), guard, options);
|
|
6927
|
+
}
|
|
6928
|
+
/** A global as stored under `key` (see `globalKey`); empty when there is no key. */
|
|
6929
|
+
async readGlobal(config, key2, guard, options) {
|
|
6930
|
+
const saved = key2 ? await this.db.findGlobal({ slug: key2 }) : null;
|
|
6931
|
+
const stored = saved && options.draft ? await this.globalDraft(config, saved, key2) : saved;
|
|
6858
6932
|
const data = fillMissing(
|
|
6859
6933
|
config.fields,
|
|
6860
6934
|
applyDefaults(config.fields, stored ?? {}, this.config.localization)
|
|
@@ -6867,6 +6941,27 @@ var EasyCMS = class {
|
|
|
6867
6941
|
const { id: _id, ...doc } = out;
|
|
6868
6942
|
return doc;
|
|
6869
6943
|
}
|
|
6944
|
+
/**
|
|
6945
|
+
* Where a global is kept: its slug, or `<slug>@<scope>` for globals with `scope` (e.g. one per
|
|
6946
|
+
* tenant). `null` when the call has no scope: reads give the global empty. `scope` is given by
|
|
6947
|
+
* scheduled jobs, which have no request.
|
|
6948
|
+
*/
|
|
6949
|
+
globalKey(config, guard, scope) {
|
|
6950
|
+
if (scope !== void 0) return `${config.slug}@${scope}`;
|
|
6951
|
+
if (!config.scope) return config.slug;
|
|
6952
|
+
const own = config.scope({ context: guard.context, user: guard.user });
|
|
6953
|
+
if (own === void 0) return config.slug;
|
|
6954
|
+
return own === null ? null : `${config.slug}@${own}`;
|
|
6955
|
+
}
|
|
6956
|
+
/** `globalKey` for changes, which need a scope. */
|
|
6957
|
+
writeKey(config, guard, scope) {
|
|
6958
|
+
const key2 = this.globalKey(config, guard, scope);
|
|
6959
|
+
if (key2 === null)
|
|
6960
|
+
throw new QueryError(
|
|
6961
|
+
`"${config.slug}" has a value per scope (e.g. per tenant): choose one to change it`
|
|
6962
|
+
);
|
|
6963
|
+
return key2;
|
|
6964
|
+
}
|
|
6870
6965
|
async updateGlobal(slug2, data, options = {}) {
|
|
6871
6966
|
return this.saveGlobal(slug2, asObject(data, slug2), options);
|
|
6872
6967
|
}
|
|
@@ -6880,20 +6975,21 @@ var EasyCMS = class {
|
|
|
6880
6975
|
const config = this.global(slug2);
|
|
6881
6976
|
const guard = guardOf(options);
|
|
6882
6977
|
await this.checkGlobalAccess(config, "update", guard);
|
|
6883
|
-
const
|
|
6884
|
-
|
|
6885
|
-
|
|
6978
|
+
const key2 = this.writeKey(config, guard);
|
|
6979
|
+
const saved = await this.db.findGlobal({ slug: key2 });
|
|
6980
|
+
if (saved && await this.globalDraft(config, saved, key2) !== saved) {
|
|
6981
|
+
await this.saveVersion(config, globalParent(key2), 0, saved, guard);
|
|
6886
6982
|
}
|
|
6887
6983
|
return this.findGlobal(slug2, { ...options, draft: true });
|
|
6888
6984
|
}
|
|
6889
6985
|
/** Saved versions of a global, newest first. Needs update access. */
|
|
6890
6986
|
async findGlobalVersions(slug2, options = {}) {
|
|
6891
|
-
await this.globalVersionTarget(slug2, options);
|
|
6892
|
-
return this.versions.list(globalParent(
|
|
6987
|
+
const { key: key2 } = await this.globalVersionTarget(slug2, options);
|
|
6988
|
+
return this.versions.list(globalParent(key2), 0, options.limit ?? 20, options.page ?? 1);
|
|
6893
6989
|
}
|
|
6894
6990
|
async findGlobalVersion(slug2, versionId, options = {}) {
|
|
6895
|
-
const config = await this.globalVersionTarget(slug2, options);
|
|
6896
|
-
const version = await this.versions.get(globalParent(
|
|
6991
|
+
const { config, key: key2 } = await this.globalVersionTarget(slug2, options);
|
|
6992
|
+
const version = await this.versions.get(globalParent(key2), 0, versionId);
|
|
6897
6993
|
if (!version) return null;
|
|
6898
6994
|
const data = fillMissing(
|
|
6899
6995
|
config.fields,
|
|
@@ -6908,8 +7004,8 @@ var EasyCMS = class {
|
|
|
6908
7004
|
}
|
|
6909
7005
|
/** Makes an old version of a global current (as a draft when the global has drafts). */
|
|
6910
7006
|
async restoreGlobalVersion(slug2, versionId, options = {}) {
|
|
6911
|
-
const config = await this.globalVersionTarget(slug2, options);
|
|
6912
|
-
const version = await this.versions.get(globalParent(
|
|
7007
|
+
const { config, key: key2 } = await this.globalVersionTarget(slug2, options);
|
|
7008
|
+
const version = await this.versions.get(globalParent(key2), 0, versionId);
|
|
6913
7009
|
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
6914
7010
|
const { status: _status, ...data } = version.data;
|
|
6915
7011
|
return this.saveGlobal(
|
|
@@ -6922,20 +7018,22 @@ var EasyCMS = class {
|
|
|
6922
7018
|
async globalVersionTarget(slug2, options) {
|
|
6923
7019
|
const config = this.global(slug2);
|
|
6924
7020
|
if (!versionLimit(config)) throw new QueryError(`"${slug2}" has no versions`);
|
|
6925
|
-
|
|
6926
|
-
|
|
7021
|
+
const guard = guardOf(options);
|
|
7022
|
+
await this.checkGlobalAccess(config, "update", guard);
|
|
7023
|
+
return { config, key: this.writeKey(config, guard) };
|
|
6927
7024
|
}
|
|
6928
7025
|
/** The global's pending draft when it has one (see `withDrafts`), otherwise `saved`. */
|
|
6929
|
-
async globalDraft(config, saved) {
|
|
6930
|
-
if (!this.separateDrafts(config) || saved.status !== "published") return saved;
|
|
6931
|
-
const version = (await this.versions.latest(globalParent(
|
|
7026
|
+
async globalDraft(config, saved, key2) {
|
|
7027
|
+
if (!key2 || !this.separateDrafts(config) || saved.status !== "published") return saved;
|
|
7028
|
+
const version = (await this.versions.latest(globalParent(key2), [0])).get("0");
|
|
6932
7029
|
if (version?.status !== "draft") return saved;
|
|
6933
7030
|
return { ...version.data, updatedAt: version.createdAt, status: "draft" };
|
|
6934
7031
|
}
|
|
6935
|
-
async saveGlobal(slug2, raw, options, mode = "save") {
|
|
7032
|
+
async saveGlobal(slug2, raw, options, mode = "save", scope) {
|
|
6936
7033
|
const config = this.global(slug2);
|
|
6937
7034
|
const guard = guardOf(options);
|
|
6938
7035
|
await this.checkGlobalAccess(config, "update", guard);
|
|
7036
|
+
const key2 = this.writeKey(config, guard, scope);
|
|
6939
7037
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
6940
7038
|
await this.checkGrant(guard, { global: slug2 }, "publish");
|
|
6941
7039
|
const input = await filterInput(
|
|
@@ -6943,8 +7041,8 @@ var EasyCMS = class {
|
|
|
6943
7041
|
raw,
|
|
6944
7042
|
await this.fieldChecker("update", guard, void 0, raw)
|
|
6945
7043
|
);
|
|
6946
|
-
const existing = await this.db.findGlobal({ slug:
|
|
6947
|
-
const current = await this.globalDraft(config, existing);
|
|
7044
|
+
const existing = await this.db.findGlobal({ slug: key2 }) ?? {};
|
|
7045
|
+
const current = await this.globalDraft(config, existing, key2);
|
|
6948
7046
|
const localized = mode === "restore" ? input : this.toMaps(config, input, current, options);
|
|
6949
7047
|
const merged = generateSlugs(
|
|
6950
7048
|
config.fields,
|
|
@@ -6957,7 +7055,7 @@ var EasyCMS = class {
|
|
|
6957
7055
|
);
|
|
6958
7056
|
if (config.drafts) merged.status = input.status ?? current.status ?? "draft";
|
|
6959
7057
|
const base = this.hookArgs(config, guard);
|
|
6960
|
-
let prepared = await this.prepare(config, merged, "update", void 0, options, guard
|
|
7058
|
+
let prepared = await this.prepare(config, merged, "update", void 0, options, guard);
|
|
6961
7059
|
prepared = await this.transform(
|
|
6962
7060
|
config.hooks?.beforeChange,
|
|
6963
7061
|
"data",
|
|
@@ -6967,7 +7065,7 @@ var EasyCMS = class {
|
|
|
6967
7065
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6968
7066
|
if (mode !== "unpublish" && this.separateDrafts(config) && existing.status === "published" && prepared.status === "draft") {
|
|
6969
7067
|
const draft = { ...prepared, updatedAt: now };
|
|
6970
|
-
await this.saveVersion(config, globalParent(
|
|
7068
|
+
await this.saveVersion(config, globalParent(key2), 0, draft, guard);
|
|
6971
7069
|
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
6972
7070
|
...base,
|
|
6973
7071
|
doc: draft,
|
|
@@ -6976,10 +7074,10 @@ var EasyCMS = class {
|
|
|
6976
7074
|
});
|
|
6977
7075
|
this.emit(config, "draft", draft);
|
|
6978
7076
|
await this.audited(config, "draft", draft, current, guard, mode);
|
|
6979
|
-
return this.
|
|
7077
|
+
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
6980
7078
|
}
|
|
6981
|
-
const doc = await this.db.updateGlobal({ slug:
|
|
6982
|
-
await this.saveVersion(config, globalParent(
|
|
7079
|
+
const doc = await this.db.updateGlobal({ slug: key2, data: { ...prepared, updatedAt: now } });
|
|
7080
|
+
await this.saveVersion(config, globalParent(key2), 0, doc, guard);
|
|
6983
7081
|
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
6984
7082
|
...base,
|
|
6985
7083
|
doc,
|
|
@@ -6988,17 +7086,17 @@ var EasyCMS = class {
|
|
|
6988
7086
|
});
|
|
6989
7087
|
this.emit(config, "update", doc, existing.status);
|
|
6990
7088
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
6991
|
-
return this.
|
|
7089
|
+
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
6992
7090
|
}
|
|
6993
7091
|
/**
|
|
6994
7092
|
* What `user` may do with one document. Resolves `where`-style access
|
|
6995
7093
|
* against the document, so the admin UI can hide actions precisely.
|
|
6996
7094
|
*/
|
|
6997
|
-
async documentPermissions(collection2, id, user) {
|
|
7095
|
+
async documentPermissions(collection2, id, user, context = NO_CONTEXT) {
|
|
6998
7096
|
const config = this.collection(collection2);
|
|
6999
7097
|
const parsed = parseId(id);
|
|
7000
7098
|
if (parsed === void 0) return { update: false, delete: false };
|
|
7001
|
-
const guard = { enforce: true, user };
|
|
7099
|
+
const guard = { enforce: true, user, context };
|
|
7002
7100
|
const check2 = (operation) => this.checkDocumentAccess(config, operation, guard, parsed, void 0).then(
|
|
7003
7101
|
() => true,
|
|
7004
7102
|
(error) => {
|
|
@@ -7166,9 +7264,10 @@ var EasyCMS = class {
|
|
|
7166
7264
|
const action = row.action;
|
|
7167
7265
|
try {
|
|
7168
7266
|
if (parent.startsWith("global:")) {
|
|
7169
|
-
const slug2 = parent.slice("global:".length);
|
|
7170
|
-
if (action === "publish")
|
|
7171
|
-
|
|
7267
|
+
const { slug: slug2, scope } = splitGlobalKey(parent.slice("global:".length));
|
|
7268
|
+
if (action === "publish")
|
|
7269
|
+
await this.saveGlobal(slug2, { status: "published" }, {}, "save", scope);
|
|
7270
|
+
else await this.saveGlobal(slug2, { status: "draft" }, {}, "unpublish", scope);
|
|
7172
7271
|
} else if (action === "publish") {
|
|
7173
7272
|
await this.updateDocument(parent, row.doc, { status: "published" }, { depth: 0 });
|
|
7174
7273
|
} else {
|
|
@@ -7213,16 +7312,16 @@ var EasyCMS = class {
|
|
|
7213
7312
|
await this.removeJob(collection2, parsed, jobId);
|
|
7214
7313
|
}
|
|
7215
7314
|
async scheduleGlobal(slug2, job, options = {}) {
|
|
7216
|
-
await this.globalScheduleTarget(slug2, options);
|
|
7217
|
-
return this.addJob(`global:${
|
|
7315
|
+
const key2 = await this.globalScheduleTarget(slug2, options);
|
|
7316
|
+
return this.addJob(`global:${key2}`, 0, job, options);
|
|
7218
7317
|
}
|
|
7219
7318
|
async scheduledGlobal(slug2, options = {}) {
|
|
7220
|
-
await this.globalScheduleTarget(slug2, options);
|
|
7221
|
-
return this.pendingJobs(`global:${
|
|
7319
|
+
const key2 = await this.globalScheduleTarget(slug2, options);
|
|
7320
|
+
return this.pendingJobs(`global:${key2}`, 0);
|
|
7222
7321
|
}
|
|
7223
7322
|
async cancelGlobalSchedule(slug2, jobId, options = {}) {
|
|
7224
|
-
await this.globalScheduleTarget(slug2, options);
|
|
7225
|
-
await this.removeJob(`global:${
|
|
7323
|
+
const key2 = await this.globalScheduleTarget(slug2, options);
|
|
7324
|
+
await this.removeJob(`global:${key2}`, 0, jobId);
|
|
7226
7325
|
}
|
|
7227
7326
|
async scheduleTarget(collection2, id, options) {
|
|
7228
7327
|
const config = this.collection(collection2);
|
|
@@ -7237,8 +7336,10 @@ var EasyCMS = class {
|
|
|
7237
7336
|
async globalScheduleTarget(slug2, options) {
|
|
7238
7337
|
const config = this.global(slug2);
|
|
7239
7338
|
if (!config.schedule) throw new QueryError(`"${slug2}" has no schedule`);
|
|
7240
|
-
|
|
7241
|
-
await this.
|
|
7339
|
+
const guard = guardOf(options);
|
|
7340
|
+
await this.checkGlobalAccess(config, "update", guard);
|
|
7341
|
+
await this.checkGrant(guard, { global: slug2 }, "publish");
|
|
7342
|
+
return this.writeKey(config, guard);
|
|
7242
7343
|
}
|
|
7243
7344
|
async addJob(parent, doc, job, options) {
|
|
7244
7345
|
const runAt = new Date(job.at);
|
|
@@ -7295,9 +7396,16 @@ var EasyCMS = class {
|
|
|
7295
7396
|
if (!guard.enforce) return true;
|
|
7296
7397
|
const cached = allowed2.get(parent);
|
|
7297
7398
|
if (cached !== void 0) return cached;
|
|
7298
|
-
const
|
|
7299
|
-
|
|
7300
|
-
|
|
7399
|
+
const global2 = parent.startsWith("global:") ? this.config.globals.find(
|
|
7400
|
+
(g) => g.slug === splitGlobalKey(parent.slice("global:".length)).slug
|
|
7401
|
+
) : void 0;
|
|
7402
|
+
if (global2 && this.globalKey(global2, guard) !== parent.slice("global:".length)) {
|
|
7403
|
+
allowed2.set(parent, false);
|
|
7404
|
+
return false;
|
|
7405
|
+
}
|
|
7406
|
+
const config = parent.startsWith("global:") ? global2 : this.config.collections.find((c) => c.slug === parent);
|
|
7407
|
+
const access = config ? await evaluateAccess(config.access?.update, { user: guard.user, context: guard.context }) : false;
|
|
7408
|
+
const target = parent.startsWith("global:") ? { global: splitGlobalKey(parent.slice("global:".length)).slug } : { collection: parent };
|
|
7301
7409
|
const result = access !== false && await this.roles.allows(guard.user, target, "update") !== false;
|
|
7302
7410
|
allowed2.set(parent, result);
|
|
7303
7411
|
return result;
|
|
@@ -7306,7 +7414,7 @@ var EasyCMS = class {
|
|
|
7306
7414
|
for (const row of rows.docs) {
|
|
7307
7415
|
const parent = String(row.parent);
|
|
7308
7416
|
if (!await may2(parent)) continue;
|
|
7309
|
-
const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
|
|
7417
|
+
const target = parent.startsWith("global:") ? { global: splitGlobalKey(parent.slice("global:".length)).slug } : { collection: parent };
|
|
7310
7418
|
jobs.push({ ...toJob(row), ...target, doc: row.doc });
|
|
7311
7419
|
if (jobs.length === limit) break;
|
|
7312
7420
|
}
|
|
@@ -7418,7 +7526,10 @@ var EasyCMS = class {
|
|
|
7418
7526
|
async readWhere(config, guard, where) {
|
|
7419
7527
|
if (!guard.enforce) return where;
|
|
7420
7528
|
const own = await this.checkGrant(guard, { collection: config.slug }, "read");
|
|
7421
|
-
const access = await evaluateAccess(config.access?.read, {
|
|
7529
|
+
const access = await evaluateAccess(config.access?.read, {
|
|
7530
|
+
user: guard.user,
|
|
7531
|
+
context: guard.context
|
|
7532
|
+
});
|
|
7422
7533
|
if (access === false) throw deny(guard.user);
|
|
7423
7534
|
await this.checkQueryFields(config, guard, where);
|
|
7424
7535
|
return andWhere(andWhere(where, access), own);
|
|
@@ -7428,6 +7539,7 @@ var EasyCMS = class {
|
|
|
7428
7539
|
const own = await this.checkGrant(guard, { collection: config.slug }, operation);
|
|
7429
7540
|
const allowed2 = await evaluateAccess(config.access?.[operation], {
|
|
7430
7541
|
user: guard.user,
|
|
7542
|
+
context: guard.context,
|
|
7431
7543
|
id,
|
|
7432
7544
|
...data ? { data } : {}
|
|
7433
7545
|
});
|
|
@@ -7445,7 +7557,10 @@ var EasyCMS = class {
|
|
|
7445
7557
|
async checkGlobalAccess(config, operation, guard) {
|
|
7446
7558
|
if (!guard.enforce) return;
|
|
7447
7559
|
await this.checkGrant(guard, { global: config.slug }, operation);
|
|
7448
|
-
const access = await evaluateAccess(config.access?.[operation], {
|
|
7560
|
+
const access = await evaluateAccess(config.access?.[operation], {
|
|
7561
|
+
user: guard.user,
|
|
7562
|
+
context: guard.context
|
|
7563
|
+
});
|
|
7449
7564
|
if (typeof access === "object")
|
|
7450
7565
|
throw new QueryError(`${operation} access of global "${config.slug}" must return a boolean`);
|
|
7451
7566
|
if (!access) throw deny(guard.user);
|
|
@@ -7456,6 +7571,7 @@ var EasyCMS = class {
|
|
|
7456
7571
|
kind,
|
|
7457
7572
|
{
|
|
7458
7573
|
user: guard.user,
|
|
7574
|
+
context: guard.context,
|
|
7459
7575
|
...id !== void 0 ? { id } : {},
|
|
7460
7576
|
...data ? { data } : {}
|
|
7461
7577
|
},
|
|
@@ -7536,7 +7652,11 @@ var EasyCMS = class {
|
|
|
7536
7652
|
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
7537
7653
|
if (config.drafts && raw.status === "published")
|
|
7538
7654
|
await this.checkGrant(guard, { collection: collection2 }, "publish");
|
|
7539
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
7655
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
7656
|
+
user: guard.user,
|
|
7657
|
+
context: guard.context,
|
|
7658
|
+
data: raw
|
|
7659
|
+
});
|
|
7540
7660
|
if (typeof allowed2 === "object")
|
|
7541
7661
|
throw new QueryError(`create access of "${collection2}" must return a boolean`);
|
|
7542
7662
|
if (!allowed2) throw deny(guard.user);
|
|
@@ -7568,7 +7688,7 @@ var EasyCMS = class {
|
|
|
7568
7688
|
"create",
|
|
7569
7689
|
void 0,
|
|
7570
7690
|
options,
|
|
7571
|
-
|
|
7691
|
+
hookGuard ?? guard
|
|
7572
7692
|
);
|
|
7573
7693
|
prepared = await this.transform(
|
|
7574
7694
|
config.hooks?.beforeChange,
|
|
@@ -7706,7 +7826,12 @@ var EasyCMS = class {
|
|
|
7706
7826
|
});
|
|
7707
7827
|
}
|
|
7708
7828
|
hookArgs(config, guard) {
|
|
7709
|
-
return {
|
|
7829
|
+
return {
|
|
7830
|
+
user: guard.user,
|
|
7831
|
+
cms: this,
|
|
7832
|
+
slug: config.slug,
|
|
7833
|
+
context: guard.context
|
|
7834
|
+
};
|
|
7710
7835
|
}
|
|
7711
7836
|
/**
|
|
7712
7837
|
* Runs hooks that may replace a value, passed as `data` (before hooks) or `doc` (afterRead).
|
|
@@ -7768,7 +7893,7 @@ var EasyCMS = class {
|
|
|
7768
7893
|
return result;
|
|
7769
7894
|
}
|
|
7770
7895
|
/** Validates, checks uniqueness and references. Returns clean data or throws `ValidationError`. */
|
|
7771
|
-
async prepare(config, data, operation, selfId, options = {},
|
|
7896
|
+
async prepare(config, data, operation, selfId, options = {}, guard = { user: null, context: NO_CONTEXT }) {
|
|
7772
7897
|
const isDraft = config.drafts === true && (data.status ?? "draft") === "draft";
|
|
7773
7898
|
const locale = this.localeOf(options);
|
|
7774
7899
|
const result = await validateFields(config.fields, data, {
|
|
@@ -7796,7 +7921,7 @@ var EasyCMS = class {
|
|
|
7796
7921
|
}
|
|
7797
7922
|
errors.push(...await this.checkReferences(result.references));
|
|
7798
7923
|
if (errors.length === 0)
|
|
7799
|
-
errors.push(...await this.checkFilterOptions(result.references, selfId,
|
|
7924
|
+
errors.push(...await this.checkFilterOptions(result.references, selfId, guard));
|
|
7800
7925
|
if (errors.length > 0) throw new ValidationError(config.slug, errors);
|
|
7801
7926
|
return clean2;
|
|
7802
7927
|
}
|
|
@@ -7855,9 +7980,10 @@ var EasyCMS = class {
|
|
|
7855
7980
|
locale === this.config.localization?.defaultLocale ? field.name : `${field.name}.${locale}`,
|
|
7856
7981
|
v
|
|
7857
7982
|
]) : [[field.name, field.name, value]];
|
|
7983
|
+
const scope = field.uniqueWithin === void 0 ? void 0 : { [field.uniqueWithin]: { equals: data[field.uniqueWithin] ?? null } };
|
|
7858
7984
|
for (const [path, errorField, v] of checks) {
|
|
7859
7985
|
if (v === null || v === void 0) continue;
|
|
7860
|
-
if (await this.isTaken(config.slug, path, v, selfId)) {
|
|
7986
|
+
if (await this.isTaken(config.slug, path, v, selfId, scope)) {
|
|
7861
7987
|
errors.push({ field: errorField, message: "must be unique" });
|
|
7862
7988
|
}
|
|
7863
7989
|
}
|
|
@@ -7875,14 +8001,16 @@ var EasyCMS = class {
|
|
|
7875
8001
|
return await this.db.count({ collection: collection2, where }) > 0;
|
|
7876
8002
|
}
|
|
7877
8003
|
/** Each reference is one its relationship's `filterOptions` allow. */
|
|
7878
|
-
async checkFilterOptions(references2, selfId,
|
|
8004
|
+
async checkFilterOptions(references2, selfId, guard) {
|
|
7879
8005
|
const errors = [];
|
|
7880
8006
|
const cms = this;
|
|
8007
|
+
const { user, context } = guard;
|
|
7881
8008
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
7882
8009
|
for (const ref of references2) {
|
|
7883
8010
|
const filter = ref.filterOptions;
|
|
7884
8011
|
if (!filter) continue;
|
|
7885
|
-
if (!allowed2.has(filter))
|
|
8012
|
+
if (!allowed2.has(filter))
|
|
8013
|
+
allowed2.set(filter, await filter({ id: selfId, user, cms, context }));
|
|
7886
8014
|
const where = allowed2.get(filter);
|
|
7887
8015
|
if (where === true) continue;
|
|
7888
8016
|
const target = this.config.collections.find((c) => c.slug === ref.collection);
|
|
@@ -7968,8 +8096,21 @@ function wherePaths(where) {
|
|
|
7968
8096
|
}
|
|
7969
8097
|
return out;
|
|
7970
8098
|
}
|
|
8099
|
+
var NO_CONTEXT = Object.freeze({});
|
|
8100
|
+
function withoutContext(permissions) {
|
|
8101
|
+
const { context: _context, ...rest } = permissions ?? {};
|
|
8102
|
+
return rest;
|
|
8103
|
+
}
|
|
8104
|
+
function splitGlobalKey(key2) {
|
|
8105
|
+
const at = key2.indexOf("@");
|
|
8106
|
+
return at < 0 ? { slug: key2 } : { slug: key2.slice(0, at), scope: key2.slice(at + 1) };
|
|
8107
|
+
}
|
|
7971
8108
|
function guardOf(options) {
|
|
7972
|
-
return {
|
|
8109
|
+
return {
|
|
8110
|
+
enforce: options.overrideAccess === false,
|
|
8111
|
+
user: options.user ?? null,
|
|
8112
|
+
context: options.context ?? NO_CONTEXT
|
|
8113
|
+
};
|
|
7973
8114
|
}
|
|
7974
8115
|
function deny(user) {
|
|
7975
8116
|
return user ? new ForbiddenError() : new UnauthorizedError();
|
|
@@ -8256,8 +8397,8 @@ function componentRef(component) {
|
|
|
8256
8397
|
if (typeof component === "string") return { tag: component };
|
|
8257
8398
|
return component.props ? { tag: component.tag, props: JSON.parse(JSON.stringify(component.props)) } : { tag: component.tag };
|
|
8258
8399
|
}
|
|
8259
|
-
async function allowed(access, user) {
|
|
8260
|
-
return await evaluateAccess(access, { user }) !== false;
|
|
8400
|
+
async function allowed(access, user, context) {
|
|
8401
|
+
return await evaluateAccess(access, { user, context }) !== false;
|
|
8261
8402
|
}
|
|
8262
8403
|
async function serializeFields(fields, checks, localized, folderId) {
|
|
8263
8404
|
const { update: update2 } = checks;
|
|
@@ -8339,21 +8480,21 @@ async function serializeFields(fields, checks, localized, folderId) {
|
|
|
8339
8480
|
}
|
|
8340
8481
|
return out;
|
|
8341
8482
|
}
|
|
8342
|
-
async function checkers(cms, user) {
|
|
8483
|
+
async function checkers(cms, user, context) {
|
|
8343
8484
|
const rules = await cms.roles.fieldRules(user);
|
|
8344
8485
|
return {
|
|
8345
|
-
update: new FieldAccessChecker("update", { user }, rules),
|
|
8346
|
-
read: new FieldAccessChecker("read", { user }, rules)
|
|
8486
|
+
update: new FieldAccessChecker("update", { user, context }, rules),
|
|
8487
|
+
read: new FieldAccessChecker("read", { user, context }, rules)
|
|
8347
8488
|
};
|
|
8348
8489
|
}
|
|
8349
|
-
async function may(cms, access, user, target, operation) {
|
|
8350
|
-
return await allowed(access, user) && await cms.roles.allows(user, target, operation) !== false;
|
|
8490
|
+
async function may(cms, access, user, target, operation, context) {
|
|
8491
|
+
return await allowed(access, user, context) && await cms.roles.allows(user, target, operation) !== false;
|
|
8351
8492
|
}
|
|
8352
|
-
async function collection(cms, config, user, localized, roles) {
|
|
8493
|
+
async function collection(cms, config, user, localized, roles, context) {
|
|
8353
8494
|
const target = { collection: config.slug };
|
|
8354
8495
|
const fields = await serializeFields(
|
|
8355
8496
|
config.fields,
|
|
8356
|
-
await checkers(cms, user),
|
|
8497
|
+
await checkers(cms, user, context),
|
|
8357
8498
|
localized,
|
|
8358
8499
|
folderIds(cms)
|
|
8359
8500
|
);
|
|
@@ -8372,11 +8513,11 @@ async function collection(cms, config, user, localized, roles) {
|
|
|
8372
8513
|
schedule: config.schedule === true,
|
|
8373
8514
|
fields,
|
|
8374
8515
|
permissions: {
|
|
8375
|
-
read: await may(cms, config.access?.read, user, target, "read"),
|
|
8376
|
-
create: await may(cms, config.access?.create, user, target, "create"),
|
|
8377
|
-
update: await may(cms, config.access?.update, user, target, "update"),
|
|
8378
|
-
delete: await may(cms, config.access?.delete, user, target, "delete"),
|
|
8379
|
-
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
8516
|
+
read: await may(cms, config.access?.read, user, target, "read", context),
|
|
8517
|
+
create: await may(cms, config.access?.create, user, target, "create", context),
|
|
8518
|
+
update: await may(cms, config.access?.update, user, target, "update", context),
|
|
8519
|
+
delete: await may(cms, config.access?.delete, user, target, "delete", context),
|
|
8520
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish", context)
|
|
8380
8521
|
}
|
|
8381
8522
|
};
|
|
8382
8523
|
if (config.labels) result.labels = config.labels;
|
|
@@ -8392,9 +8533,9 @@ async function collection(cms, config, user, localized, roles) {
|
|
|
8392
8533
|
result.editIn = "drawer";
|
|
8393
8534
|
return result;
|
|
8394
8535
|
}
|
|
8395
|
-
async function global(cms, config, user, localized) {
|
|
8536
|
+
async function global(cms, config, user, localized, context) {
|
|
8396
8537
|
const target = { global: config.slug };
|
|
8397
|
-
const checks = await checkers(cms, user);
|
|
8538
|
+
const checks = await checkers(cms, user, context);
|
|
8398
8539
|
const result = {
|
|
8399
8540
|
slug: config.slug,
|
|
8400
8541
|
drafts: config.drafts === true,
|
|
@@ -8403,9 +8544,9 @@ async function global(cms, config, user, localized) {
|
|
|
8403
8544
|
schedule: config.schedule === true,
|
|
8404
8545
|
fields: await serializeFields(config.fields, checks, localized, folderIds(cms)),
|
|
8405
8546
|
permissions: {
|
|
8406
|
-
read: await may(cms, config.access?.read, user, target, "read"),
|
|
8407
|
-
update: await may(cms, config.access?.update, user, target, "update"),
|
|
8408
|
-
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
8547
|
+
read: await may(cms, config.access?.read, user, target, "read", context),
|
|
8548
|
+
update: await may(cms, config.access?.update, user, target, "update", context),
|
|
8549
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish", context)
|
|
8409
8550
|
}
|
|
8410
8551
|
};
|
|
8411
8552
|
if (config.label !== void 0) result.label = config.label;
|
|
@@ -8413,11 +8554,11 @@ async function global(cms, config, user, localized) {
|
|
|
8413
8554
|
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
8414
8555
|
return result;
|
|
8415
8556
|
}
|
|
8416
|
-
async function adminSchema(cms, user, origin) {
|
|
8557
|
+
async function adminSchema(cms, user, origin, context = {}) {
|
|
8417
8558
|
const collections = cms.config.collections.filter((c) => !INTERNAL_COLLECTIONS.has(c.slug));
|
|
8418
8559
|
const localization = cms.config.localization;
|
|
8419
8560
|
const localized = localization !== null;
|
|
8420
|
-
const admin = user
|
|
8561
|
+
const admin = isSystemAdmin(user);
|
|
8421
8562
|
const deliveries = cms.config.collections.some(
|
|
8422
8563
|
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
8423
8564
|
);
|
|
@@ -8433,13 +8574,16 @@ async function adminSchema(cms, user, origin) {
|
|
|
8433
8574
|
const roles = await cms.roles.options();
|
|
8434
8575
|
return {
|
|
8435
8576
|
locale: cms.config.admin.locale,
|
|
8577
|
+
switcher: cms.config.admin.switcher,
|
|
8436
8578
|
passwordLinks: cms.auth.canSendPasswordLinks(origin),
|
|
8437
8579
|
menu: [...cms.config.admin.menu],
|
|
8438
8580
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
8439
8581
|
collections: await Promise.all(
|
|
8440
|
-
collections.map((c) => collection(cms, c, user, localized, roles))
|
|
8582
|
+
collections.map((c) => collection(cms, c, user, localized, roles, context))
|
|
8583
|
+
),
|
|
8584
|
+
globals: await Promise.all(
|
|
8585
|
+
cms.config.globals.map((g) => global(cms, g, user, localized, context))
|
|
8441
8586
|
),
|
|
8442
|
-
globals: await Promise.all(cms.config.globals.map((g) => global(cms, g, user, localized))),
|
|
8443
8587
|
modules: adminModuleUrls(cms),
|
|
8444
8588
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
8445
8589
|
upload: {
|
|
@@ -8504,7 +8648,7 @@ function folderIds(cms) {
|
|
|
8504
8648
|
}
|
|
8505
8649
|
|
|
8506
8650
|
// src/version.ts
|
|
8507
|
-
var VERSION = true ? "0.
|
|
8651
|
+
var VERSION = true ? "0.44.0" : "0.0.0-dev";
|
|
8508
8652
|
|
|
8509
8653
|
// src/rest/admin-status.ts
|
|
8510
8654
|
var DAY2 = 864e5;
|
|
@@ -8592,7 +8736,7 @@ function fieldAt(fields, path) {
|
|
|
8592
8736
|
}
|
|
8593
8737
|
return found;
|
|
8594
8738
|
}
|
|
8595
|
-
async function pickerFilter(cms, url, target, user) {
|
|
8739
|
+
async function pickerFilter(cms, url, target, user, context = {}) {
|
|
8596
8740
|
const forCollection = url.searchParams.get("filterFor");
|
|
8597
8741
|
const forGlobal = url.searchParams.get("filterForGlobal");
|
|
8598
8742
|
const spec = forCollection ?? forGlobal;
|
|
@@ -8606,7 +8750,7 @@ async function pickerFilter(cms, url, target, user) {
|
|
|
8606
8750
|
if (!field.filterOptions) return void 0;
|
|
8607
8751
|
const rawId = url.searchParams.get("filterId");
|
|
8608
8752
|
const id = rawId === null || rawId === "" ? void 0 : /^\d+$/.test(rawId) ? Number(rawId) : rawId;
|
|
8609
|
-
const where = await field.filterOptions({ id, user, cms });
|
|
8753
|
+
const where = await field.filterOptions({ id, user, cms, context });
|
|
8610
8754
|
return where === true ? void 0 : where;
|
|
8611
8755
|
}
|
|
8612
8756
|
|
|
@@ -8766,10 +8910,19 @@ async function respond(cms, request, url, path, root, options, production) {
|
|
|
8766
8910
|
if (path === null) throw new HttpError("Not found", 404);
|
|
8767
8911
|
const segments = pathSegments(path);
|
|
8768
8912
|
const { token, via } = readToken(request);
|
|
8769
|
-
const
|
|
8770
|
-
if (!
|
|
8913
|
+
const signedIn = token ? await cms.auth.verify(token) : null;
|
|
8914
|
+
if (!signedIn && via === "bearer" && token?.startsWith("ecms_"))
|
|
8771
8915
|
throw new UnauthorizedError("Invalid or expired API key");
|
|
8772
|
-
const
|
|
8916
|
+
const { user, context } = await cms.applyOnRequest(request.headers, url, signedIn);
|
|
8917
|
+
const ctx = {
|
|
8918
|
+
request,
|
|
8919
|
+
url,
|
|
8920
|
+
user,
|
|
8921
|
+
context,
|
|
8922
|
+
via: signedIn ? via : null,
|
|
8923
|
+
token,
|
|
8924
|
+
headers
|
|
8925
|
+
};
|
|
8773
8926
|
const method = request.method.toUpperCase();
|
|
8774
8927
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
8775
8928
|
const audit = {
|
|
@@ -8833,6 +8986,7 @@ async function customEndpoint(cms, ctx, method, segments, root, options) {
|
|
|
8833
8986
|
url: ctx.url,
|
|
8834
8987
|
params,
|
|
8835
8988
|
user: ctx.user,
|
|
8989
|
+
context: ctx.context,
|
|
8836
8990
|
ip: options.getClientIp?.(ctx.request),
|
|
8837
8991
|
cms,
|
|
8838
8992
|
json: () => readJson(ctx.request)
|
|
@@ -8859,7 +9013,12 @@ function matchPath(path, segments) {
|
|
|
8859
9013
|
}
|
|
8860
9014
|
async function route(cms, ctx, method, segments, options) {
|
|
8861
9015
|
const [first, second, third] = segments;
|
|
8862
|
-
const access = {
|
|
9016
|
+
const access = {
|
|
9017
|
+
overrideAccess: false,
|
|
9018
|
+
user: ctx.user,
|
|
9019
|
+
context: ctx.context,
|
|
9020
|
+
...parseLocale(ctx.url)
|
|
9021
|
+
};
|
|
8863
9022
|
const draft = ctx.user !== null && ctx.url.searchParams.get("draft") === "true";
|
|
8864
9023
|
if (segments.length === 0) throw new HttpError("Not found", 404);
|
|
8865
9024
|
if (first === "auth") return ssoRoute(cms, ctx, method, segments);
|
|
@@ -8957,14 +9116,14 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
8957
9116
|
const secret = cms.config.cronSecret ?? process.env.CRON_SECRET;
|
|
8958
9117
|
const header = ctx.request.headers.get("authorization") ?? "";
|
|
8959
9118
|
const byCron = !!secret && safeEqual(header, `Bearer ${secret}`);
|
|
8960
|
-
if (!byCron && ctx.user
|
|
9119
|
+
if (!byCron && !isSystemAdmin(ctx.user)) {
|
|
8961
9120
|
throw ctx.user ? new HttpError("Forbidden", 403) : new UnauthorizedError();
|
|
8962
9121
|
}
|
|
8963
9122
|
return { body: await cms.runJobs() };
|
|
8964
9123
|
}
|
|
8965
9124
|
if (first === "admin" && second === "backups") {
|
|
8966
9125
|
if (!ctx.user) throw new UnauthorizedError();
|
|
8967
|
-
if (ctx.user
|
|
9126
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
8968
9127
|
const [, , id2, action, extra] = segments;
|
|
8969
9128
|
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
8970
9129
|
if (id2 === void 0) {
|
|
@@ -9006,7 +9165,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9006
9165
|
}
|
|
9007
9166
|
if (first === "admin" && second === "email") {
|
|
9008
9167
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9009
|
-
if (ctx.user
|
|
9168
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9010
9169
|
const [, , action, extra] = segments;
|
|
9011
9170
|
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
9012
9171
|
if (action === void 0) {
|
|
@@ -9025,7 +9184,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9025
9184
|
}
|
|
9026
9185
|
if (first === "admin" && second === "sso" && third === void 0) {
|
|
9027
9186
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9028
|
-
if (ctx.user
|
|
9187
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9029
9188
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9030
9189
|
return { body: cms.auth.sso.settings(ctx.url.origin) };
|
|
9031
9190
|
}
|
|
@@ -9066,7 +9225,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9066
9225
|
}
|
|
9067
9226
|
if (first === "admin" && second === "owned" && third !== void 0 && segments.length === 3) {
|
|
9068
9227
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9069
|
-
if (ctx.user
|
|
9228
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9070
9229
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9071
9230
|
const parsed = parseId(third);
|
|
9072
9231
|
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
@@ -9080,7 +9239,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9080
9239
|
}
|
|
9081
9240
|
if (first === "admin" && second === "roles") {
|
|
9082
9241
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9083
|
-
if (ctx.user
|
|
9242
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9084
9243
|
if (!cms.roles.enabled) throw new HttpError("Not found", 404);
|
|
9085
9244
|
const [, , id2, action, extra] = segments;
|
|
9086
9245
|
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
@@ -9166,7 +9325,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9166
9325
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9167
9326
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9168
9327
|
if (second === "schema" && third === void 0)
|
|
9169
|
-
return { body: await adminSchema(cms, ctx.user, ctx.url.origin) };
|
|
9328
|
+
return { body: await adminSchema(cms, ctx.user, ctx.url.origin, ctx.context) };
|
|
9170
9329
|
if (second === "modules" && third !== void 0 && segments.length === 3) {
|
|
9171
9330
|
const index = /^(\d+)\.js$/.exec(third)?.[1];
|
|
9172
9331
|
const file = index === void 0 ? void 0 : await readAdminModule(cms, Number(index));
|
|
@@ -9186,13 +9345,19 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9186
9345
|
return { body: await adminStatus(cms) };
|
|
9187
9346
|
}
|
|
9188
9347
|
if (second === "scheduled" && third === void 0)
|
|
9189
|
-
return {
|
|
9348
|
+
return {
|
|
9349
|
+
body: await cms.upcomingJobs({
|
|
9350
|
+
user: ctx.user,
|
|
9351
|
+
context: ctx.context,
|
|
9352
|
+
overrideAccess: false
|
|
9353
|
+
})
|
|
9354
|
+
};
|
|
9190
9355
|
const [, , collection3, id2, extra] = segments;
|
|
9191
9356
|
if (second === "access" && collection3 && id2 && extra === void 0) {
|
|
9192
9357
|
if (INTERNAL_COLLECTIONS.has(collection3) || !cms.config.collections.some((c) => c.slug === collection3)) {
|
|
9193
9358
|
throw new HttpError(`Unknown collection "${collection3}"`, 404);
|
|
9194
9359
|
}
|
|
9195
|
-
return { body: await cms.documentPermissions(collection3, id2, ctx.user) };
|
|
9360
|
+
return { body: await cms.documentPermissions(collection3, id2, ctx.user, ctx.context) };
|
|
9196
9361
|
}
|
|
9197
9362
|
throw new HttpError("Not found", 404);
|
|
9198
9363
|
}
|
|
@@ -9297,7 +9462,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9297
9462
|
if (second === void 0) {
|
|
9298
9463
|
if (method === "GET") {
|
|
9299
9464
|
const query = parseListQuery(ctx.url);
|
|
9300
|
-
const filter = await pickerFilter(cms, ctx.url, collection2, ctx.user);
|
|
9465
|
+
const filter = await pickerFilter(cms, ctx.url, collection2, ctx.user, ctx.context);
|
|
9301
9466
|
if (filter) query.where = query.where ? { and: [query.where, filter] } : filter;
|
|
9302
9467
|
return { body: await cms.find(collection2, { ...access, ...query, draft }) };
|
|
9303
9468
|
}
|
|
@@ -9342,12 +9507,17 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9342
9507
|
throw methodNotAllowed(ctx, "GET, PATCH, DELETE");
|
|
9343
9508
|
}
|
|
9344
9509
|
async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
9345
|
-
const access = {
|
|
9510
|
+
const access = {
|
|
9511
|
+
overrideAccess: false,
|
|
9512
|
+
user: ctx.user,
|
|
9513
|
+
context: ctx.context,
|
|
9514
|
+
...parseLocale(ctx.url)
|
|
9515
|
+
};
|
|
9346
9516
|
const depth = parseDepth(ctx.url);
|
|
9347
9517
|
const [action, versionId, extra] = path;
|
|
9348
9518
|
if (collection2 === USERS && action === "password-link" && path.length === 1) {
|
|
9349
9519
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
9350
|
-
if (ctx.user
|
|
9520
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9351
9521
|
const body = await readJson(ctx.request).catch(() => ({}));
|
|
9352
9522
|
const sent = await cms.auth.sendPasswordLink(/^\d+$/.test(id) ? Number(id) : id, {
|
|
9353
9523
|
origin: ctx.url.origin,
|
|
@@ -9400,7 +9570,12 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
9400
9570
|
throw new HttpError("Not found", 404);
|
|
9401
9571
|
}
|
|
9402
9572
|
async function globalAction(cms, ctx, method, slug2, path) {
|
|
9403
|
-
const access = {
|
|
9573
|
+
const access = {
|
|
9574
|
+
overrideAccess: false,
|
|
9575
|
+
user: ctx.user,
|
|
9576
|
+
context: ctx.context,
|
|
9577
|
+
...parseLocale(ctx.url)
|
|
9578
|
+
};
|
|
9404
9579
|
const depth = parseDepth(ctx.url);
|
|
9405
9580
|
const [action, versionId, extra] = path;
|
|
9406
9581
|
if (action === "versions" && versionId === void 0) {
|
|
@@ -9563,6 +9738,7 @@ async function servePrivateFile(cms, ctx, key2, head) {
|
|
|
9563
9738
|
limit: 5,
|
|
9564
9739
|
depth: 0,
|
|
9565
9740
|
user: ctx.user,
|
|
9741
|
+
context: ctx.context,
|
|
9566
9742
|
overrideAccess: false
|
|
9567
9743
|
});
|
|
9568
9744
|
const owns = found.docs.some(
|
|
@@ -9663,7 +9839,7 @@ async function ssoRoute(cms, ctx, method, segments) {
|
|
|
9663
9839
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9664
9840
|
const asked = ctx.url.searchParams.get("user");
|
|
9665
9841
|
const target = asked === null ? ctx.user.id : parseId(asked) ?? asked;
|
|
9666
|
-
if (String(target) !== String(ctx.user.id) && (ctx.user
|
|
9842
|
+
if (String(target) !== String(ctx.user.id) && !isSystemAdmin(ctx.user))
|
|
9667
9843
|
throw new ForbiddenError();
|
|
9668
9844
|
return { body: await sso.identities(target) };
|
|
9669
9845
|
}
|
|
@@ -10061,6 +10237,7 @@ export {
|
|
|
10061
10237
|
isAdmin,
|
|
10062
10238
|
isHasMany,
|
|
10063
10239
|
isLoggedIn,
|
|
10240
|
+
isSystemAdmin,
|
|
10064
10241
|
keyAllows,
|
|
10065
10242
|
loadConfig,
|
|
10066
10243
|
localStorage,
|