@plank-cms/plank 0.15.1 → 0.15.2

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-BvcFQsXq.js"></script>
15
+ <script type="module" crossorigin src="/admin/assets/index-NxBzeHGf.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/admin/assets/index-DU5dGjXR.css">
17
17
  </head>
18
18
  <body>
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { randomBytes } from "crypto";
7
7
  import { resolve, join } from "path";
8
8
  import fs from "fs-extra";
9
9
  import { execa } from "execa";
10
- var PACKAGE_VERSION = "0.15.1";
10
+ var PACKAGE_VERSION = "0.15.2";
11
11
  function generateSecret() {
12
12
  return randomBytes(32).toString("hex");
13
13
  }
@@ -101,7 +101,7 @@ import { dirname, join as join2, resolve as resolve2 } from "path";
101
101
  async function start() {
102
102
  config({ path: resolve2(process.cwd(), ".env") });
103
103
  process.env.PLANK_ADMIN_DIST = join2(dirname(fileURLToPath(import.meta.url)), "admin");
104
- const { start: startServer } = await import("./server-D4FUWQTZ.js");
104
+ const { start: startServer } = await import("./server-YYJGX3SJ.js");
105
105
  await startServer();
106
106
  }
107
107
 
@@ -2315,7 +2315,9 @@ var RATE_LIMIT_WINDOW_MS = 15 * 60 * 1e3;
2315
2315
  var LOGIN_RATE_LIMIT_MAX = 10;
2316
2316
  var LOGIN_2FA_RATE_LIMIT_MAX = 5;
2317
2317
  var ACCESS_TOKEN_COOKIE = "plank_session";
2318
- var ACCESS_TOKEN_EXPIRES_SECONDS = 60 * 15;
2318
+ var ACCESS_TOKEN_EXPIRES_SECONDS = 60 * 60 * 24 * 30;
2319
+ var REFRESH_TOKEN_COOKIE = "plank_refresh";
2320
+ var REFRESH_TOKEN_EXPIRES_SECONDS = 60 * 60 * 24 * 30;
2319
2321
  function isProduction() {
2320
2322
  return process.env.NODE_ENV === "production";
2321
2323
  }
@@ -2328,6 +2330,15 @@ function setSessionCookie(res, token) {
2328
2330
  maxAge: ACCESS_TOKEN_EXPIRES_SECONDS * 1e3
2329
2331
  });
2330
2332
  }
2333
+ function setRefreshCookie(res, token) {
2334
+ res.cookie(REFRESH_TOKEN_COOKIE, token, {
2335
+ httpOnly: true,
2336
+ secure: isProduction(),
2337
+ sameSite: "lax",
2338
+ path: "/",
2339
+ maxAge: REFRESH_TOKEN_EXPIRES_SECONDS * 1e3
2340
+ });
2341
+ }
2331
2342
  function clearSessionCookie(res) {
2332
2343
  res.clearCookie(ACCESS_TOKEN_COOKIE, {
2333
2344
  httpOnly: true,
@@ -2336,6 +2347,14 @@ function clearSessionCookie(res) {
2336
2347
  path: "/"
2337
2348
  });
2338
2349
  }
2350
+ function clearRefreshCookie(res) {
2351
+ res.clearCookie(REFRESH_TOKEN_COOKIE, {
2352
+ httpOnly: true,
2353
+ secure: isProduction(),
2354
+ sameSite: "lax",
2355
+ path: "/"
2356
+ });
2357
+ }
2339
2358
  async function consumeRateLimit(scope, rateKey, max) {
2340
2359
  const resetAt = new Date(Date.now() + RATE_LIMIT_WINDOW_MS);
2341
2360
  const { rows } = await pool_default.query(`INSERT INTO plank_auth_rate_limits (id, scope, rate_key, count, reset_at)
@@ -2361,7 +2380,11 @@ async function clearRateLimit(scope, rateKey) {
2361
2380
  ]);
2362
2381
  }
2363
2382
  function buildAccessToken(payload) {
2364
- return jwt.sign(payload, process.env.PLANK_JWT_SECRET, { expiresIn: "15m" });
2383
+ return jwt.sign(payload, process.env.PLANK_JWT_SECRET, { expiresIn: "30d" });
2384
+ }
2385
+ function buildRefreshToken(payload) {
2386
+ const refreshPayload = { ...payload, type: "refresh" };
2387
+ return jwt.sign(refreshPayload, process.env.PLANK_JWT_SECRET, { expiresIn: "30d" });
2365
2388
  }
2366
2389
  function buildChallengeToken(payload) {
2367
2390
  return jwt.sign(payload, process.env.PLANK_JWT_SECRET, { expiresIn: "5m" });
@@ -2426,6 +2449,7 @@ async function login(req, res) {
2426
2449
  }
2427
2450
  const auth = await buildAuthPayload(user);
2428
2451
  setSessionCookie(res, auth.token);
2452
+ setRefreshCookie(res, buildRefreshToken({ sub: user.id, roleId: user.role_id, sv: user.session_version }));
2429
2453
  res.json({
2430
2454
  requiresTwoFactor: false,
2431
2455
  user: auth.user
@@ -2494,6 +2518,7 @@ async function loginWithTwoFactor(req, res) {
2494
2518
  await clearRateLimit("login-2fa", rateKey);
2495
2519
  const auth = await buildAuthPayload(user);
2496
2520
  setSessionCookie(res, auth.token);
2521
+ setRefreshCookie(res, buildRefreshToken({ sub: user.id, roleId: user.role_id, sv: user.session_version }));
2497
2522
  res.json({
2498
2523
  requiresTwoFactor: false,
2499
2524
  user: auth.user
@@ -2512,6 +2537,7 @@ async function logout(req, res) {
2512
2537
  }
2513
2538
  }
2514
2539
  clearSessionCookie(res);
2540
+ clearRefreshCookie(res);
2515
2541
  const ip = req.ip ?? "unknown";
2516
2542
  await pool_default.query("DELETE FROM plank_auth_rate_limits WHERE rate_key LIKE $1", [`${ip}:%`]);
2517
2543
  res.status(204).end();
@@ -2553,6 +2579,24 @@ import { Router as Router2 } from "express";
2553
2579
 
2554
2580
  // ../core/dist/middlewares/authenticate.js
2555
2581
  import jwt2 from "jsonwebtoken";
2582
+ var ACCESS_TOKEN_COOKIE2 = "plank_session";
2583
+ var REFRESH_TOKEN_COOKIE2 = "plank_refresh";
2584
+ var ACCESS_TOKEN_EXPIRES_SECONDS2 = 60 * 60 * 24 * 30;
2585
+ function isProduction2() {
2586
+ return process.env.NODE_ENV === "production";
2587
+ }
2588
+ function setSessionCookie2(res, token) {
2589
+ res.cookie(ACCESS_TOKEN_COOKIE2, token, {
2590
+ httpOnly: true,
2591
+ secure: isProduction2(),
2592
+ sameSite: "lax",
2593
+ path: "/",
2594
+ maxAge: ACCESS_TOKEN_EXPIRES_SECONDS2 * 1e3
2595
+ });
2596
+ }
2597
+ function buildAccessToken2(payload) {
2598
+ return jwt2.sign(payload, process.env.PLANK_JWT_SECRET, { expiresIn: "30d" });
2599
+ }
2556
2600
  function cookieValue(raw, key) {
2557
2601
  if (!raw)
2558
2602
  return null;
@@ -2567,27 +2611,61 @@ function cookieValue(raw, key) {
2567
2611
  async function authenticate(req, res, next) {
2568
2612
  const header = req.headers.authorization;
2569
2613
  const bearer = header?.startsWith("Bearer ") ? header.slice(7) : null;
2570
- const cookieToken = cookieValue(req.headers.cookie, "plank_session");
2614
+ const cookieToken = cookieValue(req.headers.cookie, ACCESS_TOKEN_COOKIE2);
2615
+ const refreshToken = cookieValue(req.headers.cookie, REFRESH_TOKEN_COOKIE2);
2571
2616
  const token = bearer ?? cookieToken;
2572
2617
  if (!token) {
2573
2618
  res.status(401).json({ error: "Unauthorized" });
2574
2619
  return;
2575
2620
  }
2576
- try {
2577
- const payload = jwt2.verify(token, process.env.PLANK_JWT_SECRET);
2621
+ async function validateSession(payload) {
2578
2622
  if (typeof payload.sv !== "number") {
2579
- res.status(401).json({ error: "Invalid session token" });
2580
- return;
2623
+ return { ok: false };
2581
2624
  }
2582
2625
  const { rows } = await pool_default.query("SELECT session_version FROM plank_users WHERE id = $1", [payload.sub]);
2583
2626
  if (!rows[0] || rows[0].session_version !== payload.sv) {
2627
+ return { ok: false };
2628
+ }
2629
+ return { ok: true, sessionVersion: rows[0].session_version };
2630
+ }
2631
+ try {
2632
+ const payload = jwt2.verify(token, process.env.PLANK_JWT_SECRET);
2633
+ const session = await validateSession(payload);
2634
+ if (!session.ok) {
2584
2635
  res.status(401).json({ error: "Session has been revoked" });
2585
2636
  return;
2586
2637
  }
2587
2638
  req.user = { id: payload.sub, roleId: payload.roleId };
2588
2639
  next();
2589
2640
  } catch {
2590
- res.status(401).json({ error: "Invalid or expired token" });
2641
+ if (bearer || !refreshToken) {
2642
+ res.status(401).json({ error: "Invalid or expired token" });
2643
+ return;
2644
+ }
2645
+ try {
2646
+ const refreshPayload = jwt2.verify(refreshToken, process.env.PLANK_JWT_SECRET);
2647
+ if (refreshPayload.type !== "refresh") {
2648
+ res.status(401).json({ error: "Invalid or expired token" });
2649
+ return;
2650
+ }
2651
+ const session = await validateSession(refreshPayload);
2652
+ if (!session.ok || typeof session.sessionVersion !== "number") {
2653
+ res.status(401).json({ error: "Session has been revoked" });
2654
+ return;
2655
+ }
2656
+ const renewed = buildAccessToken2({
2657
+ sub: refreshPayload.sub,
2658
+ roleId: refreshPayload.roleId,
2659
+ sv: session.sessionVersion
2660
+ });
2661
+ setSessionCookie2(res, renewed);
2662
+ req.user = { id: refreshPayload.sub, roleId: refreshPayload.roleId };
2663
+ next();
2664
+ return;
2665
+ } catch {
2666
+ res.status(401).json({ error: "Invalid or expired token" });
2667
+ return;
2668
+ }
2591
2669
  }
2592
2670
  }
2593
2671
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plank-cms/plank",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "Self-hosted headless CMS. Deploy in minutes on your own infrastructure.",
5
5
  "type": "module",
6
6
  "files": [
@@ -55,9 +55,9 @@
55
55
  "devDependencies": {
56
56
  "@types/fs-extra": "^11.0.4",
57
57
  "tsup": "^8.5.0",
58
- "@plank-cms/core": "0.15.1",
59
- "@plank-cms/db": "0.15.1",
60
- "@plank-cms/schema": "0.15.1"
58
+ "@plank-cms/core": "0.15.2",
59
+ "@plank-cms/db": "0.15.2",
60
+ "@plank-cms/schema": "0.15.2"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",