@strapi-community/plugin-io 5.1.0 → 5.2.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 +127 -0
- package/dist/_chunks/{LivePresencePanel-CNaEK-Gk.js → LivePresencePanel-2U3I3yL0.js} +35 -5
- package/dist/_chunks/{LivePresencePanel-BeNq_EnQ.mjs → LivePresencePanel-CIFG_05s.mjs} +35 -5
- package/dist/_chunks/{MonitoringPage-K5Y3hhKF.js → MonitoringPage-9f4Gzd2X.js} +1 -1
- package/dist/_chunks/{MonitoringPage-Bn9XJSlg.mjs → MonitoringPage-Bbkoh6ih.mjs} +1 -1
- package/dist/_chunks/{SettingsPage-DMbMGU6J.mjs → SettingsPage-Btz_5MuC.mjs} +1 -1
- package/dist/_chunks/{SettingsPage-4OkXJAjU.js → SettingsPage-CsRazf0j.js} +1 -1
- package/dist/_chunks/{index--2NeIKGR.js → index-BEZDDgvZ.js} +3 -3
- package/dist/_chunks/{index-CzvX8YTe.mjs → index-Dof_eA3e.mjs} +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +279 -45
- package/dist/server/index.mjs +279 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -770,6 +770,130 @@ Configure permissions in the Strapi admin panel:
|
|
|
770
770
|
|
|
771
771
|
---
|
|
772
772
|
|
|
773
|
+
## Security
|
|
774
|
+
|
|
775
|
+
The plugin implements multiple security layers to protect your real-time connections.
|
|
776
|
+
|
|
777
|
+
### Admin Session Tokens
|
|
778
|
+
|
|
779
|
+
For admin panel connections (Live Presence), the plugin uses secure session tokens:
|
|
780
|
+
|
|
781
|
+
```
|
|
782
|
+
+------------------+ +------------------+ +------------------+
|
|
783
|
+
| Admin Browser | ---> | Session Endpoint| ---> | Socket.IO |
|
|
784
|
+
| (Strapi Admin) | | /io/presence/ | | Server |
|
|
785
|
+
+------------------+ +------------------+ +------------------+
|
|
786
|
+
| | |
|
|
787
|
+
| 1. Request session | |
|
|
788
|
+
| (Admin JWT in header) | |
|
|
789
|
+
+------------------------>| |
|
|
790
|
+
| | |
|
|
791
|
+
| 2. Return session token | |
|
|
792
|
+
| (UUID, 10 min TTL) | |
|
|
793
|
+
|<------------------------+ |
|
|
794
|
+
| | |
|
|
795
|
+
| 3. Connect Socket.IO | |
|
|
796
|
+
| (Session token in auth) | |
|
|
797
|
+
+-------------------------------------------------->|
|
|
798
|
+
| | |
|
|
799
|
+
| 4. Validate & connect | |
|
|
800
|
+
|<--------------------------------------------------+
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Security Features:**
|
|
804
|
+
- **Token Hashing**: Tokens stored as SHA-256 hashes (plaintext never persisted)
|
|
805
|
+
- **Short TTL**: 10-minute expiration with automatic refresh at 70%
|
|
806
|
+
- **Usage Limits**: Max 10 reconnects per token to prevent replay attacks
|
|
807
|
+
- **Rate Limiting**: 30-second cooldown between token requests
|
|
808
|
+
- **Minimal Data**: Only essential user info stored (ID, firstname, lastname)
|
|
809
|
+
|
|
810
|
+
### Rate Limiting
|
|
811
|
+
|
|
812
|
+
Prevent abuse with configurable rate limits:
|
|
813
|
+
|
|
814
|
+
```javascript
|
|
815
|
+
// In config/plugins.js
|
|
816
|
+
module.exports = {
|
|
817
|
+
io: {
|
|
818
|
+
enabled: true,
|
|
819
|
+
config: {
|
|
820
|
+
security: {
|
|
821
|
+
rateLimit: {
|
|
822
|
+
enabled: true,
|
|
823
|
+
maxEventsPerSecond: 10, // Max events per socket per second
|
|
824
|
+
maxConnectionsPerIp: 50 // Max connections from single IP
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### IP Whitelisting/Blacklisting
|
|
833
|
+
|
|
834
|
+
Restrict access by IP address:
|
|
835
|
+
|
|
836
|
+
```javascript
|
|
837
|
+
// In config/plugins.js
|
|
838
|
+
module.exports = {
|
|
839
|
+
io: {
|
|
840
|
+
enabled: true,
|
|
841
|
+
config: {
|
|
842
|
+
security: {
|
|
843
|
+
ipWhitelist: ['192.168.1.0/24', '10.0.0.1'], // Only these IPs allowed
|
|
844
|
+
ipBlacklist: ['203.0.113.50'], // These IPs blocked
|
|
845
|
+
requireAuthentication: true // Require JWT/API token
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Security Monitoring API
|
|
853
|
+
|
|
854
|
+
Monitor active sessions via admin API:
|
|
855
|
+
|
|
856
|
+
```bash
|
|
857
|
+
# Get session statistics
|
|
858
|
+
GET /io/security/sessions
|
|
859
|
+
Authorization: Bearer <admin-jwt>
|
|
860
|
+
|
|
861
|
+
# Response:
|
|
862
|
+
{
|
|
863
|
+
"data": {
|
|
864
|
+
"activeSessions": 5,
|
|
865
|
+
"expiringSoon": 1,
|
|
866
|
+
"activeSocketConnections": 3,
|
|
867
|
+
"sessionTTL": 600000,
|
|
868
|
+
"refreshCooldown": 30000
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
# Force logout a user (invalidate all their sessions)
|
|
873
|
+
POST /io/security/invalidate/:userId
|
|
874
|
+
Authorization: Bearer <admin-jwt>
|
|
875
|
+
|
|
876
|
+
# Response:
|
|
877
|
+
{
|
|
878
|
+
"data": {
|
|
879
|
+
"userId": 1,
|
|
880
|
+
"invalidatedSessions": 2,
|
|
881
|
+
"message": "Successfully invalidated 2 session(s)"
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Best Practices
|
|
887
|
+
|
|
888
|
+
1. **Always use HTTPS** in production for encrypted WebSocket connections
|
|
889
|
+
2. **Enable authentication** for sensitive content types
|
|
890
|
+
3. **Configure CORS** to only allow your frontend domains
|
|
891
|
+
4. **Monitor connections** via the admin dashboard
|
|
892
|
+
5. **Set reasonable rate limits** based on your use case
|
|
893
|
+
6. **Review access logs** periodically for suspicious activity
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
773
897
|
## Admin Panel
|
|
774
898
|
|
|
775
899
|
The plugin provides a full admin interface for configuration and monitoring.
|
|
@@ -1327,6 +1451,9 @@ Copyright (c) 2024 Strapi Community
|
|
|
1327
1451
|
- **Admin Panel Sidebar** - Live presence panel integrated into edit view
|
|
1328
1452
|
- **Admin Session Authentication** - Secure session tokens for Socket.IO
|
|
1329
1453
|
- **Admin JWT Strategy** - New authentication strategy for admin users
|
|
1454
|
+
- **Enhanced Security** - Token hashing (SHA-256), usage limits, rate limiting
|
|
1455
|
+
- **Automatic Token Refresh** - Tokens auto-refresh at 70% of TTL
|
|
1456
|
+
- **Security Monitoring API** - Session stats and force-logout endpoints
|
|
1330
1457
|
|
|
1331
1458
|
### v5.0.0
|
|
1332
1459
|
- Strapi v5 support
|
|
@@ -7,7 +7,7 @@ const designSystem = require("@strapi/design-system");
|
|
|
7
7
|
const admin = require("@strapi/strapi/admin");
|
|
8
8
|
const styled = require("styled-components");
|
|
9
9
|
const socket_ioClient = require("socket.io-client");
|
|
10
|
-
const index$1 = require("./index
|
|
10
|
+
const index$1 = require("./index-BEZDDgvZ.js");
|
|
11
11
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
12
12
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
13
13
|
const pulse = styled.keyframes`
|
|
@@ -181,19 +181,46 @@ const LivePresencePanel = ({ documentId, model, document }) => {
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
let cancelled = false;
|
|
184
|
-
|
|
184
|
+
let refreshTimeoutId = null;
|
|
185
|
+
const getSession = async (isRefresh = false) => {
|
|
185
186
|
try {
|
|
186
|
-
|
|
187
|
+
if (!isRefresh) {
|
|
188
|
+
setPresenceState((prev) => ({ ...prev, status: "requesting" }));
|
|
189
|
+
}
|
|
187
190
|
const { data } = await post(`/${index$1.PLUGIN_ID}/presence/session`, {});
|
|
188
191
|
if (cancelled) return;
|
|
189
192
|
if (!data || !data.token) {
|
|
190
193
|
throw new Error("Invalid session response");
|
|
191
194
|
}
|
|
192
|
-
console.log(`[${index$1.PLUGIN_ID}]
|
|
195
|
+
console.log(`[${index$1.PLUGIN_ID}] Session ${isRefresh ? "refreshed" : "obtained"}:`, {
|
|
196
|
+
expiresIn: Math.round((data.expiresAt - Date.now()) / 1e3) + "s",
|
|
197
|
+
refreshAfter: Math.round((data.refreshAfter - Date.now()) / 1e3) + "s"
|
|
198
|
+
});
|
|
193
199
|
setSessionData(data);
|
|
194
|
-
|
|
200
|
+
if (!isRefresh) {
|
|
201
|
+
setPresenceState((prev) => ({ ...prev, status: "connecting" }));
|
|
202
|
+
}
|
|
203
|
+
if (data.refreshAfter) {
|
|
204
|
+
const refreshIn = data.refreshAfter - Date.now();
|
|
205
|
+
if (refreshIn > 0) {
|
|
206
|
+
console.log(`[${index$1.PLUGIN_ID}] Token refresh scheduled in ${Math.round(refreshIn / 1e3)}s`);
|
|
207
|
+
refreshTimeoutId = setTimeout(() => {
|
|
208
|
+
if (!cancelled) {
|
|
209
|
+
console.log(`[${index$1.PLUGIN_ID}] Refreshing session token...`);
|
|
210
|
+
getSession(true);
|
|
211
|
+
}
|
|
212
|
+
}, refreshIn);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
195
215
|
} catch (error2) {
|
|
196
216
|
if (cancelled) return;
|
|
217
|
+
if (error2.response?.status === 429) {
|
|
218
|
+
console.warn(`[${index$1.PLUGIN_ID}] Rate limited, retrying in 30s...`);
|
|
219
|
+
refreshTimeoutId = setTimeout(() => {
|
|
220
|
+
if (!cancelled) getSession(isRefresh);
|
|
221
|
+
}, 3e4);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
197
224
|
console.error(`[${index$1.PLUGIN_ID}] Failed to get presence session:`, error2);
|
|
198
225
|
setPresenceState((prev) => ({
|
|
199
226
|
...prev,
|
|
@@ -205,6 +232,9 @@ const LivePresencePanel = ({ documentId, model, document }) => {
|
|
|
205
232
|
getSession();
|
|
206
233
|
return () => {
|
|
207
234
|
cancelled = true;
|
|
235
|
+
if (refreshTimeoutId) {
|
|
236
|
+
clearTimeout(refreshTimeoutId);
|
|
237
|
+
}
|
|
208
238
|
};
|
|
209
239
|
}, [uid, documentId, post]);
|
|
210
240
|
React.useEffect(() => {
|
|
@@ -5,7 +5,7 @@ import { Flex } from "@strapi/design-system";
|
|
|
5
5
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
6
6
|
import styled, { css, keyframes } from "styled-components";
|
|
7
7
|
import { io } from "socket.io-client";
|
|
8
|
-
import { P as PLUGIN_ID } from "./index-
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-Dof_eA3e.mjs";
|
|
9
9
|
const pulse = keyframes`
|
|
10
10
|
0%, 100% {
|
|
11
11
|
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
|
|
@@ -177,19 +177,46 @@ const LivePresencePanel = ({ documentId, model, document }) => {
|
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
179
|
let cancelled = false;
|
|
180
|
-
|
|
180
|
+
let refreshTimeoutId = null;
|
|
181
|
+
const getSession = async (isRefresh = false) => {
|
|
181
182
|
try {
|
|
182
|
-
|
|
183
|
+
if (!isRefresh) {
|
|
184
|
+
setPresenceState((prev) => ({ ...prev, status: "requesting" }));
|
|
185
|
+
}
|
|
183
186
|
const { data } = await post(`/${PLUGIN_ID}/presence/session`, {});
|
|
184
187
|
if (cancelled) return;
|
|
185
188
|
if (!data || !data.token) {
|
|
186
189
|
throw new Error("Invalid session response");
|
|
187
190
|
}
|
|
188
|
-
console.log(`[${PLUGIN_ID}]
|
|
191
|
+
console.log(`[${PLUGIN_ID}] Session ${isRefresh ? "refreshed" : "obtained"}:`, {
|
|
192
|
+
expiresIn: Math.round((data.expiresAt - Date.now()) / 1e3) + "s",
|
|
193
|
+
refreshAfter: Math.round((data.refreshAfter - Date.now()) / 1e3) + "s"
|
|
194
|
+
});
|
|
189
195
|
setSessionData(data);
|
|
190
|
-
|
|
196
|
+
if (!isRefresh) {
|
|
197
|
+
setPresenceState((prev) => ({ ...prev, status: "connecting" }));
|
|
198
|
+
}
|
|
199
|
+
if (data.refreshAfter) {
|
|
200
|
+
const refreshIn = data.refreshAfter - Date.now();
|
|
201
|
+
if (refreshIn > 0) {
|
|
202
|
+
console.log(`[${PLUGIN_ID}] Token refresh scheduled in ${Math.round(refreshIn / 1e3)}s`);
|
|
203
|
+
refreshTimeoutId = setTimeout(() => {
|
|
204
|
+
if (!cancelled) {
|
|
205
|
+
console.log(`[${PLUGIN_ID}] Refreshing session token...`);
|
|
206
|
+
getSession(true);
|
|
207
|
+
}
|
|
208
|
+
}, refreshIn);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
191
211
|
} catch (error2) {
|
|
192
212
|
if (cancelled) return;
|
|
213
|
+
if (error2.response?.status === 429) {
|
|
214
|
+
console.warn(`[${PLUGIN_ID}] Rate limited, retrying in 30s...`);
|
|
215
|
+
refreshTimeoutId = setTimeout(() => {
|
|
216
|
+
if (!cancelled) getSession(isRefresh);
|
|
217
|
+
}, 3e4);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
193
220
|
console.error(`[${PLUGIN_ID}] Failed to get presence session:`, error2);
|
|
194
221
|
setPresenceState((prev) => ({
|
|
195
222
|
...prev,
|
|
@@ -201,6 +228,9 @@ const LivePresencePanel = ({ documentId, model, document }) => {
|
|
|
201
228
|
getSession();
|
|
202
229
|
return () => {
|
|
203
230
|
cancelled = true;
|
|
231
|
+
if (refreshTimeoutId) {
|
|
232
|
+
clearTimeout(refreshTimeoutId);
|
|
233
|
+
}
|
|
204
234
|
};
|
|
205
235
|
}, [uid, documentId, post]);
|
|
206
236
|
useEffect(() => {
|
|
@@ -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 index = require("./index-DkTxsEqL.js");
|
|
9
|
-
const index$1 = require("./index
|
|
9
|
+
const index$1 = require("./index-BEZDDgvZ.js");
|
|
10
10
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
11
11
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
12
12
|
const UsersIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" }) });
|
|
@@ -4,7 +4,7 @@ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
|
|
|
4
4
|
import styled, { css, keyframes } from "styled-components";
|
|
5
5
|
import { Box, Loader, Flex, Field, TextInput, Badge, Typography } from "@strapi/design-system";
|
|
6
6
|
import { u as useIntl } from "./index-CEh8vkxY.mjs";
|
|
7
|
-
import { P as PLUGIN_ID } from "./index-
|
|
7
|
+
import { P as PLUGIN_ID } from "./index-Dof_eA3e.mjs";
|
|
8
8
|
const UsersIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" }) });
|
|
9
9
|
const BoltIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" }) });
|
|
10
10
|
const ChartBarIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" }) });
|
|
@@ -5,7 +5,7 @@ import { Download, Upload, Check } from "@strapi/icons";
|
|
|
5
5
|
import { useFetchClient, useNotification } from "@strapi/strapi/admin";
|
|
6
6
|
import { u as useIntl } from "./index-CEh8vkxY.mjs";
|
|
7
7
|
import styled from "styled-components";
|
|
8
|
-
import { P as PLUGIN_ID } from "./index-
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-Dof_eA3e.mjs";
|
|
9
9
|
const ResponsiveMain = styled(Main)`
|
|
10
10
|
& > div {
|
|
11
11
|
padding: 16px !important;
|
|
@@ -7,7 +7,7 @@ const icons = require("@strapi/icons");
|
|
|
7
7
|
const admin = require("@strapi/strapi/admin");
|
|
8
8
|
const index = require("./index-DkTxsEqL.js");
|
|
9
9
|
const styled = require("styled-components");
|
|
10
|
-
const index$1 = require("./index
|
|
10
|
+
const index$1 = require("./index-BEZDDgvZ.js");
|
|
11
11
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
12
12
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
13
13
|
const ResponsiveMain = styled__default.default(designSystem.Main)`
|
|
@@ -51,7 +51,7 @@ const index = {
|
|
|
51
51
|
},
|
|
52
52
|
id: `${PLUGIN_ID}-settings`,
|
|
53
53
|
to: `${PLUGIN_ID}/settings`,
|
|
54
|
-
Component: () => Promise.resolve().then(() => require("./SettingsPage-
|
|
54
|
+
Component: () => Promise.resolve().then(() => require("./SettingsPage-CsRazf0j.js")).then((mod) => ({ default: mod.SettingsPage }))
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
intlLabel: {
|
|
@@ -60,7 +60,7 @@ const index = {
|
|
|
60
60
|
},
|
|
61
61
|
id: `${PLUGIN_ID}-monitoring`,
|
|
62
62
|
to: `${PLUGIN_ID}/monitoring`,
|
|
63
|
-
Component: () => Promise.resolve().then(() => require("./MonitoringPage-
|
|
63
|
+
Component: () => Promise.resolve().then(() => require("./MonitoringPage-9f4Gzd2X.js")).then((mod) => ({ default: mod.MonitoringPage }))
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
66
|
);
|
|
@@ -84,7 +84,7 @@ const index = {
|
|
|
84
84
|
async bootstrap(app) {
|
|
85
85
|
console.log(`[${PLUGIN_ID}] [INFO] Bootstrapping plugin...`);
|
|
86
86
|
try {
|
|
87
|
-
const { default: LivePresencePanel } = await Promise.resolve().then(() => require("./LivePresencePanel-
|
|
87
|
+
const { default: LivePresencePanel } = await Promise.resolve().then(() => require("./LivePresencePanel-2U3I3yL0.js"));
|
|
88
88
|
const contentManagerPlugin = app.getPlugin("content-manager");
|
|
89
89
|
if (contentManagerPlugin && contentManagerPlugin.apis) {
|
|
90
90
|
contentManagerPlugin.apis.addEditViewSidePanel([LivePresencePanel]);
|
|
@@ -50,7 +50,7 @@ const index = {
|
|
|
50
50
|
},
|
|
51
51
|
id: `${PLUGIN_ID}-settings`,
|
|
52
52
|
to: `${PLUGIN_ID}/settings`,
|
|
53
|
-
Component: () => import("./SettingsPage-
|
|
53
|
+
Component: () => import("./SettingsPage-Btz_5MuC.mjs").then((mod) => ({ default: mod.SettingsPage }))
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
intlLabel: {
|
|
@@ -59,7 +59,7 @@ const index = {
|
|
|
59
59
|
},
|
|
60
60
|
id: `${PLUGIN_ID}-monitoring`,
|
|
61
61
|
to: `${PLUGIN_ID}/monitoring`,
|
|
62
|
-
Component: () => import("./MonitoringPage-
|
|
62
|
+
Component: () => import("./MonitoringPage-Bbkoh6ih.mjs").then((mod) => ({ default: mod.MonitoringPage }))
|
|
63
63
|
}
|
|
64
64
|
]
|
|
65
65
|
);
|
|
@@ -83,7 +83,7 @@ const index = {
|
|
|
83
83
|
async bootstrap(app) {
|
|
84
84
|
console.log(`[${PLUGIN_ID}] [INFO] Bootstrapping plugin...`);
|
|
85
85
|
try {
|
|
86
|
-
const { default: LivePresencePanel } = await import("./LivePresencePanel-
|
|
86
|
+
const { default: LivePresencePanel } = await import("./LivePresencePanel-CIFG_05s.mjs");
|
|
87
87
|
const contentManagerPlugin = app.getPlugin("content-manager");
|
|
88
88
|
if (contentManagerPlugin && contentManagerPlugin.apis) {
|
|
89
89
|
contentManagerPlugin.apis.addEditViewSidePanel([LivePresencePanel]);
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -350,31 +350,56 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
350
350
|
return next(new Error("Max connections reached"));
|
|
351
351
|
}
|
|
352
352
|
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
|
|
353
|
+
const strategy2 = socket.handshake.auth?.strategy;
|
|
354
|
+
const isAdmin = socket.handshake.auth?.isAdmin === true;
|
|
353
355
|
if (token) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
filters: { id: decoded.id },
|
|
360
|
-
populate: { role: true },
|
|
361
|
-
limit: 1
|
|
362
|
-
});
|
|
363
|
-
const user = users.length > 0 ? users[0] : null;
|
|
364
|
-
if (user) {
|
|
356
|
+
if (isAdmin || strategy2 === "admin-jwt") {
|
|
357
|
+
try {
|
|
358
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
359
|
+
const session = presenceController.consumeSessionToken(token);
|
|
360
|
+
if (session) {
|
|
365
361
|
socket.user = {
|
|
366
|
-
id:
|
|
367
|
-
username: user.
|
|
368
|
-
email: user.email
|
|
369
|
-
role:
|
|
362
|
+
id: session.userId,
|
|
363
|
+
username: `${session.user.firstname || ""} ${session.user.lastname || ""}`.trim() || `Admin ${session.userId}`,
|
|
364
|
+
email: session.user.email || `admin-${session.userId}`,
|
|
365
|
+
role: "strapi-super-admin",
|
|
366
|
+
isAdmin: true
|
|
370
367
|
};
|
|
371
|
-
|
|
368
|
+
socket.adminUser = session.user;
|
|
369
|
+
presenceController.registerSocket(socket.id, token);
|
|
370
|
+
strapi2.log.info(`socket.io: Admin authenticated - ${socket.user.username} (ID: ${session.userId})`);
|
|
372
371
|
} else {
|
|
373
|
-
strapi2.log.warn(`socket.io:
|
|
372
|
+
strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
|
|
374
373
|
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
try {
|
|
379
|
+
const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
|
|
380
|
+
strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
|
|
381
|
+
if (decoded.id) {
|
|
382
|
+
const users = await strapi2.documents("plugin::users-permissions.user").findMany({
|
|
383
|
+
filters: { id: decoded.id },
|
|
384
|
+
populate: { role: true },
|
|
385
|
+
limit: 1
|
|
386
|
+
});
|
|
387
|
+
const user = users.length > 0 ? users[0] : null;
|
|
388
|
+
if (user) {
|
|
389
|
+
socket.user = {
|
|
390
|
+
id: user.id,
|
|
391
|
+
username: user.username,
|
|
392
|
+
email: user.email,
|
|
393
|
+
role: user.role?.name || "authenticated"
|
|
394
|
+
};
|
|
395
|
+
strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
|
|
396
|
+
} else {
|
|
397
|
+
strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
375
402
|
}
|
|
376
|
-
} catch (err) {
|
|
377
|
-
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
378
403
|
}
|
|
379
404
|
} else {
|
|
380
405
|
strapi2.log.debug(`socket.io: No token provided, connecting as public`);
|
|
@@ -690,6 +715,13 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
690
715
|
if (settings2.livePreview?.enabled !== false) {
|
|
691
716
|
previewService.cleanupSocket(socket.id);
|
|
692
717
|
}
|
|
718
|
+
try {
|
|
719
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
720
|
+
if (presenceController?.unregisterSocket) {
|
|
721
|
+
presenceController.unregisterSocket(socket.id);
|
|
722
|
+
}
|
|
723
|
+
} catch (e) {
|
|
724
|
+
}
|
|
693
725
|
});
|
|
694
726
|
socket.on("error", (error2) => {
|
|
695
727
|
strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
|
|
@@ -1311,19 +1343,38 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1311
1343
|
};
|
|
1312
1344
|
}
|
|
1313
1345
|
});
|
|
1314
|
-
const { randomUUID } = require$$1__default.default;
|
|
1346
|
+
const { randomUUID, createHash } = require$$1__default.default;
|
|
1315
1347
|
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1348
|
+
const activeSockets = /* @__PURE__ */ new Map();
|
|
1349
|
+
const refreshThrottle = /* @__PURE__ */ new Map();
|
|
1350
|
+
const SESSION_TTL = 10 * 60 * 1e3;
|
|
1351
|
+
const REFRESH_COOLDOWN = 30 * 1e3;
|
|
1352
|
+
const CLEANUP_INTERVAL = 2 * 60 * 1e3;
|
|
1353
|
+
const hashToken = (token) => {
|
|
1354
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1355
|
+
};
|
|
1316
1356
|
setInterval(() => {
|
|
1317
1357
|
const now = Date.now();
|
|
1318
|
-
|
|
1358
|
+
let cleaned = 0;
|
|
1359
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1319
1360
|
if (session.expiresAt < now) {
|
|
1320
|
-
sessionTokens.delete(
|
|
1361
|
+
sessionTokens.delete(tokenHash);
|
|
1362
|
+
cleaned++;
|
|
1321
1363
|
}
|
|
1322
1364
|
}
|
|
1323
|
-
|
|
1365
|
+
for (const [userId, lastRefresh] of refreshThrottle.entries()) {
|
|
1366
|
+
if (now - lastRefresh > 60 * 60 * 1e3) {
|
|
1367
|
+
refreshThrottle.delete(userId);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (cleaned > 0) {
|
|
1371
|
+
console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
|
|
1372
|
+
}
|
|
1373
|
+
}, CLEANUP_INTERVAL);
|
|
1324
1374
|
var presence$3 = ({ strapi: strapi2 }) => ({
|
|
1325
1375
|
/**
|
|
1326
1376
|
* Creates a session token for admin users to connect to Socket.IO
|
|
1377
|
+
* Implements rate limiting and secure token storage
|
|
1327
1378
|
* @param {object} ctx - Koa context
|
|
1328
1379
|
*/
|
|
1329
1380
|
async createSession(ctx) {
|
|
@@ -1332,28 +1383,40 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1332
1383
|
strapi2.log.warn("[plugin-io] Presence session requested without admin user");
|
|
1333
1384
|
return ctx.unauthorized("Admin authentication required");
|
|
1334
1385
|
}
|
|
1386
|
+
const lastRefresh = refreshThrottle.get(adminUser.id);
|
|
1387
|
+
const now = Date.now();
|
|
1388
|
+
if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN) {
|
|
1389
|
+
const waitTime = Math.ceil((REFRESH_COOLDOWN - (now - lastRefresh)) / 1e3);
|
|
1390
|
+
strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
|
|
1391
|
+
return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
|
|
1392
|
+
}
|
|
1335
1393
|
try {
|
|
1336
1394
|
const token = randomUUID();
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1395
|
+
const tokenHash = hashToken(token);
|
|
1396
|
+
const expiresAt = now + SESSION_TTL;
|
|
1397
|
+
sessionTokens.set(tokenHash, {
|
|
1398
|
+
tokenHash,
|
|
1399
|
+
userId: adminUser.id,
|
|
1340
1400
|
user: {
|
|
1341
1401
|
id: adminUser.id,
|
|
1342
|
-
|
|
1402
|
+
// Only store minimal user data needed for display
|
|
1343
1403
|
firstname: adminUser.firstname,
|
|
1344
1404
|
lastname: adminUser.lastname
|
|
1345
1405
|
},
|
|
1346
|
-
|
|
1406
|
+
createdAt: now,
|
|
1407
|
+
expiresAt,
|
|
1408
|
+
usageCount: 0,
|
|
1409
|
+
maxUsage: 10
|
|
1410
|
+
// Max reconnects with same token
|
|
1347
1411
|
});
|
|
1348
|
-
|
|
1412
|
+
refreshThrottle.set(adminUser.id, now);
|
|
1413
|
+
strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.id}`);
|
|
1349
1414
|
ctx.body = {
|
|
1350
1415
|
token,
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
lastname: adminUser.lastname
|
|
1356
|
-
},
|
|
1416
|
+
// Send plaintext token to client (only time it's exposed)
|
|
1417
|
+
expiresAt,
|
|
1418
|
+
refreshAfter: now + SESSION_TTL * 0.7,
|
|
1419
|
+
// Suggest refresh at 70% of TTL
|
|
1357
1420
|
wsPath: "/socket.io",
|
|
1358
1421
|
wsUrl: `${ctx.protocol}://${ctx.host}`
|
|
1359
1422
|
};
|
|
@@ -1363,23 +1426,142 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1363
1426
|
}
|
|
1364
1427
|
},
|
|
1365
1428
|
/**
|
|
1366
|
-
* Validates
|
|
1429
|
+
* Validates a session token and tracks usage
|
|
1430
|
+
* Implements usage limits to prevent token abuse
|
|
1367
1431
|
* @param {string} token - Session token to validate
|
|
1368
1432
|
* @returns {object|null} Session data or null if invalid/expired
|
|
1369
1433
|
*/
|
|
1370
1434
|
consumeSessionToken(token) {
|
|
1371
|
-
if (!token) {
|
|
1435
|
+
if (!token || typeof token !== "string") {
|
|
1372
1436
|
return null;
|
|
1373
1437
|
}
|
|
1374
|
-
const
|
|
1438
|
+
const tokenHash = hashToken(token);
|
|
1439
|
+
const session = sessionTokens.get(tokenHash);
|
|
1375
1440
|
if (!session) {
|
|
1441
|
+
strapi2.log.debug("[plugin-io] Token not found in session store");
|
|
1376
1442
|
return null;
|
|
1377
1443
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1444
|
+
const now = Date.now();
|
|
1445
|
+
if (session.expiresAt < now) {
|
|
1446
|
+
sessionTokens.delete(tokenHash);
|
|
1447
|
+
strapi2.log.debug("[plugin-io] Token expired, removed from store");
|
|
1380
1448
|
return null;
|
|
1381
1449
|
}
|
|
1450
|
+
if (session.usageCount >= session.maxUsage) {
|
|
1451
|
+
strapi2.log.warn(`[plugin-io] Token usage limit exceeded for user ${session.userId}`);
|
|
1452
|
+
sessionTokens.delete(tokenHash);
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
session.usageCount++;
|
|
1456
|
+
session.lastUsed = now;
|
|
1382
1457
|
return session;
|
|
1458
|
+
},
|
|
1459
|
+
/**
|
|
1460
|
+
* Registers a socket as using a specific token
|
|
1461
|
+
* @param {string} socketId - Socket ID
|
|
1462
|
+
* @param {string} token - The token being used
|
|
1463
|
+
*/
|
|
1464
|
+
registerSocket(socketId, token) {
|
|
1465
|
+
if (!socketId || !token) return;
|
|
1466
|
+
const tokenHash = hashToken(token);
|
|
1467
|
+
activeSockets.set(socketId, tokenHash);
|
|
1468
|
+
},
|
|
1469
|
+
/**
|
|
1470
|
+
* Unregisters a socket when it disconnects
|
|
1471
|
+
* @param {string} socketId - Socket ID
|
|
1472
|
+
*/
|
|
1473
|
+
unregisterSocket(socketId) {
|
|
1474
|
+
activeSockets.delete(socketId);
|
|
1475
|
+
},
|
|
1476
|
+
/**
|
|
1477
|
+
* Invalidates all sessions for a specific user (e.g., on logout)
|
|
1478
|
+
* @param {number} userId - User ID to invalidate
|
|
1479
|
+
* @returns {number} Number of sessions invalidated
|
|
1480
|
+
*/
|
|
1481
|
+
invalidateUserSessions(userId) {
|
|
1482
|
+
let invalidated = 0;
|
|
1483
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1484
|
+
if (session.userId === userId) {
|
|
1485
|
+
sessionTokens.delete(tokenHash);
|
|
1486
|
+
invalidated++;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
refreshThrottle.delete(userId);
|
|
1490
|
+
strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
|
|
1491
|
+
return invalidated;
|
|
1492
|
+
},
|
|
1493
|
+
/**
|
|
1494
|
+
* Gets session statistics (for monitoring) - internal method
|
|
1495
|
+
* @returns {object} Session statistics
|
|
1496
|
+
*/
|
|
1497
|
+
getSessionStatsInternal() {
|
|
1498
|
+
const now = Date.now();
|
|
1499
|
+
let active = 0;
|
|
1500
|
+
let expiringSoon = 0;
|
|
1501
|
+
for (const session of sessionTokens.values()) {
|
|
1502
|
+
if (session.expiresAt > now) {
|
|
1503
|
+
active++;
|
|
1504
|
+
if (session.expiresAt - now < 2 * 60 * 1e3) {
|
|
1505
|
+
expiringSoon++;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
activeSessions: active,
|
|
1511
|
+
expiringSoon,
|
|
1512
|
+
activeSocketConnections: activeSockets.size,
|
|
1513
|
+
sessionTTL: SESSION_TTL,
|
|
1514
|
+
refreshCooldown: REFRESH_COOLDOWN
|
|
1515
|
+
};
|
|
1516
|
+
},
|
|
1517
|
+
/**
|
|
1518
|
+
* HTTP Handler: Gets session statistics for admin monitoring
|
|
1519
|
+
* @param {object} ctx - Koa context
|
|
1520
|
+
*/
|
|
1521
|
+
async getSessionStats(ctx) {
|
|
1522
|
+
const adminUser = ctx.state.user;
|
|
1523
|
+
if (!adminUser) {
|
|
1524
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1525
|
+
}
|
|
1526
|
+
try {
|
|
1527
|
+
const stats = this.getSessionStatsInternal();
|
|
1528
|
+
ctx.body = { data: stats };
|
|
1529
|
+
} catch (error2) {
|
|
1530
|
+
strapi2.log.error("[plugin-io] Failed to get session stats:", error2);
|
|
1531
|
+
return ctx.internalServerError("Failed to get session statistics");
|
|
1532
|
+
}
|
|
1533
|
+
},
|
|
1534
|
+
/**
|
|
1535
|
+
* HTTP Handler: Invalidates all sessions for a specific user
|
|
1536
|
+
* @param {object} ctx - Koa context
|
|
1537
|
+
*/
|
|
1538
|
+
async invalidateUserSessionsHandler(ctx) {
|
|
1539
|
+
const adminUser = ctx.state.user;
|
|
1540
|
+
if (!adminUser) {
|
|
1541
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1542
|
+
}
|
|
1543
|
+
const { userId } = ctx.params;
|
|
1544
|
+
if (!userId) {
|
|
1545
|
+
return ctx.badRequest("User ID is required");
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
const userIdNum = parseInt(userId, 10);
|
|
1549
|
+
if (isNaN(userIdNum)) {
|
|
1550
|
+
return ctx.badRequest("Invalid user ID");
|
|
1551
|
+
}
|
|
1552
|
+
const invalidated = this.invalidateUserSessions(userIdNum);
|
|
1553
|
+
strapi2.log.info(`[plugin-io] Admin ${adminUser.id} invalidated ${invalidated} sessions for user ${userIdNum}`);
|
|
1554
|
+
ctx.body = {
|
|
1555
|
+
data: {
|
|
1556
|
+
userId: userIdNum,
|
|
1557
|
+
invalidatedSessions: invalidated,
|
|
1558
|
+
message: `Successfully invalidated ${invalidated} session(s)`
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
} catch (error2) {
|
|
1562
|
+
strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
|
|
1563
|
+
return ctx.internalServerError("Failed to invalidate sessions");
|
|
1564
|
+
}
|
|
1383
1565
|
}
|
|
1384
1566
|
});
|
|
1385
1567
|
const settings$2 = settings$3;
|
|
@@ -1471,6 +1653,24 @@ var admin$1 = {
|
|
|
1471
1653
|
config: {
|
|
1472
1654
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
1473
1655
|
}
|
|
1656
|
+
},
|
|
1657
|
+
// Security: Session statistics
|
|
1658
|
+
{
|
|
1659
|
+
method: "GET",
|
|
1660
|
+
path: "/security/sessions",
|
|
1661
|
+
handler: "presence.getSessionStats",
|
|
1662
|
+
config: {
|
|
1663
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1664
|
+
}
|
|
1665
|
+
},
|
|
1666
|
+
// Security: Invalidate user sessions (force logout)
|
|
1667
|
+
{
|
|
1668
|
+
method: "POST",
|
|
1669
|
+
path: "/security/invalidate/:userId",
|
|
1670
|
+
handler: "presence.invalidateUserSessionsHandler",
|
|
1671
|
+
config: {
|
|
1672
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1673
|
+
}
|
|
1474
1674
|
}
|
|
1475
1675
|
]
|
|
1476
1676
|
};
|
|
@@ -29677,21 +29877,55 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29677
29877
|
credentials: function(user) {
|
|
29678
29878
|
return `${this.name}-${user.id}`;
|
|
29679
29879
|
},
|
|
29680
|
-
|
|
29880
|
+
/**
|
|
29881
|
+
* Authenticates admin user via session token
|
|
29882
|
+
* @param {object} auth - Auth object containing token
|
|
29883
|
+
* @param {object} socket - Socket instance for registration
|
|
29884
|
+
* @returns {object} User data if authenticated
|
|
29885
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
29886
|
+
*/
|
|
29887
|
+
authenticate: async function(auth, socket) {
|
|
29681
29888
|
const token2 = auth.token;
|
|
29682
|
-
if (!token2) {
|
|
29889
|
+
if (!token2 || typeof token2 !== "string") {
|
|
29890
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: No token provided");
|
|
29683
29891
|
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29684
29892
|
}
|
|
29893
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29894
|
+
if (!uuidRegex.test(token2)) {
|
|
29895
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Invalid token format");
|
|
29896
|
+
throw new UnauthorizedError2("Invalid token format");
|
|
29897
|
+
}
|
|
29685
29898
|
try {
|
|
29686
29899
|
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29687
29900
|
const session = presenceController.consumeSessionToken(token2);
|
|
29688
29901
|
if (!session) {
|
|
29902
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Token not valid or expired");
|
|
29689
29903
|
throw new UnauthorizedError2("Invalid or expired session token");
|
|
29690
29904
|
}
|
|
29691
|
-
|
|
29905
|
+
if (socket?.id) {
|
|
29906
|
+
presenceController.registerSocket(socket.id, token2);
|
|
29907
|
+
}
|
|
29908
|
+
strapi2.log.info(`[plugin-io] Admin authenticated: User ID ${session.userId}`);
|
|
29909
|
+
return {
|
|
29910
|
+
id: session.userId,
|
|
29911
|
+
...session.user
|
|
29912
|
+
};
|
|
29692
29913
|
} catch (error2) {
|
|
29693
|
-
|
|
29694
|
-
|
|
29914
|
+
if (error2 instanceof UnauthorizedError2) {
|
|
29915
|
+
throw error2;
|
|
29916
|
+
}
|
|
29917
|
+
strapi2.log.error("[plugin-io] Admin session verification error:", error2.message);
|
|
29918
|
+
throw new UnauthorizedError2("Authentication failed");
|
|
29919
|
+
}
|
|
29920
|
+
},
|
|
29921
|
+
/**
|
|
29922
|
+
* Cleanup when socket disconnects
|
|
29923
|
+
* @param {object} socket - Socket instance
|
|
29924
|
+
*/
|
|
29925
|
+
onDisconnect: function(socket) {
|
|
29926
|
+
if (socket?.id) {
|
|
29927
|
+
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29928
|
+
presenceController.unregisterSocket(socket.id);
|
|
29695
29929
|
}
|
|
29696
29930
|
},
|
|
29697
29931
|
getRoomName: function(user) {
|
package/dist/server/index.mjs
CHANGED
|
@@ -318,31 +318,56 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
318
318
|
return next(new Error("Max connections reached"));
|
|
319
319
|
}
|
|
320
320
|
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
|
|
321
|
+
const strategy2 = socket.handshake.auth?.strategy;
|
|
322
|
+
const isAdmin = socket.handshake.auth?.isAdmin === true;
|
|
321
323
|
if (token) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
filters: { id: decoded.id },
|
|
328
|
-
populate: { role: true },
|
|
329
|
-
limit: 1
|
|
330
|
-
});
|
|
331
|
-
const user = users.length > 0 ? users[0] : null;
|
|
332
|
-
if (user) {
|
|
324
|
+
if (isAdmin || strategy2 === "admin-jwt") {
|
|
325
|
+
try {
|
|
326
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
327
|
+
const session = presenceController.consumeSessionToken(token);
|
|
328
|
+
if (session) {
|
|
333
329
|
socket.user = {
|
|
334
|
-
id:
|
|
335
|
-
username: user.
|
|
336
|
-
email: user.email
|
|
337
|
-
role:
|
|
330
|
+
id: session.userId,
|
|
331
|
+
username: `${session.user.firstname || ""} ${session.user.lastname || ""}`.trim() || `Admin ${session.userId}`,
|
|
332
|
+
email: session.user.email || `admin-${session.userId}`,
|
|
333
|
+
role: "strapi-super-admin",
|
|
334
|
+
isAdmin: true
|
|
338
335
|
};
|
|
339
|
-
|
|
336
|
+
socket.adminUser = session.user;
|
|
337
|
+
presenceController.registerSocket(socket.id, token);
|
|
338
|
+
strapi2.log.info(`socket.io: Admin authenticated - ${socket.user.username} (ID: ${session.userId})`);
|
|
340
339
|
} else {
|
|
341
|
-
strapi2.log.warn(`socket.io:
|
|
340
|
+
strapi2.log.warn(`socket.io: Admin session token invalid or expired`);
|
|
342
341
|
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
strapi2.log.warn(`socket.io: Admin session verification failed: ${err.message}`);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
try {
|
|
347
|
+
const decoded = await strapi2.plugin("users-permissions").service("jwt").verify(token);
|
|
348
|
+
strapi2.log.info(`socket.io: JWT decoded - user id: ${decoded.id}`);
|
|
349
|
+
if (decoded.id) {
|
|
350
|
+
const users = await strapi2.documents("plugin::users-permissions.user").findMany({
|
|
351
|
+
filters: { id: decoded.id },
|
|
352
|
+
populate: { role: true },
|
|
353
|
+
limit: 1
|
|
354
|
+
});
|
|
355
|
+
const user = users.length > 0 ? users[0] : null;
|
|
356
|
+
if (user) {
|
|
357
|
+
socket.user = {
|
|
358
|
+
id: user.id,
|
|
359
|
+
username: user.username,
|
|
360
|
+
email: user.email,
|
|
361
|
+
role: user.role?.name || "authenticated"
|
|
362
|
+
};
|
|
363
|
+
strapi2.log.info(`socket.io: User authenticated - ${user.username} (${user.email})`);
|
|
364
|
+
} else {
|
|
365
|
+
strapi2.log.warn(`socket.io: User not found for id: ${decoded.id}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
343
370
|
}
|
|
344
|
-
} catch (err) {
|
|
345
|
-
strapi2.log.warn(`socket.io: JWT verification failed: ${err.message}`);
|
|
346
371
|
}
|
|
347
372
|
} else {
|
|
348
373
|
strapi2.log.debug(`socket.io: No token provided, connecting as public`);
|
|
@@ -658,6 +683,13 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
658
683
|
if (settings2.livePreview?.enabled !== false) {
|
|
659
684
|
previewService.cleanupSocket(socket.id);
|
|
660
685
|
}
|
|
686
|
+
try {
|
|
687
|
+
const presenceController = strapi2.plugin(pluginId$6).controller("presence");
|
|
688
|
+
if (presenceController?.unregisterSocket) {
|
|
689
|
+
presenceController.unregisterSocket(socket.id);
|
|
690
|
+
}
|
|
691
|
+
} catch (e) {
|
|
692
|
+
}
|
|
661
693
|
});
|
|
662
694
|
socket.on("error", (error2) => {
|
|
663
695
|
strapi2.log.error(`socket.io: Socket error (id: ${socket.id}): ${error2.message}`);
|
|
@@ -1279,19 +1311,38 @@ var settings$3 = ({ strapi: strapi2 }) => ({
|
|
|
1279
1311
|
};
|
|
1280
1312
|
}
|
|
1281
1313
|
});
|
|
1282
|
-
const { randomUUID } = require$$1;
|
|
1314
|
+
const { randomUUID, createHash } = require$$1;
|
|
1283
1315
|
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1316
|
+
const activeSockets = /* @__PURE__ */ new Map();
|
|
1317
|
+
const refreshThrottle = /* @__PURE__ */ new Map();
|
|
1318
|
+
const SESSION_TTL = 10 * 60 * 1e3;
|
|
1319
|
+
const REFRESH_COOLDOWN = 30 * 1e3;
|
|
1320
|
+
const CLEANUP_INTERVAL = 2 * 60 * 1e3;
|
|
1321
|
+
const hashToken = (token) => {
|
|
1322
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1323
|
+
};
|
|
1284
1324
|
setInterval(() => {
|
|
1285
1325
|
const now = Date.now();
|
|
1286
|
-
|
|
1326
|
+
let cleaned = 0;
|
|
1327
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1287
1328
|
if (session.expiresAt < now) {
|
|
1288
|
-
sessionTokens.delete(
|
|
1329
|
+
sessionTokens.delete(tokenHash);
|
|
1330
|
+
cleaned++;
|
|
1289
1331
|
}
|
|
1290
1332
|
}
|
|
1291
|
-
|
|
1333
|
+
for (const [userId, lastRefresh] of refreshThrottle.entries()) {
|
|
1334
|
+
if (now - lastRefresh > 60 * 60 * 1e3) {
|
|
1335
|
+
refreshThrottle.delete(userId);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (cleaned > 0) {
|
|
1339
|
+
console.log(`[plugin-io] [CLEANUP] Removed ${cleaned} expired session tokens`);
|
|
1340
|
+
}
|
|
1341
|
+
}, CLEANUP_INTERVAL);
|
|
1292
1342
|
var presence$3 = ({ strapi: strapi2 }) => ({
|
|
1293
1343
|
/**
|
|
1294
1344
|
* Creates a session token for admin users to connect to Socket.IO
|
|
1345
|
+
* Implements rate limiting and secure token storage
|
|
1295
1346
|
* @param {object} ctx - Koa context
|
|
1296
1347
|
*/
|
|
1297
1348
|
async createSession(ctx) {
|
|
@@ -1300,28 +1351,40 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1300
1351
|
strapi2.log.warn("[plugin-io] Presence session requested without admin user");
|
|
1301
1352
|
return ctx.unauthorized("Admin authentication required");
|
|
1302
1353
|
}
|
|
1354
|
+
const lastRefresh = refreshThrottle.get(adminUser.id);
|
|
1355
|
+
const now = Date.now();
|
|
1356
|
+
if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN) {
|
|
1357
|
+
const waitTime = Math.ceil((REFRESH_COOLDOWN - (now - lastRefresh)) / 1e3);
|
|
1358
|
+
strapi2.log.warn(`[plugin-io] Rate limit: User ${adminUser.id} must wait ${waitTime}s`);
|
|
1359
|
+
return ctx.tooManyRequests(`Please wait ${waitTime} seconds before requesting a new session`);
|
|
1360
|
+
}
|
|
1303
1361
|
try {
|
|
1304
1362
|
const token = randomUUID();
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1363
|
+
const tokenHash = hashToken(token);
|
|
1364
|
+
const expiresAt = now + SESSION_TTL;
|
|
1365
|
+
sessionTokens.set(tokenHash, {
|
|
1366
|
+
tokenHash,
|
|
1367
|
+
userId: adminUser.id,
|
|
1308
1368
|
user: {
|
|
1309
1369
|
id: adminUser.id,
|
|
1310
|
-
|
|
1370
|
+
// Only store minimal user data needed for display
|
|
1311
1371
|
firstname: adminUser.firstname,
|
|
1312
1372
|
lastname: adminUser.lastname
|
|
1313
1373
|
},
|
|
1314
|
-
|
|
1374
|
+
createdAt: now,
|
|
1375
|
+
expiresAt,
|
|
1376
|
+
usageCount: 0,
|
|
1377
|
+
maxUsage: 10
|
|
1378
|
+
// Max reconnects with same token
|
|
1315
1379
|
});
|
|
1316
|
-
|
|
1380
|
+
refreshThrottle.set(adminUser.id, now);
|
|
1381
|
+
strapi2.log.info(`[plugin-io] Presence session created for admin user: ${adminUser.id}`);
|
|
1317
1382
|
ctx.body = {
|
|
1318
1383
|
token,
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
lastname: adminUser.lastname
|
|
1324
|
-
},
|
|
1384
|
+
// Send plaintext token to client (only time it's exposed)
|
|
1385
|
+
expiresAt,
|
|
1386
|
+
refreshAfter: now + SESSION_TTL * 0.7,
|
|
1387
|
+
// Suggest refresh at 70% of TTL
|
|
1325
1388
|
wsPath: "/socket.io",
|
|
1326
1389
|
wsUrl: `${ctx.protocol}://${ctx.host}`
|
|
1327
1390
|
};
|
|
@@ -1331,23 +1394,142 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1331
1394
|
}
|
|
1332
1395
|
},
|
|
1333
1396
|
/**
|
|
1334
|
-
* Validates
|
|
1397
|
+
* Validates a session token and tracks usage
|
|
1398
|
+
* Implements usage limits to prevent token abuse
|
|
1335
1399
|
* @param {string} token - Session token to validate
|
|
1336
1400
|
* @returns {object|null} Session data or null if invalid/expired
|
|
1337
1401
|
*/
|
|
1338
1402
|
consumeSessionToken(token) {
|
|
1339
|
-
if (!token) {
|
|
1403
|
+
if (!token || typeof token !== "string") {
|
|
1340
1404
|
return null;
|
|
1341
1405
|
}
|
|
1342
|
-
const
|
|
1406
|
+
const tokenHash = hashToken(token);
|
|
1407
|
+
const session = sessionTokens.get(tokenHash);
|
|
1343
1408
|
if (!session) {
|
|
1409
|
+
strapi2.log.debug("[plugin-io] Token not found in session store");
|
|
1344
1410
|
return null;
|
|
1345
1411
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1412
|
+
const now = Date.now();
|
|
1413
|
+
if (session.expiresAt < now) {
|
|
1414
|
+
sessionTokens.delete(tokenHash);
|
|
1415
|
+
strapi2.log.debug("[plugin-io] Token expired, removed from store");
|
|
1348
1416
|
return null;
|
|
1349
1417
|
}
|
|
1418
|
+
if (session.usageCount >= session.maxUsage) {
|
|
1419
|
+
strapi2.log.warn(`[plugin-io] Token usage limit exceeded for user ${session.userId}`);
|
|
1420
|
+
sessionTokens.delete(tokenHash);
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
session.usageCount++;
|
|
1424
|
+
session.lastUsed = now;
|
|
1350
1425
|
return session;
|
|
1426
|
+
},
|
|
1427
|
+
/**
|
|
1428
|
+
* Registers a socket as using a specific token
|
|
1429
|
+
* @param {string} socketId - Socket ID
|
|
1430
|
+
* @param {string} token - The token being used
|
|
1431
|
+
*/
|
|
1432
|
+
registerSocket(socketId, token) {
|
|
1433
|
+
if (!socketId || !token) return;
|
|
1434
|
+
const tokenHash = hashToken(token);
|
|
1435
|
+
activeSockets.set(socketId, tokenHash);
|
|
1436
|
+
},
|
|
1437
|
+
/**
|
|
1438
|
+
* Unregisters a socket when it disconnects
|
|
1439
|
+
* @param {string} socketId - Socket ID
|
|
1440
|
+
*/
|
|
1441
|
+
unregisterSocket(socketId) {
|
|
1442
|
+
activeSockets.delete(socketId);
|
|
1443
|
+
},
|
|
1444
|
+
/**
|
|
1445
|
+
* Invalidates all sessions for a specific user (e.g., on logout)
|
|
1446
|
+
* @param {number} userId - User ID to invalidate
|
|
1447
|
+
* @returns {number} Number of sessions invalidated
|
|
1448
|
+
*/
|
|
1449
|
+
invalidateUserSessions(userId) {
|
|
1450
|
+
let invalidated = 0;
|
|
1451
|
+
for (const [tokenHash, session] of sessionTokens.entries()) {
|
|
1452
|
+
if (session.userId === userId) {
|
|
1453
|
+
sessionTokens.delete(tokenHash);
|
|
1454
|
+
invalidated++;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
refreshThrottle.delete(userId);
|
|
1458
|
+
strapi2.log.info(`[plugin-io] Invalidated ${invalidated} sessions for user ${userId}`);
|
|
1459
|
+
return invalidated;
|
|
1460
|
+
},
|
|
1461
|
+
/**
|
|
1462
|
+
* Gets session statistics (for monitoring) - internal method
|
|
1463
|
+
* @returns {object} Session statistics
|
|
1464
|
+
*/
|
|
1465
|
+
getSessionStatsInternal() {
|
|
1466
|
+
const now = Date.now();
|
|
1467
|
+
let active = 0;
|
|
1468
|
+
let expiringSoon = 0;
|
|
1469
|
+
for (const session of sessionTokens.values()) {
|
|
1470
|
+
if (session.expiresAt > now) {
|
|
1471
|
+
active++;
|
|
1472
|
+
if (session.expiresAt - now < 2 * 60 * 1e3) {
|
|
1473
|
+
expiringSoon++;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
activeSessions: active,
|
|
1479
|
+
expiringSoon,
|
|
1480
|
+
activeSocketConnections: activeSockets.size,
|
|
1481
|
+
sessionTTL: SESSION_TTL,
|
|
1482
|
+
refreshCooldown: REFRESH_COOLDOWN
|
|
1483
|
+
};
|
|
1484
|
+
},
|
|
1485
|
+
/**
|
|
1486
|
+
* HTTP Handler: Gets session statistics for admin monitoring
|
|
1487
|
+
* @param {object} ctx - Koa context
|
|
1488
|
+
*/
|
|
1489
|
+
async getSessionStats(ctx) {
|
|
1490
|
+
const adminUser = ctx.state.user;
|
|
1491
|
+
if (!adminUser) {
|
|
1492
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
const stats = this.getSessionStatsInternal();
|
|
1496
|
+
ctx.body = { data: stats };
|
|
1497
|
+
} catch (error2) {
|
|
1498
|
+
strapi2.log.error("[plugin-io] Failed to get session stats:", error2);
|
|
1499
|
+
return ctx.internalServerError("Failed to get session statistics");
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
/**
|
|
1503
|
+
* HTTP Handler: Invalidates all sessions for a specific user
|
|
1504
|
+
* @param {object} ctx - Koa context
|
|
1505
|
+
*/
|
|
1506
|
+
async invalidateUserSessionsHandler(ctx) {
|
|
1507
|
+
const adminUser = ctx.state.user;
|
|
1508
|
+
if (!adminUser) {
|
|
1509
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1510
|
+
}
|
|
1511
|
+
const { userId } = ctx.params;
|
|
1512
|
+
if (!userId) {
|
|
1513
|
+
return ctx.badRequest("User ID is required");
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
const userIdNum = parseInt(userId, 10);
|
|
1517
|
+
if (isNaN(userIdNum)) {
|
|
1518
|
+
return ctx.badRequest("Invalid user ID");
|
|
1519
|
+
}
|
|
1520
|
+
const invalidated = this.invalidateUserSessions(userIdNum);
|
|
1521
|
+
strapi2.log.info(`[plugin-io] Admin ${adminUser.id} invalidated ${invalidated} sessions for user ${userIdNum}`);
|
|
1522
|
+
ctx.body = {
|
|
1523
|
+
data: {
|
|
1524
|
+
userId: userIdNum,
|
|
1525
|
+
invalidatedSessions: invalidated,
|
|
1526
|
+
message: `Successfully invalidated ${invalidated} session(s)`
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
} catch (error2) {
|
|
1530
|
+
strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
|
|
1531
|
+
return ctx.internalServerError("Failed to invalidate sessions");
|
|
1532
|
+
}
|
|
1351
1533
|
}
|
|
1352
1534
|
});
|
|
1353
1535
|
const settings$2 = settings$3;
|
|
@@ -1439,6 +1621,24 @@ var admin$1 = {
|
|
|
1439
1621
|
config: {
|
|
1440
1622
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
1441
1623
|
}
|
|
1624
|
+
},
|
|
1625
|
+
// Security: Session statistics
|
|
1626
|
+
{
|
|
1627
|
+
method: "GET",
|
|
1628
|
+
path: "/security/sessions",
|
|
1629
|
+
handler: "presence.getSessionStats",
|
|
1630
|
+
config: {
|
|
1631
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1632
|
+
}
|
|
1633
|
+
},
|
|
1634
|
+
// Security: Invalidate user sessions (force logout)
|
|
1635
|
+
{
|
|
1636
|
+
method: "POST",
|
|
1637
|
+
path: "/security/invalidate/:userId",
|
|
1638
|
+
handler: "presence.invalidateUserSessionsHandler",
|
|
1639
|
+
config: {
|
|
1640
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1641
|
+
}
|
|
1442
1642
|
}
|
|
1443
1643
|
]
|
|
1444
1644
|
};
|
|
@@ -29645,21 +29845,55 @@ var strategies = ({ strapi: strapi2 }) => {
|
|
|
29645
29845
|
credentials: function(user) {
|
|
29646
29846
|
return `${this.name}-${user.id}`;
|
|
29647
29847
|
},
|
|
29648
|
-
|
|
29848
|
+
/**
|
|
29849
|
+
* Authenticates admin user via session token
|
|
29850
|
+
* @param {object} auth - Auth object containing token
|
|
29851
|
+
* @param {object} socket - Socket instance for registration
|
|
29852
|
+
* @returns {object} User data if authenticated
|
|
29853
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
29854
|
+
*/
|
|
29855
|
+
authenticate: async function(auth, socket) {
|
|
29649
29856
|
const token2 = auth.token;
|
|
29650
|
-
if (!token2) {
|
|
29857
|
+
if (!token2 || typeof token2 !== "string") {
|
|
29858
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: No token provided");
|
|
29651
29859
|
throw new UnauthorizedError2("Invalid admin credentials");
|
|
29652
29860
|
}
|
|
29861
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29862
|
+
if (!uuidRegex.test(token2)) {
|
|
29863
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Invalid token format");
|
|
29864
|
+
throw new UnauthorizedError2("Invalid token format");
|
|
29865
|
+
}
|
|
29653
29866
|
try {
|
|
29654
29867
|
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29655
29868
|
const session = presenceController.consumeSessionToken(token2);
|
|
29656
29869
|
if (!session) {
|
|
29870
|
+
strapi2.log.warn("[plugin-io] Admin auth failed: Token not valid or expired");
|
|
29657
29871
|
throw new UnauthorizedError2("Invalid or expired session token");
|
|
29658
29872
|
}
|
|
29659
|
-
|
|
29873
|
+
if (socket?.id) {
|
|
29874
|
+
presenceController.registerSocket(socket.id, token2);
|
|
29875
|
+
}
|
|
29876
|
+
strapi2.log.info(`[plugin-io] Admin authenticated: User ID ${session.userId}`);
|
|
29877
|
+
return {
|
|
29878
|
+
id: session.userId,
|
|
29879
|
+
...session.user
|
|
29880
|
+
};
|
|
29660
29881
|
} catch (error2) {
|
|
29661
|
-
|
|
29662
|
-
|
|
29882
|
+
if (error2 instanceof UnauthorizedError2) {
|
|
29883
|
+
throw error2;
|
|
29884
|
+
}
|
|
29885
|
+
strapi2.log.error("[plugin-io] Admin session verification error:", error2.message);
|
|
29886
|
+
throw new UnauthorizedError2("Authentication failed");
|
|
29887
|
+
}
|
|
29888
|
+
},
|
|
29889
|
+
/**
|
|
29890
|
+
* Cleanup when socket disconnects
|
|
29891
|
+
* @param {object} socket - Socket instance
|
|
29892
|
+
*/
|
|
29893
|
+
onDisconnect: function(socket) {
|
|
29894
|
+
if (socket?.id) {
|
|
29895
|
+
const presenceController = strapi2.plugin("io").controller("presence");
|
|
29896
|
+
presenceController.unregisterSocket(socket.id);
|
|
29663
29897
|
}
|
|
29664
29898
|
},
|
|
29665
29899
|
getRoomName: function(user) {
|
package/package.json
CHANGED