@sonicjs-cms/core 2.16.0 → 2.17.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.
Files changed (64) hide show
  1. package/dist/{app-COElO4Rm.d.cts → app-C9esKLmh.d.cts} +3 -0
  2. package/dist/{app-COElO4Rm.d.ts → app-C9esKLmh.d.ts} +3 -0
  3. package/dist/{chunk-Q5VFZUXV.cjs → chunk-4HCUJ3MG.cjs} +34 -6
  4. package/dist/chunk-4HCUJ3MG.cjs.map +1 -0
  5. package/dist/{chunk-MVSCB4E3.js → chunk-6F57Z6SD.js} +3 -3
  6. package/dist/{chunk-MVSCB4E3.js.map → chunk-6F57Z6SD.js.map} +1 -1
  7. package/dist/{chunk-INSDRCG3.js → chunk-FDXNIZ6N.js} +34 -6
  8. package/dist/chunk-FDXNIZ6N.js.map +1 -0
  9. package/dist/{chunk-OCLUXJ7E.cjs → chunk-FSWP4FBW.cjs} +48 -5
  10. package/dist/chunk-FSWP4FBW.cjs.map +1 -0
  11. package/dist/{chunk-6ENX7QSA.cjs → chunk-J5MYHM6Z.cjs} +352 -176
  12. package/dist/chunk-J5MYHM6Z.cjs.map +1 -0
  13. package/dist/{chunk-WLSIUKNM.js → chunk-LZJLWW7E.js} +227 -51
  14. package/dist/chunk-LZJLWW7E.js.map +1 -0
  15. package/dist/{chunk-VFQUULAV.js → chunk-NMJT6BJR.js} +48 -5
  16. package/dist/chunk-NMJT6BJR.js.map +1 -0
  17. package/dist/{chunk-YQW2GCJ3.cjs → chunk-QBLBIAVZ.cjs} +3 -3
  18. package/dist/{chunk-YQW2GCJ3.cjs.map → chunk-QBLBIAVZ.cjs.map} +1 -1
  19. package/dist/{chunk-TBJY2FF7.js → chunk-QFWHAFEO.js} +22 -2
  20. package/dist/chunk-QFWHAFEO.js.map +1 -0
  21. package/dist/{chunk-CZ6BVQZX.cjs → chunk-RE3NVA23.cjs} +155 -17
  22. package/dist/chunk-RE3NVA23.cjs.map +1 -0
  23. package/dist/{chunk-Y5EH32F5.js → chunk-S7K4FRJ2.js} +149 -14
  24. package/dist/chunk-S7K4FRJ2.js.map +1 -0
  25. package/dist/{chunk-NZWFCUDA.cjs → chunk-WAEQXGCX.cjs} +22 -2
  26. package/dist/chunk-WAEQXGCX.cjs.map +1 -0
  27. package/dist/index.cjs +209 -185
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +47 -23
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.cjs +41 -29
  34. package/dist/middleware.d.cts +38 -4
  35. package/dist/middleware.d.ts +38 -4
  36. package/dist/middleware.js +3 -3
  37. package/dist/migrations-HQI62CAO.js +4 -0
  38. package/dist/{migrations-7HQ7LYAL.js.map → migrations-HQI62CAO.js.map} +1 -1
  39. package/dist/migrations-ZYPYVSXI.cjs +13 -0
  40. package/dist/{migrations-SVQTT7NV.cjs.map → migrations-ZYPYVSXI.cjs.map} +1 -1
  41. package/dist/routes.cjs +29 -29
  42. package/dist/routes.d.cts +1 -1
  43. package/dist/routes.d.ts +1 -1
  44. package/dist/routes.js +6 -6
  45. package/dist/services.cjs +39 -39
  46. package/dist/services.d.cts +12 -0
  47. package/dist/services.d.ts +12 -0
  48. package/dist/services.js +3 -3
  49. package/dist/utils.cjs +11 -11
  50. package/dist/utils.js +1 -1
  51. package/migrations/035_user_profiles_data_column.sql +14 -15
  52. package/package.json +1 -1
  53. package/dist/chunk-6ENX7QSA.cjs.map +0 -1
  54. package/dist/chunk-CZ6BVQZX.cjs.map +0 -1
  55. package/dist/chunk-INSDRCG3.js.map +0 -1
  56. package/dist/chunk-NZWFCUDA.cjs.map +0 -1
  57. package/dist/chunk-OCLUXJ7E.cjs.map +0 -1
  58. package/dist/chunk-Q5VFZUXV.cjs.map +0 -1
  59. package/dist/chunk-TBJY2FF7.js.map +0 -1
  60. package/dist/chunk-VFQUULAV.js.map +0 -1
  61. package/dist/chunk-WLSIUKNM.js.map +0 -1
  62. package/dist/chunk-Y5EH32F5.js.map +0 -1
  63. package/dist/migrations-7HQ7LYAL.js +0 -4
  64. package/dist/migrations-SVQTT7NV.cjs +0 -13
@@ -1,17 +1,17 @@
1
- import { getCacheService, CACHE_CONFIGS, SettingsService, getLogger, getAppInstance, buildRouteList, CATEGORY_INFO } from './chunk-TBJY2FF7.js';
2
- import { requireAuth, requireRole, isPluginActive, optionalAuth, rateLimit, AuthManager, logActivity, generateCsrfToken } from './chunk-Y5EH32F5.js';
3
- import { PluginService, PLUGIN_REGISTRY, findPluginByCodeName, createContentFromSubmission } from './chunk-INSDRCG3.js';
4
- import { MigrationService } from './chunk-VFQUULAV.js';
1
+ import { getCacheService, CACHE_CONFIGS, SettingsService, getLogger, getAppInstance, buildRouteList, CATEGORY_INFO } from './chunk-QFWHAFEO.js';
2
+ import { requireAuth, requireRole, isPluginActive, optionalAuth, rateLimit, AuthManager, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, logActivity, generateCsrfToken } from './chunk-S7K4FRJ2.js';
3
+ import { PluginService, PLUGIN_REGISTRY, findPluginByCodeName, createContentFromSubmission } from './chunk-FDXNIZ6N.js';
4
+ import { MigrationService } from './chunk-NMJT6BJR.js';
5
5
  import { renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-XWIA3HVX.js';
6
6
  import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-55RDMDOP.js';
7
7
  import { PluginBuilder, TurnstileService } from './chunk-EXNEW5US.js';
8
- import { QueryFilterBuilder, getCoreVersion, getBlocksFieldConfig, parseBlocksValue } from './chunk-MVSCB4E3.js';
8
+ import { QueryFilterBuilder, getCoreVersion, getBlocksFieldConfig, parseBlocksValue } from './chunk-6F57Z6SD.js';
9
9
  import { metricsTracker } from './chunk-FICTAGD4.js';
10
10
  import { escapeHtml, sanitizeRichText, sanitizeInput } from './chunk-TQABQWOP.js';
11
11
  import { Hono } from 'hono';
12
12
  import { cors } from 'hono/cors';
13
13
  import { z } from 'zod';
14
- import { setCookie } from 'hono/cookie';
14
+ import { setCookie, getCookie } from 'hono/cookie';
15
15
  import { html, raw } from 'hono/html';
16
16
 
17
17
  // src/schemas/index.ts
@@ -2351,7 +2351,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
2351
2351
  });
2352
2352
  adminApiRoutes.get("/migrations/status", async (c) => {
2353
2353
  try {
2354
- const { MigrationService: MigrationService2 } = await import('./migrations-7HQ7LYAL.js');
2354
+ const { MigrationService: MigrationService2 } = await import('./migrations-HQI62CAO.js');
2355
2355
  const db = c.env.DB;
2356
2356
  const migrationService = new MigrationService2(db);
2357
2357
  const status = await migrationService.getMigrationStatus();
@@ -2376,7 +2376,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
2376
2376
  error: "Unauthorized. Admin access required."
2377
2377
  }, 403);
2378
2378
  }
2379
- const { MigrationService: MigrationService2 } = await import('./migrations-7HQ7LYAL.js');
2379
+ const { MigrationService: MigrationService2 } = await import('./migrations-HQI62CAO.js');
2380
2380
  const db = c.env.DB;
2381
2381
  const migrationService = new MigrationService2(db);
2382
2382
  const result = await migrationService.runPendingMigrations();
@@ -2398,7 +2398,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
2398
2398
  });
2399
2399
  adminApiRoutes.get("/migrations/validate", async (c) => {
2400
2400
  try {
2401
- const { MigrationService: MigrationService2 } = await import('./migrations-7HQ7LYAL.js');
2401
+ const { MigrationService: MigrationService2 } = await import('./migrations-HQI62CAO.js');
2402
2402
  const db = c.env.DB;
2403
2403
  const migrationService = new MigrationService2(db);
2404
2404
  const validation = await migrationService.validateSchema();
@@ -5148,16 +5148,17 @@ var userProfilesPlugin = createUserProfilesPlugin();
5148
5148
 
5149
5149
  // src/routes/auth.ts
5150
5150
  var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
5151
- async function setCsrfCookie(c) {
5151
+ async function setCsrfCookie(c, maxAge) {
5152
5152
  const secret = c.env?.JWT_SECRET || JWT_SECRET_FALLBACK;
5153
5153
  const isDev = c.env?.ENVIRONMENT === "development" || !c.env?.ENVIRONMENT;
5154
5154
  const csrfToken = await generateCsrfToken(secret);
5155
+ const cookieMaxAge = await getJwtExpirySecondsFromDb(c.env?.DB, c.env);
5155
5156
  setCookie(c, "csrf_token", csrfToken, {
5156
5157
  httpOnly: false,
5157
5158
  secure: !isDev,
5158
5159
  sameSite: "Strict",
5159
5160
  path: "/",
5160
- maxAge: 86400
5161
+ maxAge: cookieMaxAge
5161
5162
  });
5162
5163
  }
5163
5164
  function clearCsrfCookie(c) {
@@ -5279,13 +5280,13 @@ authRoutes.post(
5279
5280
  await saveCustomData(db, userId, sanitized);
5280
5281
  }
5281
5282
  }
5282
- const token = await AuthManager.generateToken(userId, normalizedEmail, "viewer", c.env.JWT_SECRET);
5283
+ const tokenTtl = await getJwtExpirySecondsFromDb(c.env.DB, c.env);
5284
+ const token = await AuthManager.generateToken(userId, normalizedEmail, "viewer", c.env.JWT_SECRET, tokenTtl);
5283
5285
  setCookie(c, "auth_token", token, {
5284
5286
  httpOnly: true,
5285
5287
  secure: true,
5286
5288
  sameSite: "Strict",
5287
- maxAge: 60 * 60 * 24
5288
- // 24 hours
5289
+ maxAge: tokenTtl
5289
5290
  });
5290
5291
  await setCsrfCookie(c);
5291
5292
  return c.json({
@@ -5348,13 +5349,13 @@ authRoutes.post(
5348
5349
  console.error("Password rehash failed (non-fatal):", rehashError);
5349
5350
  }
5350
5351
  }
5351
- const token = await AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
5352
+ const tokenTtl = await getJwtExpirySecondsFromDb(c.env.DB, c.env);
5353
+ const token = await AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET, tokenTtl);
5352
5354
  setCookie(c, "auth_token", token, {
5353
5355
  httpOnly: true,
5354
5356
  secure: true,
5355
5357
  sameSite: "Strict",
5356
- maxAge: 60 * 60 * 24
5357
- // 24 hours
5358
+ maxAge: tokenTtl
5358
5359
  });
5359
5360
  await setCsrfCookie(c);
5360
5361
  await db.prepare("UPDATE users SET last_login_at = ? WHERE id = ?").bind((/* @__PURE__ */ new Date()).getTime(), user.id).run();
@@ -5418,27 +5419,45 @@ authRoutes.get("/me", requireAuth(), async (c) => {
5418
5419
  return c.json({ error: "Failed to get user" }, 500);
5419
5420
  }
5420
5421
  });
5421
- authRoutes.post("/refresh", requireAuth(), async (c) => {
5422
- try {
5423
- const user = c.get("user");
5424
- if (!user) {
5425
- return c.json({ error: "Not authenticated" }, 401);
5422
+ authRoutes.post(
5423
+ "/refresh",
5424
+ rateLimit({ max: 60, windowMs: 60 * 1e3, keyPrefix: "refresh" }),
5425
+ async (c) => {
5426
+ try {
5427
+ let token = c.req.header("Authorization")?.replace("Bearer ", "");
5428
+ if (!token) token = getCookie(c, "auth_token");
5429
+ if (!token) {
5430
+ return c.json({ error: "Authentication required" }, 401);
5431
+ }
5432
+ const db = c.env.DB;
5433
+ const grace = await getJwtRefreshGraceSecondsFromDb(db, c.env);
5434
+ const payload = await AuthManager.verifyToken(token, c.env.JWT_SECRET, grace);
5435
+ if (!payload) {
5436
+ return c.json({ error: "Invalid or expired token" }, 401);
5437
+ }
5438
+ const row = await db.prepare("SELECT id, email, role, is_active FROM users WHERE id = ?").bind(payload.userId).first();
5439
+ if (!row || !row.is_active) {
5440
+ return c.json({ error: "User is not active" }, 401);
5441
+ }
5442
+ const tokenTtl = await getJwtExpirySecondsFromDb(db, c.env);
5443
+ const newToken = await AuthManager.generateToken(row.id, row.email, row.role, c.env.JWT_SECRET, tokenTtl);
5444
+ setCookie(c, "auth_token", newToken, {
5445
+ httpOnly: true,
5446
+ secure: true,
5447
+ sameSite: "Strict",
5448
+ maxAge: tokenTtl
5449
+ });
5450
+ await setCsrfCookie(c);
5451
+ return c.json({
5452
+ token: newToken,
5453
+ expiresIn: tokenTtl
5454
+ });
5455
+ } catch (error) {
5456
+ console.error("Token refresh error:", error);
5457
+ return c.json({ error: "Token refresh failed" }, 500);
5426
5458
  }
5427
- const token = await AuthManager.generateToken(user.userId, user.email, user.role, c.env.JWT_SECRET);
5428
- setCookie(c, "auth_token", token, {
5429
- httpOnly: true,
5430
- secure: true,
5431
- sameSite: "Strict",
5432
- maxAge: 60 * 60 * 24
5433
- // 24 hours
5434
- });
5435
- await setCsrfCookie(c);
5436
- return c.json({ token });
5437
- } catch (error) {
5438
- console.error("Token refresh error:", error);
5439
- return c.json({ error: "Token refresh failed" }, 500);
5440
5459
  }
5441
- });
5460
+ );
5442
5461
  authRoutes.post(
5443
5462
  "/register/form",
5444
5463
  rateLimit({ max: 30, windowMs: 60 * 1e3, keyPrefix: "register" }),
@@ -5523,14 +5542,14 @@ authRoutes.post(
5523
5542
  await saveCustomData(db, userId, sanitized);
5524
5543
  }
5525
5544
  }
5526
- const token = await AuthManager.generateToken(userId, normalizedEmail, role, c.env.JWT_SECRET);
5545
+ const tokenTtl = await getJwtExpirySecondsFromDb(c.env.DB, c.env);
5546
+ const token = await AuthManager.generateToken(userId, normalizedEmail, role, c.env.JWT_SECRET, tokenTtl);
5527
5547
  setCookie(c, "auth_token", token, {
5528
5548
  httpOnly: true,
5529
5549
  secure: false,
5530
5550
  // Set to true in production with HTTPS
5531
5551
  sameSite: "Strict",
5532
- maxAge: 60 * 60 * 24
5533
- // 24 hours
5552
+ maxAge: tokenTtl
5534
5553
  });
5535
5554
  await setCsrfCookie(c);
5536
5555
  const redirectUrl = role === "admin" ? "/admin/dashboard" : "/admin/dashboard";
@@ -5596,14 +5615,14 @@ authRoutes.post(
5596
5615
  console.error("Password rehash failed (non-fatal):", rehashError);
5597
5616
  }
5598
5617
  }
5599
- const token = await AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET);
5618
+ const tokenTtl = await getJwtExpirySecondsFromDb(c.env.DB, c.env);
5619
+ const token = await AuthManager.generateToken(user.id, user.email, user.role, c.env.JWT_SECRET, tokenTtl);
5600
5620
  setCookie(c, "auth_token", token, {
5601
5621
  httpOnly: true,
5602
5622
  secure: false,
5603
5623
  // Set to true in production with HTTPS
5604
5624
  sameSite: "Strict",
5605
- maxAge: 60 * 60 * 24
5606
- // 24 hours
5625
+ maxAge: tokenTtl
5607
5626
  });
5608
5627
  await setCsrfCookie(c);
5609
5628
  await db.prepare("UPDATE users SET last_login_at = ? WHERE id = ?").bind((/* @__PURE__ */ new Date()).getTime(), user.id).run();
@@ -5912,13 +5931,13 @@ authRoutes.post("/accept-invitation", async (c) => {
5912
5931
  Date.now(),
5913
5932
  invitedUser.id
5914
5933
  ).run();
5915
- const authToken = await AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role, c.env.JWT_SECRET);
5934
+ const tokenTtl = await getJwtExpirySecondsFromDb(c.env.DB, c.env);
5935
+ const authToken = await AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role, c.env.JWT_SECRET, tokenTtl);
5916
5936
  setCookie(c, "auth_token", authToken, {
5917
5937
  httpOnly: true,
5918
5938
  secure: true,
5919
5939
  sameSite: "Strict",
5920
- maxAge: 60 * 60 * 24
5921
- // 24 hours
5940
+ maxAge: tokenTtl
5922
5941
  });
5923
5942
  await setCsrfCookie(c);
5924
5943
  return c.redirect("/admin/dashboard?welcome=true");
@@ -24239,6 +24258,43 @@ function renderSettingsPage(data) {
24239
24258
  }
24240
24259
  }
24241
24260
 
24261
+ async function saveSecuritySettings() {
24262
+ const formData = new FormData();
24263
+ const expiry = document.getElementById('jwtExpiresIn');
24264
+ const grace = document.getElementById('jwtRefreshGraceSeconds');
24265
+ if (expiry) formData.append('jwtExpiresIn', expiry.value);
24266
+ if (grace) formData.append('jwtRefreshGraceSeconds', grace.value);
24267
+
24268
+ const saveBtn = document.querySelector('button[onclick="saveSecuritySettings()"]');
24269
+ const originalText = saveBtn ? saveBtn.innerHTML : '';
24270
+ if (saveBtn) {
24271
+ saveBtn.innerHTML = '<svg class="animate-spin -ml-0.5 mr-1.5 h-5 w-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>Saving...';
24272
+ saveBtn.disabled = true;
24273
+ }
24274
+
24275
+ try {
24276
+ const response = await fetch('/admin/settings/security', {
24277
+ method: 'POST',
24278
+ body: formData
24279
+ });
24280
+ const result = await response.json();
24281
+ if (result.success) {
24282
+ showNotification(result.message || 'Security settings saved successfully!', 'success');
24283
+ } else {
24284
+ showNotification(result.error || 'Failed to save security settings', 'error');
24285
+ }
24286
+ } catch (error) {
24287
+ console.error('Error saving security settings:', error);
24288
+ showNotification('Failed to save security settings. Please try again.', 'error');
24289
+ } finally {
24290
+ if (saveBtn) {
24291
+ saveBtn.innerHTML = originalText;
24292
+ saveBtn.disabled = false;
24293
+ }
24294
+ }
24295
+ }
24296
+ window.saveSecuritySettings = saveSecuritySettings;
24297
+
24242
24298
  // Migration functions
24243
24299
  window.refreshMigrationStatus = async function() {
24244
24300
  try {
@@ -24821,9 +24877,71 @@ function renderAppearanceSettings(settings) {
24821
24877
  `;
24822
24878
  }
24823
24879
  function renderSecuritySettings(settings) {
24880
+ const jwtExpiresIn = settings?.jwtExpiresIn ?? "30d";
24881
+ const jwtRefreshGraceSeconds = typeof settings?.jwtRefreshGraceSeconds === "number" ? settings.jwtRefreshGraceSeconds : 60 * 60 * 24 * 7;
24824
24882
  return `
24825
24883
  <div class="space-y-6">
24826
- <!-- WIP Notice -->
24884
+ <!-- Session / JWT card (live) -->
24885
+ <div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/10">
24886
+ <h3 class="text-lg/7 font-semibold text-zinc-950 dark:text-white">Session / JWT</h3>
24887
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">
24888
+ Configure how long a signed-in session lasts and how long an expired token can still be refreshed.
24889
+ The <code class="text-xs">JWT_EXPIRES_IN</code> and <code class="text-xs">JWT_REFRESH_GRACE_SECONDS</code>
24890
+ environment variables, when set, override the values below.
24891
+ </p>
24892
+
24893
+ <div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
24894
+ <div>
24895
+ <label for="jwtExpiresIn" class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">
24896
+ JWT Expiration
24897
+ </label>
24898
+ <input
24899
+ type="text"
24900
+ id="jwtExpiresIn"
24901
+ name="jwtExpiresIn"
24902
+ value="${jwtExpiresIn}"
24903
+ placeholder="30d"
24904
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
24905
+ />
24906
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
24907
+ Accepts <code>30d</code>, <code>12h</code>, <code>3600s</code>, or bare seconds. Default: 30 days.
24908
+ </p>
24909
+ </div>
24910
+
24911
+ <div>
24912
+ <label for="jwtRefreshGraceSeconds" class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">
24913
+ Refresh Grace Window (seconds)
24914
+ </label>
24915
+ <input
24916
+ type="number"
24917
+ id="jwtRefreshGraceSeconds"
24918
+ name="jwtRefreshGraceSeconds"
24919
+ value="${jwtRefreshGraceSeconds}"
24920
+ min="0"
24921
+ max="7776000"
24922
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
24923
+ />
24924
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
24925
+ How long an expired token can still be exchanged at <code>/auth/refresh</code>. Default: 604800 (7 days).
24926
+ </p>
24927
+ </div>
24928
+ </div>
24929
+
24930
+ <div class="mt-6 pt-4 border-t border-zinc-950/5 dark:border-white/10 flex justify-end">
24931
+ <button
24932
+ type="button"
24933
+ onclick="saveSecuritySettings()"
24934
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
24935
+ >
24936
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
24937
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
24938
+ </svg>
24939
+ Save Session Settings
24940
+ </button>
24941
+ </div>
24942
+ </div>
24943
+
24944
+ <!-- WIP Notice for remaining fields -->
24827
24945
  <div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
24828
24946
  <div class="flex items-start space-x-3">
24829
24947
  <svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -24832,7 +24950,7 @@ function renderSecuritySettings(settings) {
24832
24950
  <div class="flex-1">
24833
24951
  <h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
24834
24952
  <p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
24835
- This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
24953
+ The fields below are under development and provided for reference and design feedback only. Changes made here will not be saved.
24836
24954
  </p>
24837
24955
  </div>
24838
24956
  </div>
@@ -25717,15 +25835,24 @@ adminSettingsRoutes.get("/appearance", (c) => {
25717
25835
  };
25718
25836
  return c.html(renderSettingsPage(pageData));
25719
25837
  });
25720
- adminSettingsRoutes.get("/security", (c) => {
25838
+ adminSettingsRoutes.get("/security", async (c) => {
25721
25839
  const user = c.get("user");
25840
+ const db = c.env.DB;
25841
+ const settingsService = new SettingsService(db);
25842
+ const persisted = await settingsService.getSecuritySettings();
25843
+ const mockSettings = getMockSettings(user);
25844
+ mockSettings.security = {
25845
+ ...mockSettings.security,
25846
+ jwtExpiresIn: persisted.jwtExpiresIn,
25847
+ jwtRefreshGraceSeconds: persisted.jwtRefreshGraceSeconds
25848
+ };
25722
25849
  const pageData = {
25723
25850
  user: user ? {
25724
25851
  name: user.email,
25725
25852
  email: user.email,
25726
25853
  role: user.role
25727
25854
  } : void 0,
25728
- settings: getMockSettings(user),
25855
+ settings: mockSettings,
25729
25856
  activeTab: "security",
25730
25857
  version: c.get("appVersion")
25731
25858
  };
@@ -26036,6 +26163,55 @@ adminSettingsRoutes.post("/general", async (c) => {
26036
26163
  }, 500);
26037
26164
  }
26038
26165
  });
26166
+ adminSettingsRoutes.post("/security", async (c) => {
26167
+ try {
26168
+ const user = c.get("user");
26169
+ if (!user || user.role !== "admin") {
26170
+ return c.json({
26171
+ success: false,
26172
+ error: "Unauthorized. Admin access required."
26173
+ }, 403);
26174
+ }
26175
+ const formData = await c.req.formData();
26176
+ const db = c.env.DB;
26177
+ const settingsService = new SettingsService(db);
26178
+ const jwtExpiresInRaw = formData.get("jwtExpiresIn")?.trim() || "";
26179
+ const graceRaw = formData.get("jwtRefreshGraceSeconds")?.trim() || "";
26180
+ if (!/^\d+(?:s|m|h|d)?$/i.test(jwtExpiresInRaw)) {
26181
+ return c.json({
26182
+ success: false,
26183
+ error: "JWT expiration must be a number optionally suffixed with s/m/h/d (e.g. 30d, 12h, 3600)."
26184
+ }, 400);
26185
+ }
26186
+ const graceSeconds = Number.parseInt(graceRaw, 10);
26187
+ if (!Number.isFinite(graceSeconds) || graceSeconds < 0 || graceSeconds > 60 * 60 * 24 * 90) {
26188
+ return c.json({
26189
+ success: false,
26190
+ error: "Refresh grace must be an integer between 0 and 7776000 seconds (90 days)."
26191
+ }, 400);
26192
+ }
26193
+ const success = await settingsService.saveSecuritySettings({
26194
+ jwtExpiresIn: jwtExpiresInRaw,
26195
+ jwtRefreshGraceSeconds: graceSeconds
26196
+ });
26197
+ if (success) {
26198
+ return c.json({
26199
+ success: true,
26200
+ message: "Security settings saved successfully!"
26201
+ });
26202
+ }
26203
+ return c.json({
26204
+ success: false,
26205
+ error: "Failed to save settings"
26206
+ }, 500);
26207
+ } catch (error) {
26208
+ console.error("Error saving security settings:", error);
26209
+ return c.json({
26210
+ success: false,
26211
+ error: "Failed to save settings. Please try again."
26212
+ }, 500);
26213
+ }
26214
+ });
26039
26215
  adminSettingsRoutes.post("/", async (c) => {
26040
26216
  return c.redirect("/admin/settings/general");
26041
26217
  });
@@ -28960,5 +29136,5 @@ var ROUTES_INFO = {
28960
29136
  };
28961
29137
 
28962
29138
  export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminFormsRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, createUserProfilesPlugin, defineUserProfile, getConfirmationDialogScript2 as getConfirmationDialogScript, getUserProfileConfig, public_forms_default, renderConfirmationDialog2 as renderConfirmationDialog, router, router2, test_cleanup_default, userProfilesPlugin, userRoutes };
28963
- //# sourceMappingURL=chunk-WLSIUKNM.js.map
28964
- //# sourceMappingURL=chunk-WLSIUKNM.js.map
29139
+ //# sourceMappingURL=chunk-LZJLWW7E.js.map
29140
+ //# sourceMappingURL=chunk-LZJLWW7E.js.map