@plank-cms/plank 0.30.1 → 0.31.1

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.
@@ -12,7 +12,7 @@
12
12
  href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap"
13
13
  rel="stylesheet"
14
14
  />
15
- <script type="module" crossorigin src="/admin/assets/index-RfZ7f9RV.js"></script>
15
+ <script type="module" crossorigin src="/admin/assets/index-laQsuJ53.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/admin/assets/index-CHJ8C--M.css">
17
17
  </head>
18
18
  <body>
package/dist/index.js CHANGED
@@ -93,7 +93,7 @@ function getUpdateScriptCommand(name) {
93
93
  }
94
94
 
95
95
  // src/commands/init.ts
96
- var PACKAGE_VERSION = "0.30.1";
96
+ var PACKAGE_VERSION = "0.31.1";
97
97
  function generateSecret() {
98
98
  return randomBytes(32).toString("hex");
99
99
  }
@@ -203,7 +203,7 @@ import { dirname, join as join3, resolve as resolve2 } from "path";
203
203
  async function start() {
204
204
  config({ path: resolve2(process.cwd(), ".env") });
205
205
  process.env.PLANK_ADMIN_DIST = join3(dirname(fileURLToPath(import.meta.url)), "admin");
206
- const { start: startServer } = await import("./server-BDQYF2ZA.js");
206
+ const { start: startServer } = await import("./server-JNWXLTI3.js");
207
207
  await startServer();
208
208
  }
209
209
 
@@ -0,0 +1,14 @@
1
+ CREATE TABLE IF NOT EXISTS plank_password_reset_tokens (
2
+ id TEXT PRIMARY KEY,
3
+ user_id TEXT NOT NULL REFERENCES plank_users(id) ON DELETE CASCADE,
4
+ token_hash VARCHAR(255) NOT NULL UNIQUE,
5
+ expires_at TIMESTAMP NOT NULL,
6
+ used_at TIMESTAMP,
7
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
8
+ );
9
+
10
+ CREATE INDEX IF NOT EXISTS idx_plank_password_reset_tokens_user_id
11
+ ON plank_password_reset_tokens(user_id);
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_plank_password_reset_tokens_expires_at
14
+ ON plank_password_reset_tokens(expires_at);
@@ -653,8 +653,8 @@ import express from "express";
653
653
  import cors from "cors";
654
654
  import helmet from "helmet";
655
655
  import { readFile as readFile5 } from "fs/promises";
656
- import { join as join6, dirname as dirname3 } from "path";
657
- import { fileURLToPath as fileURLToPath4 } from "url";
656
+ import { join as join7, dirname as dirname4 } from "path";
657
+ import { fileURLToPath as fileURLToPath5 } from "url";
658
658
 
659
659
  // ../core/dist/routes/auth.js
660
660
  import { Router } from "express";
@@ -662,6 +662,7 @@ import { Router } from "express";
662
662
  // ../core/dist/controllers/auth.js
663
663
  import bcrypt from "bcryptjs";
664
664
  import jwt from "jsonwebtoken";
665
+ import { createHash, randomBytes as randomBytes6 } from "crypto";
665
666
 
666
667
  // ../../node_modules/.pnpm/@otplib+core@13.4.0/node_modules/@otplib/core/dist/index.js
667
668
  var i = class extends Error {
@@ -2143,7 +2144,8 @@ function decrypt(stored) {
2143
2144
 
2144
2145
  // ../core/dist/lib/settings.js
2145
2146
  var SENSITIVE_FIELDS = {
2146
- media: /* @__PURE__ */ new Set(["s3.secret_access_key", "r2.secret_access_key"])
2147
+ media: /* @__PURE__ */ new Set(["s3.secret_access_key", "r2.secret_access_key"]),
2148
+ mailing: /* @__PURE__ */ new Set(["smtp.password"])
2147
2149
  };
2148
2150
  function isSensitive(namespace, key) {
2149
2151
  return SENSITIVE_FIELDS[namespace]?.has(key) ?? false;
@@ -2474,6 +2476,113 @@ async function resolveUniquePublicAuthorSlug(input, excludeUserId) {
2474
2476
  }
2475
2477
  }
2476
2478
 
2479
+ // ../core/dist/lib/mailer.js
2480
+ function parseBoolean(value, fallback = false) {
2481
+ if (value == null)
2482
+ return fallback;
2483
+ return value.toLowerCase() === "true";
2484
+ }
2485
+ function parsePort(value) {
2486
+ const port = Number(value ?? 587);
2487
+ return Number.isFinite(port) ? port : 587;
2488
+ }
2489
+ function formatFrom(name, email) {
2490
+ if (!name.trim())
2491
+ return email;
2492
+ return `"${name.replaceAll('"', '\\"')}" <${email}>`;
2493
+ }
2494
+ async function getMailingSettings() {
2495
+ const settings = await getSettings("mailing");
2496
+ return {
2497
+ enabled: parseBoolean(settings.enabled),
2498
+ host: settings["smtp.host"] ?? "",
2499
+ port: parsePort(settings["smtp.port"]),
2500
+ secure: parseBoolean(settings["smtp.secure"]),
2501
+ user: settings["smtp.user"] ?? "",
2502
+ password: settings["smtp.password"] ?? "",
2503
+ fromEmail: settings["from.email"] ?? "",
2504
+ fromName: settings["from.name"] ?? "Plank CMS",
2505
+ replyTo: settings.reply_to ?? ""
2506
+ };
2507
+ }
2508
+ async function sendMail(input) {
2509
+ const settings = await getMailingSettings();
2510
+ if (!settings.enabled)
2511
+ throw new Error("Mailing is disabled.");
2512
+ if (!settings.host || !settings.user || !settings.password || !settings.fromEmail) {
2513
+ throw new Error("Mailing is not configured.");
2514
+ }
2515
+ const nodemailer = await import("nodemailer");
2516
+ const transporter = nodemailer.createTransport({
2517
+ host: settings.host,
2518
+ port: settings.port,
2519
+ secure: settings.secure,
2520
+ auth: {
2521
+ user: settings.user,
2522
+ pass: settings.password
2523
+ }
2524
+ });
2525
+ await transporter.sendMail({
2526
+ from: formatFrom(settings.fromName, settings.fromEmail),
2527
+ to: input.to,
2528
+ subject: input.subject,
2529
+ html: input.html,
2530
+ text: input.text,
2531
+ replyTo: settings.replyTo || void 0
2532
+ });
2533
+ }
2534
+
2535
+ // ../core/dist/lib/mailTemplates.js
2536
+ import { existsSync, readdirSync, readFileSync } from "fs";
2537
+ import { dirname as dirname2, join as join4 } from "path";
2538
+ import { fileURLToPath as fileURLToPath2 } from "url";
2539
+ import handlebars from "handlebars";
2540
+ var templates = /* @__PURE__ */ new Map();
2541
+ var partialsLoaded = false;
2542
+ function templatesRoot() {
2543
+ const moduleDir = dirname2(fileURLToPath2(import.meta.url));
2544
+ const candidates = [
2545
+ join4(process.cwd(), "templates"),
2546
+ join4(process.cwd(), "packages/core/templates"),
2547
+ join4(moduleDir, "../../templates")
2548
+ ];
2549
+ const root = candidates.find((candidate) => existsSync(candidate));
2550
+ if (!root)
2551
+ return candidates[0];
2552
+ return root;
2553
+ }
2554
+ function loadPartials() {
2555
+ if (partialsLoaded)
2556
+ return;
2557
+ partialsLoaded = true;
2558
+ const partialsDir = join4(templatesRoot(), "mail", "partials");
2559
+ if (!existsSync(partialsDir))
2560
+ return;
2561
+ for (const file of readdirSync(partialsDir)) {
2562
+ if (!file.endsWith(".hbs"))
2563
+ continue;
2564
+ const name = file.replace(/\.hbs$/, "");
2565
+ const source = readFileSync(join4(partialsDir, file), "utf8");
2566
+ handlebars.registerPartial(name, source);
2567
+ }
2568
+ }
2569
+ handlebars.registerHelper("year", () => (/* @__PURE__ */ new Date()).getFullYear());
2570
+ function loadTemplate(name) {
2571
+ loadPartials();
2572
+ const cached = templates.get(name);
2573
+ if (cached)
2574
+ return cached;
2575
+ const source = readFileSync(join4(templatesRoot(), `${name}.hbs`), "utf8");
2576
+ const compiled = handlebars.compile(source);
2577
+ templates.set(name, compiled);
2578
+ return compiled;
2579
+ }
2580
+ function renderMailTemplate(name, data) {
2581
+ const body = loadTemplate(`mail/${name}`)(data);
2582
+ const base = loadTemplate("mail/base");
2583
+ return base({ ...data, body });
2584
+ }
2585
+
2477
2586
  // ../core/dist/controllers/auth.js
2478
2587
  var LoginSchema = z.object({
2479
2588
  email: z.email(),
@@ -2489,9 +2598,18 @@ var RegisterSchema = z.object({
2489
2598
  lastName: z.string().trim().min(1).max(100),
2490
2599
  password: z.string().min(8)
2491
2600
  });
2601
+ var RequestPasswordResetSchema = z.object({
2602
+ email: z.email()
2603
+ });
2604
+ var ResetPasswordSchema = z.object({
2605
+ token: z.string().min(32),
2606
+ password: z.string().min(8)
2607
+ });
2492
2608
  var RATE_LIMIT_WINDOW_MS = 15 * 60 * 1e3;
2493
2609
  var LOGIN_RATE_LIMIT_MAX = 10;
2494
2610
  var LOGIN_2FA_RATE_LIMIT_MAX = 5;
2611
+ var PASSWORD_RESET_RATE_LIMIT_MAX = 5;
2612
+ var PASSWORD_RESET_EXPIRES_MS = 30 * 60 * 1e3;
2495
2613
  var ACCESS_TOKEN_COOKIE = "plank_session";
2496
2614
  var ACCESS_TOKEN_EXPIRES_SECONDS = 60 * 60 * 24 * 30;
2497
2615
  var REFRESH_TOKEN_COOKIE = "plank_refresh";
@@ -2567,6 +2685,27 @@ function buildRefreshToken(payload) {
2567
2685
  function buildChallengeToken(payload) {
2568
2686
  return jwt.sign(payload, process.env.PLANK_JWT_SECRET, { expiresIn: "5m" });
2569
2687
  }
2688
+ function hashToken(token) {
2689
+ return createHash("sha256").update(token).digest("hex");
2690
+ }
2691
+ function adminBaseUrl(req) {
2692
+ const referer = req.get("referer");
2693
+ if (referer) {
2694
+ try {
2695
+ const url = new URL(referer);
2696
+ if (url.pathname === "/admin" || url.pathname.startsWith("/admin/")) {
2697
+ return `${url.origin}/admin`;
2698
+ }
2699
+ return url.origin;
2700
+ } catch {
2701
+ }
2702
+ }
2703
+ const origin = req.get("origin");
2704
+ if (origin)
2705
+ return origin;
2706
+ const protocol = req.protocol;
2707
+ return `${protocol}://${req.get("host")}`;
2708
+ }
2570
2709
  async function buildAuthPayload(user) {
2571
2710
  const { rows: roleRows } = await pool_default.query("SELECT id, name, permissions FROM plank_roles WHERE id = $1", [user.role_id]);
2572
2711
  let avatarUrl = user.avatar_url;
@@ -2592,6 +2731,12 @@ async function buildAuthPayload(user) {
2592
2731
  }
2593
2732
  };
2594
2733
  }
2734
+ async function getAuthFeatures(_req, res) {
2735
+ const mailing = await getMailingSettings();
2736
+ res.json({
2737
+ passwordRecovery: mailing.enabled && Boolean(mailing.host && mailing.user && mailing.password && mailing.fromEmail)
2738
+ });
2739
+ }
2595
2740
  async function login(req, res) {
2596
2741
  const ip = req.ip ?? "unknown";
2597
2742
  const parsed = LoginSchema.safeParse(req.body);
@@ -2637,6 +2782,95 @@ async function login(req, res) {
2637
2782
  user: auth.user
2638
2783
  });
2639
2784
  }
2785
+ async function requestPasswordReset(req, res) {
2786
+ const ip = req.ip ?? "unknown";
2787
+ const parsed = RequestPasswordResetSchema.safeParse(req.body);
2788
+ if (!parsed.success) {
2789
+ res.status(400).json({ errors: flattenError(parsed.error, (i2) => i2.message) });
2790
+ return;
2791
+ }
2792
+ const email = parsed.data.email.toLowerCase();
2793
+ const rateKey = `${ip}:${email}`;
2794
+ if (!await consumeRateLimit("password-reset", rateKey, PASSWORD_RESET_RATE_LIMIT_MAX)) {
2795
+ res.status(429).json({ error: "Too many password reset attempts. Try again in 15 minutes." });
2796
+ return;
2797
+ }
2798
+ const mailing = await getMailingSettings();
2799
+ if (!mailing.enabled || !mailing.host || !mailing.user || !mailing.password || !mailing.fromEmail) {
2800
+ res.status(204).end();
2801
+ return;
2802
+ }
2803
+ const { rows } = await pool_default.query(`SELECT id, email, first_name, last_name, enabled
2804
+ FROM plank_users
2805
+ WHERE email = $1`, [email]);
2806
+ const user = rows[0];
2807
+ if (!user || !user.enabled) {
2808
+ res.status(204).end();
2809
+ return;
2810
+ }
2811
+ const token = randomBytes6(32).toString("base64url");
2812
+ const resetUrl = `${adminBaseUrl(req)}/reset-password?token=${encodeURIComponent(token)}`;
2813
+ const expiresAt = new Date(Date.now() + PASSWORD_RESET_EXPIRES_MS);
2814
+ await pool_default.query(`INSERT INTO plank_password_reset_tokens (id, user_id, token_hash, expires_at)
2815
+ VALUES ($1, $2, $3, $4)`, [createId(), user.id, hashToken(token), expiresAt]);
2816
+ const html = renderMailTemplate("password-reset", {
2817
+ subject: "Reset your password",
2818
+ resetUrl,
2819
+ expiresIn: "30 minutes",
2820
+ userName: [user.first_name, user.last_name].filter(Boolean).join(" ") || user.email
2821
+ });
2822
+ await sendMail({
2823
+ to: user.email,
2824
+ subject: "Reset your Plank CMS password",
2825
+ html
2826
+ });
2827
+ await clearRateLimit("password-reset", rateKey);
2828
+ res.status(204).end();
2829
+ }
2830
+ async function resetPassword(req, res) {
2831
+ const parsed = ResetPasswordSchema.safeParse(req.body);
2832
+ if (!parsed.success) {
2833
+ res.status(400).json({ errors: flattenError(parsed.error, (i2) => i2.message) });
2834
+ return;
2835
+ }
2836
+ const tokenHash = hashToken(parsed.data.token);
2837
+ const { rows } = await pool_default.query(`SELECT t.id, t.user_id, u.enabled
2838
+ FROM plank_password_reset_tokens t
2839
+ JOIN plank_users u ON u.id = t.user_id
2840
+ WHERE t.token_hash = $1
2841
+ AND t.used_at IS NULL
2842
+ AND t.expires_at > NOW()`, [tokenHash]);
2843
+ const resetToken = rows[0];
2844
+ if (!resetToken || !resetToken.enabled) {
2845
+ res.status(400).json({ error: "Invalid or expired password reset link" });
2846
+ return;
2847
+ }
2848
+ const hashed = await bcrypt.hash(parsed.data.password, 12);
2849
+ const client = await pool_default.connect();
2850
+ try {
2851
+ await client.query("BEGIN");
2852
+ await client.query(`UPDATE plank_users
2853
+ SET password = $1,
2854
+ two_factor_enabled = FALSE,
2855
+ two_factor_secret = NULL,
2856
+ two_factor_temp_secret = NULL,
2857
+ session_version = session_version + 1
2858
+ WHERE id = $2`, [hashed, resetToken.user_id]);
2859
+ await client.query("DELETE FROM plank_user_backup_codes WHERE user_id = $1", [
2860
+ resetToken.user_id
2861
+ ]);
2862
+ await client.query(`UPDATE plank_password_reset_tokens
2863
+ SET used_at = NOW()
2864
+ WHERE user_id = $1 AND used_at IS NULL`, [resetToken.user_id]);
2865
+ await client.query("COMMIT");
2866
+ } catch (err) {
2867
+ await client.query("ROLLBACK");
2868
+ throw err;
2869
+ } finally {
2870
+ client.release();
2871
+ }
2872
+ res.status(204).end();
2873
+ }
2640
2874
  async function loginWithTwoFactor(req, res) {
2641
2875
  const ip = req.ip ?? "unknown";
2642
2876
  const parsed = Login2FASchema.safeParse(req.body);
@@ -2762,10 +2996,13 @@ async function register(req, res) {
2762
2996
  // ../core/dist/routes/auth.js
2763
2997
  var router = Router();
2764
2998
  router.get("/setup", setup);
2999
+ router.get("/features", getAuthFeatures);
2765
3000
  router.post("/login", login);
2766
3001
  router.post("/login/2fa", loginWithTwoFactor);
2767
3002
  router.post("/logout", logout);
2768
3003
  router.post("/register", register);
3004
+ router.post("/password-reset", requestPasswordReset);
3005
+ router.post("/password-reset/confirm", resetPassword);
2769
3006
  var auth_default = router;
2770
3007
 
2771
3008
  // ../core/dist/routes/admin.js
@@ -4015,7 +4252,7 @@ var deleteEntry = async (req, res) => {
4015
4252
 
4016
4253
  // ../core/dist/controllers/users.js
4017
4254
  import bcrypt2 from "bcryptjs";
4018
- import { randomBytes as randomBytes6 } from "crypto";
4255
+ import { randomBytes as randomBytes7 } from "crypto";
4019
4256
  import { z as z4, flattenError as flattenError4 } from "zod";
4020
4257
  var CreateUserSchema = z4.object({
4021
4258
  email: z4.email(),
@@ -4036,6 +4273,9 @@ var ChangePasswordSchema = z4.object({
4036
4273
  currentPassword: z4.string().min(1),
4037
4274
  newPassword: z4.string().min(8)
4038
4275
  });
4276
+ var ResetUserPasswordSchema = z4.object({
4277
+ password: z4.string().min(8)
4278
+ });
4039
4279
  var UpdateMeSchema = z4.object({
4040
4280
  firstName: z4.string().max(100).optional(),
4041
4281
  lastName: z4.string().max(100).optional(),
@@ -4057,7 +4297,7 @@ var RegenerateBackupCodesSchema = z4.object({
4057
4297
  var BACKUP_CODE_COUNT = 8;
4058
4298
  var BACKUP_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
4059
4299
  function generateBackupCode() {
4060
- const bytes = randomBytes6(8);
4300
+ const bytes = randomBytes7(8);
4061
4301
  let raw = "";
4062
4302
  for (let i2 = 0; i2 < 8; i2++) {
4063
4303
  raw += BACKUP_CODE_CHARS[bytes[i2] % BACKUP_CODE_CHARS.length];
@@ -4348,6 +4588,49 @@ async function changePassword(req, res) {
4348
4588
  await pool_default.query("UPDATE plank_users SET password = $1, session_version = session_version + 1 WHERE id = $2", [hashed, req.user.id]);
4349
4589
  res.status(204).end();
4350
4590
  }
4591
+ async function resetUserPassword(req, res) {
4592
+ const parsed = ResetUserPasswordSchema.safeParse(req.body);
4593
+ if (!parsed.success) {
4594
+ res.status(400).json({ errors: flattenError4(parsed.error, (i2) => i2.message) });
4595
+ return;
4596
+ }
4597
+ if (req.params.id === req.user.id) {
4598
+ res.status(403).json({ error: "You cannot reset your own password here" });
4599
+ return;
4600
+ }
4601
+ const requesterRoleName = await roleNameById(req.user.roleId);
4602
+ if (requesterRoleName !== "Super Admin") {
4603
+ res.status(403).json({ error: "Only Super Admin can reset user passwords" });
4604
+ return;
4605
+ }
4606
+ const { rows } = await pool_default.query("SELECT id FROM plank_users WHERE id = $1", [
4607
+ req.params.id
4608
+ ]);
4609
+ if (!rows[0]) {
4610
+ res.status(404).json({ error: "User not found" });
4611
+ return;
4612
+ }
4613
+ const hashed = await bcrypt2.hash(parsed.data.password, 12);
4614
+ const client = await pool_default.connect();
4615
+ try {
4616
+ await client.query("BEGIN");
4617
+ await client.query(`UPDATE plank_users
4618
+ SET password = $1,
4619
+ two_factor_enabled = FALSE,
4620
+ two_factor_secret = NULL,
4621
+ two_factor_temp_secret = NULL,
4622
+ session_version = session_version + 1
4623
+ WHERE id = $2`, [hashed, req.params.id]);
4624
+ await client.query("DELETE FROM plank_user_backup_codes WHERE user_id = $1", [req.params.id]);
4625
+ await client.query("COMMIT");
4626
+ } catch (err) {
4627
+ await client.query("ROLLBACK");
4628
+ throw err;
4629
+ } finally {
4630
+ client.release();
4631
+ }
4632
+ res.status(204).end();
4633
+ }
4351
4634
  async function createUser(req, res) {
4352
4635
  const parsed = CreateUserSchema.safeParse(req.body);
4353
4636
  if (!parsed.success) {
@@ -4462,8 +4745,8 @@ async function deleteUser(req, res) {
4462
4745
  return;
4463
4746
  }
4464
4747
  const { rows: roleRows } = await pool_default.query("SELECT name FROM plank_roles WHERE id = $1", [rows[0].role_id]);
4465
- if (roleRows[0]?.name === "Super Admin") {
4466
- res.status(403).json({ error: "Super Admin users cannot be deleted" });
4748
+ if (roleRows[0]?.name === "Super Admin" && await roleNameById(req.user.roleId) !== "Super Admin") {
4749
+ res.status(403).json({ error: "Only Super Admin can delete Super Admin users" });
4467
4750
  return;
4468
4751
  }
4469
4752
  await pool_default.query("DELETE FROM plank_users WHERE id = $1", [req.params.id]);
@@ -4524,14 +4807,14 @@ async function resetRoles(_req, res) {
4524
4807
  }
4525
4808
 
4526
4809
  // ../core/dist/controllers/apiTokens.js
4527
- import { randomBytes as randomBytes7, createHash } from "crypto";
4810
+ import { randomBytes as randomBytes8, createHash as createHash2 } from "crypto";
4528
4811
  import { z as z7, flattenError as flattenError5 } from "zod";
4529
4812
  var CreateTokenSchema = z7.object({
4530
4813
  name: z7.string().min(1),
4531
4814
  accessType: z7.enum(["read-only", "full-access", "mcp-server"])
4532
4815
  });
4533
- function hashToken(token) {
4534
- return createHash("sha256").update(token).digest("hex");
4816
+ function hashToken2(token) {
4817
+ return createHash2("sha256").update(token).digest("hex");
4535
4818
  }
4536
4819
  async function listApiTokens(_req, res) {
4537
4820
  const { rows } = await pool_default.query("SELECT id, name, access_type, created_at FROM plank_api_tokens ORDER BY created_at DESC");
@@ -4545,8 +4828,8 @@ async function createApiToken(req, res) {
4545
4828
  }
4546
4829
  const { name, accessType } = parsed.data;
4547
4830
  const id = createId();
4548
- const token = `plank_${randomBytes7(32).toString("hex")}`;
4549
- const hashed = hashToken(token);
4831
+ const token = `plank_${randomBytes8(32).toString("hex")}`;
4832
+ const hashed = hashToken2(token);
4550
4833
  await pool_default.query("INSERT INTO plank_api_tokens (id, name, token, access_type, created_by) VALUES ($1, $2, $3, $4, $5)", [id, name, hashed, accessType, req.user.id]);
4551
4834
  res.status(201).json({ id, name, accessType, token });
4552
4835
  }
@@ -4560,7 +4843,7 @@ async function deleteApiToken(req, res) {
4560
4843
  }
4561
4844
 
4562
4845
  // ../core/dist/controllers/media.js
4563
- import { randomBytes as randomBytes8 } from "crypto";
4846
+ import { randomBytes as randomBytes9 } from "crypto";
4564
4847
  var MEDIA_PREFIX = "media";
4565
4848
  function buildDefaultAlt(filename) {
4566
4849
  const baseName = filename.split("/").pop() ?? filename;
@@ -4641,7 +4924,7 @@ async function uploadMedia(req, res) {
4641
4924
  res.status(400).json({ error: "No .m3u8 file found in bundle" });
4642
4925
  return;
4643
4926
  }
4644
- const bundleId = randomBytes8(8).toString("hex");
4927
+ const bundleId = randomBytes9(8).toString("hex");
4645
4928
  const prefix = [MEDIA_PREFIX, folderId, bundleId].filter(Boolean).join("/");
4646
4929
  const rootDir = m3u8File.originalname.includes("/") ? m3u8File.originalname.split("/")[0] : null;
4647
4930
  const stripRoot = (path) => rootDir && path.startsWith(`${rootDir}/`) ? path.slice(rootDir.length + 1) : path;
@@ -4867,7 +5150,8 @@ async function deleteFolder(req, res) {
4867
5150
 
4868
5151
  // ../core/dist/controllers/settings.js
4869
5152
  var SENSITIVE_FIELDS2 = {
4870
- media: /* @__PURE__ */ new Set(["s3.secret_access_key", "r2.secret_access_key"])
5153
+ media: /* @__PURE__ */ new Set(["s3.secret_access_key", "r2.secret_access_key"]),
5154
+ mailing: /* @__PURE__ */ new Set(["smtp.password"])
4871
5155
  };
4872
5156
  var MASKED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
4873
5157
  function maskSettings(namespace, settings) {
@@ -4940,8 +5224,8 @@ async function updateNamespaceSettings(req, res) {
4940
5224
 
4941
5225
  // ../core/dist/lib/version.js
4942
5226
  import { readFile as readFile2 } from "fs/promises";
4943
- import { join as join4 } from "path";
4944
- import { fileURLToPath as fileURLToPath2 } from "url";
5227
+ import { join as join5 } from "path";
5228
+ import { fileURLToPath as fileURLToPath3 } from "url";
4945
5229
  var PACKAGE_NAME = "@plank-cms/plank";
4946
5230
  var CHANGELOG_BASE_URL = "https://github.com/plank-cms/plank/releases";
4947
5231
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
@@ -4976,7 +5260,7 @@ function getUpdateCommandForPackageManager(packageManager) {
4976
5260
  }
4977
5261
  async function detectProjectPackageManager() {
4978
5262
  try {
4979
- const raw = await readFile2(join4(process.cwd(), "package.json"), "utf8");
5263
+ const raw = await readFile2(join5(process.cwd(), "package.json"), "utf8");
4980
5264
  const parsed = JSON.parse(raw);
4981
5265
  if (parsed.packageManager?.startsWith("pnpm@") || parsed.packageManager === "pnpm") {
4982
5266
  return "pnpm";
@@ -4991,7 +5275,7 @@ async function detectProjectPackageManager() {
4991
5275
  }
4992
5276
  async function hasLockfile(filename) {
4993
5277
  try {
4994
- await readFile2(join4(process.cwd(), filename), "utf8");
5278
+ await readFile2(join5(process.cwd(), filename), "utf8");
4995
5279
  return true;
4996
5280
  } catch {
4997
5281
  return false;
@@ -5009,7 +5293,7 @@ async function detectPackageManagerFromLockfiles() {
5009
5293
  async function getCurrentVersion() {
5010
5294
  for (const packageJsonUrl of packageJsonUrls) {
5011
5295
  try {
5012
- const packageJsonPath = fileURLToPath2(packageJsonUrl);
5296
+ const packageJsonPath = fileURLToPath3(packageJsonUrl);
5013
5297
  const raw = await readFile2(packageJsonPath, "utf8");
5014
5298
  const parsed = JSON.parse(raw);
5015
5299
  if (parsed.version) {
@@ -5070,12 +5354,12 @@ import { z as z9 } from "zod";
5070
5354
  // ../core/dist/lib/addons.js
5071
5355
  import { access, readFile as readFile3 } from "fs/promises";
5072
5356
  import { createRequire } from "module";
5073
- import { dirname as dirname2, join as join5, resolve } from "path";
5074
- import { fileURLToPath as fileURLToPath3 } from "url";
5357
+ import { dirname as dirname3, join as join6, resolve } from "path";
5358
+ import { fileURLToPath as fileURLToPath4 } from "url";
5075
5359
  import { z as z8 } from "zod";
5076
5360
  var ADDON_PACKAGE_PREFIX = "@plank-cms/addon-";
5077
5361
  function getHostRequire() {
5078
- return createRequire(join5(process.cwd(), "package.json"));
5362
+ return createRequire(join6(process.cwd(), "package.json"));
5079
5363
  }
5080
5364
  var addonSlotSchema = z8.object({
5081
5365
  id: z8.string().min(1),
@@ -5170,13 +5454,13 @@ async function resolvePackageJsonPath(packageName) {
5170
5454
  } catch {
5171
5455
  try {
5172
5456
  const entryPath = hostRequire.resolve(packageName);
5173
- let currentDir = dirname2(entryPath);
5457
+ let currentDir = dirname3(entryPath);
5174
5458
  for (; ; ) {
5175
- const candidate = join5(currentDir, "package.json");
5459
+ const candidate = join6(currentDir, "package.json");
5176
5460
  if (await pathExists(candidate)) {
5177
5461
  return candidate;
5178
5462
  }
5179
- const parentDir = dirname2(currentDir);
5463
+ const parentDir = dirname3(currentDir);
5180
5464
  if (parentDir === currentDir)
5181
5465
  return null;
5182
5466
  currentDir = parentDir;
@@ -5189,16 +5473,16 @@ async function resolvePackageJsonPath(packageName) {
5189
5473
  async function resolvePackageRoot(packageName) {
5190
5474
  const packageJsonPath = await resolvePackageJsonPath(packageName);
5191
5475
  if (packageJsonPath)
5192
- return dirname2(packageJsonPath);
5476
+ return dirname3(packageJsonPath);
5193
5477
  try {
5194
5478
  const manifestUrl = import.meta.resolve(`${packageName}/plank`);
5195
- let currentDir = dirname2(fileURLToPath3(manifestUrl));
5479
+ let currentDir = dirname3(fileURLToPath4(manifestUrl));
5196
5480
  for (; ; ) {
5197
- const candidate = join5(currentDir, "package.json");
5481
+ const candidate = join6(currentDir, "package.json");
5198
5482
  if (await pathExists(candidate)) {
5199
5483
  return currentDir;
5200
5484
  }
5201
- const parentDir = dirname2(currentDir);
5485
+ const parentDir = dirname3(currentDir);
5202
5486
  if (parentDir === currentDir)
5203
5487
  return null;
5204
5488
  currentDir = parentDir;
@@ -5219,7 +5503,7 @@ async function readInstalledPackageJson(packageName) {
5219
5503
  }
5220
5504
  }
5221
5505
  async function readHostPackageJson() {
5222
- const raw = await readFile3(join5(process.cwd(), "package.json"), "utf8");
5506
+ const raw = await readFile3(join6(process.cwd(), "package.json"), "utf8");
5223
5507
  return JSON.parse(raw);
5224
5508
  }
5225
5509
  function listDeclaredAddonPackages(packageJson) {
@@ -5792,6 +6076,7 @@ router2.post("/roles/reset", authorize("settings:roles:write"), resetRoles);
5792
6076
  router2.get("/users", authorize("settings:users:read"), listUsers);
5793
6077
  router2.post("/users", authorize("settings:users:write"), createUser);
5794
6078
  router2.put("/users/:id", authorize("settings:users:write"), updateUser);
6079
+ router2.post("/users/:id/password/reset", authorize("settings:users:write"), resetUserPassword);
5795
6080
  router2.delete("/users/:id", authorize("settings:users:delete"), deleteUser);
5796
6081
  router2.get("/api-tokens", authorize("settings:api-tokens:read"), listApiTokens);
5797
6082
  router2.post("/api-tokens", authorize("settings:api-tokens:write"), createApiToken);
@@ -5832,7 +6117,7 @@ var admin_default = router2;
5832
6117
  import { Router as Router3 } from "express";
5833
6118
 
5834
6119
  // ../core/dist/middlewares/apiToken.js
5835
- import { createHash as createHash2 } from "crypto";
6120
+ import { createHash as createHash3 } from "crypto";
5836
6121
  var READ_ONLY_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
5837
6122
  var PUBLIC_API_ACCESS_TYPES = /* @__PURE__ */ new Set(["read-only", "full-access"]);
5838
6123
  var MCP_ACCESS_TYPES = /* @__PURE__ */ new Set(["mcp-server"]);
@@ -5843,7 +6128,7 @@ async function enforceApiToken(req, res, next, allowedAccessTypes) {
5843
6128
  return;
5844
6129
  }
5845
6130
  const raw = header.slice(7);
5846
- const hashed = createHash2("sha256").update(raw).digest("hex");
6131
+ const hashed = createHash3("sha256").update(raw).digest("hex");
5847
6132
  const { rows } = await pool_default.query("SELECT id, access_type FROM plank_api_tokens WHERE token = $1", [hashed]);
5848
6133
  if (!rows[0]) {
5849
6134
  res.status(401).json({ error: "Invalid API token" });
@@ -6821,11 +7106,11 @@ if (isDev) {
6821
7106
  app.get("/admin/*path", (_req, res) => res.redirect(adminDevUrl));
6822
7107
  app.get("/admin", (_req, res) => res.redirect(adminDevUrl));
6823
7108
  } else {
6824
- const adminDist = process.env.PLANK_ADMIN_DIST ?? join6(dirname3(fileURLToPath4(import.meta.url)), "../public/admin");
7109
+ const adminDist = process.env.PLANK_ADMIN_DIST ?? join7(dirname4(fileURLToPath5(import.meta.url)), "../public/admin");
6825
7110
  app.use("/admin", express.static(adminDist));
6826
7111
  app.get("/admin/*path", async (_req, res) => {
6827
7112
  try {
6828
- const source = await readFile5(join6(adminDist, "index.html"), "utf8");
7113
+ const source = await readFile5(join7(adminDist, "index.html"), "utf8");
6829
7114
  res.type("text/html");
6830
7115
  res.send(source);
6831
7116
  } catch {