sproutkit-code 1.4.0 → 1.5.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.js +517 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -184,6 +184,10 @@ var require_db = __commonJS({
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
for (const col of [
|
|
187
|
+
// Stable per-site key encrypting admin-auth.json in Blob. MUST survive
|
|
188
|
+
// redeploys: SESSION_SECRET is regenerated every deploy, so deriving from
|
|
189
|
+
// it would make the client's self-chosen password undecryptable each time.
|
|
190
|
+
"auth_enc_key TEXT",
|
|
187
191
|
"booking_admin_url TEXT",
|
|
188
192
|
"booking_admin_password TEXT",
|
|
189
193
|
"handed_over_at TEXT",
|
|
@@ -33004,10 +33008,11 @@ var require_cms_runtime = __commonJS({
|
|
|
33004
33008
|
};
|
|
33005
33009
|
})();
|
|
33006
33010
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
33007
|
-
exports2.ADMIN_APP_JS = exports2.ADMIN_INDEX_HTML = exports2.API_ADMIN_UPLOAD_JS = exports2.API_ADMIN_WHOAMI_JS = exports2.API_ADMIN_LOGIN_JS = exports2.API_CONTENT_JS = void 0;
|
|
33011
|
+
exports2.ADMIN_APP_JS = exports2.ADMIN_INDEX_HTML = exports2.API_ADMIN_UPLOAD_JS = exports2.API_ADMIN_PASSWORD_JS = exports2.API_ADMIN_WHOAMI_JS = exports2.API_ADMIN_LOGIN_JS = exports2.API_CONTENT_JS = void 0;
|
|
33008
33012
|
exports2.generateFriendlyPassword = generateFriendlyPassword;
|
|
33009
33013
|
exports2.hashPassword = hashPassword;
|
|
33010
33014
|
exports2.generateSessionSecret = generateSessionSecret;
|
|
33015
|
+
var node_crypto_1 = require("node:crypto");
|
|
33011
33016
|
exports2.API_CONTENT_JS = `import { list, put } from "@vercel/blob";
|
|
33012
33017
|
|
|
33013
33018
|
// Per-site Blob namespace so many CMS sites share one store (one token per
|
|
@@ -33167,10 +33172,131 @@ export default async function handler(req, res) {
|
|
|
33167
33172
|
}
|
|
33168
33173
|
`;
|
|
33169
33174
|
exports2.API_ADMIN_LOGIN_JS = `import bcrypt from "bcryptjs";
|
|
33170
|
-
import { createHmac } from "node:crypto";
|
|
33175
|
+
import { createHmac, createHash, createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
33176
|
+
import { list, put } from "@vercel/blob";
|
|
33171
33177
|
|
|
33172
33178
|
export const config = { runtime: "nodejs" };
|
|
33173
33179
|
|
|
33180
|
+
// Per-site Blob namespace, matching the other runtime files.
|
|
33181
|
+
const PFX = process.env.SITE_ID ? "sites/" + process.env.SITE_ID + "/" : "";
|
|
33182
|
+
// The client's self-chosen password, if they've changed it. ADMIN_PASSWORD_HASH
|
|
33183
|
+
// is a Vercel env var and the site can't rewrite its own env, so an override
|
|
33184
|
+
// hash lives in Blob instead and takes precedence when present.
|
|
33185
|
+
const AUTH_KEY = PFX + "admin-auth.json";
|
|
33186
|
+
// Failed-attempt counters. Blob is the only shared state a serverless function
|
|
33187
|
+
// has \u2014 there's no memory between invocations. Best-effort by nature: two
|
|
33188
|
+
// concurrent attempts can read the same counter before either writes. That
|
|
33189
|
+
// still turns an unlimited online guessing attack into a slow one, which is the
|
|
33190
|
+
// point; bcrypt's own cost does the rest.
|
|
33191
|
+
const THROTTLE_KEY = PFX + "admin-throttle.json";
|
|
33192
|
+
|
|
33193
|
+
const MAX_FAILS = 8; // per IP, per window
|
|
33194
|
+
const WINDOW_MS = 15 * 60 * 1000;
|
|
33195
|
+
const LOCKOUT_MS = 15 * 60 * 1000;
|
|
33196
|
+
|
|
33197
|
+
|
|
33198
|
+
// admin-auth.json holds a bcrypt hash and lives in a PUBLIC Blob store at a
|
|
33199
|
+
// non-random path, so treat its URL as readable by anyone who learns it (the
|
|
33200
|
+
// store hostname leaks via photo URLs; only the per-site UUID stands between an
|
|
33201
|
+
// outsider and this file). A leaked bcrypt hash is crackable offline where our
|
|
33202
|
+
// login rate limit gives zero protection \u2014 so the contents are encrypted with
|
|
33203
|
+
// AES-256-GCM under a key derived from AUTH_ENC_KEY, which only ever exists as
|
|
33204
|
+
// a server-side env var. A public read then yields opaque bytes.
|
|
33205
|
+
function authKey() {
|
|
33206
|
+
const material = process.env.AUTH_ENC_KEY || "";
|
|
33207
|
+
if (!material) return null;
|
|
33208
|
+
return createHash("sha256").update("sproutkit-admin-auth:" + material).digest();
|
|
33209
|
+
}
|
|
33210
|
+
|
|
33211
|
+
function sealAuth(obj) {
|
|
33212
|
+
const key = authKey();
|
|
33213
|
+
const plain = JSON.stringify(obj);
|
|
33214
|
+
if (!key) return JSON.stringify({ v: 0, plain: obj });
|
|
33215
|
+
const iv = randomBytes(12);
|
|
33216
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
33217
|
+
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
33218
|
+
return JSON.stringify({
|
|
33219
|
+
v: 1,
|
|
33220
|
+
iv: iv.toString("base64"),
|
|
33221
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
33222
|
+
ct: ct.toString("base64"),
|
|
33223
|
+
});
|
|
33224
|
+
}
|
|
33225
|
+
|
|
33226
|
+
function openAuth(raw) {
|
|
33227
|
+
if (!raw || typeof raw !== "object") return null;
|
|
33228
|
+
// v0 = written before encryption shipped (or with no key available).
|
|
33229
|
+
if (raw.v === 0 && raw.plain) return raw.plain;
|
|
33230
|
+
if (raw.v !== 1) return null;
|
|
33231
|
+
const key = authKey();
|
|
33232
|
+
if (!key) return null;
|
|
33233
|
+
try {
|
|
33234
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(raw.iv, "base64"));
|
|
33235
|
+
decipher.setAuthTag(Buffer.from(raw.tag, "base64"));
|
|
33236
|
+
const out = Buffer.concat([
|
|
33237
|
+
decipher.update(Buffer.from(raw.ct, "base64")),
|
|
33238
|
+
decipher.final(),
|
|
33239
|
+
]).toString("utf8");
|
|
33240
|
+
return JSON.parse(out);
|
|
33241
|
+
} catch {
|
|
33242
|
+
// Wrong key or tampered payload \u2014 fall back to the env-var password rather
|
|
33243
|
+
// than locking the owner out.
|
|
33244
|
+
return null;
|
|
33245
|
+
}
|
|
33246
|
+
}
|
|
33247
|
+
|
|
33248
|
+
async function readJson(key, fallback) {
|
|
33249
|
+
try {
|
|
33250
|
+
const { blobs } = await list({ prefix: key, limit: 1 });
|
|
33251
|
+
const target = blobs.find((b) => b.pathname === key);
|
|
33252
|
+
if (!target) return fallback;
|
|
33253
|
+
// cache: no-store \u2014 a stale throttle count would let attempts through.
|
|
33254
|
+
const res = await fetch(target.url, { cache: "no-store" });
|
|
33255
|
+
if (!res.ok) return fallback;
|
|
33256
|
+
return await res.json();
|
|
33257
|
+
} catch {
|
|
33258
|
+
return fallback;
|
|
33259
|
+
}
|
|
33260
|
+
}
|
|
33261
|
+
|
|
33262
|
+
async function writeJson(key, value) {
|
|
33263
|
+
try {
|
|
33264
|
+
await put(key, JSON.stringify(value), {
|
|
33265
|
+
access: "public",
|
|
33266
|
+
addRandomSuffix: false,
|
|
33267
|
+
allowOverwrite: true,
|
|
33268
|
+
contentType: "application/json",
|
|
33269
|
+
});
|
|
33270
|
+
} catch {
|
|
33271
|
+
// Non-fatal: failing to persist a counter must never lock out a real owner.
|
|
33272
|
+
}
|
|
33273
|
+
}
|
|
33274
|
+
|
|
33275
|
+
// Prefer headers Vercel sets itself. The leftmost x-forwarded-for entry is
|
|
33276
|
+
// client-controlled, so it's the last resort \u2014 and note a spoofer can only
|
|
33277
|
+
// throttle the identity they invent, not a real owner.
|
|
33278
|
+
function clientKey(req) {
|
|
33279
|
+
const h = req.headers;
|
|
33280
|
+
const raw =
|
|
33281
|
+
h["x-vercel-forwarded-for"] ||
|
|
33282
|
+
h["x-real-ip"] ||
|
|
33283
|
+
String(h["x-forwarded-for"] || "").split(",")[0].trim() ||
|
|
33284
|
+
"unknown";
|
|
33285
|
+
// Hashed because this file is world-readable in public Blob; we never want to
|
|
33286
|
+
// publish a list of visitor IPs.
|
|
33287
|
+
return createHash("sha256").update(String(raw)).digest("hex").slice(0, 16);
|
|
33288
|
+
}
|
|
33289
|
+
|
|
33290
|
+
function prune(state, now) {
|
|
33291
|
+
const out = {};
|
|
33292
|
+
for (const [k, v] of Object.entries(state || {})) {
|
|
33293
|
+
if (!v || typeof v !== "object") continue;
|
|
33294
|
+
const fresh = (v.until && v.until > now) || (v.first && now - v.first < WINDOW_MS);
|
|
33295
|
+
if (fresh) out[k] = v;
|
|
33296
|
+
}
|
|
33297
|
+
return out;
|
|
33298
|
+
}
|
|
33299
|
+
|
|
33174
33300
|
function b64url(buf) {
|
|
33175
33301
|
return Buffer.from(buf).toString("base64").replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=+$/, "");
|
|
33176
33302
|
}
|
|
@@ -33197,12 +33323,30 @@ export default async function handler(req, res) {
|
|
|
33197
33323
|
res.status(405).json({ error: "method_not_allowed" });
|
|
33198
33324
|
return;
|
|
33199
33325
|
}
|
|
33200
|
-
const hash = process.env.ADMIN_PASSWORD_HASH;
|
|
33201
33326
|
const secret = process.env.SESSION_SECRET;
|
|
33202
|
-
|
|
33327
|
+
const envHash = process.env.ADMIN_PASSWORD_HASH;
|
|
33328
|
+
if (!envHash || !secret) {
|
|
33203
33329
|
res.status(500).json({ error: "not_configured" });
|
|
33204
33330
|
return;
|
|
33205
33331
|
}
|
|
33332
|
+
|
|
33333
|
+
const now = Date.now();
|
|
33334
|
+
const who = clientKey(req);
|
|
33335
|
+
let throttle = prune(await readJson(THROTTLE_KEY, {}), now);
|
|
33336
|
+
const entry = throttle[who];
|
|
33337
|
+
if (entry && entry.until && entry.until > now) {
|
|
33338
|
+
const retryAfter = Math.ceil((entry.until - now) / 1000);
|
|
33339
|
+
res.setHeader("Retry-After", String(retryAfter));
|
|
33340
|
+
res.status(429).json({ error: "too_many_attempts", retryAfter });
|
|
33341
|
+
return;
|
|
33342
|
+
}
|
|
33343
|
+
|
|
33344
|
+
// The client's own password wins over the one we generated at build time.
|
|
33345
|
+
const override = openAuth(await readJson(AUTH_KEY, null));
|
|
33346
|
+
const hash =
|
|
33347
|
+
override && typeof override.passwordHash === "string" && override.passwordHash
|
|
33348
|
+
? override.passwordHash
|
|
33349
|
+
: envHash;
|
|
33206
33350
|
let body = req.body;
|
|
33207
33351
|
if (typeof body === "string") {
|
|
33208
33352
|
try { body = JSON.parse(body); } catch { body = null; }
|
|
@@ -33221,10 +33365,25 @@ export default async function handler(req, res) {
|
|
|
33221
33365
|
}
|
|
33222
33366
|
const ok = await bcrypt.compare(password, hash);
|
|
33223
33367
|
if (!ok) {
|
|
33368
|
+
const prev = throttle[who] && throttle[who].first && now - throttle[who].first < WINDOW_MS
|
|
33369
|
+
? throttle[who]
|
|
33370
|
+
: { fails: 0, first: now };
|
|
33371
|
+
const fails = (prev.fails || 0) + 1;
|
|
33372
|
+
const next = { fails, first: prev.first || now };
|
|
33373
|
+
if (fails >= MAX_FAILS) next.until = now + LOCKOUT_MS;
|
|
33374
|
+
throttle[who] = next;
|
|
33375
|
+
await writeJson(THROTTLE_KEY, throttle);
|
|
33224
33376
|
await new Promise((r) => setTimeout(r, 300));
|
|
33225
|
-
|
|
33377
|
+
const remaining = Math.max(0, MAX_FAILS - fails);
|
|
33378
|
+
res.status(401).json({ error: "invalid_password", remaining });
|
|
33226
33379
|
return;
|
|
33227
33380
|
}
|
|
33381
|
+
|
|
33382
|
+
// Clean slate on success so a forgetful owner isn't punished later.
|
|
33383
|
+
if (throttle[who]) {
|
|
33384
|
+
delete throttle[who];
|
|
33385
|
+
await writeJson(THROTTLE_KEY, throttle);
|
|
33386
|
+
}
|
|
33228
33387
|
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 8; // 8h
|
|
33229
33388
|
const token = sign({ role: "admin", exp }, secret);
|
|
33230
33389
|
res.setHeader(
|
|
@@ -33270,6 +33429,191 @@ export default function handler(req, res) {
|
|
|
33270
33429
|
}
|
|
33271
33430
|
res.status(200).json({ ok: true });
|
|
33272
33431
|
}
|
|
33432
|
+
`;
|
|
33433
|
+
exports2.API_ADMIN_PASSWORD_JS = `import bcrypt from "bcryptjs";
|
|
33434
|
+
import {
|
|
33435
|
+
createHmac,
|
|
33436
|
+
timingSafeEqual,
|
|
33437
|
+
createHash,
|
|
33438
|
+
createCipheriv,
|
|
33439
|
+
createDecipheriv,
|
|
33440
|
+
randomBytes,
|
|
33441
|
+
} from "node:crypto";
|
|
33442
|
+
import { list, put } from "@vercel/blob";
|
|
33443
|
+
|
|
33444
|
+
export const config = { runtime: "nodejs" };
|
|
33445
|
+
|
|
33446
|
+
const PFX = process.env.SITE_ID ? "sites/" + process.env.SITE_ID + "/" : "";
|
|
33447
|
+
const AUTH_KEY = PFX + "admin-auth.json";
|
|
33448
|
+
|
|
33449
|
+
const MIN_LENGTH = 10;
|
|
33450
|
+
|
|
33451
|
+
|
|
33452
|
+
// admin-auth.json holds a bcrypt hash and lives in a PUBLIC Blob store at a
|
|
33453
|
+
// non-random path, so treat its URL as readable by anyone who learns it (the
|
|
33454
|
+
// store hostname leaks via photo URLs; only the per-site UUID stands between an
|
|
33455
|
+
// outsider and this file). A leaked bcrypt hash is crackable offline where our
|
|
33456
|
+
// login rate limit gives zero protection \u2014 so the contents are encrypted with
|
|
33457
|
+
// AES-256-GCM under a key derived from AUTH_ENC_KEY, which only ever exists as
|
|
33458
|
+
// a server-side env var. A public read then yields opaque bytes.
|
|
33459
|
+
function authKey() {
|
|
33460
|
+
const material = process.env.AUTH_ENC_KEY || "";
|
|
33461
|
+
if (!material) return null;
|
|
33462
|
+
return createHash("sha256").update("sproutkit-admin-auth:" + material).digest();
|
|
33463
|
+
}
|
|
33464
|
+
|
|
33465
|
+
function sealAuth(obj) {
|
|
33466
|
+
const key = authKey();
|
|
33467
|
+
const plain = JSON.stringify(obj);
|
|
33468
|
+
if (!key) return JSON.stringify({ v: 0, plain: obj });
|
|
33469
|
+
const iv = randomBytes(12);
|
|
33470
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
33471
|
+
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
33472
|
+
return JSON.stringify({
|
|
33473
|
+
v: 1,
|
|
33474
|
+
iv: iv.toString("base64"),
|
|
33475
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
33476
|
+
ct: ct.toString("base64"),
|
|
33477
|
+
});
|
|
33478
|
+
}
|
|
33479
|
+
|
|
33480
|
+
function openAuth(raw) {
|
|
33481
|
+
if (!raw || typeof raw !== "object") return null;
|
|
33482
|
+
// v0 = written before encryption shipped (or with no key available).
|
|
33483
|
+
if (raw.v === 0 && raw.plain) return raw.plain;
|
|
33484
|
+
if (raw.v !== 1) return null;
|
|
33485
|
+
const key = authKey();
|
|
33486
|
+
if (!key) return null;
|
|
33487
|
+
try {
|
|
33488
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(raw.iv, "base64"));
|
|
33489
|
+
decipher.setAuthTag(Buffer.from(raw.tag, "base64"));
|
|
33490
|
+
const out = Buffer.concat([
|
|
33491
|
+
decipher.update(Buffer.from(raw.ct, "base64")),
|
|
33492
|
+
decipher.final(),
|
|
33493
|
+
]).toString("utf8");
|
|
33494
|
+
return JSON.parse(out);
|
|
33495
|
+
} catch {
|
|
33496
|
+
// Wrong key or tampered payload \u2014 fall back to the env-var password rather
|
|
33497
|
+
// than locking the owner out.
|
|
33498
|
+
return null;
|
|
33499
|
+
}
|
|
33500
|
+
}
|
|
33501
|
+
|
|
33502
|
+
function verifySession(req) {
|
|
33503
|
+
const cookie = req.headers.cookie || "";
|
|
33504
|
+
const match = cookie.match(/sk_admin=([^;]+)/);
|
|
33505
|
+
if (!match) return false;
|
|
33506
|
+
const secret = process.env.SESSION_SECRET;
|
|
33507
|
+
if (!secret) return false;
|
|
33508
|
+
const [payload, sig] = match[1].split(".");
|
|
33509
|
+
if (!payload || !sig) return false;
|
|
33510
|
+
try {
|
|
33511
|
+
const expected = createHmac("sha256", secret).update(payload).digest();
|
|
33512
|
+
const provided = Buffer.from(sig.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
33513
|
+
if (expected.length !== provided.length) return false;
|
|
33514
|
+
if (!timingSafeEqual(expected, provided)) return false;
|
|
33515
|
+
const data = JSON.parse(
|
|
33516
|
+
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")
|
|
33517
|
+
);
|
|
33518
|
+
return typeof data.exp === "number" && data.exp > Date.now() / 1000;
|
|
33519
|
+
} catch {
|
|
33520
|
+
return false;
|
|
33521
|
+
}
|
|
33522
|
+
}
|
|
33523
|
+
|
|
33524
|
+
function readBody(req) {
|
|
33525
|
+
return new Promise((resolve, reject) => {
|
|
33526
|
+
const chunks = [];
|
|
33527
|
+
req.on("data", (c) => chunks.push(c));
|
|
33528
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
33529
|
+
req.on("error", reject);
|
|
33530
|
+
});
|
|
33531
|
+
}
|
|
33532
|
+
|
|
33533
|
+
async function currentHash() {
|
|
33534
|
+
try {
|
|
33535
|
+
const { blobs } = await list({ prefix: AUTH_KEY, limit: 1 });
|
|
33536
|
+
const target = blobs.find((b) => b.pathname === AUTH_KEY);
|
|
33537
|
+
if (target) {
|
|
33538
|
+
const res = await fetch(target.url, { cache: "no-store" });
|
|
33539
|
+
if (res.ok) {
|
|
33540
|
+
const j = openAuth(await res.json());
|
|
33541
|
+
if (j && typeof j.passwordHash === "string" && j.passwordHash) return j.passwordHash;
|
|
33542
|
+
}
|
|
33543
|
+
}
|
|
33544
|
+
} catch {
|
|
33545
|
+
// fall through to the env var
|
|
33546
|
+
}
|
|
33547
|
+
return process.env.ADMIN_PASSWORD_HASH || "";
|
|
33548
|
+
}
|
|
33549
|
+
|
|
33550
|
+
export default async function handler(req, res) {
|
|
33551
|
+
if (req.method !== "POST") {
|
|
33552
|
+
res.status(405).json({ error: "method_not_allowed" });
|
|
33553
|
+
return;
|
|
33554
|
+
}
|
|
33555
|
+
// Must already be logged in AND know the current password \u2014 a stolen session
|
|
33556
|
+
// cookie alone must not be enough to lock the real owner out.
|
|
33557
|
+
if (!verifySession(req)) {
|
|
33558
|
+
res.status(401).json({ error: "unauthorized" });
|
|
33559
|
+
return;
|
|
33560
|
+
}
|
|
33561
|
+
let body = req.body;
|
|
33562
|
+
if (typeof body === "string") {
|
|
33563
|
+
try { body = JSON.parse(body); } catch { body = null; }
|
|
33564
|
+
} else if (!body) {
|
|
33565
|
+
try {
|
|
33566
|
+
const raw = await readBody(req);
|
|
33567
|
+
body = raw ? JSON.parse(raw) : {};
|
|
33568
|
+
} catch {
|
|
33569
|
+
body = {};
|
|
33570
|
+
}
|
|
33571
|
+
}
|
|
33572
|
+
const currentPassword = String((body && body.currentPassword) || "");
|
|
33573
|
+
const newPassword = String((body && body.newPassword) || "");
|
|
33574
|
+
|
|
33575
|
+
if (newPassword.length < MIN_LENGTH) {
|
|
33576
|
+
res.status(400).json({ error: "too_short", minLength: MIN_LENGTH });
|
|
33577
|
+
return;
|
|
33578
|
+
}
|
|
33579
|
+
if (newPassword.length > 200) {
|
|
33580
|
+
res.status(400).json({ error: "too_long" });
|
|
33581
|
+
return;
|
|
33582
|
+
}
|
|
33583
|
+
if (newPassword === currentPassword) {
|
|
33584
|
+
res.status(400).json({ error: "unchanged" });
|
|
33585
|
+
return;
|
|
33586
|
+
}
|
|
33587
|
+
|
|
33588
|
+
const existing = await currentHash();
|
|
33589
|
+
if (!existing) {
|
|
33590
|
+
res.status(500).json({ error: "not_configured" });
|
|
33591
|
+
return;
|
|
33592
|
+
}
|
|
33593
|
+
const ok = await bcrypt.compare(currentPassword, existing);
|
|
33594
|
+
if (!ok) {
|
|
33595
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
33596
|
+
res.status(401).json({ error: "wrong_current_password" });
|
|
33597
|
+
return;
|
|
33598
|
+
}
|
|
33599
|
+
|
|
33600
|
+
const salt = await bcrypt.genSalt(10);
|
|
33601
|
+
const passwordHash = await bcrypt.hash(newPassword, salt);
|
|
33602
|
+
try {
|
|
33603
|
+
// clientChanged is read (unencrypted) by nothing sensitive \u2014 it only tells
|
|
33604
|
+
// the agency "your stored copy is no longer the live password".
|
|
33605
|
+
await put(AUTH_KEY, sealAuth({ passwordHash, updatedAt: new Date().toISOString(), clientChanged: true }), {
|
|
33606
|
+
access: "public",
|
|
33607
|
+
addRandomSuffix: false,
|
|
33608
|
+
allowOverwrite: true,
|
|
33609
|
+
contentType: "application/json",
|
|
33610
|
+
});
|
|
33611
|
+
} catch (e) {
|
|
33612
|
+
res.status(500).json({ error: "save_failed", detail: (e && e.message) || "unknown" });
|
|
33613
|
+
return;
|
|
33614
|
+
}
|
|
33615
|
+
res.status(200).json({ ok: true });
|
|
33616
|
+
}
|
|
33273
33617
|
`;
|
|
33274
33618
|
exports2.API_ADMIN_UPLOAD_JS = `import { put } from "@vercel/blob";
|
|
33275
33619
|
|
|
@@ -33990,6 +34334,98 @@ export default async function handler(req, res) {
|
|
|
33990
34334
|
);
|
|
33991
34335
|
}
|
|
33992
34336
|
|
|
34337
|
+
function TabBar(props) {
|
|
34338
|
+
return h("div", { style: { display: "flex", gap: 4, marginBottom: 24, borderBottom: "1px solid var(--border)", paddingBottom: 12 } },
|
|
34339
|
+
props.tabs.map(function (t) {
|
|
34340
|
+
var active = props.section[0] === t.id;
|
|
34341
|
+
return h("button", {
|
|
34342
|
+
key: t.id,
|
|
34343
|
+
onClick: function () { props.section[1](t.id); },
|
|
34344
|
+
style: {
|
|
34345
|
+
background: active ? "var(--accent)" : "transparent",
|
|
34346
|
+
color: active ? "#000" : "var(--text)",
|
|
34347
|
+
border: "1px solid " + (active ? "var(--accent)" : "var(--border)"),
|
|
34348
|
+
borderRadius: 999, padding: "8px 16px",
|
|
34349
|
+
font: "inherit", fontWeight: 500, fontSize: 13, cursor: "pointer"
|
|
34350
|
+
}
|
|
34351
|
+
}, t.label);
|
|
34352
|
+
})
|
|
34353
|
+
);
|
|
34354
|
+
}
|
|
34355
|
+
|
|
34356
|
+
// Lets the owner replace the password we generated for them. Without this the
|
|
34357
|
+
// agency permanently knows the client's login, which isn't acceptable for a
|
|
34358
|
+
// panel holding customer names and phone numbers.
|
|
34359
|
+
function PasswordPanel() {
|
|
34360
|
+
var cur = useState(""); var next = useState(""); var again = useState("");
|
|
34361
|
+
var busy = useState(false); var msg = useState(null); var err = useState("");
|
|
34362
|
+
|
|
34363
|
+
async function submit(e) {
|
|
34364
|
+
e.preventDefault();
|
|
34365
|
+
err[1](""); msg[1](null);
|
|
34366
|
+
if (next[0].length < 10) { err[1]("New password must be at least 10 characters."); return; }
|
|
34367
|
+
if (next[0] !== again[0]) { err[1]("The two new passwords don't match."); return; }
|
|
34368
|
+
busy[1](true);
|
|
34369
|
+
try {
|
|
34370
|
+
var res = await fetch("/api/admin/password", {
|
|
34371
|
+
method: "POST",
|
|
34372
|
+
credentials: "same-origin",
|
|
34373
|
+
headers: { "Content-Type": "application/json" },
|
|
34374
|
+
body: JSON.stringify({ currentPassword: cur[0], newPassword: next[0] })
|
|
34375
|
+
});
|
|
34376
|
+
var data = null;
|
|
34377
|
+
try { data = await res.json(); } catch (_) { data = null; }
|
|
34378
|
+
if (res.ok) {
|
|
34379
|
+
msg[1]("Password changed. Use the new one next time you sign in.");
|
|
34380
|
+
cur[1](""); next[1](""); again[1]("");
|
|
34381
|
+
} else if (res.status === 401) {
|
|
34382
|
+
err[1]((data && data.error) === "wrong_current_password"
|
|
34383
|
+
? "That's not your current password."
|
|
34384
|
+
: "Your session expired \u2014 reload and sign in again.");
|
|
34385
|
+
} else if (data && data.error === "too_short") {
|
|
34386
|
+
err[1]("New password must be at least " + (data.minLength || 10) + " characters.");
|
|
34387
|
+
} else if (data && data.error === "unchanged") {
|
|
34388
|
+
err[1]("That's already your password.");
|
|
34389
|
+
} else {
|
|
34390
|
+
err[1]("Couldn't change it (" + res.status + "). Try again.");
|
|
34391
|
+
}
|
|
34392
|
+
} catch (_) {
|
|
34393
|
+
err[1]("Network error \u2014 check your connection and try again.");
|
|
34394
|
+
}
|
|
34395
|
+
busy[1](false);
|
|
34396
|
+
}
|
|
34397
|
+
|
|
34398
|
+
var field = function (label, st, auto) {
|
|
34399
|
+
return h("label", { style: { display: "block", marginBottom: 12 } },
|
|
34400
|
+
h("div", { style: { fontSize: 12, color: "var(--muted)", marginBottom: 4 } }, label),
|
|
34401
|
+
h("input", {
|
|
34402
|
+
type: "password", value: st[0], autoComplete: auto,
|
|
34403
|
+
onChange: function (e) { st[1](e.target.value); },
|
|
34404
|
+
style: { width: "100%", maxWidth: 360 }
|
|
34405
|
+
})
|
|
34406
|
+
);
|
|
34407
|
+
};
|
|
34408
|
+
|
|
34409
|
+
return h("div", null,
|
|
34410
|
+
h("div", { className: "bar" },
|
|
34411
|
+
h("div", null,
|
|
34412
|
+
h("div", { style: { fontSize: 15, fontWeight: 600 } }, "Password"),
|
|
34413
|
+
h("div", { style: { fontSize: 12, color: "var(--muted)" } },
|
|
34414
|
+
"Change the password you were given")
|
|
34415
|
+
)
|
|
34416
|
+
),
|
|
34417
|
+
h("form", { onSubmit: submit, style: { maxWidth: 400 } },
|
|
34418
|
+
field("Current password", cur, "current-password"),
|
|
34419
|
+
field("New password", next, "new-password"),
|
|
34420
|
+
field("Repeat new password", again, "new-password"),
|
|
34421
|
+
err[0] ? h("div", { style: { color: "#b91c1c", fontSize: 13, margin: "8px 0" } }, err[0]) : null,
|
|
34422
|
+
msg[0] ? h("div", { style: { color: "var(--accent)", fontSize: 13, margin: "8px 0" } }, msg[0]) : null,
|
|
34423
|
+
h("button", { type: "submit", className: "primary", disabled: busy[0] },
|
|
34424
|
+
busy[0] ? "Saving\u2026" : "Change password")
|
|
34425
|
+
)
|
|
34426
|
+
);
|
|
34427
|
+
}
|
|
34428
|
+
|
|
33993
34429
|
function App() {
|
|
33994
34430
|
var authed = useState(false);
|
|
33995
34431
|
var section = useState("content");
|
|
@@ -34003,33 +34439,29 @@ export default async function handler(req, res) {
|
|
|
34003
34439
|
// Bookings-only site: no Site content tab to switch to, and the public
|
|
34004
34440
|
// booking page is the site root rather than /book.
|
|
34005
34441
|
if (!ctl.available || !ctl.content) {
|
|
34006
|
-
return h("div", { className: "app" },
|
|
34442
|
+
return h("div", { className: "app" },
|
|
34443
|
+
h(TabBar, { tabs: [{ id: "bookings", label: "Bookings" }, { id: "password", label: "Password" }], section: section }),
|
|
34444
|
+
section[0] === "password"
|
|
34445
|
+
? h(PasswordPanel, null)
|
|
34446
|
+
: h(BookingsPanel, { bookingPath: "/" })
|
|
34447
|
+
);
|
|
34007
34448
|
}
|
|
34008
34449
|
|
|
34009
|
-
// Top nav lets the owner flip between
|
|
34450
|
+
// Top nav lets the owner flip between site content, bookings and password.
|
|
34010
34451
|
return h("div", { className: "app" },
|
|
34011
|
-
h(
|
|
34012
|
-
[
|
|
34452
|
+
h(TabBar, {
|
|
34453
|
+
tabs: [
|
|
34013
34454
|
{ id: "content", label: "Site content" },
|
|
34014
|
-
{ id: "bookings", label: "Bookings" }
|
|
34015
|
-
|
|
34016
|
-
|
|
34017
|
-
|
|
34018
|
-
|
|
34019
|
-
onClick: function () { section[1](t.id); },
|
|
34020
|
-
style: {
|
|
34021
|
-
background: active ? "var(--accent)" : "transparent",
|
|
34022
|
-
color: active ? "#000" : "var(--text)",
|
|
34023
|
-
border: "1px solid " + (active ? "var(--accent)" : "var(--border)"),
|
|
34024
|
-
borderRadius: 999, padding: "8px 16px",
|
|
34025
|
-
font: "inherit", fontWeight: 500, fontSize: 13, cursor: "pointer"
|
|
34026
|
-
}
|
|
34027
|
-
}, t.label);
|
|
34028
|
-
})
|
|
34029
|
-
),
|
|
34455
|
+
{ id: "bookings", label: "Bookings" },
|
|
34456
|
+
{ id: "password", label: "Password" }
|
|
34457
|
+
],
|
|
34458
|
+
section: section
|
|
34459
|
+
}),
|
|
34030
34460
|
section[0] === "content"
|
|
34031
34461
|
? h(Editor, { content: ctl.content, setContent: ctl.setContent })
|
|
34032
|
-
:
|
|
34462
|
+
: section[0] === "password"
|
|
34463
|
+
? h(PasswordPanel, null)
|
|
34464
|
+
: h(BookingsPanel, null)
|
|
34033
34465
|
);
|
|
34034
34466
|
}
|
|
34035
34467
|
|
|
@@ -34057,7 +34489,19 @@ export default async function handler(req, res) {
|
|
|
34057
34489
|
"tidy",
|
|
34058
34490
|
"warm",
|
|
34059
34491
|
"wise",
|
|
34060
|
-
"zesty"
|
|
34492
|
+
"zesty",
|
|
34493
|
+
"bold",
|
|
34494
|
+
"crisp",
|
|
34495
|
+
"fair",
|
|
34496
|
+
"glad",
|
|
34497
|
+
"jolly",
|
|
34498
|
+
"keen",
|
|
34499
|
+
"merry",
|
|
34500
|
+
"noble",
|
|
34501
|
+
"plucky",
|
|
34502
|
+
"spry",
|
|
34503
|
+
"tough",
|
|
34504
|
+
"vivid"
|
|
34061
34505
|
];
|
|
34062
34506
|
var ANIMALS = [
|
|
34063
34507
|
"badger",
|
|
@@ -34079,13 +34523,29 @@ export default async function handler(req, res) {
|
|
|
34079
34523
|
"wolf",
|
|
34080
34524
|
"zebra",
|
|
34081
34525
|
"boar",
|
|
34082
|
-
"crane"
|
|
34526
|
+
"crane",
|
|
34527
|
+
"bison",
|
|
34528
|
+
"dingo",
|
|
34529
|
+
"eagle",
|
|
34530
|
+
"falcon",
|
|
34531
|
+
"gecko",
|
|
34532
|
+
"ibex",
|
|
34533
|
+
"jackal",
|
|
34534
|
+
"kestrel",
|
|
34535
|
+
"marten",
|
|
34536
|
+
"osprey",
|
|
34537
|
+
"panda",
|
|
34538
|
+
"quail"
|
|
34083
34539
|
];
|
|
34084
34540
|
function generateFriendlyPassword() {
|
|
34085
|
-
const
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34541
|
+
const pick = (arr) => arr[(0, node_crypto_1.randomInt)(arr.length)];
|
|
34542
|
+
return [
|
|
34543
|
+
pick(ADJECTIVES),
|
|
34544
|
+
pick(ANIMALS),
|
|
34545
|
+
pick(ADJECTIVES),
|
|
34546
|
+
pick(ANIMALS),
|
|
34547
|
+
String(100 + (0, node_crypto_1.randomInt)(900))
|
|
34548
|
+
].join("-");
|
|
34089
34549
|
}
|
|
34090
34550
|
async function hashPassword(plain) {
|
|
34091
34551
|
const bcrypt = await Promise.resolve().then(() => __importStar(require_umd()));
|
|
@@ -79825,6 +80285,15 @@ var require_builder = __commonJS({
|
|
|
79825
80285
|
db2.run("UPDATE leads SET site_uid = ? WHERE id = ?", [uid, leadId]);
|
|
79826
80286
|
return uid;
|
|
79827
80287
|
}
|
|
80288
|
+
function authEncKeyFor(leadId) {
|
|
80289
|
+
const db2 = (0, db_1.getDb)();
|
|
80290
|
+
const row = db2.get("SELECT auth_enc_key FROM leads WHERE id = ?", [leadId]);
|
|
80291
|
+
if (row?.auth_enc_key)
|
|
80292
|
+
return row.auth_enc_key;
|
|
80293
|
+
const key = (0, crypto_1.randomUUID)() + (0, crypto_1.randomUUID)();
|
|
80294
|
+
db2.run("UPDATE leads SET auth_enc_key = ? WHERE id = ?", [key, leadId]);
|
|
80295
|
+
return key;
|
|
80296
|
+
}
|
|
79828
80297
|
var STOCK_HOSTS = [
|
|
79829
80298
|
"unsplash.com",
|
|
79830
80299
|
"images.unsplash.com",
|
|
@@ -80572,6 +81041,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80572
81041
|
SITE_ID: cmsSiteUid,
|
|
80573
81042
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
80574
81043
|
SESSION_SECRET: sessionSecret,
|
|
81044
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
80575
81045
|
BUSINESS_NAME: lead.name
|
|
80576
81046
|
});
|
|
80577
81047
|
const vercelJson = JSON.stringify({
|
|
@@ -80585,6 +81055,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80585
81055
|
{ file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" },
|
|
80586
81056
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
80587
81057
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
81058
|
+
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
80588
81059
|
{ file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" },
|
|
80589
81060
|
{ file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" },
|
|
80590
81061
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" }
|
|
@@ -80682,7 +81153,7 @@ createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMo
|
|
|
80682
81153
|
const viteConfigCms = `import { defineConfig } from 'vite';
|
|
80683
81154
|
import react from '@vitejs/plugin-react';
|
|
80684
81155
|
export default defineConfig({ plugins: [react()] });`;
|
|
80685
|
-
filesToRedeploy.push({ file: "index.html", data: indexHtmlCms, encoding: "utf-8" }, { file: "src/main.jsx", data: mainJsxCms, encoding: "utf-8" }, { file: "vite.config.js", data: viteConfigCms, encoding: "utf-8" }, { file: "package.json", data: packageJsonCms, encoding: "utf-8" }, { file: "vercel.json", data: vercelJson, encoding: "utf-8" }, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81156
|
+
filesToRedeploy.push({ file: "index.html", data: indexHtmlCms, encoding: "utf-8" }, { file: "src/main.jsx", data: mainJsxCms, encoding: "utf-8" }, { file: "vite.config.js", data: viteConfigCms, encoding: "utf-8" }, { file: "package.json", data: packageJsonCms, encoding: "utf-8" }, { file: "vercel.json", data: vercelJson, encoding: "utf-8" }, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
80686
81157
|
if (!filesToRedeploy.some((f) => f.file === "src/App.jsx")) {
|
|
80687
81158
|
throw new Error("Couldn't fetch src/App.jsx from the existing deployment. Try Regenerate website first, then Redeploy CMS.");
|
|
80688
81159
|
}
|
|
@@ -80692,6 +81163,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80692
81163
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
80693
81164
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
80694
81165
|
SESSION_SECRET: sessionSecret,
|
|
81166
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
80695
81167
|
BUSINESS_NAME: lead.name
|
|
80696
81168
|
};
|
|
80697
81169
|
if (existingUid)
|
|
@@ -80790,7 +81262,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80790
81262
|
const adminEnv = {
|
|
80791
81263
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
80792
81264
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
80793
|
-
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)()
|
|
81265
|
+
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81266
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id)
|
|
80794
81267
|
};
|
|
80795
81268
|
if (bookingExistingUid)
|
|
80796
81269
|
adminEnv.SITE_ID = bookingExistingUid;
|
|
@@ -80837,6 +81310,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80837
81310
|
{ file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" },
|
|
80838
81311
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
80839
81312
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
81313
|
+
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
80840
81314
|
{ file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }
|
|
80841
81315
|
);
|
|
80842
81316
|
onProgress("Deploying\u2026");
|
|
@@ -80882,7 +81356,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80882
81356
|
SITE_ID: standaloneSiteUid,
|
|
80883
81357
|
// Admin panel auth.
|
|
80884
81358
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
80885
|
-
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)()
|
|
81359
|
+
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81360
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id)
|
|
80886
81361
|
});
|
|
80887
81362
|
const files = [
|
|
80888
81363
|
{ file: "index.html", data: booking_runtime_1.BOOK_INDEX_HTML, encoding: "utf-8" },
|
|
@@ -80895,6 +81370,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80895
81370
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
80896
81371
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
80897
81372
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
81373
|
+
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
80898
81374
|
// Serve /admin without a trailing-slash redirect dance.
|
|
80899
81375
|
{
|
|
80900
81376
|
file: "vercel.json",
|
|
@@ -81076,11 +81552,12 @@ export default defineConfig({ plugins: [react()] });`,
|
|
|
81076
81552
|
]
|
|
81077
81553
|
}),
|
|
81078
81554
|
encoding: "utf-8"
|
|
81079
|
-
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81555
|
+
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81080
81556
|
const env = {
|
|
81081
81557
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
81082
81558
|
ADMIN_PASSWORD_HASH: await (0, cms_runtime_1.hashPassword)(adminPassword),
|
|
81083
81559
|
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81560
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
81084
81561
|
BUSINESS_NAME: lead.name
|
|
81085
81562
|
};
|
|
81086
81563
|
if (siteUid)
|
|
@@ -95356,7 +95833,7 @@ var require_package2 = __commonJS({
|
|
|
95356
95833
|
"package.json"(exports2, module2) {
|
|
95357
95834
|
module2.exports = {
|
|
95358
95835
|
name: "sproutkit-code",
|
|
95359
|
-
version: "1.
|
|
95836
|
+
version: "1.5.0",
|
|
95360
95837
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
95361
95838
|
bin: {
|
|
95362
95839
|
sproutkit: "./dist/index.js"
|
|
@@ -96756,6 +97233,7 @@ async function cmdHandover(argv) {
|
|
|
96756
97233
|
if (r.admin) {
|
|
96757
97234
|
say(` Admin panel: ${r.admin.url}`);
|
|
96758
97235
|
say(` Password: ${c.bold(r.admin.password)}`);
|
|
97236
|
+
say(c.dim(" (unless the client has already changed it in the panel)"));
|
|
96759
97237
|
}
|
|
96760
97238
|
if (r.bookingUrl) say(` Booking page: ${r.bookingUrl}`);
|
|
96761
97239
|
if (r.bookingAdmin) {
|
|
@@ -96867,6 +97345,8 @@ function sayAdminHandover(adminUrl, adminPassword) {
|
|
|
96867
97345
|
say(` Admin panel: ${adminUrl}`);
|
|
96868
97346
|
say(` Password: ${adminPassword}`);
|
|
96869
97347
|
say(c.dim(" Send these to the client \u2014 they manage bookings and content there."));
|
|
97348
|
+
say(c.dim(" They can change this password under Password in the panel; after that"));
|
|
97349
|
+
say(c.dim(" your copy is out of date and only they know the live one."));
|
|
96870
97350
|
}
|
|
96871
97351
|
async function cmdBook(argv) {
|
|
96872
97352
|
const { flags, rest } = parseArgs(argv);
|