strapi-plugin-magic-sessionmanager 4.4.7 → 4.5.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.
@@ -6,8 +6,8 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
- const index = require("./index-BEh2DizI.js");
10
- const useLicense = require("./useLicense-DFdVp_qI.js");
9
+ const index = require("./index-BRESWp1b.js");
10
+ const useLicense = require("./useLicense-D_wQcoMn.js");
11
11
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
12
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
13
  const theme = {
@@ -4,8 +4,8 @@ import { useFetchClient } from "@strapi/strapi/admin";
4
4
  import styled, { css, keyframes } from "styled-components";
5
5
  import { Loader, Typography, Box, Flex, Badge } from "@strapi/design-system";
6
6
  import { ChartBubble, Crown, User, Clock, Monitor } from "@strapi/icons";
7
- import { a as pluginId } from "./index-B0wQeSSu.mjs";
8
- import { u as useLicense } from "./useLicense-RxDUbCoU.mjs";
7
+ import { a as pluginId } from "./index-BbbrBv3t.mjs";
8
+ import { u as useLicense } from "./useLicense-Bszkymz3.mjs";
9
9
  const theme = {
10
10
  shadows: {
11
11
  sm: "0 1px 3px 0 rgba(0, 0, 0, 0.1)",
@@ -5,10 +5,10 @@ const react = require("react");
5
5
  const reactIntl = require("react-intl");
6
6
  const admin = require("@strapi/strapi/admin");
7
7
  const styled = require("styled-components");
8
- const index = require("./index-BEh2DizI.js");
8
+ const index = require("./index-BRESWp1b.js");
9
9
  const designSystem = require("@strapi/design-system");
10
10
  const icons = require("@strapi/icons");
11
- const useLicense = require("./useLicense-DFdVp_qI.js");
11
+ const useLicense = require("./useLicense-D_wQcoMn.js");
12
12
  const StyledButtons = require("./StyledButtons-DDuxnYz8.js");
13
13
  const reactRouterDom = require("react-router-dom");
14
14
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -2025,7 +2025,11 @@ const LicenseGuard = ({ children }) => {
2025
2025
  }
2026
2026
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
2027
2027
  };
2028
+ const pluginId = "magic-sessionmanager";
2029
+ const pluginPermissions = {
2030
+ access: [{ action: `plugin::${pluginId}.access`, subject: null }]
2031
+ };
2028
2032
  const App = () => {
2029
- return /* @__PURE__ */ jsxRuntime.jsx(LicenseGuard, { children: /* @__PURE__ */ jsxRuntime.jsx(HomePage, {}) });
2033
+ return /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Protect, { permissions: pluginPermissions.access, children: /* @__PURE__ */ jsxRuntime.jsx(LicenseGuard, { children: /* @__PURE__ */ jsxRuntime.jsx(HomePage, {}) }) });
2030
2034
  };
2031
2035
  exports.default = App;
@@ -1,12 +1,12 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
3
  import { useIntl } from "react-intl";
4
- import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
+ import { useFetchClient, useNotification, Page } from "@strapi/strapi/admin";
5
5
  import styled, { css, keyframes } from "styled-components";
6
- import { p as parseUserAgent, a as pluginId, g as getTranslation } from "./index-B0wQeSSu.mjs";
6
+ import { p as parseUserAgent, a as pluginId$1, g as getTranslation } from "./index-BbbrBv3t.mjs";
7
7
  import { Modal, Flex, Box, Typography, Divider, Button, Loader, SingleSelect, SingleSelectOption, Thead, Tr, Th, Tbody, Td, Table, TextInput } from "@strapi/design-system";
8
8
  import { Check, Information, Monitor, Server, Clock, Cross, Earth, Shield, Crown, Phone, Download, User, Eye, Trash, Search, Key } from "@strapi/icons";
9
- import { u as useLicense } from "./useLicense-RxDUbCoU.mjs";
9
+ import { u as useLicense } from "./useLicense-Bszkymz3.mjs";
10
10
  import { S as ShowHideButton, T as TertiaryButton, D as DangerButton, I as IconButtonPrimary, a as IconButtonWarning, b as IconButtonDanger } from "./StyledButtons-Cz8oYhmc.mjs";
11
11
  import { useNavigate } from "react-router-dom";
12
12
  const theme = {
@@ -145,7 +145,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
145
145
  const fetchGeolocationData = async () => {
146
146
  setGeoLoading(true);
147
147
  try {
148
- const { data } = await get(`/${pluginId}/geolocation/${session.ipAddress}`);
148
+ const { data } = await get(`/${pluginId$1}/geolocation/${session.ipAddress}`);
149
149
  setGeoData(data.data);
150
150
  } catch (err) {
151
151
  console.error("[SessionDetailModal] Error fetching geolocation:", err);
@@ -185,7 +185,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
185
185
  }
186
186
  setTerminating(true);
187
187
  try {
188
- await post(`/${pluginId}/sessions/${session.id}/terminate`);
188
+ await post(`/${pluginId$1}/sessions/${session.id}/terminate`);
189
189
  toggleNotification({
190
190
  type: "success",
191
191
  message: t("notifications.success.terminated", "Session terminated successfully")
@@ -889,7 +889,7 @@ const HomePage = () => {
889
889
  const fetchSessions = async () => {
890
890
  setLoading(true);
891
891
  try {
892
- const { data } = await get(`/${pluginId}/sessions`);
892
+ const { data } = await get(`/${pluginId$1}/sessions`);
893
893
  setSessions(data.data || []);
894
894
  } catch (err) {
895
895
  console.error("[SessionManager] Error fetching sessions:", err);
@@ -902,7 +902,7 @@ const HomePage = () => {
902
902
  return;
903
903
  }
904
904
  try {
905
- await post(`/${pluginId}/sessions/${sessionId}/terminate`);
905
+ await post(`/${pluginId$1}/sessions/${sessionId}/terminate`);
906
906
  fetchSessions();
907
907
  } catch (err) {
908
908
  console.error("[SessionManager] Error terminating session:", err);
@@ -913,7 +913,7 @@ const HomePage = () => {
913
913
  return;
914
914
  }
915
915
  try {
916
- await del(`/${pluginId}/sessions/${sessionId}`);
916
+ await del(`/${pluginId$1}/sessions/${sessionId}`);
917
917
  fetchSessions();
918
918
  toggleNotification({
919
919
  type: "success",
@@ -1645,7 +1645,7 @@ const LicenseGuard = ({ children }) => {
1645
1645
  const checkLicenseStatus = async () => {
1646
1646
  setIsChecking(true);
1647
1647
  try {
1648
- const response = await get(`/${pluginId}/license/status`);
1648
+ const response = await get(`/${pluginId$1}/license/status`);
1649
1649
  if (response.data.valid) {
1650
1650
  setNeedsLicense(false);
1651
1651
  } else {
@@ -1662,7 +1662,7 @@ const LicenseGuard = ({ children }) => {
1662
1662
  e.preventDefault();
1663
1663
  setIsCreating(true);
1664
1664
  try {
1665
- const response = await post(`/${pluginId}/license/auto-create`, {});
1665
+ const response = await post(`/${pluginId$1}/license/auto-create`, {});
1666
1666
  if (response.data && response.data.success) {
1667
1667
  toggleNotification({
1668
1668
  type: "success",
@@ -1696,7 +1696,7 @@ const LicenseGuard = ({ children }) => {
1696
1696
  }
1697
1697
  setIsCreating(true);
1698
1698
  try {
1699
- const response = await post(`/${pluginId}/license/create`, formData);
1699
+ const response = await post(`/${pluginId$1}/license/create`, formData);
1700
1700
  if (response.data && response.data.success) {
1701
1701
  toggleNotification({
1702
1702
  type: "success",
@@ -1729,7 +1729,7 @@ const LicenseGuard = ({ children }) => {
1729
1729
  }
1730
1730
  setIsCreating(true);
1731
1731
  try {
1732
- const response = await post(`/${pluginId}/license/store-key`, {
1732
+ const response = await post(`/${pluginId$1}/license/store-key`, {
1733
1733
  licenseKey: existingLicenseKey.trim(),
1734
1734
  email: existingEmail.trim()
1735
1735
  });
@@ -2021,8 +2021,12 @@ const LicenseGuard = ({ children }) => {
2021
2021
  }
2022
2022
  return /* @__PURE__ */ jsx(Fragment, { children });
2023
2023
  };
2024
+ const pluginId = "magic-sessionmanager";
2025
+ const pluginPermissions = {
2026
+ access: [{ action: `plugin::${pluginId}.access`, subject: null }]
2027
+ };
2024
2028
  const App = () => {
2025
- return /* @__PURE__ */ jsx(LicenseGuard, { children: /* @__PURE__ */ jsx(HomePage, {}) });
2029
+ return /* @__PURE__ */ jsx(Page.Protect, { permissions: pluginPermissions.access, children: /* @__PURE__ */ jsx(LicenseGuard, { children: /* @__PURE__ */ jsx(HomePage, {}) }) });
2026
2030
  };
2027
2031
  export {
2028
2032
  App as default
@@ -4,7 +4,7 @@ import { Loader, Box, Alert, Flex, Typography, Button, Badge, Accordion } from "
4
4
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
5
  import { ArrowClockwise, Duplicate, Download, User, Shield, Sparkle, ChartBubble } from "@strapi/icons";
6
6
  import styled, { css, keyframes } from "styled-components";
7
- import { a as pluginId } from "./index-B0wQeSSu.mjs";
7
+ import { a as pluginId } from "./index-BbbrBv3t.mjs";
8
8
  const theme = {
9
9
  borderRadius: { lg: "12px" }
10
10
  };
@@ -6,7 +6,7 @@ const designSystem = require("@strapi/design-system");
6
6
  const admin = require("@strapi/strapi/admin");
7
7
  const icons = require("@strapi/icons");
8
8
  const styled = require("styled-components");
9
- const index = require("./index-BEh2DizI.js");
9
+ const index = require("./index-BRESWp1b.js");
10
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
11
11
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
12
12
  const theme = {
@@ -6,7 +6,7 @@ const reactIntl = require("react-intl");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const icons = require("@strapi/icons");
8
8
  const admin = require("@strapi/strapi/admin");
9
- const index = require("./index-BEh2DizI.js");
9
+ const index = require("./index-BRESWp1b.js");
10
10
  const OnlineUsersWidget = () => {
11
11
  const { formatMessage } = reactIntl.useIntl();
12
12
  const { get } = admin.useFetchClient();
@@ -4,7 +4,7 @@ import { useIntl } from "react-intl";
4
4
  import { Box, Typography, Flex, Grid } from "@strapi/design-system";
5
5
  import { Check, Cross, Clock, User } from "@strapi/icons";
6
6
  import { useFetchClient } from "@strapi/strapi/admin";
7
- import { g as getTranslation } from "./index-B0wQeSSu.mjs";
7
+ import { g as getTranslation } from "./index-BbbrBv3t.mjs";
8
8
  const OnlineUsersWidget = () => {
9
9
  const { formatMessage } = useIntl();
10
10
  const { get } = useFetchClient();
@@ -7,8 +7,8 @@ const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
8
  const icons = require("@strapi/icons");
9
9
  const styled = require("styled-components");
10
- const index = require("./index-BEh2DizI.js");
11
- const useLicense = require("./useLicense-DFdVp_qI.js");
10
+ const index = require("./index-BRESWp1b.js");
11
+ const useLicense = require("./useLicense-D_wQcoMn.js");
12
12
  const StyledButtons = require("./StyledButtons-DDuxnYz8.js");
13
13
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
14
14
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
@@ -349,6 +349,17 @@ const SettingsPage = () => {
349
349
  cleanupInterval: 30,
350
350
  lastSeenRateLimit: 30,
351
351
  retentionDays: 90,
352
+ maxSessionAgeDays: 30,
353
+ // Grace window (ms) during which a freshly-issued JWT is accepted
354
+ // without a matching session row. Prevents strict-session enforcement
355
+ // from blocking the very first request after login. 0 disables.
356
+ sessionCreationGraceMs: 5e3,
357
+ // Opt-in fast path for the periodic idle-session cleanup: single
358
+ // SQL UPDATE instead of batched Document-Service writes. Bypasses
359
+ // lifecycle hooks; required for very large installations.
360
+ cleanupUseDbDirect: false,
361
+ strictSessionEnforcement: false,
362
+ trustedProxies: false,
352
363
  enableGeolocation: true,
353
364
  enableSecurityScoring: true,
354
365
  blockSuspiciousSessions: false,
@@ -394,7 +405,7 @@ const SettingsPage = () => {
394
405
  }
395
406
  });
396
407
  }
397
- setSettings(loadedSettings);
408
+ setSettings((prev) => ({ ...prev, ...loadedSettings }));
398
409
  } else {
399
410
  setSettings((prev) => ({ ...prev, emailTemplates: getDefaultTemplates() }));
400
411
  }
@@ -629,6 +640,45 @@ const SettingsPage = () => {
629
640
  ),
630
641
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: settings.retentionDays === -1 ? t("settings.general.retention.hintNever", "Old sessions deleted after never") : t("settings.general.retention.hint", "Old sessions deleted after {days}", { days: `${settings.retentionDays} days` }) })
631
642
  ] }) }),
643
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
644
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: t("settings.general.grace.title", "Post-Login Grace Period") }),
645
+ /* @__PURE__ */ jsxRuntime.jsxs(
646
+ designSystem.SingleSelect,
647
+ {
648
+ value: String(settings.sessionCreationGraceMs ?? 5e3),
649
+ onChange: (value) => handleChange("sessionCreationGraceMs", parseInt(value)),
650
+ children: [
651
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "0", children: t("settings.general.grace.off", "Off (strict from first request)") }),
652
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "2000", children: t("settings.general.grace.2s", "2 seconds") }),
653
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "5000", children: t("settings.general.grace.5s", "5 seconds (Recommended)") }),
654
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "10000", children: t("settings.general.grace.10s", "10 seconds") }),
655
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "30000", children: t("settings.general.grace.30s", "30 seconds") })
656
+ ]
657
+ }
658
+ ),
659
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: t(
660
+ "settings.general.grace.hint",
661
+ "Accepts a freshly-issued JWT without a matching session row for this window, preventing strict-enforcement races right after login."
662
+ ) })
663
+ ] }) }),
664
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
665
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: t("settings.general.cleanupMode.title", "Cleanup Strategy") }),
666
+ /* @__PURE__ */ jsxRuntime.jsxs(
667
+ designSystem.SingleSelect,
668
+ {
669
+ value: settings.cleanupUseDbDirect ? "db" : "document-service",
670
+ onChange: (value) => handleChange("cleanupUseDbDirect", value === "db"),
671
+ children: [
672
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "document-service", children: t("settings.general.cleanupMode.ds", "Document Service (Recommended, safe)") }),
673
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "db", children: t("settings.general.cleanupMode.db", "Direct SQL UPDATE (large-scale)") })
674
+ ]
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: t(
678
+ "settings.general.cleanupMode.hint",
679
+ "Direct SQL drains the entire idle-session backlog in one statement but bypasses lifecycle hooks. Only switch if you run >50 k active sessions."
680
+ ) })
681
+ ] }) }),
632
682
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 4, background: "danger100", style: { borderRadius: theme.borderRadius.md, border: `2px solid rgba(220, 38, 38, 0.2)` }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "flex-start", children: [
633
683
  /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { style: { width: "18px", height: "18px", color: "var(--colors-danger600, #DC2626)", flexShrink: 0, marginTop: "2px" } }),
634
684
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { flex: 1 }, children: [
@@ -787,6 +837,119 @@ const SettingsPage = () => {
787
837
  ] })
788
838
  }
789
839
  ),
840
+ /* @__PURE__ */ jsxRuntime.jsx(
841
+ designSystem.Box,
842
+ {
843
+ background: "neutral0",
844
+ padding: 6,
845
+ style: {
846
+ borderRadius: theme.borderRadius.lg,
847
+ marginBottom: "32px",
848
+ border: `2px solid ${settings.strictSessionEnforcement ? "rgba(220, 38, 38, 0.25)" : "rgba(2, 132, 199, 0.12)"}`,
849
+ background: settings.strictSessionEnforcement ? "rgba(220, 38, 38, 0.04)" : "rgba(2, 132, 199, 0.04)"
850
+ },
851
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
852
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 3, children: [
853
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Shield, { style: { width: 24, height: 24, color: settings.strictSessionEnforcement ? "var(--colors-danger600, #DC2626)" : "var(--colors-primary600, #0284C7)" } }),
854
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: t("settings.security.enforcement.title", "Session Enforcement Policy") }),
855
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: settings.strictSessionEnforcement ? "danger100" : "neutral100", textColor: settings.strictSessionEnforcement ? "danger700" : "neutral700", children: settings.strictSessionEnforcement ? t("settings.security.enforcement.badgeStrict", "STRICT") : t("settings.security.enforcement.badgeRelaxed", "RELAXED") })
856
+ ] }),
857
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", style: { lineHeight: 1.6 }, children: t("settings.security.enforcement.description", "Controls how aggressively JWT tokens are tied to an active session record. Strict mode rejects any token without a matching session, which is more secure but breaks tokens issued before this plugin was installed.") }),
858
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 6, children: [
859
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
860
+ ToggleCard,
861
+ {
862
+ $active: settings.strictSessionEnforcement,
863
+ onClick: () => handleChange("strictSessionEnforcement", !settings.strictSessionEnforcement),
864
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "row", gap: 4, style: { width: "100%" }, alignItems: "center", children: [
865
+ /* @__PURE__ */ jsxRuntime.jsx(GreenToggle, { $isActive: settings.strictSessionEnforcement, children: /* @__PURE__ */ jsxRuntime.jsx(
866
+ designSystem.Toggle,
867
+ {
868
+ checked: settings.strictSessionEnforcement,
869
+ onChange: () => handleChange("strictSessionEnforcement", !settings.strictSessionEnforcement)
870
+ }
871
+ ) }),
872
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, style: { flex: 1 }, children: [
873
+ /* @__PURE__ */ jsxRuntime.jsx(
874
+ designSystem.Typography,
875
+ {
876
+ variant: "delta",
877
+ fontWeight: "bold",
878
+ textColor: settings.strictSessionEnforcement ? "success700" : "neutral800",
879
+ style: { fontSize: "15px" },
880
+ children: t("settings.security.enforcement.strict.title", "Strict Session Enforcement")
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "12px", lineHeight: "1.5" }, children: t("settings.security.enforcement.strict.description", "Reject every authenticated request that does not have a matching session record. Recommended for production.") })
884
+ ] })
885
+ ] })
886
+ }
887
+ ) }),
888
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
889
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: [
890
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Clock, { style: { width: 14, height: 14, verticalAlign: "middle", marginRight: 6 } }),
891
+ t("settings.security.enforcement.maxAge.title", "Max Session Age")
892
+ ] }),
893
+ /* @__PURE__ */ jsxRuntime.jsxs(
894
+ designSystem.SingleSelect,
895
+ {
896
+ value: String(settings.maxSessionAgeDays),
897
+ onChange: (value) => handleChange("maxSessionAgeDays", parseInt(value)),
898
+ children: [
899
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "1", children: t("settings.security.enforcement.maxAge.1day", "1 day (Very Strict)") }),
900
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "7", children: t("settings.security.enforcement.maxAge.7days", "7 days") }),
901
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "14", children: t("settings.security.enforcement.maxAge.14days", "14 days") }),
902
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "30", children: t("settings.security.enforcement.maxAge.30days", "30 days (Recommended)") }),
903
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "60", children: t("settings.security.enforcement.maxAge.60days", "60 days") }),
904
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "90", children: t("settings.security.enforcement.maxAge.90days", "90 days") }),
905
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "180", children: t("settings.security.enforcement.maxAge.180days", "180 days") }),
906
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "365", children: t("settings.security.enforcement.maxAge.365days", "1 year") })
907
+ ]
908
+ }
909
+ ),
910
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: t("settings.security.enforcement.maxAge.hint", "Sessions older than {days} days are automatically terminated, even if still active", { days: settings.maxSessionAgeDays }) })
911
+ ] }) }),
912
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
913
+ ToggleCard,
914
+ {
915
+ $active: settings.trustedProxies,
916
+ onClick: () => handleChange("trustedProxies", !settings.trustedProxies),
917
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "row", gap: 4, style: { width: "100%" }, alignItems: "center", children: [
918
+ /* @__PURE__ */ jsxRuntime.jsx(GreenToggle, { $isActive: settings.trustedProxies, children: /* @__PURE__ */ jsxRuntime.jsx(
919
+ designSystem.Toggle,
920
+ {
921
+ checked: settings.trustedProxies,
922
+ onChange: () => handleChange("trustedProxies", !settings.trustedProxies)
923
+ }
924
+ ) }),
925
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, style: { flex: 1 }, children: [
926
+ /* @__PURE__ */ jsxRuntime.jsx(
927
+ designSystem.Typography,
928
+ {
929
+ variant: "delta",
930
+ fontWeight: "bold",
931
+ textColor: settings.trustedProxies ? "success700" : "neutral800",
932
+ style: { fontSize: "15px" },
933
+ children: t("settings.security.enforcement.trustedProxies.title", "Trust Upstream Proxy")
934
+ }
935
+ ),
936
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "12px", lineHeight: "1.5" }, children: t("settings.security.enforcement.trustedProxies.description", "Only enable when Strapi sits behind a trusted reverse proxy (nginx, Cloudflare). Otherwise clients can spoof X-Forwarded-For.") })
937
+ ] })
938
+ ] })
939
+ }
940
+ ) })
941
+ ] }),
942
+ !settings.strictSessionEnforcement && /* @__PURE__ */ jsxRuntime.jsx(
943
+ designSystem.Alert,
944
+ {
945
+ variant: "warning",
946
+ title: t("settings.security.enforcement.warning.title", "Running in Relaxed Mode"),
947
+ children: t("settings.security.enforcement.warning.body", "Tokens without a matching session record are allowed through. Manual session termination is still enforced via the token hash, but we strongly recommend enabling strict mode in production.")
948
+ }
949
+ )
950
+ ] })
951
+ }
952
+ ),
790
953
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { background: "neutral100", padding: 5, style: { borderRadius: theme.borderRadius.md, marginBottom: "32px" }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
791
954
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
792
955
  ToggleCard,
@@ -3,10 +3,10 @@ import { useState, useEffect } from "react";
3
3
  import { useIntl } from "react-intl";
4
4
  import { Flex, Loader, Typography, Button, Box, Badge, Accordion, Grid, SingleSelect, SingleSelectOption, Divider, Alert, TextInput, Toggle, NumberInput, Checkbox, Tabs } from "@strapi/design-system";
5
5
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
6
- import { Check, Information, Cog, Trash, Shield, Code, Duplicate, Mail } from "@strapi/icons";
6
+ import { Check, Information, Cog, Trash, Shield, Code, Duplicate, Clock, Mail } from "@strapi/icons";
7
7
  import styled, { css, keyframes } from "styled-components";
8
- import { a as pluginId, g as getTranslation } from "./index-B0wQeSSu.mjs";
9
- import { u as useLicense } from "./useLicense-RxDUbCoU.mjs";
8
+ import { a as pluginId, g as getTranslation } from "./index-BbbrBv3t.mjs";
9
+ import { u as useLicense } from "./useLicense-Bszkymz3.mjs";
10
10
  import { D as DangerButton, S as ShowHideButton, G as GradientButton, C as CopyButton, T as TertiaryButton, c as SecondaryButton } from "./StyledButtons-Cz8oYhmc.mjs";
11
11
  const theme = {
12
12
  borderRadius: { md: "8px", lg: "12px" }
@@ -345,6 +345,17 @@ const SettingsPage = () => {
345
345
  cleanupInterval: 30,
346
346
  lastSeenRateLimit: 30,
347
347
  retentionDays: 90,
348
+ maxSessionAgeDays: 30,
349
+ // Grace window (ms) during which a freshly-issued JWT is accepted
350
+ // without a matching session row. Prevents strict-session enforcement
351
+ // from blocking the very first request after login. 0 disables.
352
+ sessionCreationGraceMs: 5e3,
353
+ // Opt-in fast path for the periodic idle-session cleanup: single
354
+ // SQL UPDATE instead of batched Document-Service writes. Bypasses
355
+ // lifecycle hooks; required for very large installations.
356
+ cleanupUseDbDirect: false,
357
+ strictSessionEnforcement: false,
358
+ trustedProxies: false,
348
359
  enableGeolocation: true,
349
360
  enableSecurityScoring: true,
350
361
  blockSuspiciousSessions: false,
@@ -390,7 +401,7 @@ const SettingsPage = () => {
390
401
  }
391
402
  });
392
403
  }
393
- setSettings(loadedSettings);
404
+ setSettings((prev) => ({ ...prev, ...loadedSettings }));
394
405
  } else {
395
406
  setSettings((prev) => ({ ...prev, emailTemplates: getDefaultTemplates() }));
396
407
  }
@@ -625,6 +636,45 @@ const SettingsPage = () => {
625
636
  ),
626
637
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: settings.retentionDays === -1 ? t("settings.general.retention.hintNever", "Old sessions deleted after never") : t("settings.general.retention.hint", "Old sessions deleted after {days}", { days: `${settings.retentionDays} days` }) })
627
638
  ] }) }),
639
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Box, { children: [
640
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: t("settings.general.grace.title", "Post-Login Grace Period") }),
641
+ /* @__PURE__ */ jsxs(
642
+ SingleSelect,
643
+ {
644
+ value: String(settings.sessionCreationGraceMs ?? 5e3),
645
+ onChange: (value) => handleChange("sessionCreationGraceMs", parseInt(value)),
646
+ children: [
647
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "0", children: t("settings.general.grace.off", "Off (strict from first request)") }),
648
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "2000", children: t("settings.general.grace.2s", "2 seconds") }),
649
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "5000", children: t("settings.general.grace.5s", "5 seconds (Recommended)") }),
650
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "10000", children: t("settings.general.grace.10s", "10 seconds") }),
651
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "30000", children: t("settings.general.grace.30s", "30 seconds") })
652
+ ]
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: t(
656
+ "settings.general.grace.hint",
657
+ "Accepts a freshly-issued JWT without a matching session row for this window, preventing strict-enforcement races right after login."
658
+ ) })
659
+ ] }) }),
660
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Box, { children: [
661
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: t("settings.general.cleanupMode.title", "Cleanup Strategy") }),
662
+ /* @__PURE__ */ jsxs(
663
+ SingleSelect,
664
+ {
665
+ value: settings.cleanupUseDbDirect ? "db" : "document-service",
666
+ onChange: (value) => handleChange("cleanupUseDbDirect", value === "db"),
667
+ children: [
668
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "document-service", children: t("settings.general.cleanupMode.ds", "Document Service (Recommended, safe)") }),
669
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "db", children: t("settings.general.cleanupMode.db", "Direct SQL UPDATE (large-scale)") })
670
+ ]
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: t(
674
+ "settings.general.cleanupMode.hint",
675
+ "Direct SQL drains the entire idle-session backlog in one statement but bypasses lifecycle hooks. Only switch if you run >50 k active sessions."
676
+ ) })
677
+ ] }) }),
628
678
  /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsx(Box, { padding: 4, background: "danger100", style: { borderRadius: theme.borderRadius.md, border: `2px solid rgba(220, 38, 38, 0.2)` }, children: /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
629
679
  /* @__PURE__ */ jsx(Trash, { style: { width: "18px", height: "18px", color: "var(--colors-danger600, #DC2626)", flexShrink: 0, marginTop: "2px" } }),
630
680
  /* @__PURE__ */ jsxs(Box, { style: { flex: 1 }, children: [
@@ -783,6 +833,119 @@ const SettingsPage = () => {
783
833
  ] })
784
834
  }
785
835
  ),
836
+ /* @__PURE__ */ jsx(
837
+ Box,
838
+ {
839
+ background: "neutral0",
840
+ padding: 6,
841
+ style: {
842
+ borderRadius: theme.borderRadius.lg,
843
+ marginBottom: "32px",
844
+ border: `2px solid ${settings.strictSessionEnforcement ? "rgba(220, 38, 38, 0.25)" : "rgba(2, 132, 199, 0.12)"}`,
845
+ background: settings.strictSessionEnforcement ? "rgba(220, 38, 38, 0.04)" : "rgba(2, 132, 199, 0.04)"
846
+ },
847
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
848
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [
849
+ /* @__PURE__ */ jsx(Shield, { style: { width: 24, height: 24, color: settings.strictSessionEnforcement ? "var(--colors-danger600, #DC2626)" : "var(--colors-primary600, #0284C7)" } }),
850
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: t("settings.security.enforcement.title", "Session Enforcement Policy") }),
851
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: settings.strictSessionEnforcement ? "danger100" : "neutral100", textColor: settings.strictSessionEnforcement ? "danger700" : "neutral700", children: settings.strictSessionEnforcement ? t("settings.security.enforcement.badgeStrict", "STRICT") : t("settings.security.enforcement.badgeRelaxed", "RELAXED") })
852
+ ] }),
853
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", style: { lineHeight: 1.6 }, children: t("settings.security.enforcement.description", "Controls how aggressively JWT tokens are tied to an active session record. Strict mode rejects any token without a matching session, which is more secure but breaks tokens issued before this plugin was installed.") }),
854
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 6, children: [
855
+ /* @__PURE__ */ jsx(Grid.Item, { col: 12, s: 12, children: /* @__PURE__ */ jsx(
856
+ ToggleCard,
857
+ {
858
+ $active: settings.strictSessionEnforcement,
859
+ onClick: () => handleChange("strictSessionEnforcement", !settings.strictSessionEnforcement),
860
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "row", gap: 4, style: { width: "100%" }, alignItems: "center", children: [
861
+ /* @__PURE__ */ jsx(GreenToggle, { $isActive: settings.strictSessionEnforcement, children: /* @__PURE__ */ jsx(
862
+ Toggle,
863
+ {
864
+ checked: settings.strictSessionEnforcement,
865
+ onChange: () => handleChange("strictSessionEnforcement", !settings.strictSessionEnforcement)
866
+ }
867
+ ) }),
868
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, style: { flex: 1 }, children: [
869
+ /* @__PURE__ */ jsx(
870
+ Typography,
871
+ {
872
+ variant: "delta",
873
+ fontWeight: "bold",
874
+ textColor: settings.strictSessionEnforcement ? "success700" : "neutral800",
875
+ style: { fontSize: "15px" },
876
+ children: t("settings.security.enforcement.strict.title", "Strict Session Enforcement")
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "12px", lineHeight: "1.5" }, children: t("settings.security.enforcement.strict.description", "Reject every authenticated request that does not have a matching session record. Recommended for production.") })
880
+ ] })
881
+ ] })
882
+ }
883
+ ) }),
884
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Box, { children: [
885
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: [
886
+ /* @__PURE__ */ jsx(Clock, { style: { width: 14, height: 14, verticalAlign: "middle", marginRight: 6 } }),
887
+ t("settings.security.enforcement.maxAge.title", "Max Session Age")
888
+ ] }),
889
+ /* @__PURE__ */ jsxs(
890
+ SingleSelect,
891
+ {
892
+ value: String(settings.maxSessionAgeDays),
893
+ onChange: (value) => handleChange("maxSessionAgeDays", parseInt(value)),
894
+ children: [
895
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "1", children: t("settings.security.enforcement.maxAge.1day", "1 day (Very Strict)") }),
896
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "7", children: t("settings.security.enforcement.maxAge.7days", "7 days") }),
897
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "14", children: t("settings.security.enforcement.maxAge.14days", "14 days") }),
898
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "30", children: t("settings.security.enforcement.maxAge.30days", "30 days (Recommended)") }),
899
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "60", children: t("settings.security.enforcement.maxAge.60days", "60 days") }),
900
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "90", children: t("settings.security.enforcement.maxAge.90days", "90 days") }),
901
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "180", children: t("settings.security.enforcement.maxAge.180days", "180 days") }),
902
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "365", children: t("settings.security.enforcement.maxAge.365days", "1 year") })
903
+ ]
904
+ }
905
+ ),
906
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "11px", marginTop: "8px" }, children: t("settings.security.enforcement.maxAge.hint", "Sessions older than {days} days are automatically terminated, even if still active", { days: settings.maxSessionAgeDays }) })
907
+ ] }) }),
908
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsx(
909
+ ToggleCard,
910
+ {
911
+ $active: settings.trustedProxies,
912
+ onClick: () => handleChange("trustedProxies", !settings.trustedProxies),
913
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "row", gap: 4, style: { width: "100%" }, alignItems: "center", children: [
914
+ /* @__PURE__ */ jsx(GreenToggle, { $isActive: settings.trustedProxies, children: /* @__PURE__ */ jsx(
915
+ Toggle,
916
+ {
917
+ checked: settings.trustedProxies,
918
+ onChange: () => handleChange("trustedProxies", !settings.trustedProxies)
919
+ }
920
+ ) }),
921
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, style: { flex: 1 }, children: [
922
+ /* @__PURE__ */ jsx(
923
+ Typography,
924
+ {
925
+ variant: "delta",
926
+ fontWeight: "bold",
927
+ textColor: settings.trustedProxies ? "success700" : "neutral800",
928
+ style: { fontSize: "15px" },
929
+ children: t("settings.security.enforcement.trustedProxies.title", "Trust Upstream Proxy")
930
+ }
931
+ ),
932
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontSize: "12px", lineHeight: "1.5" }, children: t("settings.security.enforcement.trustedProxies.description", "Only enable when Strapi sits behind a trusted reverse proxy (nginx, Cloudflare). Otherwise clients can spoof X-Forwarded-For.") })
933
+ ] })
934
+ ] })
935
+ }
936
+ ) })
937
+ ] }),
938
+ !settings.strictSessionEnforcement && /* @__PURE__ */ jsx(
939
+ Alert,
940
+ {
941
+ variant: "warning",
942
+ title: t("settings.security.enforcement.warning.title", "Running in Relaxed Mode"),
943
+ children: t("settings.security.enforcement.warning.body", "Tokens without a matching session record are allowed through. Manual session termination is still enforced via the token hash, but we strongly recommend enabling strict mode in production.")
944
+ }
945
+ )
946
+ ] })
947
+ }
948
+ ),
786
949
  /* @__PURE__ */ jsx(Box, { background: "neutral100", padding: 5, style: { borderRadius: theme.borderRadius.md, marginBottom: "32px" }, children: /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
787
950
  /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsx(
788
951
  ToggleCard,
@@ -6,7 +6,7 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
- const index = require("./index-BEh2DizI.js");
9
+ const index = require("./index-BRESWp1b.js");
10
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
11
11
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
12
12
  const Container = styled__default.default(designSystem.Box)`