strapi-security-suite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/dist/_chunks/App-_xsdnv0p.js +191 -0
- package/dist/_chunks/App-h9NYFBrR.mjs +191 -0
- package/dist/_chunks/en-B4KWt_jN.js +4 -0
- package/dist/_chunks/en-Byx4XI2L.mjs +4 -0
- package/dist/admin/index.js +119 -0
- package/dist/admin/index.mjs +120 -0
- package/dist/server/index.js +25487 -0
- package/dist/server/index.mjs +25476 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 🛡️ Strapi Security Suite (Beta)
|
|
2
|
+
|
|
3
|
+
## **The Last Plugin You’ll Ever Need to Sleep at Night**
|
|
4
|
+
|
|
5
|
+
A high-performance, in-memory security enhancement plugin for **Strapi v5**, Session-obsessed. Built for the **chaotic genius admin** who refuses to get breached by a stale token.\
|
|
6
|
+
Powered by **rage, memory maps, and accountability.**
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ✨ Why This Exists
|
|
11
|
+
|
|
12
|
+
Because “just trusting sessions” is how *breaches happen*.\
|
|
13
|
+
Because the admin panel deserves better.\
|
|
14
|
+
Because your team deserves **a real security layer**, not a checkbox.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## ⚔️ Features That Slap
|
|
19
|
+
|
|
20
|
+
### 🔒 Auto Logout (with taste)
|
|
21
|
+
|
|
22
|
+
Kick idle admins like it’s office closing time.
|
|
23
|
+
|
|
24
|
+
- 🔍 Tracks every request
|
|
25
|
+
- ⏲️ Custom inactivity timeout from DB
|
|
26
|
+
- 🧠 Memory-first with `sessionActivityMap`
|
|
27
|
+
- 💨 Triggers soft or *nuclear* logout depending on your vibe
|
|
28
|
+
- 💾 Graceful 440s, JS responses, and gentle redirects
|
|
29
|
+
|
|
30
|
+
### 🚷 Multi-Session Lock
|
|
31
|
+
|
|
32
|
+
One admin = one session. No shadow clones allowed.
|
|
33
|
+
|
|
34
|
+
- 💥 First login wins, others are denied
|
|
35
|
+
- 🧹 Cleans old sessions like a digital janitor
|
|
36
|
+
|
|
37
|
+
### 🧄 Session Exorcism Layer™
|
|
38
|
+
|
|
39
|
+
Revoked tokens get ghosted *instantly*.\
|
|
40
|
+
Even if Strapi tries to pretend they’re still cute.
|
|
41
|
+
|
|
42
|
+
- 🔪 Middleware blocks
|
|
43
|
+
- 🪦 Session cookie wipeout
|
|
44
|
+
- 📩 Headers set for frontend rejections
|
|
45
|
+
- 🗑️ `isLoggedIn` purged with prejudice
|
|
46
|
+
|
|
47
|
+
### 🧠 Smart Middleware Stack
|
|
48
|
+
|
|
49
|
+
- `trackActivity`: Updates timestamps on every move
|
|
50
|
+
- `rejectRevokedTokens`: Blocks dead sessions like a haunted firewall
|
|
51
|
+
- `interceptRenewToken`: Stops Strapi’s clingy `/renew-token` requests from reviving zombies
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🧪 Configuration Schema
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"autoLogoutTime": 30,
|
|
60
|
+
"multipleSessionsControl": true,
|
|
61
|
+
"passwordExpiryDays": 30,
|
|
62
|
+
"nonReusablePassword": true,
|
|
63
|
+
"enablePasswordManagement": true
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Defined in the content-type:\
|
|
68
|
+
`plugin::strapi-security-suite.security_settings`
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🧠 Architecture You’ll Brag About
|
|
73
|
+
|
|
74
|
+
- 🧬 In-memory tracking via `Map()`
|
|
75
|
+
- ⏱️ `startAutoLogoutWatcher()` with 5s intervals
|
|
76
|
+
- 🔄 Frontend fetch interceptor for 440s
|
|
77
|
+
- 🧹 JS logout payload injected server-side to destroy sessions, cookies, and self-respect
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## ⚙️ Admin Panel UI
|
|
82
|
+
|
|
83
|
+
- 🎛️ Control timeouts, session logic, and password rules
|
|
84
|
+
- 📜 Planned audit logs, charts, and drama
|
|
85
|
+
- 🌌 Future dashboard: all your infra sins visualized
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🔐 Frontend Catch Logic
|
|
90
|
+
|
|
91
|
+
- Fetch wrapper intercepts `440`
|
|
92
|
+
- Purges local/session storage
|
|
93
|
+
- Sends you crying to `/session-expired`
|
|
94
|
+
- Optionally calls `/admin/logout` for drama
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 📦 Installation
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
yarn add strapi-security-suite
|
|
102
|
+
```
|
|
103
|
+
or
|
|
104
|
+
```bash
|
|
105
|
+
npm install strapi-security-suite
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 🔹 `config/plugins.js`
|
|
109
|
+
Add the following entry inside your `config/plugins.js` file:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
module.exports = ({ env }) => ({
|
|
113
|
+
'strapi-security-suite': {
|
|
114
|
+
enabled: true,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🔮 Upcoming
|
|
122
|
+
|
|
123
|
+
| Feature | Status |
|
|
124
|
+
| ------------------------------- | -------------- |
|
|
125
|
+
| Password Expiry | 🛠️ In Dev |
|
|
126
|
+
| Non-Reusable Passwords | 🛠️ In Dev |
|
|
127
|
+
| Admin Activity Logs | 🔜 |
|
|
128
|
+
| Security Dashboard | 🔜 |
|
|
129
|
+
| Brute Force Detection | 🔜 |
|
|
130
|
+
| Real-time Session Visualization | 🔜 (and spicy) |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 💥 Real-World Impact
|
|
135
|
+
|
|
136
|
+
> “We installed this and now our interns can’t share logins anymore.”\
|
|
137
|
+
> — CTO, probably
|
|
138
|
+
|
|
139
|
+
> “Our admin panel feels like it judges us now. I love it.”\
|
|
140
|
+
> — That one developer who cares
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 🧑💻 Author
|
|
145
|
+
|
|
146
|
+
[LPIX-11](mohamed.johnson@orange-sonatel.com)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 💡 Philosophy
|
|
151
|
+
|
|
152
|
+
Security should be:
|
|
153
|
+
|
|
154
|
+
- Fast
|
|
155
|
+
- Unforgiving
|
|
156
|
+
- Elegant
|
|
157
|
+
- **Mildly judgmental**
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## ⚠️ Legal Drama
|
|
162
|
+
|
|
163
|
+
> This plugin is in **Beta**.\
|
|
164
|
+
> You break it, it breaks you back, but we’ll still love you.\
|
|
165
|
+
> Not liable for insecure vibes.
|
|
166
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const admin = require("@strapi/strapi/admin");
|
|
5
|
+
const reactRouterDom = require("react-router-dom");
|
|
6
|
+
const react = require("react");
|
|
7
|
+
const designSystem = require("@strapi/design-system");
|
|
8
|
+
function IsLoggedInDebugger() {
|
|
9
|
+
const [value, setValue] = react.useState(localStorage.getItem("isLoggedIn"));
|
|
10
|
+
react.useEffect(() => {
|
|
11
|
+
const originalSetItem = Storage.prototype.setItem;
|
|
12
|
+
Storage.prototype.setItem = function(key, val) {
|
|
13
|
+
if (key === "isLoggedIn") {
|
|
14
|
+
console.log(`🕵️ isLoggedIn changed to:`, val);
|
|
15
|
+
setValue(val);
|
|
16
|
+
}
|
|
17
|
+
return originalSetItem.apply(this, arguments);
|
|
18
|
+
};
|
|
19
|
+
const interval = setInterval(() => {
|
|
20
|
+
const current = localStorage.getItem("isLoggedIn");
|
|
21
|
+
setValue(current);
|
|
22
|
+
}, 1e3);
|
|
23
|
+
return () => {
|
|
24
|
+
clearInterval(interval);
|
|
25
|
+
Storage.prototype.setItem = originalSetItem;
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
|
|
29
|
+
position: "fixed",
|
|
30
|
+
bottom: "10px",
|
|
31
|
+
right: "10px",
|
|
32
|
+
backgroundColor: "rgba(0,0,0,0.7)",
|
|
33
|
+
color: "lime",
|
|
34
|
+
padding: "10px",
|
|
35
|
+
borderRadius: "10px",
|
|
36
|
+
fontSize: "14px",
|
|
37
|
+
zIndex: 9999,
|
|
38
|
+
fontFamily: "monospace"
|
|
39
|
+
}, children: [
|
|
40
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: 'localStorage["isLoggedIn"]:' }),
|
|
41
|
+
" ",
|
|
42
|
+
String(value)
|
|
43
|
+
] });
|
|
44
|
+
}
|
|
45
|
+
const HomePage = () => {
|
|
46
|
+
const [config, setConfig] = react.useState({
|
|
47
|
+
autoLogoutTime: 30,
|
|
48
|
+
multipleSessionsControl: false,
|
|
49
|
+
passwordExpiryDays: 365,
|
|
50
|
+
nonReusablePassword: false,
|
|
51
|
+
enablePasswordManagement: false
|
|
52
|
+
});
|
|
53
|
+
const [success, setSuccess] = react.useState(false);
|
|
54
|
+
react.useEffect(() => {
|
|
55
|
+
const fetchConfig = async () => {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch("/strapi-security-suite/admin/settings");
|
|
58
|
+
const json = await res.json();
|
|
59
|
+
setConfig(json);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("❌ Failed to fetch config", e);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
fetchConfig();
|
|
65
|
+
}, []);
|
|
66
|
+
const handleChange = (key, value) => {
|
|
67
|
+
setConfig((prev) => ({ ...prev, [key]: value }));
|
|
68
|
+
};
|
|
69
|
+
const saveConfig = async () => {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch("/strapi-security-suite/admin/settings", {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json"
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(config)
|
|
77
|
+
});
|
|
78
|
+
if (res.ok) {
|
|
79
|
+
setSuccess(true);
|
|
80
|
+
setTimeout(() => setSuccess(false), 3e3);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error("❌ Save failed:", err);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 10, background: "neutral0", shadow: "filterShadow", borderRadius: "12px", children: [
|
|
87
|
+
/* @__PURE__ */ jsxRuntime.jsx(IsLoggedInDebugger, {}),
|
|
88
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", as: "h1", fontWeight: "bold", children: "Security & Session Settings" }),
|
|
89
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", paddingTop: 2, paddingBottom: 4, children: "Configure session duration, password policies, and security settings." }),
|
|
90
|
+
success && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Alert, { title: "Success", variant: "success", marginBottom: 4, children: "Configuration saved successfully!" }),
|
|
91
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "flex-start", justifyContent: "space-between", gap: 6, paddingTop: 6, wrap: "wrap", children: [
|
|
92
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
93
|
+
designSystem.Box,
|
|
94
|
+
{
|
|
95
|
+
flex: "1",
|
|
96
|
+
minWidth: "400px",
|
|
97
|
+
padding: 6,
|
|
98
|
+
background: "neutral100",
|
|
99
|
+
borderRadius: "8px",
|
|
100
|
+
shadow: "tableShadow",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "semiBold", children: "Session Management" }),
|
|
103
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, { marginTop: 2, marginBottom: 4 }),
|
|
104
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-star", gap: 4, children: [
|
|
105
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "row", gap: 4, children: [
|
|
106
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "Auto Logout Time (minutes)" }),
|
|
107
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
108
|
+
designSystem.NumberInput,
|
|
109
|
+
{
|
|
110
|
+
minValue: 1,
|
|
111
|
+
value: config.autoLogoutTime,
|
|
112
|
+
onValueChange: (value) => handleChange("autoLogoutTime", value)
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
] }),
|
|
116
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, children: [
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "Activate multiple sessions control?" }),
|
|
118
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
119
|
+
designSystem.Switch,
|
|
120
|
+
{
|
|
121
|
+
checked: config.multipleSessionsControl,
|
|
122
|
+
onCheckedChange: () => handleChange("multipleSessionsControl", !config.multipleSessionsControl)
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
] })
|
|
126
|
+
] })
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
),
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
131
|
+
designSystem.Box,
|
|
132
|
+
{
|
|
133
|
+
flex: "1",
|
|
134
|
+
minWidth: "400px",
|
|
135
|
+
minHeight: "173px",
|
|
136
|
+
padding: 6,
|
|
137
|
+
background: "neutral100",
|
|
138
|
+
borderRadius: "8px",
|
|
139
|
+
shadow: "tableShadow",
|
|
140
|
+
children: [
|
|
141
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "semiBold", children: "Password Management" }),
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, { marginTop: 2, marginBottom: 4 }),
|
|
143
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "flex-start", direction: "column", gap: 4, children: [
|
|
144
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, children: [
|
|
145
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "Activate Password Control?" }),
|
|
146
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
147
|
+
designSystem.Switch,
|
|
148
|
+
{
|
|
149
|
+
checked: config.enablePasswordManagement,
|
|
150
|
+
onCheckedChange: () => handleChange("enablePasswordManagement", !config.enablePasswordManagement)
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
] }),
|
|
154
|
+
config.enablePasswordManagement && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "flex-start", direction: "column", gap: 5, children: [
|
|
155
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "flex-start", direction: "column", gap: 3, children: [
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "Password Expiry (days)?" }),
|
|
157
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
158
|
+
designSystem.NumberInput,
|
|
159
|
+
{
|
|
160
|
+
minValue: 1,
|
|
161
|
+
value: config.passwordExpiryDays,
|
|
162
|
+
onValueChange: (value) => handleChange("passwordExpiryDays", value)
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
] }),
|
|
166
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 3, children: [
|
|
167
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "Activate Non-Reusable Password?" }),
|
|
168
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
169
|
+
designSystem.Switch,
|
|
170
|
+
{
|
|
171
|
+
checked: config.nonReusablePassword,
|
|
172
|
+
onCheckedChange: () => handleChange("nonReusablePassword", !config.nonReusablePassword)
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
] })
|
|
176
|
+
] })
|
|
177
|
+
] })
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
] }),
|
|
182
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", paddingTop: 6, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: saveConfig, variant: "secondary", size: "L", shadow: "buttonShadow", children: "Save Settings" }) })
|
|
183
|
+
] });
|
|
184
|
+
};
|
|
185
|
+
const App = () => {
|
|
186
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Routes, { children: [
|
|
187
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { index: true, element: /* @__PURE__ */ jsxRuntime.jsx(HomePage, {}) }),
|
|
188
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { path: "*", element: /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Error, {}) })
|
|
189
|
+
] });
|
|
190
|
+
};
|
|
191
|
+
exports.App = App;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Page } from "@strapi/strapi/admin";
|
|
3
|
+
import { Routes, Route } from "react-router-dom";
|
|
4
|
+
import { useState, useEffect } from "react";
|
|
5
|
+
import { Box, Typography, Alert, Flex, Divider, NumberInput, Switch, Button } from "@strapi/design-system";
|
|
6
|
+
function IsLoggedInDebugger() {
|
|
7
|
+
const [value, setValue] = useState(localStorage.getItem("isLoggedIn"));
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const originalSetItem = Storage.prototype.setItem;
|
|
10
|
+
Storage.prototype.setItem = function(key, val) {
|
|
11
|
+
if (key === "isLoggedIn") {
|
|
12
|
+
console.log(`🕵️ isLoggedIn changed to:`, val);
|
|
13
|
+
setValue(val);
|
|
14
|
+
}
|
|
15
|
+
return originalSetItem.apply(this, arguments);
|
|
16
|
+
};
|
|
17
|
+
const interval = setInterval(() => {
|
|
18
|
+
const current = localStorage.getItem("isLoggedIn");
|
|
19
|
+
setValue(current);
|
|
20
|
+
}, 1e3);
|
|
21
|
+
return () => {
|
|
22
|
+
clearInterval(interval);
|
|
23
|
+
Storage.prototype.setItem = originalSetItem;
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
27
|
+
position: "fixed",
|
|
28
|
+
bottom: "10px",
|
|
29
|
+
right: "10px",
|
|
30
|
+
backgroundColor: "rgba(0,0,0,0.7)",
|
|
31
|
+
color: "lime",
|
|
32
|
+
padding: "10px",
|
|
33
|
+
borderRadius: "10px",
|
|
34
|
+
fontSize: "14px",
|
|
35
|
+
zIndex: 9999,
|
|
36
|
+
fontFamily: "monospace"
|
|
37
|
+
}, children: [
|
|
38
|
+
/* @__PURE__ */ jsx("strong", { children: 'localStorage["isLoggedIn"]:' }),
|
|
39
|
+
" ",
|
|
40
|
+
String(value)
|
|
41
|
+
] });
|
|
42
|
+
}
|
|
43
|
+
const HomePage = () => {
|
|
44
|
+
const [config, setConfig] = useState({
|
|
45
|
+
autoLogoutTime: 30,
|
|
46
|
+
multipleSessionsControl: false,
|
|
47
|
+
passwordExpiryDays: 365,
|
|
48
|
+
nonReusablePassword: false,
|
|
49
|
+
enablePasswordManagement: false
|
|
50
|
+
});
|
|
51
|
+
const [success, setSuccess] = useState(false);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const fetchConfig = async () => {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch("/strapi-security-suite/admin/settings");
|
|
56
|
+
const json = await res.json();
|
|
57
|
+
setConfig(json);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error("❌ Failed to fetch config", e);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
fetchConfig();
|
|
63
|
+
}, []);
|
|
64
|
+
const handleChange = (key, value) => {
|
|
65
|
+
setConfig((prev) => ({ ...prev, [key]: value }));
|
|
66
|
+
};
|
|
67
|
+
const saveConfig = async () => {
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch("/strapi-security-suite/admin/settings", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json"
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(config)
|
|
75
|
+
});
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
setSuccess(true);
|
|
78
|
+
setTimeout(() => setSuccess(false), 3e3);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error("❌ Save failed:", err);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
return /* @__PURE__ */ jsxs(Box, { padding: 10, background: "neutral0", shadow: "filterShadow", borderRadius: "12px", children: [
|
|
85
|
+
/* @__PURE__ */ jsx(IsLoggedInDebugger, {}),
|
|
86
|
+
/* @__PURE__ */ jsx(Typography, { variant: "alpha", as: "h1", fontWeight: "bold", children: "Security & Session Settings" }),
|
|
87
|
+
/* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", paddingTop: 2, paddingBottom: 4, children: "Configure session duration, password policies, and security settings." }),
|
|
88
|
+
success && /* @__PURE__ */ jsx(Alert, { title: "Success", variant: "success", marginBottom: 4, children: "Configuration saved successfully!" }),
|
|
89
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", justifyContent: "space-between", gap: 6, paddingTop: 6, wrap: "wrap", children: [
|
|
90
|
+
/* @__PURE__ */ jsxs(
|
|
91
|
+
Box,
|
|
92
|
+
{
|
|
93
|
+
flex: "1",
|
|
94
|
+
minWidth: "400px",
|
|
95
|
+
padding: 6,
|
|
96
|
+
background: "neutral100",
|
|
97
|
+
borderRadius: "8px",
|
|
98
|
+
shadow: "tableShadow",
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "semiBold", children: "Session Management" }),
|
|
101
|
+
/* @__PURE__ */ jsx(Divider, { marginTop: 2, marginBottom: 4 }),
|
|
102
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-star", gap: 4, children: [
|
|
103
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "row", gap: 4, children: [
|
|
104
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Auto Logout Time (minutes)" }),
|
|
105
|
+
/* @__PURE__ */ jsx(
|
|
106
|
+
NumberInput,
|
|
107
|
+
{
|
|
108
|
+
minValue: 1,
|
|
109
|
+
value: config.autoLogoutTime,
|
|
110
|
+
onValueChange: (value) => handleChange("autoLogoutTime", value)
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
] }),
|
|
114
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
|
|
115
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Activate multiple sessions control?" }),
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
Switch,
|
|
118
|
+
{
|
|
119
|
+
checked: config.multipleSessionsControl,
|
|
120
|
+
onCheckedChange: () => handleChange("multipleSessionsControl", !config.multipleSessionsControl)
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
] })
|
|
124
|
+
] })
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
/* @__PURE__ */ jsxs(
|
|
129
|
+
Box,
|
|
130
|
+
{
|
|
131
|
+
flex: "1",
|
|
132
|
+
minWidth: "400px",
|
|
133
|
+
minHeight: "173px",
|
|
134
|
+
padding: 6,
|
|
135
|
+
background: "neutral100",
|
|
136
|
+
borderRadius: "8px",
|
|
137
|
+
shadow: "tableShadow",
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "semiBold", children: "Password Management" }),
|
|
140
|
+
/* @__PURE__ */ jsx(Divider, { marginTop: 2, marginBottom: 4 }),
|
|
141
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", direction: "column", gap: 4, children: [
|
|
142
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
|
|
143
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Activate Password Control?" }),
|
|
144
|
+
/* @__PURE__ */ jsx(
|
|
145
|
+
Switch,
|
|
146
|
+
{
|
|
147
|
+
checked: config.enablePasswordManagement,
|
|
148
|
+
onCheckedChange: () => handleChange("enablePasswordManagement", !config.enablePasswordManagement)
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
] }),
|
|
152
|
+
config.enablePasswordManagement && /* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", direction: "column", gap: 5, children: [
|
|
153
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", direction: "column", gap: 3, children: [
|
|
154
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Password Expiry (days)?" }),
|
|
155
|
+
/* @__PURE__ */ jsx(
|
|
156
|
+
NumberInput,
|
|
157
|
+
{
|
|
158
|
+
minValue: 1,
|
|
159
|
+
value: config.passwordExpiryDays,
|
|
160
|
+
onValueChange: (value) => handleChange("passwordExpiryDays", value)
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
] }),
|
|
164
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [
|
|
165
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Activate Non-Reusable Password?" }),
|
|
166
|
+
/* @__PURE__ */ jsx(
|
|
167
|
+
Switch,
|
|
168
|
+
{
|
|
169
|
+
checked: config.nonReusablePassword,
|
|
170
|
+
onCheckedChange: () => handleChange("nonReusablePassword", !config.nonReusablePassword)
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
] })
|
|
174
|
+
] })
|
|
175
|
+
] })
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", paddingTop: 6, paddingBottom: 2, children: /* @__PURE__ */ jsx(Button, { onClick: saveConfig, variant: "secondary", size: "L", shadow: "buttonShadow", children: "Save Settings" }) })
|
|
181
|
+
] });
|
|
182
|
+
};
|
|
183
|
+
const App = () => {
|
|
184
|
+
return /* @__PURE__ */ jsxs(Routes, { children: [
|
|
185
|
+
/* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(HomePage, {}) }),
|
|
186
|
+
/* @__PURE__ */ jsx(Route, { path: "*", element: /* @__PURE__ */ jsx(Page.Error, {}) })
|
|
187
|
+
] });
|
|
188
|
+
};
|
|
189
|
+
export {
|
|
190
|
+
App
|
|
191
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const react = require("react");
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const icons = require("@strapi/icons");
|
|
5
|
+
const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
|
|
6
|
+
const v = glob[path];
|
|
7
|
+
if (v) {
|
|
8
|
+
return typeof v === "function" ? v() : Promise.resolve(v);
|
|
9
|
+
}
|
|
10
|
+
return new Promise((_, reject) => {
|
|
11
|
+
(typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
|
|
12
|
+
reject.bind(
|
|
13
|
+
null,
|
|
14
|
+
new Error(
|
|
15
|
+
"Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const PLUGIN_ID = "strapi-security-suite";
|
|
22
|
+
const Initializer = ({ setPlugin }) => {
|
|
23
|
+
const ref = react.useRef(setPlugin);
|
|
24
|
+
react.useEffect(() => {
|
|
25
|
+
ref.current(PLUGIN_ID);
|
|
26
|
+
}, []);
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
const PluginIcon = () => /* @__PURE__ */ jsxRuntime.jsx(icons.User, {});
|
|
30
|
+
const autologout = () => {
|
|
31
|
+
console.warn("💥 Intercepted module import failure! Executing backup plan.");
|
|
32
|
+
localStorage.setItem("isLoggedIn", "false");
|
|
33
|
+
sessionStorage.clear();
|
|
34
|
+
document.cookie = "koa.sess=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
|
|
35
|
+
document.cookie = "koa.sess.sig=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
|
|
36
|
+
document.cookie = "jwtToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
window.location.href = "/admin/auth/login?t=" + Date.now();
|
|
39
|
+
}, 50);
|
|
40
|
+
};
|
|
41
|
+
const index = {
|
|
42
|
+
register(app) {
|
|
43
|
+
app.addMenuLink({
|
|
44
|
+
to: `plugins/${PLUGIN_ID}`,
|
|
45
|
+
icon: PluginIcon,
|
|
46
|
+
intlLabel: {
|
|
47
|
+
id: `${PLUGIN_ID}.plugin.name`,
|
|
48
|
+
defaultMessage: PLUGIN_ID
|
|
49
|
+
},
|
|
50
|
+
Component: async () => {
|
|
51
|
+
const { App } = await Promise.resolve().then(() => require("../_chunks/App-_xsdnv0p.js"));
|
|
52
|
+
return App;
|
|
53
|
+
},
|
|
54
|
+
permissions: [
|
|
55
|
+
{
|
|
56
|
+
action: "plugin::strapi-security-suite.access",
|
|
57
|
+
subject: null
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
app.registerPlugin({
|
|
62
|
+
id: PLUGIN_ID,
|
|
63
|
+
initializer: Initializer,
|
|
64
|
+
isReady: false,
|
|
65
|
+
name: PLUGIN_ID
|
|
66
|
+
});
|
|
67
|
+
const originalSetItem = Storage.prototype.setItem;
|
|
68
|
+
Storage.prototype.setItem = function(key, value) {
|
|
69
|
+
console.log(`🕵️ Intercepted localStorage.setItem:`, key, value);
|
|
70
|
+
return originalSetItem.apply(this, arguments);
|
|
71
|
+
};
|
|
72
|
+
if (!window.__fetchPatchedForSession) {
|
|
73
|
+
window.addEventListener("unhandledrejection", function(event) {
|
|
74
|
+
if (event.reason?.message?.includes("Importing a module script failed")) {
|
|
75
|
+
autologout();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
window.addEventListener("error", function(event) {
|
|
79
|
+
const message = event.message || "";
|
|
80
|
+
if (message.includes("Importing a module script failed")) {
|
|
81
|
+
autologout();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const originalFetch = window.fetch;
|
|
85
|
+
window.fetch = async (...args) => {
|
|
86
|
+
try {
|
|
87
|
+
const response = await originalFetch(...args);
|
|
88
|
+
if ([400, 440].includes(response.status)) {
|
|
89
|
+
if (window.stop) window.stop();
|
|
90
|
+
if (document.execCommand) document.execCommand("Stop");
|
|
91
|
+
window.location.reload();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.log(err);
|
|
97
|
+
if (err?.message?.includes("MIME")) {
|
|
98
|
+
window.location.reload();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
window.__fetchPatchedForSession = true;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
async registerTrads({ locales }) {
|
|
107
|
+
return Promise.all(
|
|
108
|
+
locales.map(async (locale) => {
|
|
109
|
+
try {
|
|
110
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("../_chunks/en-B4KWt_jN.js")) }), `./translations/${locale}.json`, 3);
|
|
111
|
+
return { data, locale };
|
|
112
|
+
} catch {
|
|
113
|
+
return { data: {}, locale };
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
module.exports = index;
|