@easy-cms/core 0.36.0 → 0.37.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 CHANGED
@@ -194,11 +194,16 @@ declare class Auth {
194
194
  private get config();
195
195
  /** Checks credentials and starts a session. Throws `UnauthorizedError` or `TooManyRequestsError`. */
196
196
  login(args: LoginArgs): Promise<Session>;
197
- /** Creates the first admin. Only works while there are no users. */
197
+ /**
198
+ * Creates the first admin. Only works while there are no users, and with `auth.setupCode`
199
+ * (`EASY_CMS_SETUP_CODE`) only with that code.
200
+ */
198
201
  registerFirstUser(args: {
199
202
  email: string;
200
203
  password: string;
201
204
  name?: string;
205
+ setupCode?: string;
206
+ ip?: string | undefined;
202
207
  }): Promise<Session>;
203
208
  hasUsers(): Promise<boolean>;
204
209
  /** Ends the session for a signed token. Unknown tokens are ignored. */
@@ -2233,6 +2238,12 @@ interface AuthConfig {
2233
2238
  * everyone else signs in with `providers`. Default `true`.
2234
2239
  */
2235
2240
  readonly password?: boolean;
2241
+ /**
2242
+ * A code the first admin must enter to create their account, so nobody else can claim a
2243
+ * freshly deployed site. Default: the `EASY_CMS_SETUP_CODE` environment variable; without it,
2244
+ * no code is asked.
2245
+ */
2246
+ readonly setupCode?: string;
2236
2247
  /** Your own text for the password emails, e.g. in your brand's voice. Default: English or Thai. */
2237
2248
  readonly emails?: {
2238
2249
  readonly resetPassword?: PasswordEmailFn;
package/dist/index.js CHANGED
@@ -1515,9 +1515,21 @@ var Auth = class {
1515
1515
  await this.cms.audit.record({ action: "login", target: "auth", user: session.user });
1516
1516
  return session;
1517
1517
  }
1518
- /** Creates the first admin. Only works while there are no users. */
1518
+ /**
1519
+ * Creates the first admin. Only works while there are no users, and with `auth.setupCode`
1520
+ * (`EASY_CMS_SETUP_CODE`) only with that code.
1521
+ */
1519
1522
  async registerFirstUser(args) {
1520
1523
  if (await this.hasUsers()) throw new ForbiddenError("An admin already exists");
1524
+ const code = this.config.auth.setupCode;
1525
+ if (code) {
1526
+ const key2 = `setup|${args.ip ?? ""}`;
1527
+ await this.checkRateLimit(key2);
1528
+ if (!safeEqual(String(args.setupCode ?? ""), code)) {
1529
+ await this.recordFailure(key2);
1530
+ throw new ValidationError(USERS, [{ field: "setupCode", message: "is not the setup code" }]);
1531
+ }
1532
+ }
1521
1533
  const user = await this.cms.create(USERS, {
1522
1534
  email: args.email,
1523
1535
  password: args.password,
@@ -4049,6 +4061,8 @@ function validateAuth(config, add) {
4049
4061
  add("auth.rbac", "must be true or false");
4050
4062
  if (auth.password !== void 0 && typeof auth.password !== "boolean")
4051
4063
  add("auth.password", "must be true or false");
4064
+ if (auth.setupCode !== void 0 && typeof auth.setupCode !== "string")
4065
+ add("auth.setupCode", "must be a string");
4052
4066
  const providers = auth.providers;
4053
4067
  if (providers !== void 0) {
4054
4068
  if (!Array.isArray(providers)) {
@@ -4832,6 +4846,7 @@ async function resolveConfig(input) {
4832
4846
  providers: config.auth?.providers ?? [],
4833
4847
  allowSignUp: config.auth?.allowSignUp ?? { domains: [] },
4834
4848
  password: config.auth?.password !== false,
4849
+ setupCode: config.auth?.setupCode ?? process.env.EASY_CMS_SETUP_CODE ?? "",
4835
4850
  tokenExpiration: config.auth?.tokenExpiration ?? DEFAULT_TOKEN_EXPIRATION,
4836
4851
  maxLoginAttempts: config.auth?.maxLoginAttempts ?? 5,
4837
4852
  lockWindow: config.auth?.lockWindow ?? 15 * 60,
@@ -7611,7 +7626,7 @@ async function widgets(cms, user) {
7611
7626
  }
7612
7627
 
7613
7628
  // src/version.ts
7614
- var VERSION = true ? "0.36.0" : "0.0.0-dev";
7629
+ var VERSION = true ? "0.37.0" : "0.0.0-dev";
7615
7630
 
7616
7631
  // src/rest/admin-status.ts
7617
7632
  var DAY2 = 864e5;
@@ -8004,7 +8019,9 @@ async function route(cms, ctx, method, segments, options) {
8004
8019
  passwordReset: cms.auth.canSendPasswordLinks(ctx.url.origin),
8005
8020
  // Buttons to sign in with outside accounts; `password: false`: only admins use one.
8006
8021
  providers: cms.auth.sso.providers(),
8007
- password: cms.config.auth.password
8022
+ password: cms.config.auth.password,
8023
+ // The first admin needs the setup code (`EASY_CMS_SETUP_CODE`).
8024
+ setupCode: cms.config.auth.setupCode !== ""
8008
8025
  }
8009
8026
  };
8010
8027
  // Forgot password: the same answer whether or not the email has an account.
@@ -8045,7 +8062,9 @@ async function route(cms, ctx, method, segments, options) {
8045
8062
  const session = await cms.auth.registerFirstUser({
8046
8063
  email: String(body.email ?? ""),
8047
8064
  password: String(body.password ?? ""),
8048
- ...typeof body.name === "string" ? { name: body.name } : {}
8065
+ ...typeof body.name === "string" ? { name: body.name } : {},
8066
+ ...typeof body.setupCode === "string" ? { setupCode: body.setupCode } : {},
8067
+ ip: options.getClientIp?.(ctx.request)
8049
8068
  });
8050
8069
  setSessionCookies(cms, ctx, session);
8051
8070
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",