@easy-cms/core 0.47.4 → 0.48.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 +43 -5
- package/dist/index.js +76 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -137,7 +137,7 @@ interface UserIdentity {
|
|
|
137
137
|
lastUsedAt: string | null;
|
|
138
138
|
}
|
|
139
139
|
/** Why a sign-in did not work, as `?sso=` on the page it ends on. */
|
|
140
|
-
type SsoOutcome = 'linked' | 'cancelled' | 'expired' | 'failed' | 'no-account' | 'unverified' | 'inactive' | 'taken';
|
|
140
|
+
type SsoOutcome = 'linked' | 'cancelled' | 'expired' | 'failed' | 'no-account' | 'link-first' | 'unverified' | 'inactive' | 'taken';
|
|
141
141
|
/** Single sign-on with `auth.providers`. Available as `cms.auth.sso`. */
|
|
142
142
|
declare class SingleSignOn {
|
|
143
143
|
private readonly cms;
|
|
@@ -195,6 +195,8 @@ declare class SingleSignOn {
|
|
|
195
195
|
private userByEmail;
|
|
196
196
|
/** A new account for people from `allowSignUp.domains`. */
|
|
197
197
|
private signUp;
|
|
198
|
+
/** A site member (`auth.members`): matched by email, like any shop or site account. */
|
|
199
|
+
private isMember;
|
|
198
200
|
private defaultRole;
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -287,6 +289,12 @@ declare class Auth {
|
|
|
287
289
|
*/
|
|
288
290
|
private memberLink;
|
|
289
291
|
hasUsers(): Promise<boolean>;
|
|
292
|
+
private warnedNoClientIp;
|
|
293
|
+
/**
|
|
294
|
+
* Without the client's address, failed logins count per email only, so anyone can lock an
|
|
295
|
+
* account for a while. Said once, in production.
|
|
296
|
+
*/
|
|
297
|
+
private warnNoClientIp;
|
|
290
298
|
/** Ends the session for a signed token. Unknown tokens are ignored. */
|
|
291
299
|
logout(signedToken: string): Promise<void>;
|
|
292
300
|
/**
|
|
@@ -1225,6 +1233,8 @@ declare class MediaFolders {
|
|
|
1225
1233
|
interface StoredFile {
|
|
1226
1234
|
readonly body: Uint8Array;
|
|
1227
1235
|
readonly size: number;
|
|
1236
|
+
/** The type the storage serves the file with, when it says (checked after direct uploads). */
|
|
1237
|
+
readonly contentType?: string;
|
|
1228
1238
|
}
|
|
1229
1239
|
/** Where uploaded files live. Keys are generated by Easy CMS: `<name>-<random>.<ext>`. */
|
|
1230
1240
|
interface StorageAdapter {
|
|
@@ -1633,6 +1643,11 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1633
1643
|
/** The global's pending draft when it has one (see `withDrafts`), otherwise `saved`. */
|
|
1634
1644
|
private globalDraft;
|
|
1635
1645
|
private saveGlobal;
|
|
1646
|
+
/**
|
|
1647
|
+
* Users changing their own password or email say their current password first, so a session
|
|
1648
|
+
* someone else got hold of can't lock them out. Admins changing other users don't.
|
|
1649
|
+
*/
|
|
1650
|
+
private checkOwnCredentials;
|
|
1636
1651
|
/**
|
|
1637
1652
|
* What `user` may do with one document. Resolves `where`-style access
|
|
1638
1653
|
* against the document, so the admin UI can hide actions precisely.
|
|
@@ -2066,7 +2081,10 @@ interface BlocksField extends BaseField<'blocks', never> {
|
|
|
2066
2081
|
declare const uniqueWithinOf: (field: Field) => readonly string[];
|
|
2067
2082
|
/** Fields with `hasMany`: their value is a list of options, documents or files. */
|
|
2068
2083
|
declare const isHasMany: (field: Field) => boolean;
|
|
2069
|
-
/**
|
|
2084
|
+
/**
|
|
2085
|
+
* Whether a MIME type is one of `patterns` (`image/*` matches every image but SVG, which can
|
|
2086
|
+
* carry scripts and is allowed only when listed by name: `image/svg+xml`).
|
|
2087
|
+
*/
|
|
2070
2088
|
declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): boolean;
|
|
2071
2089
|
/** Fields whose value is a list of rows: `array` and `blocks`. */
|
|
2072
2090
|
declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
|
|
@@ -2286,6 +2304,12 @@ interface AuthProvider {
|
|
|
2286
2304
|
}): Promise<AuthProviderProfile>;
|
|
2287
2305
|
/** For Settings → SSO: what it connects to, never secrets. */
|
|
2288
2306
|
describe?(): Readonly<Record<string, string>>;
|
|
2307
|
+
/**
|
|
2308
|
+
* Signs staff who have an account in by their verified email the first time, without linking
|
|
2309
|
+
* the provider from their account page. Only for a provider whose emails your organization
|
|
2310
|
+
* controls (e.g. its Google Workspace). Default `false`; site members are always matched.
|
|
2311
|
+
*/
|
|
2312
|
+
readonly linkByEmail?: boolean;
|
|
2289
2313
|
}
|
|
2290
2314
|
|
|
2291
2315
|
interface ConfigIssue {
|
|
@@ -3375,9 +3399,20 @@ interface ApiHandlerOptions {
|
|
|
3375
3399
|
* Takes precedence over `trustProxy`.
|
|
3376
3400
|
*/
|
|
3377
3401
|
readonly getClientIp?: (request: Request) => string | undefined;
|
|
3378
|
-
/**
|
|
3402
|
+
/**
|
|
3403
|
+
* Behind a proxy that adds the client's address to `X-Forwarded-For`: the last address in it
|
|
3404
|
+
* (the one the proxy added; earlier ones come from the client and can say anything). Vercel
|
|
3405
|
+
* and Netlify are recognized without it.
|
|
3406
|
+
*/
|
|
3379
3407
|
readonly trustProxy?: boolean;
|
|
3380
3408
|
}
|
|
3409
|
+
/**
|
|
3410
|
+
* The client's address from `X-Forwarded-For` as the nearest proxy wrote it: its last entry.
|
|
3411
|
+
* Earlier entries were sent by the client, so they can't be trusted.
|
|
3412
|
+
*/
|
|
3413
|
+
declare function forwardedClientIp(value: string | null | undefined): string | undefined;
|
|
3414
|
+
/** The client's address on hosting platforms that set it themselves: Vercel, Netlify. */
|
|
3415
|
+
declare function platformClientIp(headers: Headers): string | undefined;
|
|
3381
3416
|
/**
|
|
3382
3417
|
* The REST API as a `(Request) => Response` handler for a framework's route, on the shared
|
|
3383
3418
|
* instance (`sharedEasyCMS`). Mount it at `routes.api` (default `/api/cms`), for every method.
|
|
@@ -3416,7 +3451,10 @@ declare function expandMimeTypes(patterns: readonly string[]): string[];
|
|
|
3416
3451
|
* tells CSV from plain text, which have the same contents.
|
|
3417
3452
|
*/
|
|
3418
3453
|
declare function sniffMimeType(data: Uint8Array, name?: string): string | undefined;
|
|
3419
|
-
/**
|
|
3454
|
+
/**
|
|
3455
|
+
* `image/*` style matching; group names (`documents`…) stand for their types. A wildcard leaves
|
|
3456
|
+
* out SVG, which can carry scripts: it is allowed only when listed as `image/svg+xml`.
|
|
3457
|
+
*/
|
|
3420
3458
|
declare function mimeAllowed(type: string, allowed: readonly string[]): boolean;
|
|
3421
3459
|
/** Width and height of PNG, GIF, JPEG and WebP images without decoding them. */
|
|
3422
3460
|
declare function imageDimensions(data: Uint8Array, type: string): {
|
|
@@ -3927,4 +3965,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
|
|
|
3927
3965
|
/** The version of `@easy-cms/core`, set when the package is built. */
|
|
3928
3966
|
declare const VERSION: string;
|
|
3929
3967
|
|
|
3930
|
-
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 AdminCommand, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminFieldArgs, type AdminGlobal, type AdminIcon, type AdminLayoutNode, type AdminLocale, type AdminNavGroup, type AdminNavItem, type AdminNavNode, 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, BUILTIN_NAV, 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, FieldCondition, 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 IncrementOptions, type InferCollection, type InferGlobal, type JobConfig, type JsonField, type Label, type LayoutNode, 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 MembersConfig, type MembersSignup, type MigrationInfo, type NavGroup, 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 SearchHit, type SelectField, type SelectOption, type Session, type SidebarPanel, type SignupArgs, type SignupResult, 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, checkFormToken, configSignature, consoleEmail, consoleLogger, copyDatabase, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, formToken, generateSlugs, generateTypes, hasRows, hashPassword, honeypotName, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSignedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, rateKeys, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, uniqueWithinOf, validateConfig, validateFields, verifyPassword, verifyTurnstile, writeBackupFile };
|
|
3968
|
+
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 AdminCommand, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminFieldArgs, type AdminGlobal, type AdminIcon, type AdminLayoutNode, type AdminLocale, type AdminNavGroup, type AdminNavItem, type AdminNavNode, 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, BUILTIN_NAV, 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, FieldCondition, 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 IncrementOptions, type InferCollection, type InferGlobal, type JobConfig, type JsonField, type Label, type LayoutNode, 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 MembersConfig, type MembersSignup, type MigrationInfo, type NavGroup, 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 SearchHit, type SelectField, type SelectOption, type Session, type SidebarPanel, type SignupArgs, type SignupResult, 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, checkFormToken, configSignature, consoleEmail, consoleLogger, copyDatabase, createApiHandler, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, expandMimeTypes, fillMissing, findConfigFile, formToken, forwardedClientIp, generateSlugs, generateTypes, hasRows, hashPassword, honeypotName, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, isSignedIn, isSystemAdmin, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, platformClientIp, populate, rateKeys, readCookie, resolveAdminModule, resolveConfig, rowFields, sharedEasyCMS, silentLogger, singularize, slugify, sniffMimeType, uniqueWithinOf, validateConfig, validateFields, verifyPassword, verifyTurnstile, writeBackupFile };
|
package/dist/index.js
CHANGED
|
@@ -1112,7 +1112,7 @@ function sniffMimeType(data, name = "") {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
function mimeAllowed(type, allowed2) {
|
|
1114
1114
|
return expandMimeTypes(allowed2).some(
|
|
1115
|
-
(pattern) => pattern.endsWith("/*") ? type.startsWith(pattern.slice(0, -1)) : pattern === type
|
|
1115
|
+
(pattern) => pattern.endsWith("/*") ? type.startsWith(pattern.slice(0, -1)) && type !== "image/svg+xml" : pattern === type
|
|
1116
1116
|
);
|
|
1117
1117
|
}
|
|
1118
1118
|
function imageDimensions(data, type) {
|
|
@@ -1190,9 +1190,10 @@ function mimeAllowedBy(mimeType, patterns) {
|
|
|
1190
1190
|
const type = mimeType.toLowerCase();
|
|
1191
1191
|
return expandMimeTypes(patterns).some((pattern) => {
|
|
1192
1192
|
const p = pattern.toLowerCase();
|
|
1193
|
-
return p.endsWith("/*") ? type.startsWith(p.slice(0, -1)) : type === p;
|
|
1193
|
+
return p.endsWith("/*") ? type.startsWith(p.slice(0, -1)) && type !== SVG : type === p;
|
|
1194
1194
|
});
|
|
1195
1195
|
}
|
|
1196
|
+
var SVG = "image/svg+xml";
|
|
1196
1197
|
var hasRows = (field) => field.type === "array" || field.type === "blocks";
|
|
1197
1198
|
function rowFields(field, row) {
|
|
1198
1199
|
if (field.type === "array") return field.fields;
|
|
@@ -1690,6 +1691,8 @@ var SingleSignOn = class {
|
|
|
1690
1691
|
if (!email2 || !profile.emailVerified)
|
|
1691
1692
|
return await back("login", email2 ? "unverified" : "no-account");
|
|
1692
1693
|
user = await this.userByEmail(email2);
|
|
1694
|
+
if (user && !this.isMember(user) && provider.linkByEmail !== true)
|
|
1695
|
+
return await back("login", "link-first");
|
|
1693
1696
|
if (user && user.emailVerified === false) {
|
|
1694
1697
|
const { id, ...rest } = user;
|
|
1695
1698
|
user = await this.cms.db.update({
|
|
@@ -1842,6 +1845,10 @@ var SingleSignOn = class {
|
|
|
1842
1845
|
this.cms.logger.info(`${email2} signed up with a provider`);
|
|
1843
1846
|
return this.cms.db.findById({ collection: USERS, id: created.id });
|
|
1844
1847
|
}
|
|
1848
|
+
/** A site member (`auth.members`): matched by email, like any shop or site account. */
|
|
1849
|
+
isMember(user) {
|
|
1850
|
+
return (this.config.auth.members?.roles ?? []).includes(String(user.role));
|
|
1851
|
+
}
|
|
1845
1852
|
defaultRole() {
|
|
1846
1853
|
const roles = this.config.auth.roles;
|
|
1847
1854
|
return roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
@@ -1880,6 +1887,7 @@ var Auth = class {
|
|
|
1880
1887
|
const email2 = typeof args.email === "string" ? args.email.trim().toLowerCase() : "";
|
|
1881
1888
|
const password = typeof args.password === "string" ? args.password : "";
|
|
1882
1889
|
const key2 = `${email2}|${args.ip ?? ""}`;
|
|
1890
|
+
if (args.ip === void 0) this.warnNoClientIp();
|
|
1883
1891
|
try {
|
|
1884
1892
|
await this.checkRateLimit(key2);
|
|
1885
1893
|
} catch (error) {
|
|
@@ -2103,6 +2111,18 @@ var Auth = class {
|
|
|
2103
2111
|
async hasUsers() {
|
|
2104
2112
|
return await this.db.count({ collection: USERS }) > 0;
|
|
2105
2113
|
}
|
|
2114
|
+
warnedNoClientIp = false;
|
|
2115
|
+
/**
|
|
2116
|
+
* Without the client's address, failed logins count per email only, so anyone can lock an
|
|
2117
|
+
* account for a while. Said once, in production.
|
|
2118
|
+
*/
|
|
2119
|
+
warnNoClientIp() {
|
|
2120
|
+
if (this.warnedNoClientIp || process.env.NODE_ENV !== "production") return;
|
|
2121
|
+
this.warnedNoClientIp = true;
|
|
2122
|
+
this.cms.logger.warn(
|
|
2123
|
+
"Easy CMS doesn't know the client's IP address, so logins are rate limited per email only. Behind a proxy that sets X-Forwarded-For, set `trustProxy: true` in the adapter."
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2106
2126
|
/** Ends the session for a signed token. Unknown tokens are ignored. */
|
|
2107
2127
|
async logout(signedToken) {
|
|
2108
2128
|
const token = unsignToken(this.config.secret, signedToken);
|
|
@@ -4409,6 +4429,7 @@ var Roles = class {
|
|
|
4409
4429
|
async canView(user, id) {
|
|
4410
4430
|
if (isSystemAdmin(user)) return true;
|
|
4411
4431
|
if (user.apiKey) return false;
|
|
4432
|
+
if (id === "deliveries" && user.scoped === true) return false;
|
|
4412
4433
|
if (id === "audit" && user.role === "admin" && user.scoped === true && this.config.audit !== false && typeof this.config.audit.scope === "function")
|
|
4413
4434
|
return true;
|
|
4414
4435
|
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
@@ -5248,8 +5269,13 @@ function validateSecret(secret, add) {
|
|
|
5248
5269
|
);
|
|
5249
5270
|
} else if (secret.length < MIN_SECRET_LENGTH) {
|
|
5250
5271
|
add("secret", `must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`, hint);
|
|
5272
|
+
} else if (process.env.NODE_ENV === "production" && guessable(secret)) {
|
|
5273
|
+
add("secret", "looks like an example or a repeated pattern, not a random string", hint);
|
|
5251
5274
|
}
|
|
5252
5275
|
}
|
|
5276
|
+
function guessable(secret) {
|
|
5277
|
+
return /change[-_ ]?me|example|placeholder|secret-?key/i.test(secret) || new Set(secret).size < 8;
|
|
5278
|
+
}
|
|
5253
5279
|
function validateAdmin(config, add) {
|
|
5254
5280
|
const admin = config.admin;
|
|
5255
5281
|
if (admin === void 0) return;
|
|
@@ -6830,6 +6856,8 @@ var EasyCMS = class {
|
|
|
6830
6856
|
const mimeType = sniffMimeType(start.body.subarray(0, SNIFF_BYTES), t.name);
|
|
6831
6857
|
if (mimeType !== t.type)
|
|
6832
6858
|
await refuse(`contains ${mimeType ?? "an unknown type"}, not ${t.type}`);
|
|
6859
|
+
const served = start.contentType?.split(";")[0]?.trim().toLowerCase();
|
|
6860
|
+
if (served && served !== t.type) await refuse(`is stored as ${served}, not ${t.type}`);
|
|
6833
6861
|
const stored = [];
|
|
6834
6862
|
try {
|
|
6835
6863
|
let dimensions;
|
|
@@ -7381,6 +7409,7 @@ var EasyCMS = class {
|
|
|
7381
7409
|
raw = rest;
|
|
7382
7410
|
}
|
|
7383
7411
|
const { input, password } = splitPassword(config, raw);
|
|
7412
|
+
if (config.slug === USERS) await this.checkOwnCredentials(guard, existing, raw, password);
|
|
7384
7413
|
const filtered = await filterInput(
|
|
7385
7414
|
config.fields,
|
|
7386
7415
|
input,
|
|
@@ -7700,6 +7729,33 @@ var EasyCMS = class {
|
|
|
7700
7729
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
7701
7730
|
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
7702
7731
|
}
|
|
7732
|
+
/**
|
|
7733
|
+
* Users changing their own password or email say their current password first, so a session
|
|
7734
|
+
* someone else got hold of can't lock them out. Admins changing other users don't.
|
|
7735
|
+
*/
|
|
7736
|
+
async checkOwnCredentials(guard, existing, raw, password) {
|
|
7737
|
+
const user = guard.user;
|
|
7738
|
+
if (!guard.enforce || !user || user.apiKey || String(user.id) !== String(existing.id)) return;
|
|
7739
|
+
const email2 = typeof raw.email === "string" ? raw.email.trim().toLowerCase() : void 0;
|
|
7740
|
+
const emailChanges = email2 !== void 0 && email2 !== String(existing.email ?? "").toLowerCase();
|
|
7741
|
+
if (password === void 0 && !emailChanges) return;
|
|
7742
|
+
const hash = typeof existing.passwordHash === "string" ? existing.passwordHash : void 0;
|
|
7743
|
+
if (!hash) {
|
|
7744
|
+
if (emailChanges)
|
|
7745
|
+
throw new ValidationError(USERS, [
|
|
7746
|
+
{ field: "email", message: "can't be changed on an account without a password" }
|
|
7747
|
+
]);
|
|
7748
|
+
return;
|
|
7749
|
+
}
|
|
7750
|
+
const current = typeof raw.currentPassword === "string" ? raw.currentPassword : "";
|
|
7751
|
+
if (!current || !await verifyPassword(current, hash))
|
|
7752
|
+
throw new ValidationError(USERS, [
|
|
7753
|
+
{
|
|
7754
|
+
field: "currentPassword",
|
|
7755
|
+
message: current ? "is not right" : "is required to change your password or email"
|
|
7756
|
+
}
|
|
7757
|
+
]);
|
|
7758
|
+
}
|
|
7703
7759
|
/**
|
|
7704
7760
|
* What `user` may do with one document. Resolves `where`-style access
|
|
7705
7761
|
* against the document, so the admin UI can hide actions precisely.
|
|
@@ -8817,8 +8873,10 @@ function deny(user) {
|
|
|
8817
8873
|
return user ? new ForbiddenError() : new UnauthorizedError();
|
|
8818
8874
|
}
|
|
8819
8875
|
function splitPassword(config, raw) {
|
|
8820
|
-
if (config.slug !== USERS
|
|
8821
|
-
const {
|
|
8876
|
+
if (config.slug !== USERS) return { input: raw };
|
|
8877
|
+
const { currentPassword: _current, ...rest } = raw;
|
|
8878
|
+
if (!Object.hasOwn(rest, "password")) return { input: rest };
|
|
8879
|
+
const { password, ...input } = rest;
|
|
8822
8880
|
if (password === void 0 || password === null || password === "") return { input };
|
|
8823
8881
|
if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
|
|
8824
8882
|
throw new ValidationError(USERS, [
|
|
@@ -9569,7 +9627,7 @@ async function adminSearch(cms, who, q, perCollection = 5) {
|
|
|
9569
9627
|
}
|
|
9570
9628
|
|
|
9571
9629
|
// src/version.ts
|
|
9572
|
-
var VERSION = true ? "0.
|
|
9630
|
+
var VERSION = true ? "0.48.0" : "0.0.0-dev";
|
|
9573
9631
|
|
|
9574
9632
|
// src/rest/admin-status.ts
|
|
9575
9633
|
var DAY2 = 864e5;
|
|
@@ -10939,9 +10997,19 @@ function sharedEasyCMS(config, options) {
|
|
|
10939
10997
|
});
|
|
10940
10998
|
return promise;
|
|
10941
10999
|
}
|
|
11000
|
+
function forwardedClientIp(value) {
|
|
11001
|
+
const last = value?.split(",").map((part) => part.trim()).filter(Boolean).at(-1);
|
|
11002
|
+
return last || void 0;
|
|
11003
|
+
}
|
|
11004
|
+
function platformClientIp(headers) {
|
|
11005
|
+
if (process.env.VERCEL)
|
|
11006
|
+
return headers.get("x-real-ip") ?? forwardedClientIp(headers.get("x-forwarded-for"));
|
|
11007
|
+
if (process.env.NETLIFY) return headers.get("x-nf-client-connection-ip") ?? void 0;
|
|
11008
|
+
return void 0;
|
|
11009
|
+
}
|
|
10942
11010
|
function createApiHandler(config, options = {}) {
|
|
10943
11011
|
const handlers = /* @__PURE__ */ new WeakMap();
|
|
10944
|
-
const getClientIp = options.getClientIp ?? (options.trustProxy ? (r) => r.headers.get("x-forwarded-for")
|
|
11012
|
+
const getClientIp = options.getClientIp ?? (options.trustProxy ? (r) => forwardedClientIp(r.headers.get("x-forwarded-for")) : (r) => platformClientIp(r.headers));
|
|
10945
11013
|
return async (request) => {
|
|
10946
11014
|
const cms = await sharedEasyCMS(config);
|
|
10947
11015
|
let handler = handlers.get(cms);
|
|
@@ -11237,6 +11305,7 @@ export {
|
|
|
11237
11305
|
fillMissing,
|
|
11238
11306
|
findConfigFile,
|
|
11239
11307
|
formToken,
|
|
11308
|
+
forwardedClientIp,
|
|
11240
11309
|
generateSlugs,
|
|
11241
11310
|
generateTypes,
|
|
11242
11311
|
hasRows,
|
|
@@ -11257,6 +11326,7 @@ export {
|
|
|
11257
11326
|
mimeAllowed,
|
|
11258
11327
|
mimeAllowedBy,
|
|
11259
11328
|
parseId,
|
|
11329
|
+
platformClientIp,
|
|
11260
11330
|
populate,
|
|
11261
11331
|
rateKeys,
|
|
11262
11332
|
readCookie,
|