├── .prettierignore ├── .eslintignore ├── server ├── src │ ├── policies │ │ └── index.js │ ├── content-types │ │ └── index.js │ ├── middlewares │ │ ├── index.js │ │ └── sanitize-sensitive-fields.js │ ├── destroy.js │ ├── controllers │ │ ├── index.js │ │ └── controller.js │ ├── register.js │ ├── bootstrap.js │ ├── structures │ │ ├── index.js │ │ └── SocketIO.js │ ├── middleware │ │ ├── index.js │ │ └── handshake.js │ ├── services │ │ ├── service.js │ │ ├── index.js │ │ ├── transform.js │ │ ├── sanitize.js │ │ ├── security.js │ │ └── strategies.js │ ├── utils │ │ ├── pluginId.js │ │ ├── constants.js │ │ └── getService.js │ ├── routes │ │ ├── index.js │ │ └── content-api.js │ ├── index.js │ ├── config │ │ ├── index.js │ │ └── schema.js │ └── bootstrap │ │ ├── index.js │ │ └── io.js ├── routes │ ├── index.js │ └── admin.js ├── controllers │ ├── index.js │ └── settings.js ├── structures │ ├── index.js │ └── SocketIO.js ├── middleware │ ├── index.js │ └── handshake.js ├── utils │ ├── pluginId.js │ ├── constants.js │ └── getService.js ├── jsconfig.json ├── config │ ├── index.js │ └── schema.js ├── services │ ├── index.js │ ├── sanitize.js │ ├── transform.js │ ├── settings.js │ ├── monitoring.js │ └── strategies.js ├── bootstrap │ ├── index.js │ └── lifecycle.js └── index.js ├── admin ├── src │ ├── pluginId.js │ ├── components │ │ ├── PluginIcon.jsx │ │ └── Initializer.jsx │ ├── utils │ │ └── getTranslation.js │ ├── pages │ │ ├── HomePage.jsx │ │ └── App.jsx │ ├── translations │ │ ├── pt.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── de.json │ │ └── en.json │ └── index.js └── jsconfig.json ├── docs ├── .gitignore ├── public │ ├── widget.png │ ├── settings.png │ ├── monitoringSettings.png │ ├── robots.txt │ ├── logo.svg │ └── sitemap-info.md ├── .vitepress │ ├── theme │ │ ├── index.mjs │ │ └── theme.css │ └── config.mjs ├── package.json ├── package-seo.json ├── guide │ ├── widget.md │ └── getting-started.md ├── ecosystem.md ├── README.md └── index.md ├── pics ├── widget.png ├── settings.png └── monitoringSettings.png ├── strapi-admin.js ├── dist ├── admin │ ├── index.mjs │ └── index.js └── _chunks │ ├── pt-I9epJZpI.mjs │ ├── es-CCwv5Ulk.mjs │ ├── pt-Bba2cd2e.js │ ├── fr-DtBI9vH_.mjs │ ├── es-Xj8RgKuQ.js │ ├── fr-D_r96iuZ.js │ ├── de-BoFxKIL3.mjs │ ├── de-Crne_WJ-.js │ ├── en-B4_6Q0aQ.mjs │ └── en-Bd2IKJzy.js ├── strapi-server.js ├── .prettierrc ├── .prettierrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug-report.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── .editorconfig ├── .eslintrc.js ├── LICENSE ├── test-client.js ├── .gitattributes ├── package.json ├── test-entity-subscriptions.js └── CHANGELOG.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | .cache 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /server/src/policies/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /admin/src/pluginId.js: -------------------------------------------------------------------------------- 1 | export const PLUGIN_ID = 'io'; 2 | -------------------------------------------------------------------------------- /server/src/content-types/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /server/src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vitepress/cache 3 | .vitepress/dist 4 | 5 | -------------------------------------------------------------------------------- /pics/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/plugin-io/HEAD/pics/widget.png -------------------------------------------------------------------------------- /strapi-admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./dist/admin'); 4 | 5 | -------------------------------------------------------------------------------- /pics/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/plugin-io/HEAD/pics/settings.png -------------------------------------------------------------------------------- /docs/public/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/plugin-io/HEAD/docs/public/widget.png -------------------------------------------------------------------------------- /docs/public/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/plugin-io/HEAD/docs/public/settings.png -------------------------------------------------------------------------------- /dist/admin/index.mjs: -------------------------------------------------------------------------------- 1 | import { i } from "../_chunks/index-DLXtrAtk.mjs"; 2 | export { 3 | i as default 4 | }; 5 | -------------------------------------------------------------------------------- /pics/monitoringSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/plugin-io/HEAD/pics/monitoringSettings.png -------------------------------------------------------------------------------- /server/src/destroy.js: -------------------------------------------------------------------------------- 1 | const destroy = ({ strapi }) => { 2 | // destroy phase 3 | }; 4 | 5 | export default destroy; 6 | -------------------------------------------------------------------------------- /server/src/controllers/index.js: -------------------------------------------------------------------------------- 1 | import controller from './controller'; 2 | 3 | export default { 4 | controller, 5 | }; 6 | -------------------------------------------------------------------------------- /server/src/register.js: -------------------------------------------------------------------------------- 1 | const register = ({ strapi }) => { 2 | // register phase 3 | }; 4 | 5 | export default register; 6 | -------------------------------------------------------------------------------- /dist/admin/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const index = require("../_chunks/index-BVQ20t1c.js"); 3 | module.exports = index.index; 4 | -------------------------------------------------------------------------------- /docs/public/monitoringSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi-community/plugin-io/HEAD/docs/public/monitoringSettings.png -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const admin = require('./admin'); 4 | 5 | module.exports = { 6 | admin, 7 | }; 8 | -------------------------------------------------------------------------------- /server/src/bootstrap.js: -------------------------------------------------------------------------------- 1 | const bootstrap = ({ strapi }) => { 2 | // bootstrap phase 3 | }; 4 | 5 | export default bootstrap; 6 | -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const settings = require('./settings'); 4 | 5 | module.exports = { 6 | settings, 7 | }; 8 | -------------------------------------------------------------------------------- /server/structures/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SocketIO } = require('./SocketIO'); 4 | 5 | module.exports = { 6 | SocketIO, 7 | }; 8 | -------------------------------------------------------------------------------- /server/middleware/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { handshake } = require('./handshake'); 4 | 5 | module.exports = { 6 | handshake, 7 | }; 8 | -------------------------------------------------------------------------------- /server/src/structures/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SocketIO } = require('./SocketIO'); 4 | 5 | module.exports = { 6 | SocketIO, 7 | }; 8 | -------------------------------------------------------------------------------- /admin/src/components/PluginIcon.jsx: -------------------------------------------------------------------------------- 1 | import { Server } from '@strapi/icons'; 2 | 3 | const PluginIcon = () => ; 4 | 5 | export { PluginIcon }; 6 | -------------------------------------------------------------------------------- /server/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { handshake } = require('./handshake'); 4 | 5 | module.exports = { 6 | handshake, 7 | }; 8 | -------------------------------------------------------------------------------- /strapi-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Always load from source in local development to avoid bundling issues 4 | module.exports = require('./server'); 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": true, 6 | "trailingComma": "es5", 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | $schema: 'http://json.schemastore.org/prettierrc', 3 | useTabs: true, 4 | printWidth: 120, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /admin/src/utils/getTranslation.js: -------------------------------------------------------------------------------- 1 | import { PLUGIN_ID } from '../pluginId'; 2 | 3 | const getTranslation = (id) => `${PLUGIN_ID}.${id}`; 4 | 5 | export { getTranslation }; 6 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.mjs: -------------------------------------------------------------------------------- 1 | // .vitepress/theme/index.js 2 | import DefaultTheme from 'vitepress/theme'; 3 | import './theme.css'; 4 | 5 | export default DefaultTheme; 6 | -------------------------------------------------------------------------------- /server/src/services/service.js: -------------------------------------------------------------------------------- 1 | const service = ({ strapi }) => ({ 2 | getWelcomeMessage() { 3 | return 'Welcome to Strapi'; 4 | }, 5 | }); 6 | 7 | export default service; 8 | -------------------------------------------------------------------------------- /server/utils/pluginId.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pluginPkg = require('../../package.json'); 4 | 5 | const pluginId = pluginPkg.strapi.name; 6 | 7 | module.exports = { pluginId }; 8 | -------------------------------------------------------------------------------- /server/src/utils/pluginId.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Hardcoded plugin ID to avoid bundling issues with package.json imports 4 | const pluginId = 'io'; 5 | 6 | module.exports = { pluginId }; 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Discussions 3 | url: https://github.com/ComfortablyCoding/strapi-plugin-io/discussions 4 | about: Use discussions for improvements and/or questions 5 | -------------------------------------------------------------------------------- /server/utils/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const API_TOKEN_TYPE = { 4 | READ_ONLY: 'read-only', 5 | FULL_ACCESS: 'full-access', 6 | CUSTOM: 'custom', 7 | }; 8 | 9 | module.exports = { 10 | API_TOKEN_TYPE, 11 | }; 12 | -------------------------------------------------------------------------------- /server/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const API_TOKEN_TYPE = { 4 | READ_ONLY: 'read-only', 5 | FULL_ACCESS: 'full-access', 6 | CUSTOM: 'custom', 7 | }; 8 | 9 | module.exports = { 10 | API_TOKEN_TYPE, 11 | }; 12 | -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import contentAPIRoutes from './content-api'; 2 | 3 | const routes = { 4 | 'content-api': { 5 | type: 'content-api', 6 | routes: contentAPIRoutes, 7 | }, 8 | }; 9 | 10 | export default routes; 11 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/theme.css: -------------------------------------------------------------------------------- 1 | /* .vitepress/theme/custom.css */ 2 | :root { 3 | --vp-c-brand: #514efe; 4 | --vp-c-brand-light: #625fff; 5 | --vp-c-brand-lighter: #7e7cfc; 6 | --vp-c-brand-dark: #5553fc; 7 | --vp-c-brand-darker: #312f91; 8 | } 9 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | const config = require('./config'); 5 | const services = require('./services'); 6 | 7 | module.exports = { 8 | bootstrap, 9 | config, 10 | services, 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | .DS_Store 5 | .env 6 | .env.* 7 | !.env.example 8 | *.tsbuildinfo 9 | coverage 10 | .cache 11 | build 12 | .strapi 13 | .vscode 14 | .idea 15 | package-lock.json 16 | yarn.lock 17 | *.swp 18 | *.swo 19 | *~ 20 | .tmp 21 | -------------------------------------------------------------------------------- /server/src/routes/content-api.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | // name of the controller file & the method. 6 | handler: 'controller.index', 7 | config: { 8 | policies: [], 9 | }, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /server/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true 7 | }, 8 | "include": ["./src/**/*.js"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /server/src/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const strategy = require('./strategies'); 4 | const sanitize = require('./sanitize'); 5 | const transform = require('./transform'); 6 | 7 | module.exports = { 8 | sanitize, 9 | strategy, 10 | transform, 11 | }; 12 | -------------------------------------------------------------------------------- /admin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "module": "esnext", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true 8 | }, 9 | "include": ["./src/**/*.js", "./src/**/*.jsx"] 10 | } 11 | -------------------------------------------------------------------------------- /server/src/controllers/controller.js: -------------------------------------------------------------------------------- 1 | const controller = ({ strapi }) => ({ 2 | index(ctx) { 3 | ctx.body = strapi 4 | .plugin('io') 5 | // the name of the service file & the method. 6 | .service('service') 7 | .getWelcomeMessage(); 8 | }, 9 | }); 10 | 11 | export default controller; 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # npm 2 | npm-debug.log 3 | 4 | # git 5 | .git 6 | .gitattributes 7 | .gitignore 8 | 9 | # vscode 10 | .vscode 11 | 12 | # RC files 13 | .eslintrc.js 14 | .eslintrc.backend.js 15 | .eslintrc.frontend.js 16 | .prettierrc.json 17 | 18 | # config files 19 | .editorconfig 20 | 21 | # github 22 | .github 23 | 24 | # docs 25 | docs 26 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | default() { 5 | return { 6 | events: [], 7 | hooks: {}, 8 | socket: { serverOptions: { cors: { origin: 'http://127.0.0.1:8080', methods: ['GET', 'POST'] } } }, 9 | }; 10 | }, 11 | validator(config) { 12 | // no-op validator for now; assume user config is valid 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /server/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const strategy = require('./strategies'); 4 | const sanitize = require('./sanitize'); 5 | const transform = require('./transform'); 6 | const settings = require('./settings'); 7 | const monitoring = require('./monitoring'); 8 | 9 | module.exports = { 10 | sanitize, 11 | strategy, 12 | transform, 13 | settings, 14 | monitoring, 15 | }; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{js,jsx,ts,tsx}] 13 | indent_style = tab 14 | indent_size = 2 15 | 16 | [*.{json,yml,yaml,md}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /server/utils/getService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { pluginId } = require('./pluginId'); 4 | 5 | function getService({ name, plugin = pluginId, type = 'plugin' }) { 6 | let serviceUID = `${type}::${plugin}`; 7 | 8 | if (name && name.length) { 9 | serviceUID += `.${name}`; 10 | } 11 | 12 | return strapi.service(serviceUID); 13 | } 14 | 15 | module.exports = { 16 | getService, 17 | }; 18 | -------------------------------------------------------------------------------- /server/src/utils/getService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { pluginId } = require('./pluginId'); 4 | 5 | function getService({ name, plugin = pluginId, type = 'plugin' }) { 6 | let serviceUID = `${type}::${plugin}`; 7 | 8 | if (name && name.length) { 9 | serviceUID += `.${name}`; 10 | } 11 | 12 | return strapi.service(serviceUID); 13 | } 14 | 15 | module.exports = { 16 | getService, 17 | }; 18 | -------------------------------------------------------------------------------- /server/src/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { plugin } = require('./schema'); 4 | 5 | module.exports = { 6 | default() { 7 | return { 8 | events: [], 9 | hooks: {}, 10 | contentTypes: [], 11 | socket: { serverOptions: { cors: { origin: 'http://127.0.0.1:8080', methods: ['GET', 'POST'] } } }, 12 | }; 13 | }, 14 | validator(config) { 15 | plugin.parse(config); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /server/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { bootstrapIO } = require('./io'); 4 | const { bootstrapLifecycles } = require('./lifecycle'); 5 | 6 | /** 7 | * Runs on bootstrap phase 8 | * 9 | * @param {*} params 10 | * @param {*} params.strapi 11 | */ 12 | async function bootstrap({ strapi }) { 13 | await bootstrapIO({ strapi }); 14 | bootstrapLifecycles({ strapi }); 15 | } 16 | 17 | module.exports = bootstrap; 18 | -------------------------------------------------------------------------------- /server/src/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { bootstrapIO } = require('./io'); 4 | const { bootstrapLifecycles } = require('./lifecycle'); 5 | 6 | /** 7 | * Runs on bootstrap phase 8 | * 9 | * @param {*} params 10 | * @param {*} params.strapi 11 | */ 12 | async function bootstrap({ strapi }) { 13 | bootstrapIO({ strapi }); 14 | bootstrapLifecycles({ strapi }); 15 | } 16 | 17 | module.exports = bootstrap; 18 | -------------------------------------------------------------------------------- /admin/src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { Main } from '@strapi/design-system'; 2 | import { useIntl } from 'react-intl'; 3 | 4 | import { getTranslation } from '../utils/getTranslation'; 5 | 6 | const HomePage = () => { 7 | const { formatMessage } = useIntl(); 8 | 9 | return ( 10 |
11 |

Welcome to {formatMessage({ id: getTranslation('plugin.name') })}

12 |
13 | ); 14 | }; 15 | 16 | export { HomePage }; 17 | -------------------------------------------------------------------------------- /admin/src/components/Initializer.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | import { PLUGIN_ID } from '../pluginId'; 4 | 5 | /** 6 | * @type {import('react').FC<{ setPlugin: (id: string) => void }>} 7 | */ 8 | const Initializer = ({ setPlugin }) => { 9 | const ref = useRef(setPlugin); 10 | 11 | useEffect(() => { 12 | ref.current(PLUGIN_ID); 13 | }, []); 14 | 15 | return null; 16 | }; 17 | 18 | export { Initializer }; 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strapi-community/plugin-io-docs", 3 | "version": "1.0.0", 4 | "description": "Documentation for @strapi-community/plugin-io - Socket.IO Integration for Strapi v5", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vitepress dev", 8 | "build": "vitepress build", 9 | "preview": "vitepress preview" 10 | }, 11 | "devDependencies": { 12 | "vitepress": "^1.6.4", 13 | "vue": "^3.5.13" 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | # robots.txt for Strapi Plugin IO Documentation 2 | 3 | # Allow all search engines 4 | User-agent: * 5 | Allow: / 6 | 7 | # Sitemap location 8 | Sitemap: https://strapi-plugin-io.netlify.app/sitemap.xml 9 | 10 | # Crawl-delay (optional - prevents overloading) 11 | Crawl-delay: 1 12 | 13 | # Disallow specific paths (if needed) 14 | # Disallow: /admin/ 15 | # Disallow: /private/ 16 | 17 | # Allow important pages explicitly 18 | Allow: /guide/ 19 | Allow: /api/ 20 | Allow: /examples/ 21 | Allow: /ecosystem 22 | 23 | -------------------------------------------------------------------------------- /admin/src/pages/App.jsx: -------------------------------------------------------------------------------- 1 | import { Page } from '@strapi/strapi/admin'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | 4 | import { HomePage } from './HomePage'; 5 | import { SettingsPage } from './SettingsPage'; 6 | import { MonitoringPage } from './MonitoringPage'; 7 | 8 | const App = () => { 9 | return ( 10 | 11 | } /> 12 | } /> 13 | } /> 14 | } /> 15 | 16 | ); 17 | }; 18 | 19 | export { App }; 20 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | const config = require('./config'); 5 | const controllers = require('./controllers'); 6 | const routes = require('./routes'); 7 | const services = require('./services'); 8 | 9 | // Strapi v5 plugin server entry expects a full object of hooks + APIs. 10 | const destroy = async () => {}; 11 | const register = async () => {}; 12 | const contentTypes = {}; 13 | const middlewares = {}; 14 | const policies = {}; 15 | 16 | module.exports = { 17 | register, 18 | bootstrap, 19 | destroy, 20 | config, 21 | controllers, 22 | routes, 23 | services, 24 | contentTypes, 25 | policies, 26 | middlewares, 27 | }; 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | $schema: 'https://json.schemastore.org/eslintrc', 5 | extends: ['@strapi/eslint-config/back', 'plugin:prettier/recommended'], 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | }, 9 | globals: { 10 | strapi: false, 11 | }, 12 | rules: { 13 | 'import/no-dynamic-require': 'off', 14 | 'global-require': 'off', 15 | 'prefer-destructuring': ['error', { AssignmentExpression: { array: false } }], 16 | 'no-underscore-dangle': 'off', 17 | 'no-use-before-define': 'off', 18 | 'no-continue': 'warn', 19 | 'no-process-exit': 'off', 20 | 'no-loop-func': 'off', 21 | 'max-classes-per-file': 'off', 22 | 'no-param-reassign': [ 23 | 'error', 24 | { 25 | props: false, 26 | }, 27 | ], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /server/config/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { z } = require('zod'); 4 | 5 | const Event = z.object({ 6 | name: z.string(), 7 | handler: z.function(), 8 | }); 9 | 10 | const InitHook = z.function(); 11 | 12 | const Hooks = z.object({ 13 | init: InitHook.optional(), 14 | }); 15 | 16 | const ContentTypeAction = z.enum(['create', 'update', 'delete']); 17 | 18 | const ContentType = z.object({ 19 | uid: z.string(), 20 | actions: z.array(ContentTypeAction), 21 | }); 22 | 23 | const Socket = z.object({ serverOptions: z.unknown().optional() }); 24 | 25 | const plugin = z.object({ 26 | events: z.array(Event).optional(), 27 | hooks: Hooks.optional(), 28 | contentTypes: z.array(z.union([z.string(), ContentType])), 29 | socket: Socket.optional(), 30 | }); 31 | 32 | module.exports = { 33 | plugin, 34 | }; 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | 8 | ### Type of change 9 | 10 | 11 | 12 | - [ ] Documentation (updates to the documentation or readme) 13 | - [ ] Bug fix (a non-breaking change that fixes an issue) 14 | - [ ] Enhancement (improving an existing functionality like performance) 15 | - [ ] New feature (a non-breaking change that adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 17 | 18 | ### Relevant Issue(s) 19 | 20 | 21 | -------------------------------------------------------------------------------- /server/services/sanitize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { sanitize } = require('@strapi/utils'); 4 | 5 | module.exports = ({ strapi }) => { 6 | /** 7 | * Sanitize data output with a provided schema for a specified role 8 | * 9 | * @param {Object} param 10 | * @param {Object} param.schema 11 | * @param {Object} param.data 12 | * @param {Object} param.auth 13 | */ 14 | function output({ schema, data, options }) { 15 | // Check if sanitize.contentAPI is available (might not be in setTimeout context) 16 | if (!sanitize || !sanitize.contentAPI || !sanitize.contentAPI.output) { 17 | // Fallback: return data as-is if sanitize is not available 18 | strapi.log.debug('socket.io: sanitize.contentAPI not available, returning raw data'); 19 | return data; 20 | } 21 | return sanitize.contentAPI.output(data, schema, options); 22 | } 23 | 24 | return { 25 | output, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /docs/public/sitemap-info.md: -------------------------------------------------------------------------------- 1 | # Sitemap Configuration 2 | 3 | This file helps search engines discover all pages of the documentation. 4 | 5 | ## Automatic Generation 6 | 7 | VitePress automatically generates a sitemap at build time when configured in `config.js`: 8 | 9 | ```javascript 10 | sitemap: { 11 | hostname: 'https://strapi-plugin-io.netlify.app' 12 | } 13 | ``` 14 | 15 | ## Manual Priority (Optional) 16 | 17 | If you want to manually set priorities, create a `sitemap.xml` in the public folder. 18 | 19 | ## Pages Included 20 | 21 | - Homepage (/) 22 | - Getting Started (/guide/getting-started) 23 | - IO Class API (/api/io-class) 24 | - Plugin Configuration (/api/plugin-config) 25 | - Examples Overview (/examples/) 26 | - Content Types (/examples/content-types) 27 | - Events (/examples/events) 28 | - Hooks (/examples/hooks) 29 | - Ecosystem (/ecosystem) 30 | 31 | ## Submission 32 | 33 | Submit the sitemap to: 34 | - Google Search Console: https://search.google.com/search-console 35 | - Bing Webmaster Tools: https://www.bing.com/webmasters 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 @ComfortablyCoding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: 'Strapi Plugin IO', 6 | lastUpdated: true, 7 | description: 'A plugin for Socket IO integration with Strapi CMS.', 8 | themeConfig: { 9 | nav: [ 10 | { text: 'Home', link: '/' }, 11 | { text: 'Examples', link: '/examples/index' }, 12 | ], 13 | 14 | sidebar: [ 15 | { 16 | text: 'Guide', 17 | items: [{ text: 'Getting Started', link: '/guide/getting-started' }], 18 | }, 19 | { 20 | text: 'API', 21 | items: [ 22 | { text: 'Plugin Configruation Options', link: '/api/plugin-config' }, 23 | { text: 'IO Class', link: '/api/io-class' }, 24 | ], 25 | }, 26 | { 27 | text: 'Examples', 28 | items: [ 29 | { text: 'Events', link: '/examples/events' }, 30 | { text: 'Content Types', link: '/examples/content-types' }, 31 | { text: 'Hooks', link: '/examples/hooks' }, 32 | ], 33 | }, 34 | ], 35 | 36 | socialLinks: [{ icon: 'github', link: 'https://github.com/ComfortablyCoding/strapi-plugin-io' }], 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: '[Bug]: ' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: issue-experiencing 11 | attributes: 12 | label: What issue are you experiencing? 13 | description: A detailed explanation of the issue 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: reproduction-steps 18 | attributes: 19 | label: Steps To Reproduce 20 | description: Steps to reproduce the behavior. 21 | placeholder: | 22 | 1. In this environment... 23 | 2. With this config... 24 | 3. Run '...' 25 | 4. See error... 26 | validations: 27 | required: false 28 | - type: input 29 | id: plugin-version 30 | attributes: 31 | label: What version of the plugin are you using? 32 | validations: 33 | required: true 34 | - type: input 35 | id: strapi-version 36 | attributes: 37 | label: What strapi version are you using? 38 | validations: 39 | required: true 40 | -------------------------------------------------------------------------------- /server/src/bootstrap/io.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SocketIO } = require('../structures'); 4 | const { pluginId } = require('../utils/pluginId'); 5 | 6 | /** 7 | * Bootstrap IO instance and related "services" 8 | * 9 | * @param {*} params 10 | * @param {*} params.strapi 11 | */ 12 | async function bootstrapIO({ strapi }) { 13 | const settings = strapi.config.get(`plugin.${pluginId}`); 14 | 15 | // initialize io 16 | const io = new SocketIO(settings.socket.serverOptions); 17 | 18 | // make io avaiable anywhere strapi global object is 19 | strapi.$io = io; 20 | 21 | // Apply sensitive fields sanitization middleware 22 | const sanitizeSensitiveFields = require('../middlewares/sanitize-sensitive-fields'); 23 | sanitizeSensitiveFields({ strapi }); 24 | 25 | // add any io server events 26 | if (settings.events?.length) { 27 | strapi.$io.server.on('connection', (socket) => { 28 | for (const event of settings.events) { 29 | // "connection" event should be executed immediately 30 | if (event.name === 'connection') { 31 | event.handler({ strapi, io }, socket); 32 | } else { 33 | // register all other events to be triggered at a later time 34 | socket.on(event.name, (...args) => event.handler({ strapi, io }, socket, ...args)); 35 | } 36 | } 37 | }); 38 | } 39 | } 40 | 41 | module.exports = { bootstrapIO }; 42 | -------------------------------------------------------------------------------- /docs/package-seo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strapi-community/plugin-io", 3 | "version": "5.0.0", 4 | "description": "Socket.IO plugin for Strapi v5 CMS with OAuth authentication, real-time events, Redis adapter, and production-ready features", 5 | "keywords": [ 6 | "strapi", 7 | "strapi-plugin", 8 | "socket.io", 9 | "websocket", 10 | "real-time", 11 | "strapi-v5", 12 | "strapi5", 13 | "oauth", 14 | "jwt", 15 | "redis", 16 | "websockets", 17 | "cms-plugin", 18 | "headless-cms", 19 | "nodejs", 20 | "real-time-events", 21 | "@strapi-community/plugin-io", 22 | "strapi-websocket", 23 | "strapi-realtime", 24 | "socket-io-strapi", 25 | "strapi-oauth" 26 | ], 27 | "homepage": "https://strapi-plugin-io.netlify.app", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/strapi-community/strapi-plugin-io.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/strapi-community/strapi-plugin-io/issues" 34 | }, 35 | "author": { 36 | "name": "ComfortablyCoding", 37 | "url": "https://github.com/ComfortablyCoding" 38 | }, 39 | "maintainers": [ 40 | { 41 | "name": "ComfortablyCoding", 42 | "url": "https://github.com/ComfortablyCoding" 43 | }, 44 | { 45 | "name": "hrdunn", 46 | "url": "https://github.com/hrdunn" 47 | }, 48 | { 49 | "name": "Schero94", 50 | "url": "https://github.com/Schero94" 51 | } 52 | ], 53 | "license": "MIT" 54 | } 55 | 56 | -------------------------------------------------------------------------------- /server/src/config/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { z } = require('zod'); 4 | 5 | const Event = z.object({ 6 | name: z.string(), 7 | handler: z.function(), 8 | }); 9 | 10 | const InitHook = z.function(); 11 | 12 | const Hooks = z.object({ 13 | init: InitHook.optional(), 14 | }); 15 | 16 | const ContentTypeAction = z.enum(['create', 'update', 'delete']); 17 | 18 | /** 19 | * Populate configuration for content type events. 20 | * Supports multiple formats: 21 | * - '*' or true: Populate all relations (1 level deep) 22 | * - string[]: Populate specific relations ['author', 'category'] 23 | * - object: Strapi populate syntax { author: { fields: ['name'] } } 24 | */ 25 | const PopulateConfig = z.union([ 26 | z.literal('*'), 27 | z.literal(true), 28 | z.array(z.string()), 29 | z.record(z.any()), 30 | ]); 31 | 32 | const ContentType = z.object({ 33 | uid: z.string(), 34 | actions: z.array(ContentTypeAction).optional(), 35 | populate: PopulateConfig.optional(), 36 | }); 37 | 38 | const Socket = z.object({ serverOptions: z.unknown().optional() }); 39 | 40 | /** 41 | * Plugin configuration schema 42 | */ 43 | const plugin = z.object({ 44 | events: z.array(Event).optional(), 45 | hooks: Hooks.optional(), 46 | contentTypes: z.array(z.union([z.string(), ContentType])), 47 | socket: Socket.optional(), 48 | /** 49 | * Additional sensitive field names to exclude from emitted data. 50 | * These are added to the default list: password, resetPasswordToken, 51 | * confirmationToken, refreshToken, accessToken, secret, apiKey, etc. 52 | */ 53 | sensitiveFields: z.array(z.string()).optional(), 54 | }); 55 | 56 | module.exports = { 57 | plugin, 58 | PopulateConfig, 59 | }; 60 | -------------------------------------------------------------------------------- /server/middleware/handshake.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getService } = require('../utils/getService'); 4 | 5 | /** 6 | * Auto assign sockets to appropriate rooms based on tokens associated name. 7 | * Defaults to default role if no token provided. 8 | * 9 | * @param {require('socket.io').Socket} socket The socket attempting to connect 10 | * @param {Function} next Function to call the next middleware in the stack 11 | */ 12 | async function handshake(socket, next) { 13 | const strategyService = getService({ name: 'strategy' }); 14 | const auth = socket.handshake.auth || {}; 15 | let strategy = auth.strategy || 'jwt'; 16 | const token = auth.token || ''; 17 | 18 | // remove strategy if no token provided 19 | if (!token.length) { 20 | strategy = ''; 21 | } 22 | 23 | try { 24 | let room; 25 | if (strategy && strategy.length) { 26 | const strategyType = strategy === 'jwt' ? 'role' : 'token'; 27 | const ctx = await strategyService[strategyType].authenticate(auth); 28 | room = strategyService[strategyType].getRoomName(ctx); 29 | } else if (strapi.plugin('users-permissions')) { 30 | // default to public users-permissions role if no supported auth provided 31 | const role = await strapi 32 | .query('plugin::users-permissions.role') 33 | .findOne({ where: { type: 'public' }, select: ['id', 'name'] }); 34 | 35 | room = strategyService['role'].getRoomName(role); 36 | } 37 | 38 | if (room) { 39 | socket.join(room.replace(' ', '-')); 40 | } else { 41 | throw new Error('No valid room found'); 42 | } 43 | 44 | next(); 45 | } catch (error) { 46 | next(new Error(error.message)); 47 | } 48 | } 49 | 50 | module.exports = { 51 | handshake, 52 | }; 53 | -------------------------------------------------------------------------------- /server/src/middleware/handshake.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getService } = require('../utils/getService'); 4 | 5 | /** 6 | * Auto assign sockets to appropriate rooms based on tokens associated name. 7 | * Defaults to default role if no token provided. 8 | * 9 | * @param {require('socket.io').Socket} socket The socket attempting to connect 10 | * @param {Function} next Function to call the next middleware in the stack 11 | */ 12 | async function handshake(socket, next) { 13 | const strategyService = getService({ name: 'strategy' }); 14 | const auth = socket.handshake.auth || {}; 15 | let strategy = auth.strategy || 'jwt'; 16 | const token = auth.token || ''; 17 | 18 | // remove strategy if no token provided 19 | if (!token.length) { 20 | strategy = ''; 21 | } 22 | 23 | try { 24 | let room; 25 | if (strategy && strategy.length) { 26 | const strategyType = strategy === 'jwt' ? 'role' : 'token'; 27 | const ctx = await strategyService[strategyType].authenticate(auth); 28 | room = strategyService[strategyType].getRoomName(ctx); 29 | } else if (strapi.plugin('users-permissions')) { 30 | // default to public users-permissions role if no supported auth provided 31 | const role = await strapi 32 | .query('plugin::users-permissions.role') 33 | .findOne({ where: { type: 'public' }, select: ['id', 'name'] }); 34 | 35 | room = strategyService['role'].getRoomName(role); 36 | } 37 | 38 | if (room) { 39 | socket.join(room.replace(' ', '-')); 40 | } else { 41 | throw new Error('No valid room found'); 42 | } 43 | 44 | next(); 45 | } catch (error) { 46 | next(new Error(error.message)); 47 | } 48 | } 49 | 50 | module.exports = { 51 | handshake, 52 | }; 53 | -------------------------------------------------------------------------------- /server/src/middlewares/sanitize-sensitive-fields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Middleware to deeply sanitize sensitive fields from Socket.IO events 5 | * Removes password hashes, tokens, secrets from all nested objects 6 | */ 7 | 8 | const SENSITIVE_FIELDS = [ 9 | 'password', 10 | 'resetPasswordToken', 11 | 'registrationToken', 12 | 'confirmationToken', 13 | 'privateKey', 14 | 'secretKey', 15 | 'apiKey', 16 | 'secret', 17 | 'hash', 18 | ]; 19 | 20 | /** 21 | * Recursively remove sensitive fields from an object 22 | * @param {*} obj - Object to sanitize 23 | * @returns {*} Sanitized object 24 | */ 25 | function deepSanitize(obj) { 26 | if (!obj || typeof obj !== 'object') { 27 | return obj; 28 | } 29 | 30 | // Handle arrays 31 | if (Array.isArray(obj)) { 32 | return obj.map(item => deepSanitize(item)); 33 | } 34 | 35 | // Handle objects 36 | const sanitized = {}; 37 | for (const [key, value] of Object.entries(obj)) { 38 | // Skip sensitive fields 39 | if (SENSITIVE_FIELDS.includes(key)) { 40 | continue; 41 | } 42 | 43 | // Recursively sanitize nested objects/arrays 44 | if (value && typeof value === 'object') { 45 | sanitized[key] = deepSanitize(value); 46 | } else { 47 | sanitized[key] = value; 48 | } 49 | } 50 | 51 | return sanitized; 52 | } 53 | 54 | module.exports = ({ strapi }) => { 55 | // Override the emit method to add sanitization 56 | const originalEmit = strapi.$io.emit.bind(strapi.$io); 57 | 58 | strapi.$io.emit = async function(params) { 59 | // Deep sanitize data before emitting 60 | if (params.data) { 61 | params.data = deepSanitize(params.data); 62 | } 63 | 64 | // Call original emit 65 | return originalEmit(params); 66 | }; 67 | 68 | strapi.log.info('socket.io: Sensitive fields sanitization middleware active'); 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /test-client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const io = require('socket.io-client'); 3 | 4 | const token = process.argv[2]; 5 | console.log('🔌 Connecting to http://localhost:1337'); 6 | console.log(`🔐 Token: ${token ? 'Yes' : 'No'}\n`); 7 | 8 | const socket = io('http://localhost:1337', { 9 | auth: token ? { token } : undefined, 10 | transports: ['websocket', 'polling'] 11 | }); 12 | 13 | socket.on('connect', () => { 14 | console.log('✅ Connected:', socket.id); 15 | 16 | // Test room join 17 | socket.emit('join-room', 'test-room', (response) => { 18 | console.log('📋 Join room:', response); 19 | }); 20 | 21 | // Test get rooms 22 | setTimeout(() => { 23 | socket.emit('get-rooms', (response) => { 24 | console.log('📋 My rooms:', response); 25 | }); 26 | }, 500); 27 | 28 | // Test private message 29 | setTimeout(() => { 30 | socket.emit('private-message', { 31 | to: socket.id, 32 | message: 'Test message' 33 | }, (response) => { 34 | console.log('📨 Private message:', response); 35 | }); 36 | }, 1000); 37 | 38 | // Disconnect after tests 39 | setTimeout(() => { 40 | console.log('\n✅ All tests completed'); 41 | socket.disconnect(); 42 | process.exit(0); 43 | }, 2000); 44 | }); 45 | 46 | socket.on('private-message', (data) => { 47 | console.log('📨 Received private message:', data); 48 | }); 49 | 50 | socket.on('connect_error', (error) => { 51 | console.error('❌ Connection error:', error.message); 52 | process.exit(1); 53 | }); 54 | 55 | // Listen for content-type events 56 | ['article', 'post', 'session'].forEach(ct => { 57 | ['create', 'update', 'delete'].forEach(action => { 58 | socket.on(`${ct}:${action}`, (data) => { 59 | console.log(`\n🔔 ${ct}:${action}`, data); 60 | }); 61 | }); 62 | }); 63 | 64 | console.log('👂 Listening for events...\n'); 65 | -------------------------------------------------------------------------------- /server/routes/admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Admin routes for io plugin settings 5 | */ 6 | module.exports = { 7 | type: 'admin', 8 | routes: [ 9 | { 10 | method: 'GET', 11 | path: '/settings', 12 | handler: 'settings.getSettings', 13 | config: { 14 | policies: ['admin::isAuthenticatedAdmin'], 15 | }, 16 | }, 17 | { 18 | method: 'PUT', 19 | path: '/settings', 20 | handler: 'settings.updateSettings', 21 | config: { 22 | policies: ['admin::isAuthenticatedAdmin'], 23 | }, 24 | }, 25 | { 26 | method: 'GET', 27 | path: '/content-types', 28 | handler: 'settings.getContentTypes', 29 | config: { 30 | policies: ['admin::isAuthenticatedAdmin'], 31 | }, 32 | }, 33 | { 34 | method: 'GET', 35 | path: '/stats', 36 | handler: 'settings.getStats', 37 | config: { 38 | policies: ['admin::isAuthenticatedAdmin'], 39 | }, 40 | }, 41 | { 42 | method: 'GET', 43 | path: '/event-log', 44 | handler: 'settings.getEventLog', 45 | config: { 46 | policies: ['admin::isAuthenticatedAdmin'], 47 | }, 48 | }, 49 | { 50 | method: 'POST', 51 | path: '/test-event', 52 | handler: 'settings.sendTestEvent', 53 | config: { 54 | policies: ['admin::isAuthenticatedAdmin'], 55 | }, 56 | }, 57 | { 58 | method: 'POST', 59 | path: '/reset-stats', 60 | handler: 'settings.resetStats', 61 | config: { 62 | policies: ['admin::isAuthenticatedAdmin'], 63 | }, 64 | }, 65 | { 66 | method: 'GET', 67 | path: '/roles', 68 | handler: 'settings.getRoles', 69 | config: { 70 | policies: ['admin::isAuthenticatedAdmin'], 71 | }, 72 | }, 73 | { 74 | method: 'GET', 75 | path: '/monitoring/stats', 76 | handler: 'settings.getMonitoringStats', 77 | config: { 78 | policies: ['admin::isAuthenticatedAdmin'], 79 | }, 80 | }, 81 | ], 82 | }; 83 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # git config 51 | .gitattributes text 52 | .gitignore text 53 | .gitconfig text 54 | 55 | # code analysis config 56 | .jshintrc text 57 | .jscsrc text 58 | .jshintignore text 59 | .csslintrc text 60 | 61 | # misc config 62 | *.yaml text 63 | *.yml text 64 | .editorconfig text 65 | 66 | # build config 67 | *.npmignore text 68 | *.bowerrc text 69 | 70 | # Documentation 71 | *.md text 72 | LICENSE text 73 | AUTHORS text 74 | 75 | 76 | # 77 | ## These files are binary and should be left untouched 78 | # 79 | 80 | # (binary is a macro for -text -diff) 81 | *.png binary 82 | *.jpg binary 83 | *.jpeg binary 84 | *.gif binary 85 | *.ico binary 86 | *.mov binary 87 | *.mp4 binary 88 | *.mp3 binary 89 | *.flv binary 90 | *.fla binary 91 | *.swf binary 92 | *.gz binary 93 | *.zip binary 94 | *.7z binary 95 | *.ttf binary 96 | *.eot binary 97 | *.woff binary 98 | *.pyc binary 99 | *.pdf binary 100 | -------------------------------------------------------------------------------- /admin/src/translations/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Estatísticas Socket.IO", 4 | "widget.connections": "Conexões Ativas", 5 | "widget.rooms": "Salas Ativas", 6 | "widget.eventsPerSec": "Eventos/seg", 7 | "widget.totalEvents": "Eventos Totais", 8 | "widget.viewMonitoring": "Ver Monitoramento", 9 | "widget.live": "Ao Vivo", 10 | "widget.offline": "Desconectado", 11 | "settings.title": "Configurações", 12 | "settings.description": "Configure a conexão Socket.IO para eventos em tempo real", 13 | "settings.save": "Salvar", 14 | "settings.saved": "Salvo", 15 | "settings.saveAndApply": "Salvar e Aplicar", 16 | "settings.success": "Configurações salvas com sucesso!", 17 | "settings.error": "Erro ao salvar configurações", 18 | "settings.loadError": "Erro ao carregar configurações", 19 | "settings.noRestart": "As alterações são aplicadas imediatamente – nenhum reinício necessário!", 20 | 21 | "cors.title": "Configurações CORS", 22 | "cors.description": "Configure quais frontends podem se conectar", 23 | "cors.origin": "URL do Frontend (CORS Origin)", 24 | "cors.originHint": "A URL do seu frontend (ex: http://localhost:3000 ou https://meu-app.com.br)", 25 | "cors.credentials": "Permitir Credentials", 26 | "cors.methods": "Métodos HTTP permitidos", 27 | 28 | "events.title": "Eventos em tempo real", 29 | "events.description": "Configure quais eventos são enviados para quais tipos de conteúdo", 30 | "events.enableAll": "Ativar todos", 31 | "events.disableAll": "Desativar todos", 32 | "events.noContentTypes": "Nenhum tipo de conteúdo API encontrado. Crie primeiro tipos de conteúdo no Content-Type Builder.", 33 | "events.contentType": "Tipo de conteúdo", 34 | "events.create": "Criar", 35 | "events.update": "Atualizar", 36 | "events.delete": "Excluir", 37 | 38 | "logging.title": "Registro", 39 | "logging.connectionLogging": "Registro de conexões", 40 | "logging.connectionLoggingHint": "Mostra conexões de clientes no log do servidor" 41 | } 42 | -------------------------------------------------------------------------------- /dist/_chunks/pt-I9epJZpI.mjs: -------------------------------------------------------------------------------- 1 | const pt = { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Estatísticas Socket.IO", 4 | "widget.connections": "Conexões Ativas", 5 | "widget.rooms": "Salas Ativas", 6 | "widget.eventsPerSec": "Eventos/seg", 7 | "widget.totalEvents": "Eventos Totais", 8 | "widget.viewMonitoring": "Ver Monitoramento", 9 | "widget.live": "Ao Vivo", 10 | "widget.offline": "Desconectado", 11 | "settings.title": "Configurações", 12 | "settings.description": "Configure a conexão Socket.IO para eventos em tempo real", 13 | "settings.save": "Salvar", 14 | "settings.saved": "Salvo", 15 | "settings.saveAndApply": "Salvar e Aplicar", 16 | "settings.success": "Configurações salvas com sucesso!", 17 | "settings.error": "Erro ao salvar configurações", 18 | "settings.loadError": "Erro ao carregar configurações", 19 | "settings.noRestart": "As alterações são aplicadas imediatamente – nenhum reinício necessário!", 20 | "cors.title": "Configurações CORS", 21 | "cors.description": "Configure quais frontends podem se conectar", 22 | "cors.origin": "URL do Frontend (CORS Origin)", 23 | "cors.originHint": "A URL do seu frontend (ex: http://localhost:3000 ou https://meu-app.com.br)", 24 | "cors.credentials": "Permitir Credentials", 25 | "cors.methods": "Métodos HTTP permitidos", 26 | "events.title": "Eventos em tempo real", 27 | "events.description": "Configure quais eventos são enviados para quais tipos de conteúdo", 28 | "events.enableAll": "Ativar todos", 29 | "events.disableAll": "Desativar todos", 30 | "events.noContentTypes": "Nenhum tipo de conteúdo API encontrado. Crie primeiro tipos de conteúdo no Content-Type Builder.", 31 | "events.contentType": "Tipo de conteúdo", 32 | "events.create": "Criar", 33 | "events.update": "Atualizar", 34 | "events.delete": "Excluir", 35 | "logging.title": "Registro", 36 | "logging.connectionLogging": "Registro de conexões", 37 | "logging.connectionLoggingHint": "Mostra conexões de clientes no log do servidor" 38 | }; 39 | export { 40 | pt as default 41 | }; 42 | -------------------------------------------------------------------------------- /admin/src/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Estadísticas Socket.IO", 4 | "widget.connections": "Conexiones Activas", 5 | "widget.rooms": "Salas Activas", 6 | "widget.eventsPerSec": "Eventos/seg", 7 | "widget.totalEvents": "Eventos Totales", 8 | "widget.viewMonitoring": "Ver Monitoreo", 9 | "widget.live": "En Vivo", 10 | "widget.offline": "Desconectado", 11 | "settings.title": "Configuración", 12 | "settings.description": "Configura la conexión Socket.IO para eventos en tiempo real", 13 | "settings.save": "Guardar", 14 | "settings.saved": "Guardado", 15 | "settings.saveAndApply": "Guardar y Aplicar", 16 | "settings.success": "¡Configuración guardada correctamente!", 17 | "settings.error": "Error al guardar la configuración", 18 | "settings.loadError": "Error al cargar la configuración", 19 | "settings.noRestart": "¡Los cambios se aplican inmediatamente – no se requiere reinicio!", 20 | 21 | "cors.title": "Configuración CORS", 22 | "cors.description": "Configura qué frontends pueden conectarse", 23 | "cors.origin": "URL del Frontend (CORS Origin)", 24 | "cors.originHint": "La URL de tu frontend (ej: http://localhost:3000 o https://mi-app.es)", 25 | "cors.credentials": "Permitir Credentials", 26 | "cors.methods": "Métodos HTTP permitidos", 27 | 28 | "events.title": "Eventos en tiempo real", 29 | "events.description": "Configura qué eventos se envían para qué tipos de contenido", 30 | "events.enableAll": "Activar todos", 31 | "events.disableAll": "Desactivar todos", 32 | "events.noContentTypes": "No se encontraron tipos de contenido API. Crea primero tipos de contenido en el Content-Type Builder.", 33 | "events.contentType": "Tipo de contenido", 34 | "events.create": "Crear", 35 | "events.update": "Actualizar", 36 | "events.delete": "Eliminar", 37 | 38 | "logging.title": "Registro", 39 | "logging.connectionLogging": "Registro de conexiones", 40 | "logging.connectionLoggingHint": "Muestra las conexiones de clientes en el registro del servidor" 41 | } 42 | -------------------------------------------------------------------------------- /dist/_chunks/es-CCwv5Ulk.mjs: -------------------------------------------------------------------------------- 1 | const es = { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Estadísticas Socket.IO", 4 | "widget.connections": "Conexiones Activas", 5 | "widget.rooms": "Salas Activas", 6 | "widget.eventsPerSec": "Eventos/seg", 7 | "widget.totalEvents": "Eventos Totales", 8 | "widget.viewMonitoring": "Ver Monitoreo", 9 | "widget.live": "En Vivo", 10 | "widget.offline": "Desconectado", 11 | "settings.title": "Configuración", 12 | "settings.description": "Configura la conexión Socket.IO para eventos en tiempo real", 13 | "settings.save": "Guardar", 14 | "settings.saved": "Guardado", 15 | "settings.saveAndApply": "Guardar y Aplicar", 16 | "settings.success": "¡Configuración guardada correctamente!", 17 | "settings.error": "Error al guardar la configuración", 18 | "settings.loadError": "Error al cargar la configuración", 19 | "settings.noRestart": "¡Los cambios se aplican inmediatamente – no se requiere reinicio!", 20 | "cors.title": "Configuración CORS", 21 | "cors.description": "Configura qué frontends pueden conectarse", 22 | "cors.origin": "URL del Frontend (CORS Origin)", 23 | "cors.originHint": "La URL de tu frontend (ej: http://localhost:3000 o https://mi-app.es)", 24 | "cors.credentials": "Permitir Credentials", 25 | "cors.methods": "Métodos HTTP permitidos", 26 | "events.title": "Eventos en tiempo real", 27 | "events.description": "Configura qué eventos se envían para qué tipos de contenido", 28 | "events.enableAll": "Activar todos", 29 | "events.disableAll": "Desactivar todos", 30 | "events.noContentTypes": "No se encontraron tipos de contenido API. Crea primero tipos de contenido en el Content-Type Builder.", 31 | "events.contentType": "Tipo de contenido", 32 | "events.create": "Crear", 33 | "events.update": "Actualizar", 34 | "events.delete": "Eliminar", 35 | "logging.title": "Registro", 36 | "logging.connectionLogging": "Registro de conexiones", 37 | "logging.connectionLoggingHint": "Muestra las conexiones de clientes en el registro del servidor" 38 | }; 39 | export { 40 | es as default 41 | }; 42 | -------------------------------------------------------------------------------- /admin/src/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Statistiques Socket.IO", 4 | "widget.connections": "Connexions Actives", 5 | "widget.rooms": "Salles Actives", 6 | "widget.eventsPerSec": "Événements/sec", 7 | "widget.totalEvents": "Événements Totaux", 8 | "widget.viewMonitoring": "Voir Surveillance", 9 | "widget.live": "En Direct", 10 | "widget.offline": "Hors Ligne", 11 | "settings.title": "Paramètres", 12 | "settings.description": "Configurez la connexion Socket.IO pour les événements en temps réel", 13 | "settings.save": "Enregistrer", 14 | "settings.saved": "Enregistré", 15 | "settings.saveAndApply": "Enregistrer & Appliquer", 16 | "settings.success": "Paramètres enregistrés avec succès !", 17 | "settings.error": "Erreur lors de l'enregistrement des paramètres", 18 | "settings.loadError": "Erreur lors du chargement des paramètres", 19 | "settings.noRestart": "Les modifications sont appliquées immédiatement – aucun redémarrage requis !", 20 | 21 | "cors.title": "Paramètres CORS", 22 | "cors.description": "Configurez quels frontends peuvent se connecter", 23 | "cors.origin": "URL Frontend (CORS Origin)", 24 | "cors.originHint": "L'URL de votre frontend (ex: http://localhost:3000 ou https://mon-app.fr)", 25 | "cors.credentials": "Autoriser les Credentials", 26 | "cors.methods": "Méthodes HTTP autorisées", 27 | 28 | "events.title": "Événements en temps réel", 29 | "events.description": "Configurez quels événements sont envoyés pour quels types de contenu", 30 | "events.enableAll": "Tout activer", 31 | "events.disableAll": "Tout désactiver", 32 | "events.noContentTypes": "Aucun type de contenu API trouvé. Créez d'abord des types de contenu dans le Content-Type Builder.", 33 | "events.contentType": "Type de contenu", 34 | "events.create": "Créer", 35 | "events.update": "Mettre à jour", 36 | "events.delete": "Supprimer", 37 | 38 | "logging.title": "Journalisation", 39 | "logging.connectionLogging": "Journal des connexions", 40 | "logging.connectionLoggingHint": "Affiche les connexions clients dans le journal serveur" 41 | } 42 | -------------------------------------------------------------------------------- /dist/_chunks/pt-Bba2cd2e.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 3 | const pt = { 4 | "plugin.name": "Socket.IO", 5 | "widget.socket-stats.title": "Estatísticas Socket.IO", 6 | "widget.connections": "Conexões Ativas", 7 | "widget.rooms": "Salas Ativas", 8 | "widget.eventsPerSec": "Eventos/seg", 9 | "widget.totalEvents": "Eventos Totais", 10 | "widget.viewMonitoring": "Ver Monitoramento", 11 | "widget.live": "Ao Vivo", 12 | "widget.offline": "Desconectado", 13 | "settings.title": "Configurações", 14 | "settings.description": "Configure a conexão Socket.IO para eventos em tempo real", 15 | "settings.save": "Salvar", 16 | "settings.saved": "Salvo", 17 | "settings.saveAndApply": "Salvar e Aplicar", 18 | "settings.success": "Configurações salvas com sucesso!", 19 | "settings.error": "Erro ao salvar configurações", 20 | "settings.loadError": "Erro ao carregar configurações", 21 | "settings.noRestart": "As alterações são aplicadas imediatamente – nenhum reinício necessário!", 22 | "cors.title": "Configurações CORS", 23 | "cors.description": "Configure quais frontends podem se conectar", 24 | "cors.origin": "URL do Frontend (CORS Origin)", 25 | "cors.originHint": "A URL do seu frontend (ex: http://localhost:3000 ou https://meu-app.com.br)", 26 | "cors.credentials": "Permitir Credentials", 27 | "cors.methods": "Métodos HTTP permitidos", 28 | "events.title": "Eventos em tempo real", 29 | "events.description": "Configure quais eventos são enviados para quais tipos de conteúdo", 30 | "events.enableAll": "Ativar todos", 31 | "events.disableAll": "Desativar todos", 32 | "events.noContentTypes": "Nenhum tipo de conteúdo API encontrado. Crie primeiro tipos de conteúdo no Content-Type Builder.", 33 | "events.contentType": "Tipo de conteúdo", 34 | "events.create": "Criar", 35 | "events.update": "Atualizar", 36 | "events.delete": "Excluir", 37 | "logging.title": "Registro", 38 | "logging.connectionLogging": "Registro de conexões", 39 | "logging.connectionLoggingHint": "Mostra conexões de clientes no log do servidor" 40 | }; 41 | exports.default = pt; 42 | -------------------------------------------------------------------------------- /dist/_chunks/fr-DtBI9vH_.mjs: -------------------------------------------------------------------------------- 1 | const fr = { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Statistiques Socket.IO", 4 | "widget.connections": "Connexions Actives", 5 | "widget.rooms": "Salles Actives", 6 | "widget.eventsPerSec": "Événements/sec", 7 | "widget.totalEvents": "Événements Totaux", 8 | "widget.viewMonitoring": "Voir Surveillance", 9 | "widget.live": "En Direct", 10 | "widget.offline": "Hors Ligne", 11 | "settings.title": "Paramètres", 12 | "settings.description": "Configurez la connexion Socket.IO pour les événements en temps réel", 13 | "settings.save": "Enregistrer", 14 | "settings.saved": "Enregistré", 15 | "settings.saveAndApply": "Enregistrer & Appliquer", 16 | "settings.success": "Paramètres enregistrés avec succès !", 17 | "settings.error": "Erreur lors de l'enregistrement des paramètres", 18 | "settings.loadError": "Erreur lors du chargement des paramètres", 19 | "settings.noRestart": "Les modifications sont appliquées immédiatement – aucun redémarrage requis !", 20 | "cors.title": "Paramètres CORS", 21 | "cors.description": "Configurez quels frontends peuvent se connecter", 22 | "cors.origin": "URL Frontend (CORS Origin)", 23 | "cors.originHint": "L'URL de votre frontend (ex: http://localhost:3000 ou https://mon-app.fr)", 24 | "cors.credentials": "Autoriser les Credentials", 25 | "cors.methods": "Méthodes HTTP autorisées", 26 | "events.title": "Événements en temps réel", 27 | "events.description": "Configurez quels événements sont envoyés pour quels types de contenu", 28 | "events.enableAll": "Tout activer", 29 | "events.disableAll": "Tout désactiver", 30 | "events.noContentTypes": "Aucun type de contenu API trouvé. Créez d'abord des types de contenu dans le Content-Type Builder.", 31 | "events.contentType": "Type de contenu", 32 | "events.create": "Créer", 33 | "events.update": "Mettre à jour", 34 | "events.delete": "Supprimer", 35 | "logging.title": "Journalisation", 36 | "logging.connectionLogging": "Journal des connexions", 37 | "logging.connectionLoggingHint": "Affiche les connexions clients dans le journal serveur" 38 | }; 39 | export { 40 | fr as default 41 | }; 42 | -------------------------------------------------------------------------------- /dist/_chunks/es-Xj8RgKuQ.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 3 | const es = { 4 | "plugin.name": "Socket.IO", 5 | "widget.socket-stats.title": "Estadísticas Socket.IO", 6 | "widget.connections": "Conexiones Activas", 7 | "widget.rooms": "Salas Activas", 8 | "widget.eventsPerSec": "Eventos/seg", 9 | "widget.totalEvents": "Eventos Totales", 10 | "widget.viewMonitoring": "Ver Monitoreo", 11 | "widget.live": "En Vivo", 12 | "widget.offline": "Desconectado", 13 | "settings.title": "Configuración", 14 | "settings.description": "Configura la conexión Socket.IO para eventos en tiempo real", 15 | "settings.save": "Guardar", 16 | "settings.saved": "Guardado", 17 | "settings.saveAndApply": "Guardar y Aplicar", 18 | "settings.success": "¡Configuración guardada correctamente!", 19 | "settings.error": "Error al guardar la configuración", 20 | "settings.loadError": "Error al cargar la configuración", 21 | "settings.noRestart": "¡Los cambios se aplican inmediatamente – no se requiere reinicio!", 22 | "cors.title": "Configuración CORS", 23 | "cors.description": "Configura qué frontends pueden conectarse", 24 | "cors.origin": "URL del Frontend (CORS Origin)", 25 | "cors.originHint": "La URL de tu frontend (ej: http://localhost:3000 o https://mi-app.es)", 26 | "cors.credentials": "Permitir Credentials", 27 | "cors.methods": "Métodos HTTP permitidos", 28 | "events.title": "Eventos en tiempo real", 29 | "events.description": "Configura qué eventos se envían para qué tipos de contenido", 30 | "events.enableAll": "Activar todos", 31 | "events.disableAll": "Desactivar todos", 32 | "events.noContentTypes": "No se encontraron tipos de contenido API. Crea primero tipos de contenido en el Content-Type Builder.", 33 | "events.contentType": "Tipo de contenido", 34 | "events.create": "Crear", 35 | "events.update": "Actualizar", 36 | "events.delete": "Eliminar", 37 | "logging.title": "Registro", 38 | "logging.connectionLogging": "Registro de conexiones", 39 | "logging.connectionLoggingHint": "Muestra las conexiones de clientes en el registro del servidor" 40 | }; 41 | exports.default = es; 42 | -------------------------------------------------------------------------------- /dist/_chunks/fr-D_r96iuZ.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 3 | const fr = { 4 | "plugin.name": "Socket.IO", 5 | "widget.socket-stats.title": "Statistiques Socket.IO", 6 | "widget.connections": "Connexions Actives", 7 | "widget.rooms": "Salles Actives", 8 | "widget.eventsPerSec": "Événements/sec", 9 | "widget.totalEvents": "Événements Totaux", 10 | "widget.viewMonitoring": "Voir Surveillance", 11 | "widget.live": "En Direct", 12 | "widget.offline": "Hors Ligne", 13 | "settings.title": "Paramètres", 14 | "settings.description": "Configurez la connexion Socket.IO pour les événements en temps réel", 15 | "settings.save": "Enregistrer", 16 | "settings.saved": "Enregistré", 17 | "settings.saveAndApply": "Enregistrer & Appliquer", 18 | "settings.success": "Paramètres enregistrés avec succès !", 19 | "settings.error": "Erreur lors de l'enregistrement des paramètres", 20 | "settings.loadError": "Erreur lors du chargement des paramètres", 21 | "settings.noRestart": "Les modifications sont appliquées immédiatement – aucun redémarrage requis !", 22 | "cors.title": "Paramètres CORS", 23 | "cors.description": "Configurez quels frontends peuvent se connecter", 24 | "cors.origin": "URL Frontend (CORS Origin)", 25 | "cors.originHint": "L'URL de votre frontend (ex: http://localhost:3000 ou https://mon-app.fr)", 26 | "cors.credentials": "Autoriser les Credentials", 27 | "cors.methods": "Méthodes HTTP autorisées", 28 | "events.title": "Événements en temps réel", 29 | "events.description": "Configurez quels événements sont envoyés pour quels types de contenu", 30 | "events.enableAll": "Tout activer", 31 | "events.disableAll": "Tout désactiver", 32 | "events.noContentTypes": "Aucun type de contenu API trouvé. Créez d'abord des types de contenu dans le Content-Type Builder.", 33 | "events.contentType": "Type de contenu", 34 | "events.create": "Créer", 35 | "events.update": "Mettre à jour", 36 | "events.delete": "Supprimer", 37 | "logging.title": "Journalisation", 38 | "logging.connectionLogging": "Journal des connexions", 39 | "logging.connectionLoggingHint": "Affiche les connexions clients dans le journal serveur" 40 | }; 41 | exports.default = fr; 42 | -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import { PLUGIN_ID } from './pluginId'; 2 | import { Initializer } from './components/Initializer'; 3 | import { PluginIcon } from './components/PluginIcon'; 4 | 5 | export default { 6 | register(app) { 7 | // Register plugin 8 | app.registerPlugin({ 9 | id: PLUGIN_ID, 10 | initializer: Initializer, 11 | isReady: false, 12 | name: PLUGIN_ID, 13 | }); 14 | 15 | // Add settings link in Strapi Settings 16 | app.createSettingSection( 17 | { 18 | id: PLUGIN_ID, 19 | intlLabel: { 20 | id: `${PLUGIN_ID}.plugin.name`, 21 | defaultMessage: 'Socket.IO', 22 | }, 23 | }, 24 | [ 25 | { 26 | intlLabel: { 27 | id: `${PLUGIN_ID}.settings.title`, 28 | defaultMessage: 'Settings', 29 | }, 30 | id: `${PLUGIN_ID}-settings`, 31 | to: `${PLUGIN_ID}/settings`, 32 | Component: () => import('./pages/SettingsPage').then((mod) => ({ default: mod.SettingsPage })), 33 | }, 34 | { 35 | intlLabel: { 36 | id: `${PLUGIN_ID}.monitoring.title`, 37 | defaultMessage: 'Monitoring', 38 | }, 39 | id: `${PLUGIN_ID}-monitoring`, 40 | to: `${PLUGIN_ID}/monitoring`, 41 | Component: () => import('./pages/MonitoringPage').then((mod) => ({ default: mod.MonitoringPage })), 42 | }, 43 | ] 44 | ); 45 | 46 | // Register Socket.IO Stats Widget for Homepage (Strapi v5.13+) 47 | if ('widgets' in app) { 48 | app.widgets.register({ 49 | icon: PluginIcon, 50 | title: { 51 | id: `${PLUGIN_ID}.widget.socket-stats.title`, 52 | defaultMessage: 'Socket.IO Stats', 53 | }, 54 | component: async () => { 55 | const component = await import('./components/SocketStatsWidget'); 56 | return component.SocketStatsWidget; 57 | }, 58 | id: 'socket-io-stats-widget', 59 | pluginId: PLUGIN_ID, 60 | }); 61 | console.log(`[${PLUGIN_ID}] ✅ Socket.IO Stats Widget registered`); 62 | } 63 | }, 64 | 65 | bootstrap(app) { 66 | console.log(`[${PLUGIN_ID}] Bootstrapping plugin...`); 67 | }, 68 | 69 | async registerTrads({ locales }) { 70 | const importedTrads = await Promise.all( 71 | locales.map((locale) => { 72 | return import(`./translations/${locale}.json`) 73 | .then(({ default: data }) => { 74 | return { 75 | data: prefixPluginTranslations(data, PLUGIN_ID), 76 | locale, 77 | }; 78 | }) 79 | .catch(() => { 80 | return { 81 | data: {}, 82 | locale, 83 | }; 84 | }); 85 | }) 86 | ); 87 | 88 | return Promise.resolve(importedTrads); 89 | }, 90 | }; 91 | 92 | // Helper to prefix translations with plugin ID 93 | const prefixPluginTranslations = (trad, pluginId) => { 94 | return Object.keys(trad).reduce((acc, current) => { 95 | acc[`${pluginId}.${current}`] = trad[current]; 96 | return acc; 97 | }, {}); 98 | }; 99 | -------------------------------------------------------------------------------- /server/services/transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isNil, isPlainObject } = require('lodash/fp'); 4 | 5 | module.exports = ({ strapi }) => { 6 | /** 7 | * Transform query response data to API format 8 | * 9 | * @param {Object} param 10 | * @param {String} param.resource 11 | * @param {Object} param.contentType 12 | */ 13 | function response({ data, schema }) { 14 | return transformResponse(data, {}, { contentType: schema }); 15 | } 16 | 17 | return { 18 | response, 19 | }; 20 | }; 21 | 22 | // adapted from https://github.com/strapi/strapi/blob/main/packages/core/strapi/src/core-api/controller/transform.ts 23 | function isEntry(property) { 24 | return property === null || isPlainObject(property) || Array.isArray(property); 25 | } 26 | 27 | function isDZEntries(property) { 28 | return Array.isArray(property); 29 | } 30 | 31 | function transformResponse(resource, meta = {}, opts = {}) { 32 | if (isNil(resource)) { 33 | return resource; 34 | } 35 | 36 | return { 37 | data: transformEntry(resource, opts?.contentType), 38 | meta, 39 | }; 40 | } 41 | 42 | function transformComponent(data, component) { 43 | if (Array.isArray(data)) { 44 | return data.map((datum) => transformComponent(datum, component)); 45 | } 46 | 47 | const res = transformEntry(data, component); 48 | 49 | if (isNil(res)) { 50 | return res; 51 | } 52 | 53 | const { id, attributes } = res; 54 | return { id, ...attributes }; 55 | } 56 | 57 | function transformEntry(entry, type) { 58 | if (isNil(entry)) { 59 | return entry; 60 | } 61 | 62 | if (Array.isArray(entry)) { 63 | return entry.map((singleEntry) => transformEntry(singleEntry, type)); 64 | } 65 | 66 | if (!isPlainObject(entry)) { 67 | throw new Error('Entry must be an object'); 68 | } 69 | 70 | const { id, ...properties } = entry; 71 | 72 | const attributeValues = {}; 73 | 74 | for (const key of Object.keys(properties)) { 75 | const property = properties[key]; 76 | const attribute = type && type.attributes[key]; 77 | 78 | if (attribute && attribute.type === 'relation' && isEntry(property) && 'target' in attribute) { 79 | const data = transformEntry(property, strapi.contentType(attribute.target)); 80 | 81 | attributeValues[key] = { data }; 82 | } else if (attribute && attribute.type === 'component' && isEntry(property)) { 83 | attributeValues[key] = transformComponent(property, strapi.components[attribute.component]); 84 | } else if (attribute && attribute.type === 'dynamiczone' && isDZEntries(property)) { 85 | if (isNil(property)) { 86 | attributeValues[key] = property; 87 | } 88 | 89 | attributeValues[key] = property.map((subProperty) => { 90 | return transformComponent(subProperty, strapi.components[subProperty.__component]); 91 | }); 92 | } else if (attribute && attribute.type === 'media' && isEntry(property)) { 93 | const data = transformEntry(property, strapi.contentType('plugin::upload.file')); 94 | 95 | attributeValues[key] = { data }; 96 | } else { 97 | attributeValues[key] = property; 98 | } 99 | } 100 | 101 | return { 102 | id, 103 | attributes: attributeValues, 104 | // NOTE: not necessary for now 105 | // meta: {}, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /server/src/services/transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isNil, isPlainObject } = require('lodash/fp'); 4 | 5 | module.exports = ({ strapi }) => { 6 | /** 7 | * Transform query response data to API format 8 | * 9 | * @param {Object} param 10 | * @param {String} param.resource 11 | * @param {Object} param.contentType 12 | */ 13 | function response({ data, schema }) { 14 | return transformResponse(data, {}, { contentType: schema }); 15 | } 16 | 17 | return { 18 | response, 19 | }; 20 | }; 21 | 22 | // adapted from https://github.com/strapi/strapi/blob/main/packages/core/strapi/src/core-api/controller/transform.ts 23 | function isEntry(property) { 24 | return property === null || isPlainObject(property) || Array.isArray(property); 25 | } 26 | 27 | function isDZEntries(property) { 28 | return Array.isArray(property); 29 | } 30 | 31 | function transformResponse(resource, meta = {}, opts = {}) { 32 | if (isNil(resource)) { 33 | return resource; 34 | } 35 | 36 | return { 37 | data: transformEntry(resource, opts?.contentType), 38 | meta, 39 | }; 40 | } 41 | 42 | function transformComponent(data, component) { 43 | if (Array.isArray(data)) { 44 | return data.map((datum) => transformComponent(datum, component)); 45 | } 46 | 47 | const res = transformEntry(data, component); 48 | 49 | if (isNil(res)) { 50 | return res; 51 | } 52 | 53 | const { id, attributes } = res; 54 | return { id, ...attributes }; 55 | } 56 | 57 | function transformEntry(entry, type) { 58 | if (isNil(entry)) { 59 | return entry; 60 | } 61 | 62 | if (Array.isArray(entry)) { 63 | return entry.map((singleEntry) => transformEntry(singleEntry, type)); 64 | } 65 | 66 | if (!isPlainObject(entry)) { 67 | throw new Error('Entry must be an object'); 68 | } 69 | 70 | const { id, ...properties } = entry; 71 | 72 | const attributeValues = {}; 73 | 74 | for (const key of Object.keys(properties)) { 75 | const property = properties[key]; 76 | const attribute = type && type.attributes[key]; 77 | 78 | if (attribute && attribute.type === 'relation' && isEntry(property) && 'target' in attribute) { 79 | const data = transformEntry(property, strapi.contentType(attribute.target)); 80 | 81 | attributeValues[key] = { data }; 82 | } else if (attribute && attribute.type === 'component' && isEntry(property)) { 83 | attributeValues[key] = transformComponent(property, strapi.components[attribute.component]); 84 | } else if (attribute && attribute.type === 'dynamiczone' && isDZEntries(property)) { 85 | if (isNil(property)) { 86 | attributeValues[key] = property; 87 | } 88 | 89 | attributeValues[key] = property.map((subProperty) => { 90 | return transformComponent(subProperty, strapi.components[subProperty.__component]); 91 | }); 92 | } else if (attribute && attribute.type === 'media' && isEntry(property)) { 93 | const data = transformEntry(property, strapi.contentType('plugin::upload.file')); 94 | 95 | attributeValues[key] = { data }; 96 | } else { 97 | attributeValues[key] = property; 98 | } 99 | } 100 | 101 | return { 102 | id, 103 | attributes: attributeValues, 104 | // NOTE: not necessary for now 105 | // meta: {}, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /server/structures/SocketIO.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Server } = require('socket.io'); 4 | const { handshake } = require('../middleware'); 5 | const { getService } = require('../utils/getService'); 6 | const { pluginId } = require('../utils/pluginId'); 7 | const { API_TOKEN_TYPE } = require('../utils/constants'); 8 | 9 | class SocketIO { 10 | constructor(options) { 11 | this._socket = new Server(strapi.server.httpServer, options); 12 | // Hooks are now optional and read from stored settings, not config 13 | // The init hook is called from bootstrap/io.js after settings are loaded 14 | this._socket.use(handshake); 15 | } 16 | 17 | // eslint-disable-next-line no-unused-vars 18 | async emit({ event, schema, data: rawData }) { 19 | const sanitizeService = getService({ name: 'sanitize' }); 20 | const strategyService = getService({ name: 'strategy' }); 21 | const transformService = getService({ name: 'transform' }); 22 | 23 | // account for unsaved single content type being null 24 | if (!rawData) { 25 | return; 26 | } 27 | 28 | const eventName = `${schema.singularName}:${event}`; 29 | 30 | // Extract entity ID for entity-specific room 31 | const entityId = rawData.id || rawData.documentId; 32 | const entityRoomName = entityId ? `${schema.uid}:${entityId}` : null; 33 | 34 | for (const strategyType in strategyService) { 35 | if (Object.hasOwnProperty.call(strategyService, strategyType)) { 36 | const strategy = strategyService[strategyType]; 37 | 38 | const rooms = await strategy.getRooms(); 39 | 40 | for (const room of rooms) { 41 | const permissions = room.permissions.map(({ action }) => ({ action })); 42 | const ability = await strapi.contentAPI.permissions.engine.generateAbility(permissions); 43 | 44 | if (room.type === API_TOKEN_TYPE.FULL_ACCESS || ability.can(schema.uid + '.' + event)) { 45 | // sanitize 46 | const sanitizedData = await sanitizeService.output({ 47 | data: rawData, 48 | schema, 49 | options: { 50 | auth: { 51 | name: strategy.name, 52 | ability, 53 | strategy: { 54 | verify: strategy.verify, 55 | }, 56 | credentials: strategy.credentials?.(room), 57 | }, 58 | }, 59 | }); 60 | 61 | const roomName = strategy.getRoomName(room); 62 | 63 | // transform 64 | const data = transformService.response({ data: sanitizedData, schema }); 65 | 66 | // Emit to role-based room (existing behavior) 67 | this._socket.to(roomName.replace(' ', '-')).emit(eventName, { ...data }); 68 | 69 | // Also emit to entity-specific room if ID exists 70 | if (entityRoomName) { 71 | this._socket.to(entityRoomName).emit(eventName, { ...data }); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | async raw({ event, data, rooms }) { 80 | let emitter = this._socket; 81 | 82 | // send to all specified rooms 83 | if (rooms && rooms.length) { 84 | rooms.forEach((r) => { 85 | emitter = emitter.to(r); 86 | }); 87 | } 88 | 89 | emitter.emit(event, { data }); 90 | } 91 | 92 | get server() { 93 | return this._socket; 94 | } 95 | } 96 | 97 | module.exports = { 98 | SocketIO, 99 | }; 100 | -------------------------------------------------------------------------------- /server/src/structures/SocketIO.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Server } = require('socket.io'); 4 | const { handshake } = require('../middleware'); 5 | const { getService } = require('../utils/getService'); 6 | const { pluginId } = require('../utils/pluginId'); 7 | const { API_TOKEN_TYPE } = require('../utils/constants'); 8 | 9 | class SocketIO { 10 | constructor(options) { 11 | this._socket = new Server(strapi.server.httpServer, options); 12 | const { hooks } = strapi.config.get(`plugin.${pluginId}`); 13 | hooks.init?.({ strapi, $io: this }); 14 | this._socket.use(handshake); 15 | } 16 | 17 | // eslint-disable-next-line no-unused-vars 18 | async emit({ event, schema, data: rawData }) { 19 | const sanitizeService = getService({ name: 'sanitize' }); 20 | const strategyService = getService({ name: 'strategy' }); 21 | const transformService = getService({ name: 'transform' }); 22 | 23 | // account for unsaved single content type being null 24 | if (!rawData) { 25 | return; 26 | } 27 | 28 | const eventName = `${schema.singularName}:${event}`; 29 | 30 | for (const strategyType in strategyService) { 31 | if (Object.hasOwnProperty.call(strategyService, strategyType)) { 32 | const strategy = strategyService[strategyType]; 33 | 34 | const rooms = await strategy.getRooms(); 35 | 36 | for (const room of rooms) { 37 | const permissions = room.permissions.map(({ action }) => ({ action })); 38 | const ability = await strapi.contentAPI.permissions.engine.generateAbility(permissions); 39 | 40 | if (room.type === API_TOKEN_TYPE.FULL_ACCESS || ability.can(schema.uid + '.' + event)) { 41 | // sanitize 42 | const sanitizedData = await sanitizeService.output({ 43 | data: rawData, 44 | schema, 45 | options: { 46 | auth: { 47 | name: strategy.name, 48 | ability, 49 | strategy: { 50 | verify: strategy.verify, 51 | }, 52 | credentials: strategy.credentials?.(room), 53 | }, 54 | }, 55 | }); 56 | 57 | const roomName = strategy.getRoomName(room); 58 | 59 | // transform 60 | const data = transformService.response({ data: sanitizedData, schema }); 61 | // emit 62 | this._socket.to(roomName.replace(' ', '-')).emit(eventName, { ...data }); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Emit a raw event without schema-based sanitization. 71 | * Still removes sensitive fields for security. 72 | * @param {object} options - Emit options 73 | * @param {string} options.event - Event name 74 | * @param {any} options.data - Data to emit 75 | * @param {string[]} options.rooms - Optional rooms to emit to 76 | */ 77 | async raw({ event, data, rooms }) { 78 | const sanitizeService = getService({ name: 'sanitize' }); 79 | 80 | let emitter = this._socket; 81 | 82 | // send to all specified rooms 83 | if (rooms && rooms.length) { 84 | rooms.forEach((r) => { 85 | emitter = emitter.to(r); 86 | }); 87 | } 88 | 89 | // Sanitize data to remove sensitive fields 90 | const sanitizedData = sanitizeService.sanitizeRaw(data); 91 | emitter.emit(event, { data: sanitizedData }); 92 | } 93 | 94 | get server() { 95 | return this._socket; 96 | } 97 | } 98 | 99 | module.exports = { 100 | SocketIO, 101 | }; 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package", 3 | "name": "@strapi-community/plugin-io", 4 | "version": "5.0.3", 5 | "description": "A plugin for Strapi CMS that provides the ability for Socket IO integration", 6 | "keywords": [ 7 | "strapi", 8 | "strapi-plugin", 9 | "plugin", 10 | "strapi plugin", 11 | "socket", 12 | "socket io", 13 | "io" 14 | ], 15 | "type": "commonjs", 16 | "exports": { 17 | "./package.json": "./package.json", 18 | "./strapi-admin": { 19 | "source": "./admin/src/index.js", 20 | "import": "./dist/admin/index.mjs", 21 | "require": "./dist/admin/index.js", 22 | "default": "./dist/admin/index.js" 23 | }, 24 | "./strapi-server": { 25 | "source": "./server/index.js", 26 | "import": "./dist/server/index.mjs", 27 | "require": "./dist/server/index.js", 28 | "default": "./dist/server/index.js" 29 | } 30 | }, 31 | "files": [ 32 | "dist", 33 | "types.d.ts", 34 | "README.md" 35 | ], 36 | "scripts": { 37 | "build": "strapi-plugin build", 38 | "watch": "strapi-plugin watch", 39 | "watch:link": "strapi-plugin watch:link", 40 | "verify": "strapi-plugin verify", 41 | "lint": "eslint . --fix", 42 | "format": "prettier --write \"./**/*.{js,json,yml,md}\"" 43 | }, 44 | "dependencies": { 45 | "date-fns": "^4.1.0", 46 | "socket.io": "^4.8.1", 47 | "zod": "^3.24.1" 48 | }, 49 | "devDependencies": { 50 | "@strapi/sdk-plugin": "^5.3.2", 51 | "@strapi/strapi": "^5.33.0", 52 | "@strapi/design-system": "^2.0.2", 53 | "@strapi/icons": "^2.0.2", 54 | "eslint": "^8.57.1", 55 | "eslint-config-prettier": "^9.1.2", 56 | "prettier": "^3.7.4", 57 | "react": "^18.3.1", 58 | "react-dom": "^18.3.1", 59 | "react-router-dom": "^6.30.2", 60 | "styled-components": "^6.1.19" 61 | }, 62 | "peerDependencies": { 63 | "@strapi/strapi": "^5.0.0", 64 | "@strapi/design-system": "^2.0.0-rc.1", 65 | "@strapi/icons": "^2.0.0-rc.1", 66 | "react": "^18.0.0", 67 | "react-dom": "^18.0.0", 68 | "react-router-dom": "^6.0.0", 69 | "styled-components": "^6.0.0" 70 | }, 71 | "peerDependenciesMeta": { 72 | "@strapi/design-system": { 73 | "optional": true 74 | }, 75 | "@strapi/icons": { 76 | "optional": true 77 | } 78 | }, 79 | "strapi": { 80 | "name": "io", 81 | "displayName": "IO", 82 | "description": "A plugin for Strapi CMS that provides the ability for Socket IO integration", 83 | "kind": "plugin" 84 | }, 85 | "engines": { 86 | "node": ">=18.0.0 <=22.x.x", 87 | "npm": ">=6.0.0" 88 | }, 89 | "author": { 90 | "name": "@ComfortablyCoding", 91 | "url": "https://github.com/ComfortablyCoding" 92 | }, 93 | "maintainers": [ 94 | { 95 | "name": "@ComfortablyCoding", 96 | "url": "https://github.com/ComfortablyCoding", 97 | "lead": true 98 | }, 99 | { 100 | "name": "@hrdunn", 101 | "url": "https://github.com/hrdunn" 102 | } 103 | ], 104 | "homepage": "https://github.com/strapi-community/strapi-plugin-io#readme", 105 | "repository": { 106 | "type": "git", 107 | "url": "https://github.com/strapi-community/strapi-plugin-io.git" 108 | }, 109 | "bugs": { 110 | "url": "https://github.com/strapi-community/strapi-plugin-io/issues" 111 | }, 112 | "license": "MIT" 113 | } 114 | -------------------------------------------------------------------------------- /server/src/services/sanitize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { sanitize } = require('@strapi/utils'); 4 | 5 | /** 6 | * Default sensitive field names that should NEVER be emitted via Socket.IO. 7 | * These are removed regardless of schema settings. 8 | */ 9 | const DEFAULT_SENSITIVE_FIELDS = [ 10 | 'password', 11 | 'resetPasswordToken', 12 | 'confirmationToken', 13 | 'refreshToken', 14 | 'accessToken', 15 | 'secret', 16 | 'apiKey', 17 | 'api_key', 18 | 'privateKey', 19 | 'private_key', 20 | 'token', 21 | 'salt', 22 | 'hash', 23 | ]; 24 | 25 | /** 26 | * Recursively removes sensitive fields from an object 27 | * @param {any} data - The data to sanitize 28 | * @param {string[]} sensitiveFields - List of field names to remove 29 | * @returns {any} Sanitized data 30 | */ 31 | function removeSensitiveFields(data, sensitiveFields) { 32 | if (!data || typeof data !== 'object') { 33 | return data; 34 | } 35 | 36 | if (Array.isArray(data)) { 37 | return data.map(item => removeSensitiveFields(item, sensitiveFields)); 38 | } 39 | 40 | const result = {}; 41 | for (const [key, value] of Object.entries(data)) { 42 | // Skip sensitive fields (case-insensitive check) 43 | const lowerKey = key.toLowerCase(); 44 | if (sensitiveFields.some(sf => lowerKey === sf.toLowerCase() || lowerKey.includes(sf.toLowerCase()))) { 45 | continue; 46 | } 47 | 48 | // Recursively sanitize nested objects 49 | if (value && typeof value === 'object') { 50 | result[key] = removeSensitiveFields(value, sensitiveFields); 51 | } else { 52 | result[key] = value; 53 | } 54 | } 55 | 56 | return result; 57 | } 58 | 59 | module.exports = ({ strapi }) => { 60 | /** 61 | * Get list of sensitive fields from plugin settings 62 | * @returns {string[]} Combined list of default and custom sensitive fields 63 | */ 64 | function getSensitiveFields() { 65 | const customFields = strapi.config.get('plugin.io.sensitiveFields', []); 66 | return [...DEFAULT_SENSITIVE_FIELDS, ...customFields]; 67 | } 68 | 69 | /** 70 | * Sanitize data output with a provided schema for a specified role. 71 | * Applies both Strapi's content API sanitization and additional 72 | * sensitive field removal. 73 | * 74 | * @param {Object} param 75 | * @param {Object} param.schema - Content type schema 76 | * @param {Object} param.data - Data to sanitize 77 | * @param {Object} param.options - Sanitization options (auth, etc.) 78 | * @returns {Object} Sanitized data 79 | */ 80 | async function output({ schema, data, options }) { 81 | let sanitizedData = data; 82 | 83 | // First: Apply Strapi's built-in content API sanitization 84 | // This handles private: true fields and permission-based filtering 85 | if (sanitize?.contentAPI?.output) { 86 | try { 87 | sanitizedData = await sanitize.contentAPI.output(data, schema, options); 88 | } catch (error) { 89 | strapi.log.debug(`[socket.io] Content API sanitization failed: ${error.message}`); 90 | // Continue with manual sanitization 91 | } 92 | } 93 | 94 | // Second: Remove any remaining sensitive fields as extra safety layer 95 | const sensitiveFields = getSensitiveFields(); 96 | sanitizedData = removeSensitiveFields(sanitizedData, sensitiveFields); 97 | 98 | return sanitizedData; 99 | } 100 | 101 | /** 102 | * Sanitize data for raw emit (without schema-based sanitization) 103 | * @param {any} data - Data to sanitize 104 | * @returns {any} Sanitized data 105 | */ 106 | function sanitizeRaw(data) { 107 | const sensitiveFields = getSensitiveFields(); 108 | return removeSensitiveFields(data, sensitiveFields); 109 | } 110 | 111 | return { 112 | output, 113 | sanitizeRaw, 114 | getSensitiveFields, 115 | DEFAULT_SENSITIVE_FIELDS, 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [main, main-v5] 6 | pull_request: 7 | branches: [main, main-v5] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | inputs: 12 | npm_tag: 13 | description: 'NPM tag (latest, beta, next)' 14 | required: true 15 | default: 'latest' 16 | type: choice 17 | options: 18 | - latest 19 | - beta 20 | - next 21 | 22 | env: 23 | NODE_VERSION: '20.x' 24 | 25 | jobs: 26 | build: 27 | name: Build & Verify 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ env.NODE_VERSION }} 38 | cache: 'npm' 39 | 40 | - name: Install dependencies 41 | run: npm ci 42 | 43 | - name: Build plugin 44 | run: npm run build 45 | 46 | - name: Verify plugin structure 47 | run: npm run verify 48 | 49 | - name: Upload build artifacts 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: dist 53 | path: dist/ 54 | retention-days: 7 55 | 56 | publish: 57 | name: Publish to NPM 58 | runs-on: ubuntu-latest 59 | needs: build 60 | if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' 61 | permissions: 62 | contents: read 63 | id-token: write 64 | steps: 65 | - name: Checkout repository 66 | uses: actions/checkout@v4 67 | 68 | - name: Setup Node.js 69 | uses: actions/setup-node@v4 70 | with: 71 | node-version: ${{ env.NODE_VERSION }} 72 | registry-url: 'https://registry.npmjs.org' 73 | cache: 'npm' 74 | 75 | - name: Download build artifacts 76 | uses: actions/download-artifact@v4 77 | with: 78 | name: dist 79 | path: dist/ 80 | 81 | - name: Verify version matches release tag 82 | if: github.event_name == 'release' 83 | run: | 84 | PKG_VERSION=$(node -p "require('./package.json').version") 85 | RELEASE_TAG="${{ github.event.release.tag_name }}" 86 | RELEASE_VERSION="${RELEASE_TAG#v}" 87 | echo "[INFO] Package: $PKG_VERSION, Release: $RELEASE_VERSION" 88 | if [ "$PKG_VERSION" != "$RELEASE_VERSION" ]; then 89 | echo "[ERROR] Version mismatch!" 90 | exit 1 91 | fi 92 | 93 | - name: Determine npm tag 94 | id: npm-tag 95 | run: | 96 | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then 97 | echo "tag=${{ github.event.inputs.npm_tag }}" >> $GITHUB_OUTPUT 98 | exit 0 99 | fi 100 | RELEASE_TAG="${{ github.event.release.tag_name }}" 101 | if [[ "$RELEASE_TAG" == *"-beta"* ]]; then 102 | echo "tag=beta" >> $GITHUB_OUTPUT 103 | elif [[ "$RELEASE_TAG" == *"-alpha"* ]]; then 104 | echo "tag=alpha" >> $GITHUB_OUTPUT 105 | elif [[ "$RELEASE_TAG" == *"-rc"* ]]; then 106 | echo "tag=next" >> $GITHUB_OUTPUT 107 | else 108 | echo "tag=latest" >> $GITHUB_OUTPUT 109 | fi 110 | 111 | - name: Publish to NPM 112 | run: npm publish --access public --tag ${{ steps.npm-tag.outputs.tag }} 113 | env: 114 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 115 | 116 | - name: Done 117 | run: | 118 | PKG_NAME=$(node -p "require('./package.json').name") 119 | PKG_VERSION=$(node -p "require('./package.json').version") 120 | echo "[SUCCESS] Published $PKG_NAME@$PKG_VERSION" 121 | -------------------------------------------------------------------------------- /server/services/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { pluginId } = require('../utils/pluginId'); 4 | 5 | /** 6 | * Settings service for io plugin 7 | * Stores and retrieves plugin settings from Strapi's plugin store 8 | */ 9 | module.exports = ({ strapi }) => { 10 | const getPluginStore = () => { 11 | return strapi.store({ 12 | type: 'plugin', 13 | name: pluginId, 14 | }); 15 | }; 16 | 17 | const getDefaultSettings = () => ({ 18 | enabled: true, 19 | 20 | // CORS Settings (only origins, methods/credentials are per-role) 21 | cors: { 22 | origins: ['http://localhost:3000'], // Multiple origins 23 | }, 24 | 25 | // Connection Settings 26 | connection: { 27 | maxConnections: 1000, 28 | pingTimeout: 20000, // ms 29 | pingInterval: 25000, // ms 30 | connectionTimeout: 45000, // ms 31 | }, 32 | 33 | // Security Settings 34 | security: { 35 | requireAuthentication: false, 36 | rateLimiting: { 37 | enabled: false, 38 | maxEventsPerSecond: 10, 39 | }, 40 | ipWhitelist: [], 41 | ipBlacklist: [], 42 | }, 43 | 44 | // Content Types with granular permissions 45 | contentTypes: {}, // Object: { 'api::session.session': { create: true, update: true, delete: false, config: {...} } } 46 | 47 | // Event Configuration (global defaults) 48 | events: { 49 | customEventNames: false, // Use custom names like 'session:created' instead of 'session:create' 50 | includeRelations: false, // Populate relations 51 | excludeFields: [], // Fields to exclude globally 52 | onlyPublished: false, // Only send events for published content (Draft & Publish) 53 | }, 54 | 55 | // Rooms & Channels 56 | rooms: { 57 | autoJoinByRole: {}, // { 'authenticated': ['users'], 'admin': ['admins'] } 58 | enablePrivateRooms: false, 59 | }, 60 | 61 | // Entity Subscriptions (NEW) 62 | entitySubscriptions: { 63 | enabled: true, // Enable/disable entity-specific subscriptions 64 | maxSubscriptionsPerSocket: 100, // Max entities a socket can subscribe to 65 | requireVerification: true, // Verify entity exists before subscribing 66 | allowedContentTypes: [], // Empty = all allowed, or specific UIDs: ['api::article.article'] 67 | enableMetrics: true, // Track subscription metrics 68 | }, 69 | 70 | // Role-based Permissions 71 | rolePermissions: { 72 | // Default: all roles can connect with all methods 73 | authenticated: { 74 | canConnect: true, 75 | allowCredentials: true, 76 | allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'], 77 | contentTypes: {}, 78 | }, 79 | public: { 80 | canConnect: true, 81 | allowCredentials: false, 82 | allowedMethods: ['GET'], 83 | contentTypes: {}, 84 | }, 85 | }, 86 | 87 | // Redis Adapter (for multi-server scaling) 88 | redis: { 89 | enabled: false, 90 | url: 'redis://localhost:6379', 91 | }, 92 | 93 | // Namespaces 94 | namespaces: { 95 | enabled: false, 96 | list: { 97 | // Example: 'admin': { requireAuth: true }, 98 | // Example: 'chat': { requireAuth: false }, 99 | }, 100 | }, 101 | 102 | // Custom Middleware 103 | middleware: { 104 | enabled: false, 105 | handlers: [], // Array of middleware functions 106 | }, 107 | 108 | // Monitoring & Logging 109 | monitoring: { 110 | enableConnectionLogging: true, 111 | enableEventLogging: false, 112 | maxEventLogSize: 100, 113 | }, 114 | }); 115 | 116 | return { 117 | /** 118 | * Get current settings (merged with defaults) 119 | */ 120 | async getSettings() { 121 | const pluginStore = getPluginStore(); 122 | const storedSettings = await pluginStore.get({ key: 'settings' }); 123 | const defaults = getDefaultSettings(); 124 | 125 | if (!storedSettings) { 126 | return defaults; 127 | } 128 | 129 | return { 130 | ...defaults, 131 | ...storedSettings, 132 | }; 133 | }, 134 | 135 | /** 136 | * Update settings 137 | */ 138 | async setSettings(newSettings) { 139 | const pluginStore = getPluginStore(); 140 | const currentSettings = await this.getSettings(); 141 | 142 | const updatedSettings = { 143 | ...currentSettings, 144 | ...newSettings, 145 | }; 146 | 147 | await pluginStore.set({ 148 | key: 'settings', 149 | value: updatedSettings, 150 | }); 151 | 152 | return updatedSettings; 153 | }, 154 | 155 | /** 156 | * Get default settings 157 | */ 158 | getDefaultSettings, 159 | }; 160 | }; 161 | -------------------------------------------------------------------------------- /server/services/monitoring.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { pluginId } = require('../utils/pluginId'); 4 | 5 | /** 6 | * Monitoring service for Socket.IO 7 | * Tracks connections, events, and provides statistics 8 | */ 9 | module.exports = ({ strapi }) => { 10 | // In-memory storage for event logs 11 | let eventLog = []; 12 | let eventStats = { 13 | totalEvents: 0, 14 | eventsByType: {}, 15 | lastReset: Date.now(), 16 | }; 17 | 18 | return { 19 | /** 20 | * Get current connection statistics 21 | */ 22 | getConnectionStats() { 23 | const io = strapi.$io?.server; 24 | if (!io) { 25 | return { 26 | connected: 0, 27 | rooms: [], 28 | sockets: [], 29 | entitySubscriptions: { 30 | total: 0, 31 | byContentType: {}, 32 | }, 33 | }; 34 | } 35 | 36 | const sockets = Array.from(io.sockets.sockets.values()); 37 | const rooms = Array.from(io.sockets.adapter.rooms.keys()) 38 | .filter((room) => !io.sockets.sockets.has(room)); // Filter out socket IDs 39 | 40 | // Calculate entity subscription metrics 41 | const entityRooms = rooms.filter(room => room.includes(':') && room.match(/^(api|plugin)::/)); 42 | const entitySubsByType = {}; 43 | entityRooms.forEach(room => { 44 | const uid = room.substring(0, room.lastIndexOf(':')); 45 | entitySubsByType[uid] = (entitySubsByType[uid] || 0) + 1; 46 | }); 47 | 48 | return { 49 | connected: sockets.length, 50 | rooms: rooms.map((room) => ({ 51 | name: room, 52 | members: io.sockets.adapter.rooms.get(room)?.size || 0, 53 | isEntityRoom: room.includes(':') && room.match(/^(api|plugin)::/) !== null, 54 | })), 55 | sockets: sockets.map((socket) => ({ 56 | id: socket.id, 57 | connected: socket.connected, 58 | rooms: Array.from(socket.rooms).filter((r) => r !== socket.id), 59 | entitySubscriptions: Array.from(socket.rooms) 60 | .filter((r) => r !== socket.id && r.includes(':') && r.match(/^(api|plugin)::/)) 61 | .map(room => { 62 | const lastColon = room.lastIndexOf(':'); 63 | return { 64 | uid: room.substring(0, lastColon), 65 | id: room.substring(lastColon + 1), 66 | room: room, 67 | }; 68 | }), 69 | handshake: { 70 | address: socket.handshake.address, 71 | time: socket.handshake.time, 72 | query: socket.handshake.query, 73 | }, 74 | // Include user info if authenticated 75 | user: socket.user || null, 76 | })), 77 | entitySubscriptions: { 78 | total: entityRooms.length, 79 | byContentType: entitySubsByType, 80 | rooms: entityRooms, 81 | }, 82 | }; 83 | }, 84 | 85 | /** 86 | * Get event statistics 87 | */ 88 | getEventStats() { 89 | return { 90 | ...eventStats, 91 | eventsPerSecond: this.getEventsPerSecond(), 92 | }; 93 | }, 94 | 95 | /** 96 | * Get recent event log 97 | */ 98 | getEventLog(limit = 50) { 99 | return eventLog.slice(-limit); 100 | }, 101 | 102 | /** 103 | * Log an event 104 | */ 105 | logEvent(eventType, data = {}) { 106 | const settings = strapi.$ioSettings || {}; 107 | if (!settings.monitoring?.enableEventLogging) return; 108 | 109 | const entry = { 110 | timestamp: Date.now(), 111 | type: eventType, 112 | data, 113 | }; 114 | 115 | eventLog.push(entry); 116 | 117 | // Update stats 118 | eventStats.totalEvents++; 119 | eventStats.eventsByType[eventType] = (eventStats.eventsByType[eventType] || 0) + 1; 120 | 121 | // Trim log if too large 122 | const maxSize = settings.monitoring?.maxEventLogSize || 100; 123 | if (eventLog.length > maxSize) { 124 | eventLog = eventLog.slice(-maxSize); 125 | } 126 | }, 127 | 128 | /** 129 | * Calculate events per second 130 | */ 131 | getEventsPerSecond() { 132 | const now = Date.now(); 133 | const elapsed = (now - eventStats.lastReset) / 1000; 134 | return elapsed > 0 ? (eventStats.totalEvents / elapsed).toFixed(2) : 0; 135 | }, 136 | 137 | /** 138 | * Reset statistics 139 | */ 140 | resetStats() { 141 | eventLog = []; 142 | eventStats = { 143 | totalEvents: 0, 144 | eventsByType: {}, 145 | lastReset: Date.now(), 146 | }; 147 | }, 148 | 149 | /** 150 | * Send test event 151 | */ 152 | sendTestEvent(eventName = 'test', data = {}) { 153 | const io = strapi.$io?.server; 154 | if (!io) { 155 | throw new Error('Socket.IO not initialized'); 156 | } 157 | 158 | const testData = { 159 | ...data, 160 | timestamp: Date.now(), 161 | test: true, 162 | }; 163 | 164 | io.emit(eventName, testData); 165 | this.logEvent('test', { eventName, data: testData }); 166 | 167 | return { 168 | success: true, 169 | eventName, 170 | data: testData, 171 | recipients: io.sockets.sockets.size, 172 | }; 173 | }, 174 | }; 175 | }; 176 | -------------------------------------------------------------------------------- /server/controllers/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { pluginId } = require('../utils/pluginId'); 4 | 5 | /** 6 | * Settings controller for io plugin 7 | */ 8 | module.exports = ({ strapi }) => ({ 9 | /** 10 | * GET /io/settings 11 | * Retrieve current plugin settings 12 | */ 13 | async getSettings(ctx) { 14 | const settingsService = strapi.plugin(pluginId).service('settings'); 15 | const settings = await settingsService.getSettings(); 16 | ctx.body = { data: settings }; 17 | }, 18 | 19 | /** 20 | * PUT /io/settings 21 | * Update plugin settings and hot-reload Socket.IO 22 | */ 23 | async updateSettings(ctx) { 24 | const settingsService = strapi.plugin(pluginId).service('settings'); 25 | const { body } = ctx.request; 26 | 27 | // Get old settings to compare 28 | const oldSettings = await settingsService.getSettings(); 29 | const updatedSettings = await settingsService.setSettings(body); 30 | 31 | // Update stored settings reference for lifecycle hooks 32 | strapi.$ioSettings = updatedSettings; 33 | 34 | // Hot-reload: Update connection logging handler 35 | let reloaded = false; 36 | if (strapi.$io?.server) { 37 | // Log the change 38 | strapi.log.info(`socket.io: Settings updated (origin: ${updatedSettings.cors?.origin}, contentTypes: ${updatedSettings.contentTypes?.length || 0})`); 39 | reloaded = true; 40 | } 41 | 42 | ctx.body = { data: updatedSettings, reloaded }; 43 | }, 44 | 45 | /** 46 | * GET /io/content-types 47 | * Get available content types for selection 48 | */ 49 | async getContentTypes(ctx) { 50 | const contentTypes = Object.keys(strapi.contentTypes) 51 | .filter((uid) => uid.startsWith('api::')) 52 | .map((uid) => { 53 | const ct = strapi.contentTypes[uid]; 54 | return { 55 | uid, 56 | displayName: ct.info?.displayName || ct.info?.singularName || uid, 57 | singularName: ct.info?.singularName, 58 | pluralName: ct.info?.pluralName, 59 | }; 60 | }); 61 | 62 | ctx.body = { data: contentTypes }; 63 | }, 64 | 65 | /** 66 | * GET /io/stats 67 | * Get connection and event statistics 68 | */ 69 | async getStats(ctx) { 70 | const monitoringService = strapi.plugin(pluginId).service('monitoring'); 71 | const connectionStats = monitoringService.getConnectionStats(); 72 | const eventStats = monitoringService.getEventStats(); 73 | 74 | ctx.body = { 75 | data: { 76 | connections: connectionStats, 77 | events: eventStats, 78 | }, 79 | }; 80 | }, 81 | 82 | /** 83 | * GET /io/event-log 84 | * Get recent event log 85 | */ 86 | async getEventLog(ctx) { 87 | const monitoringService = strapi.plugin(pluginId).service('monitoring'); 88 | const limit = parseInt(ctx.query.limit) || 50; 89 | const log = monitoringService.getEventLog(limit); 90 | 91 | ctx.body = { data: log }; 92 | }, 93 | 94 | /** 95 | * POST /io/test-event 96 | * Send a test event 97 | */ 98 | async sendTestEvent(ctx) { 99 | const monitoringService = strapi.plugin(pluginId).service('monitoring'); 100 | const { eventName, data } = ctx.request.body; 101 | 102 | try { 103 | const result = monitoringService.sendTestEvent(eventName || 'test', data || {}); 104 | ctx.body = { data: result }; 105 | } catch (error) { 106 | ctx.throw(500, error.message); 107 | } 108 | }, 109 | 110 | /** 111 | * POST /io/reset-stats 112 | * Reset monitoring statistics 113 | */ 114 | async resetStats(ctx) { 115 | const monitoringService = strapi.plugin(pluginId).service('monitoring'); 116 | monitoringService.resetStats(); 117 | ctx.body = { data: { success: true } }; 118 | }, 119 | 120 | /** 121 | * GET /io/roles 122 | * Get available user roles for permissions configuration 123 | */ 124 | async getRoles(ctx) { 125 | // Use Document Service API (Strapi v5) 126 | const roles = await strapi.documents('plugin::users-permissions.role').findMany({}); 127 | ctx.body = { 128 | data: roles.map((role) => ({ 129 | id: role.id, 130 | name: role.name, 131 | type: role.type, 132 | description: role.description, 133 | })), 134 | }; 135 | }, 136 | 137 | /** 138 | * GET /io/monitoring/stats 139 | * Get lightweight stats for dashboard widget 140 | */ 141 | async getMonitoringStats(ctx) { 142 | const monitoringService = strapi.plugin(pluginId).service('monitoring'); 143 | const connectionStats = monitoringService.getConnectionStats(); 144 | const eventStats = monitoringService.getEventStats(); 145 | 146 | // Return lightweight stats optimized for widget 147 | ctx.body = { 148 | data: { 149 | connections: { 150 | connected: connectionStats.connected, 151 | rooms: connectionStats.rooms || [], 152 | }, 153 | events: { 154 | totalEvents: eventStats.totalEvents || 0, 155 | eventsPerSecond: eventStats.eventsPerSecond || 0, 156 | eventsByType: eventStats.eventsByType || {}, 157 | }, 158 | timestamp: Date.now(), 159 | }, 160 | }; 161 | }, 162 | }); 163 | -------------------------------------------------------------------------------- /server/src/services/security.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Rate limiting service to prevent abuse 5 | */ 6 | module.exports = ({ strapi }) => { 7 | // Store rate limit data in memory (consider Redis for production) 8 | const rateLimitStore = new Map(); 9 | const connectionLimitStore = new Map(); 10 | 11 | /** 12 | * Clean up old entries periodically 13 | */ 14 | const cleanupInterval = setInterval(() => { 15 | const now = Date.now(); 16 | const maxAge = 60 * 1000; // 1 minute 17 | 18 | for (const [key, value] of rateLimitStore.entries()) { 19 | if (now - value.resetTime > maxAge) { 20 | rateLimitStore.delete(key); 21 | } 22 | } 23 | 24 | for (const [key, value] of connectionLimitStore.entries()) { 25 | if (now - value.lastSeen > maxAge) { 26 | connectionLimitStore.delete(key); 27 | } 28 | } 29 | }, 60 * 1000); 30 | 31 | // Cleanup on shutdown 32 | process.on('SIGTERM', () => clearInterval(cleanupInterval)); 33 | process.on('SIGINT', () => clearInterval(cleanupInterval)); 34 | 35 | return { 36 | /** 37 | * Check if request should be rate limited 38 | * @param {string} identifier - Unique identifier (IP, user ID, etc.) 39 | * @param {Object} options - Rate limit options 40 | * @param {number} options.maxRequests - Maximum requests allowed 41 | * @param {number} options.windowMs - Time window in milliseconds 42 | * @returns {Object} - { allowed: boolean, remaining: number, resetTime: number } 43 | */ 44 | checkRateLimit(identifier, options = {}) { 45 | const { maxRequests = 100, windowMs = 60 * 1000 } = options; 46 | const now = Date.now(); 47 | const key = `ratelimit_${identifier}`; 48 | 49 | let record = rateLimitStore.get(key); 50 | 51 | if (!record || now - record.resetTime > windowMs) { 52 | // Create new record or reset expired one 53 | record = { 54 | count: 1, 55 | resetTime: now, 56 | windowMs, 57 | }; 58 | rateLimitStore.set(key, record); 59 | 60 | return { 61 | allowed: true, 62 | remaining: maxRequests - 1, 63 | resetTime: now + windowMs, 64 | }; 65 | } 66 | 67 | // Check if limit exceeded 68 | if (record.count >= maxRequests) { 69 | return { 70 | allowed: false, 71 | remaining: 0, 72 | resetTime: record.resetTime + windowMs, 73 | retryAfter: record.resetTime + windowMs - now, 74 | }; 75 | } 76 | 77 | // Increment counter 78 | record.count++; 79 | rateLimitStore.set(key, record); 80 | 81 | return { 82 | allowed: true, 83 | remaining: maxRequests - record.count, 84 | resetTime: record.resetTime + windowMs, 85 | }; 86 | }, 87 | 88 | /** 89 | * Check connection limits per IP/user 90 | * @param {string} identifier - Unique identifier 91 | * @param {number} maxConnections - Maximum allowed connections 92 | * @returns {boolean} - Whether connection is allowed 93 | */ 94 | checkConnectionLimit(identifier, maxConnections = 5) { 95 | const key = `connlimit_${identifier}`; 96 | const record = connectionLimitStore.get(key); 97 | const now = Date.now(); 98 | 99 | if (!record) { 100 | connectionLimitStore.set(key, { 101 | count: 1, 102 | lastSeen: now, 103 | }); 104 | return true; 105 | } 106 | 107 | if (record.count >= maxConnections) { 108 | strapi.log.warn(`[Socket.IO Security] Connection limit exceeded for ${identifier}`); 109 | return false; 110 | } 111 | 112 | record.count++; 113 | record.lastSeen = now; 114 | connectionLimitStore.set(key, record); 115 | return true; 116 | }, 117 | 118 | /** 119 | * Release a connection slot 120 | * @param {string} identifier - Unique identifier 121 | */ 122 | releaseConnection(identifier) { 123 | const key = `connlimit_${identifier}`; 124 | const record = connectionLimitStore.get(key); 125 | 126 | if (record) { 127 | record.count = Math.max(0, record.count - 1); 128 | if (record.count === 0) { 129 | connectionLimitStore.delete(key); 130 | } else { 131 | connectionLimitStore.set(key, record); 132 | } 133 | } 134 | }, 135 | 136 | /** 137 | * Validate event name to prevent injection 138 | * @param {string} eventName - Event name to validate 139 | * @returns {boolean} - Whether event name is valid 140 | */ 141 | validateEventName(eventName) { 142 | // Allow alphanumeric, hyphens, underscores, colons, dots 143 | const validPattern = /^[a-zA-Z0-9:._-]+$/; 144 | return validPattern.test(eventName) && eventName.length < 100; 145 | }, 146 | 147 | /** 148 | * Get current statistics 149 | * @returns {Object} - Statistics object 150 | */ 151 | getStats() { 152 | return { 153 | rateLimitEntries: rateLimitStore.size, 154 | connectionLimitEntries: connectionLimitStore.size, 155 | }; 156 | }, 157 | 158 | /** 159 | * Clear all rate limit data 160 | */ 161 | clear() { 162 | rateLimitStore.clear(); 163 | connectionLimitStore.clear(); 164 | }, 165 | }; 166 | }; 167 | 168 | -------------------------------------------------------------------------------- /docs/guide/widget.md: -------------------------------------------------------------------------------- 1 | # Dashboard Widget 2 | 3 | Visual guide for the Socket.IO statistics widget in your Strapi admin panel. 4 | 5 | --- 6 | 7 | ## Overview 8 | 9 | The Socket.IO plugin adds a beautiful, real-time statistics widget to your Strapi admin home page. 10 | 11 | ![Socket.IO Dashboard Widget](/widget.png) 12 | 13 | *The widget automatically appears on your admin dashboard after installing the plugin.* 14 | 15 | --- 16 | 17 | ## Features 18 | 19 | ### 🟢 Live Status Indicator 20 | A pulsing green dot shows the plugin is active and receiving real-time data. If the indicator turns red, the Socket.IO server is offline or not responding. 21 | 22 | ### 👥 Active Connections 23 | Displays the current number of connected Socket.IO clients in real-time. 24 | 25 | ### 💬 Active Rooms 26 | Shows how many Socket.IO rooms are currently active. 27 | 28 | ### ⚡ Events per Second 29 | Real-time counter showing the current rate of event emissions. 30 | 31 | ### 📈 Total Events 32 | Cumulative count of all events processed since the plugin started or was reset. 33 | 34 | --- 35 | 36 | ## Auto-Refresh 37 | 38 | The widget automatically updates every **5 seconds** to provide live statistics without requiring a page refresh. 39 | 40 | --- 41 | 42 | ## Location 43 | 44 | **Navigate to:** Admin Panel → Home (Dashboard) 45 | 46 | The widget appears in the main dashboard area below the welcome message. 47 | 48 | --- 49 | 50 | ## Visual Design 51 | 52 | ![Socket.IO Settings Panel](/settings.png) 53 | 54 | *Access full settings by clicking the "View Settings" link in the widget or navigating to Settings → Socket.IO* 55 | 56 | ### Color-Coded Metrics 57 | - **Blue Cards** - Connection metrics 58 | - **Green Cards** - Activity metrics 59 | - **Orange Cards** - Performance metrics 60 | 61 | ### Responsive Layout 62 | - Desktop: Full-width cards with icons 63 | - Tablet: 2-column grid 64 | - Mobile: Single column stack 65 | 66 | --- 67 | 68 | ## Monitoring Dashboard 69 | 70 | ![Monitoring Settings](/monitoringSettings.png) 71 | 72 | *The monitoring dashboard provides detailed insights into connections, events, and performance.* 73 | 74 | ### What You Can Monitor 75 | - **Active Connections** - See who's connected right now 76 | - **Connection History** - Track connection patterns 77 | - **Event Logs** - View all emitted events 78 | - **User Details** - See authenticated user info 79 | - **IP Addresses** - Monitor connection sources 80 | - **Performance Metrics** - Events/sec, total events 81 | 82 | --- 83 | 84 | ## Customization 85 | 86 | ### Widget Width 87 | 88 | Adjust the widget width in your admin configuration: 89 | 90 | ```javascript 91 | // admin/src/index.js 92 | app.addWidget({ 93 | id: 'io-stats-widget', 94 | Component: SocketStatsWidget, 95 | width: 8, // Change: 1-12 (12 = full width) 96 | }); 97 | ``` 98 | 99 | ### Refresh Interval 100 | 101 | Change the auto-refresh interval: 102 | 103 | ```javascript 104 | // Change from 5000ms (5 seconds) to desired interval 105 | const interval = setInterval(fetchStats, 5000); 106 | ``` 107 | 108 | --- 109 | 110 | ## Benefits 111 | 112 | ### For Developers 113 | - 👀 **Visual Monitoring** - See Socket.IO activity at a glance 114 | - 🐛 **Debug Tool** - Quickly spot connection issues 115 | - 📊 **Performance Tracking** - Monitor event rates 116 | 117 | ### For Admins 118 | - 📈 **Real-Time Insights** - Live connection data 119 | - 🚨 **Issue Detection** - Notice problems immediately 120 | - ✅ **System Health** - Confirm Socket.IO is running 121 | 122 | ### For Teams 123 | - 👥 **Collaboration** - See team activity 124 | - 📊 **Metrics Dashboard** - Shared visibility 125 | - 🎯 **Decision Making** - Data-driven insights 126 | 127 | --- 128 | 129 | ## Troubleshooting 130 | 131 | ### Widget Not Showing 132 | 133 | **Check plugin is enabled:** 134 | ```javascript 135 | // config/plugins.js 136 | module.exports = { 137 | io: { enabled: true } 138 | }; 139 | ``` 140 | 141 | **Restart Strapi:** 142 | ```bash 143 | npm run develop 144 | ``` 145 | 146 | ### Stats Not Updating 147 | 148 | **Check browser console** for errors 149 | 150 | **Verify API endpoint:** 151 | ``` 152 | GET /io/monitoring/stats 153 | ``` 154 | 155 | **Clear cache and refresh:** 156 | ```bash 157 | rm -rf .cache build 158 | npm run develop 159 | ``` 160 | 161 | ### Widget Shows Zero Connections 162 | 163 | This is normal if: 164 | - No clients are currently connected 165 | - Plugin just started 166 | - All sockets disconnected 167 | 168 | **Test connection:** 169 | ```javascript 170 | import { io } from 'socket.io-client'; 171 | const socket = io('http://localhost:1337'); 172 | // Check widget - should show 1 connection 173 | ``` 174 | 175 | --- 176 | 177 | ## See Also 178 | 179 | - **[Getting Started](/guide/getting-started)** - Install and configure 180 | - **[Monitoring Service](/api/io-class#monitoring-service)** - API for monitoring 181 | - **[Configuration](/api/plugin-config)** - Admin panel settings 182 | 183 | --- 184 | 185 | **Widget Version**: 3.0.0 186 | **Compatible with**: Strapi v5.x 187 | **Auto-Refresh**: Every 5 seconds 188 | **Location**: Admin Dashboard Home Page 189 | 190 | -------------------------------------------------------------------------------- /docs/ecosystem.md: -------------------------------------------------------------------------------- 1 | # Related Plugins & Ecosystem 2 | 3 | Discover other powerful Strapi v5 plugins by [@Schero94](https://github.com/Schero94) that work seamlessly with Socket.IO. 4 | 5 | --- 6 | 7 | ## 📧 Magic-Mail 8 | 9 | **Enterprise-grade multi-account email management** 10 | 11 | ### Why You'll Love It 12 | Transform your email sending experience with enterprise-grade features. Magic-Mail handles everything from OAuth 2.0 authentication to intelligent routing across multiple providers. 13 | 14 | ### Key Highlights 15 | - ✅ **Multi-Provider Support**: Gmail OAuth, Microsoft 365, Yahoo, SMTP, SendGrid, Mailgun 16 | - ✅ **Smart Routing**: Automatic provider selection based on rules 17 | - ✅ **OAuth 2.0**: No more app passwords or security risks 18 | - ✅ **Load Balancing**: Distribute emails across accounts automatically 19 | - ✅ **Rate Limiting**: Stay within provider limits effortlessly 20 | - ✅ **Template Management**: Built-in email template system 21 | - ✅ **Analytics**: Track delivery status and performance 22 | 23 | ### Perfect For 24 | - Transactional emails (confirmations, password resets) 25 | - Marketing campaigns 26 | - Multi-tenant applications 27 | - High-volume sending 28 | - Teams needing multiple email accounts 29 | 30 | ### Get Started 31 | 🔗 **[View on GitHub](https://github.com/Schero94/Magic-Mail)** 32 | 📦 **[Install via NPM](https://www.npmjs.com/package/strapi-plugin-magic-mail-v5)** 33 | 🌐 **[Live Demo](https://store.magicdx.dev)** 34 | 35 | --- 36 | 37 | ## 🔐 Magic-Sessionmanager 38 | 39 | **Advanced session management and user tracking** 40 | 41 | ### Why You'll Love It 42 | Get complete visibility into your user sessions and connections. Perfect for monitoring Socket.IO activity, detecting anomalies, and understanding user behavior. 43 | 44 | ### Key Highlights 45 | - ✅ **Real-Time Tracking**: See active sessions as they happen 46 | - ✅ **Socket.IO Integration**: Monitor WebSocket connections 47 | - ✅ **IP-Based Analytics**: Track locations and detect suspicious activity 48 | - ✅ **Session Dashboard**: Beautiful admin interface 49 | - ✅ **Security Features**: Multiple login detection, session limits 50 | - ✅ **Activity Logs**: Complete audit trail 51 | - ✅ **Performance Metrics**: Session duration, frequency, patterns 52 | 53 | ### Perfect For 54 | - Security monitoring and auditing 55 | - User activity analytics 56 | - Concurrent session management 57 | - Admin dashboards with live stats 58 | - Compliance requirements 59 | 60 | ### Get Started 61 | 🔗 **[View on GitHub](https://github.com/Schero94/Magic-Sessionmanager)** 62 | 📦 **NPM Package Coming Soon** 63 | 64 | --- 65 | 66 | ## 🔖 Magicmark 67 | 68 | **Powerful bookmark and link management** 69 | 70 | ### Why You'll Love It 71 | Organize, share, and sync bookmarks across your team in real-time. Built with developers in mind, featuring a complete REST API and Socket.IO integration. 72 | 73 | ### Key Highlights 74 | - ✅ **Smart Organization**: Tags, categories, collections 75 | - ✅ **Team Collaboration**: Share with team or make public 76 | - ✅ **Real-Time Sync**: Instant updates via Socket.IO 77 | - ✅ **Full-Text Search**: Find bookmarks instantly 78 | - ✅ **REST API**: Complete CRUD operations 79 | - ✅ **Import/Export**: Browser bookmark compatibility 80 | - ✅ **Rich Metadata**: Automatic title, description extraction 81 | 82 | ### Perfect For 83 | - Team knowledge bases 84 | - Research organization 85 | - Content curation platforms 86 | - Link sharing communities 87 | - Developer tool collections 88 | 89 | ### Get Started 90 | 🔗 **[View on GitHub](https://github.com/Schero94/Magicmark)** 91 | 📦 **NPM Package Coming Soon** 92 | 93 | --- 94 | 95 | ## Why These Plugins? 96 | 97 | All plugins are: 98 | - ✅ **Strapi v5 Native** - Built specifically for the latest Strapi 99 | - ✅ **Production Ready** - Battle-tested in real applications 100 | - ✅ **Well Documented** - Comprehensive guides and examples 101 | - ✅ **Actively Maintained** - Regular updates and support 102 | - ✅ **MIT Licensed** - Free to use, even commercially 103 | - ✅ **TypeScript Support** - Full type definitions included 104 | 105 | --- 106 | 107 | ## Plugin Comparison 108 | 109 | | Feature | Socket.IO | Magic-Mail | Sessionmanager | Magicmark | 110 | |---------|-----------|------------|----------------|-----------| 111 | | **Real-Time** | ✅ Core | - | ✅ Yes | ✅ Yes | 112 | | **Admin UI** | ✅ Full | ✅ Full | ✅ Dashboard | ✅ Manager | 113 | | **REST API** | - | ✅ Yes | ✅ Yes | ✅ Full | 114 | | **OAuth Support** | JWT | ✅ OAuth 2.0 | - | - | 115 | | **Analytics** | ✅ Stats | ✅ Delivery | ✅ Sessions | ✅ Usage | 116 | 117 | --- 118 | 119 | ## Community & Support 120 | 121 | **All plugins maintained by [@Schero94](https://github.com/Schero94)** 122 | 123 | - 💬 GitHub Issues & Discussions 124 | - 📧 Direct Support Available 125 | - 🌟 Active Development 126 | - 🎯 Feature Requests Welcome 127 | 128 | **Maintained till December 2026** 🚀 129 | 130 | --- 131 | 132 | ## Contributing 133 | 134 | Love these plugins? Here's how you can help: 135 | 136 | 1. ⭐ Star the repositories 137 | 2. 🐛 Report bugs or suggest features 138 | 3. 📝 Improve documentation 139 | 4. 🔧 Submit pull requests 140 | 5. 📢 Share with your team 141 | 142 | --- 143 | 144 | **Built with ❤️ for the Strapi Community** 145 | 146 | -------------------------------------------------------------------------------- /dist/_chunks/de-BoFxKIL3.mjs: -------------------------------------------------------------------------------- 1 | const de = { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Socket.IO Stats", 4 | "widget.connections": "Aktive Verbindungen", 5 | "widget.rooms": "Aktive Räume", 6 | "widget.eventsPerSec": "Events/Sek.", 7 | "widget.totalEvents": "Gesamt-Events", 8 | "widget.viewMonitoring": "Monitoring anzeigen", 9 | "widget.live": "Live", 10 | "widget.offline": "Offline", 11 | "settings.title": "Einstellungen", 12 | "settings.description": "Konfiguriere die Socket.IO Verbindung für Echtzeit-Events", 13 | "settings.save": "Speichern", 14 | "settings.saved": "Gespeichert", 15 | "settings.saveAndApply": "Speichern & Anwenden", 16 | "settings.success": "Einstellungen erfolgreich gespeichert!", 17 | "settings.error": "Fehler beim Speichern der Einstellungen", 18 | "settings.loadError": "Fehler beim Laden der Einstellungen", 19 | "settings.noRestart": "Änderungen werden sofort übernommen – kein Neustart erforderlich!", 20 | "cors.title": "CORS Einstellungen", 21 | "cors.description": "Konfiguriere welche Frontends sich verbinden dürfen", 22 | "cors.origins": "Erlaubte Origins", 23 | "cors.originsHint": "Füge mehrere Frontend-URLs hinzu, die sich verbinden dürfen", 24 | "cors.add": "Hinzufügen", 25 | "cors.credentials": "Credentials erlauben", 26 | "cors.methods": "Erlaubte HTTP Methoden", 27 | "connection.title": "Verbindungseinstellungen", 28 | "connection.description": "Konfiguriere Verbindungslimits und Timeouts", 29 | "connection.maxConnections": "Max. Verbindungen", 30 | "connection.pingTimeout": "Ping Timeout (ms)", 31 | "connection.pingInterval": "Ping Intervall (ms)", 32 | "connection.connectionTimeout": "Verbindungs-Timeout (ms)", 33 | "security.title": "Sicherheitseinstellungen", 34 | "security.description": "Konfiguriere Authentifizierung und Rate Limiting", 35 | "security.requireAuth": "Authentifizierung erforderlich", 36 | "security.rateLimiting": "Rate Limiting aktivieren", 37 | "security.maxEventsPerSecond": "Max. Events/Sekunde", 38 | "events.title": "Echtzeit-Events", 39 | "events.description": "Konfiguriere welche Events für welche Content Types gesendet werden", 40 | "events.enableAll": "Alle aktivieren", 41 | "events.disableAll": "Alle deaktivieren", 42 | "events.noContentTypes": "Keine API Content Types gefunden. Erstelle zuerst Content Types im Content-Type Builder.", 43 | "events.contentType": "Content Type", 44 | "events.create": "Erstellen", 45 | "events.update": "Aktualisieren", 46 | "events.delete": "Löschen", 47 | "events.customNames": "Benutzerdefinierte Event-Namen verwenden", 48 | "events.includeRelations": "Relationen einbeziehen", 49 | "events.onlyPublished": "Nur veröffentlichte Inhalte", 50 | "permissions.title": "Rollen-Berechtigungen", 51 | "permissions.description": "Konfiguriere Socket.IO Berechtigungen pro Benutzerrolle", 52 | "permissions.role": "Rolle", 53 | "permissions.canConnect": "Kann verbinden", 54 | "permissions.blocked": "Blockiert", 55 | "permissions.contentTypesEnabled": "Content Types aktiviert", 56 | "permissions.allowConnection": "Verbindung erlauben", 57 | "permissions.allowConnectionHint": "Benutzer mit dieser Rolle können sich mit Socket.IO verbinden", 58 | "permissions.allowCredentials": "Credentials erlauben", 59 | "permissions.allowCredentialsHint": "Cookies und Auth-Header erlauben", 60 | "permissions.allowedMethods": "Erlaubte HTTP Methoden", 61 | "permissions.contentTypePermissions": "Content-Type-Berechtigungen", 62 | "permissions.noRoles": "Keine Rollen gefunden", 63 | "redis.title": "Redis Adapter", 64 | "redis.description": "Redis für Multi-Server Skalierung aktivieren", 65 | "redis.enable": "Redis Adapter aktivieren", 66 | "redis.url": "Redis URL", 67 | "namespaces.title": "Namespaces", 68 | "namespaces.description": "Separate Socket.IO Endpunkte erstellen", 69 | "namespaces.enable": "Namespaces aktivieren", 70 | "namespaces.list": "Namespaces", 71 | "namespaces.add": "Hinzufügen", 72 | "namespaces.hint": "Beispiele: admin, chat, notifications", 73 | "monitoring.title": "Monitoring & Logging", 74 | "monitoring.description": "Echtzeit-Verbindungs- und Event-Statistiken", 75 | "monitoring.connectionLogging": "Verbindungs-Logging", 76 | "monitoring.connectionLoggingHint": "Client-Verbindungen protokollieren", 77 | "monitoring.eventLogging": "Event-Logging", 78 | "monitoring.eventLoggingHint": "Alle Events zum Debuggen protokollieren", 79 | "monitoring.maxLogSize": "Max. Log-Größe", 80 | "monitoring.refresh": "Aktualisieren", 81 | "monitoring.reset": "Statistiken zurücksetzen", 82 | "monitoring.connectedClients": "Verbundene Clients", 83 | "monitoring.totalEvents": "Gesamt-Events", 84 | "monitoring.eventsPerSecond": "Events/Sekunde", 85 | "monitoring.rooms": "Aktive Räume", 86 | "monitoring.testEvent": "Test-Event senden", 87 | "monitoring.eventName": "Event-Name", 88 | "monitoring.eventData": "Event-Daten (JSON)", 89 | "monitoring.send": "Senden", 90 | "monitoring.connectedClientsList": "Verbundene Clients", 91 | "monitoring.noClients": "Keine Clients verbunden", 92 | "monitoring.eventLog": "Letzte Events", 93 | "monitoring.noEvents": "Noch keine Events protokolliert", 94 | "monitoring.invalidJson": "Ungültige JSON-Daten", 95 | "monitoring.testEventSent": "Test-Event erfolgreich gesendet!", 96 | "monitoring.testEventError": "Fehler beim Senden des Test-Events", 97 | "monitoring.statsReset": "Statistiken erfolgreich zurückgesetzt!", 98 | "monitoring.resetError": "Fehler beim Zurücksetzen der Statistiken" 99 | }; 100 | export { 101 | de as default 102 | }; 103 | -------------------------------------------------------------------------------- /admin/src/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Socket.IO Stats", 4 | "widget.connections": "Aktive Verbindungen", 5 | "widget.rooms": "Aktive Räume", 6 | "widget.eventsPerSec": "Events/Sek.", 7 | "widget.totalEvents": "Gesamt-Events", 8 | "widget.viewMonitoring": "Monitoring anzeigen", 9 | "widget.live": "Live", 10 | "widget.offline": "Offline", 11 | "settings.title": "Einstellungen", 12 | "settings.description": "Konfiguriere die Socket.IO Verbindung für Echtzeit-Events", 13 | "settings.save": "Speichern", 14 | "settings.saved": "Gespeichert", 15 | "settings.saveAndApply": "Speichern & Anwenden", 16 | "settings.success": "Einstellungen erfolgreich gespeichert!", 17 | "settings.error": "Fehler beim Speichern der Einstellungen", 18 | "settings.loadError": "Fehler beim Laden der Einstellungen", 19 | "settings.noRestart": "Änderungen werden sofort übernommen – kein Neustart erforderlich!", 20 | 21 | "cors.title": "CORS Einstellungen", 22 | "cors.description": "Konfiguriere welche Frontends sich verbinden dürfen", 23 | "cors.origins": "Erlaubte Origins", 24 | "cors.originsHint": "Füge mehrere Frontend-URLs hinzu, die sich verbinden dürfen", 25 | "cors.add": "Hinzufügen", 26 | "cors.credentials": "Credentials erlauben", 27 | "cors.methods": "Erlaubte HTTP Methoden", 28 | 29 | "connection.title": "Verbindungseinstellungen", 30 | "connection.description": "Konfiguriere Verbindungslimits und Timeouts", 31 | "connection.maxConnections": "Max. Verbindungen", 32 | "connection.pingTimeout": "Ping Timeout (ms)", 33 | "connection.pingInterval": "Ping Intervall (ms)", 34 | "connection.connectionTimeout": "Verbindungs-Timeout (ms)", 35 | 36 | "security.title": "Sicherheitseinstellungen", 37 | "security.description": "Konfiguriere Authentifizierung und Rate Limiting", 38 | "security.requireAuth": "Authentifizierung erforderlich", 39 | "security.rateLimiting": "Rate Limiting aktivieren", 40 | "security.maxEventsPerSecond": "Max. Events/Sekunde", 41 | 42 | "events.title": "Echtzeit-Events", 43 | "events.description": "Konfiguriere welche Events für welche Content Types gesendet werden", 44 | "events.enableAll": "Alle aktivieren", 45 | "events.disableAll": "Alle deaktivieren", 46 | "events.noContentTypes": "Keine API Content Types gefunden. Erstelle zuerst Content Types im Content-Type Builder.", 47 | "events.contentType": "Content Type", 48 | "events.create": "Erstellen", 49 | "events.update": "Aktualisieren", 50 | "events.delete": "Löschen", 51 | "events.customNames": "Benutzerdefinierte Event-Namen verwenden", 52 | "events.includeRelations": "Relationen einbeziehen", 53 | "events.onlyPublished": "Nur veröffentlichte Inhalte", 54 | 55 | "permissions.title": "Rollen-Berechtigungen", 56 | "permissions.description": "Konfiguriere Socket.IO Berechtigungen pro Benutzerrolle", 57 | "permissions.role": "Rolle", 58 | "permissions.canConnect": "Kann verbinden", 59 | "permissions.blocked": "Blockiert", 60 | "permissions.contentTypesEnabled": "Content Types aktiviert", 61 | "permissions.allowConnection": "Verbindung erlauben", 62 | "permissions.allowConnectionHint": "Benutzer mit dieser Rolle können sich mit Socket.IO verbinden", 63 | "permissions.allowCredentials": "Credentials erlauben", 64 | "permissions.allowCredentialsHint": "Cookies und Auth-Header erlauben", 65 | "permissions.allowedMethods": "Erlaubte HTTP Methoden", 66 | "permissions.contentTypePermissions": "Content-Type-Berechtigungen", 67 | "permissions.noRoles": "Keine Rollen gefunden", 68 | 69 | "redis.title": "Redis Adapter", 70 | "redis.description": "Redis für Multi-Server Skalierung aktivieren", 71 | "redis.enable": "Redis Adapter aktivieren", 72 | "redis.url": "Redis URL", 73 | 74 | "namespaces.title": "Namespaces", 75 | "namespaces.description": "Separate Socket.IO Endpunkte erstellen", 76 | "namespaces.enable": "Namespaces aktivieren", 77 | "namespaces.list": "Namespaces", 78 | "namespaces.add": "Hinzufügen", 79 | "namespaces.hint": "Beispiele: admin, chat, notifications", 80 | 81 | "monitoring.title": "Monitoring & Logging", 82 | "monitoring.description": "Echtzeit-Verbindungs- und Event-Statistiken", 83 | "monitoring.connectionLogging": "Verbindungs-Logging", 84 | "monitoring.connectionLoggingHint": "Client-Verbindungen protokollieren", 85 | "monitoring.eventLogging": "Event-Logging", 86 | "monitoring.eventLoggingHint": "Alle Events zum Debuggen protokollieren", 87 | "monitoring.maxLogSize": "Max. Log-Größe", 88 | "monitoring.refresh": "Aktualisieren", 89 | "monitoring.reset": "Statistiken zurücksetzen", 90 | "monitoring.connectedClients": "Verbundene Clients", 91 | "monitoring.totalEvents": "Gesamt-Events", 92 | "monitoring.eventsPerSecond": "Events/Sekunde", 93 | "monitoring.rooms": "Aktive Räume", 94 | "monitoring.testEvent": "Test-Event senden", 95 | "monitoring.eventName": "Event-Name", 96 | "monitoring.eventData": "Event-Daten (JSON)", 97 | "monitoring.send": "Senden", 98 | "monitoring.connectedClientsList": "Verbundene Clients", 99 | "monitoring.noClients": "Keine Clients verbunden", 100 | "monitoring.eventLog": "Letzte Events", 101 | "monitoring.noEvents": "Noch keine Events protokolliert", 102 | "monitoring.invalidJson": "Ungültige JSON-Daten", 103 | "monitoring.testEventSent": "Test-Event erfolgreich gesendet!", 104 | "monitoring.testEventError": "Fehler beim Senden des Test-Events", 105 | "monitoring.statsReset": "Statistiken erfolgreich zurückgesetzt!", 106 | "monitoring.resetError": "Fehler beim Zurücksetzen der Statistiken" 107 | } 108 | -------------------------------------------------------------------------------- /dist/_chunks/de-Crne_WJ-.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 3 | const de = { 4 | "plugin.name": "Socket.IO", 5 | "widget.socket-stats.title": "Socket.IO Stats", 6 | "widget.connections": "Aktive Verbindungen", 7 | "widget.rooms": "Aktive Räume", 8 | "widget.eventsPerSec": "Events/Sek.", 9 | "widget.totalEvents": "Gesamt-Events", 10 | "widget.viewMonitoring": "Monitoring anzeigen", 11 | "widget.live": "Live", 12 | "widget.offline": "Offline", 13 | "settings.title": "Einstellungen", 14 | "settings.description": "Konfiguriere die Socket.IO Verbindung für Echtzeit-Events", 15 | "settings.save": "Speichern", 16 | "settings.saved": "Gespeichert", 17 | "settings.saveAndApply": "Speichern & Anwenden", 18 | "settings.success": "Einstellungen erfolgreich gespeichert!", 19 | "settings.error": "Fehler beim Speichern der Einstellungen", 20 | "settings.loadError": "Fehler beim Laden der Einstellungen", 21 | "settings.noRestart": "Änderungen werden sofort übernommen – kein Neustart erforderlich!", 22 | "cors.title": "CORS Einstellungen", 23 | "cors.description": "Konfiguriere welche Frontends sich verbinden dürfen", 24 | "cors.origins": "Erlaubte Origins", 25 | "cors.originsHint": "Füge mehrere Frontend-URLs hinzu, die sich verbinden dürfen", 26 | "cors.add": "Hinzufügen", 27 | "cors.credentials": "Credentials erlauben", 28 | "cors.methods": "Erlaubte HTTP Methoden", 29 | "connection.title": "Verbindungseinstellungen", 30 | "connection.description": "Konfiguriere Verbindungslimits und Timeouts", 31 | "connection.maxConnections": "Max. Verbindungen", 32 | "connection.pingTimeout": "Ping Timeout (ms)", 33 | "connection.pingInterval": "Ping Intervall (ms)", 34 | "connection.connectionTimeout": "Verbindungs-Timeout (ms)", 35 | "security.title": "Sicherheitseinstellungen", 36 | "security.description": "Konfiguriere Authentifizierung und Rate Limiting", 37 | "security.requireAuth": "Authentifizierung erforderlich", 38 | "security.rateLimiting": "Rate Limiting aktivieren", 39 | "security.maxEventsPerSecond": "Max. Events/Sekunde", 40 | "events.title": "Echtzeit-Events", 41 | "events.description": "Konfiguriere welche Events für welche Content Types gesendet werden", 42 | "events.enableAll": "Alle aktivieren", 43 | "events.disableAll": "Alle deaktivieren", 44 | "events.noContentTypes": "Keine API Content Types gefunden. Erstelle zuerst Content Types im Content-Type Builder.", 45 | "events.contentType": "Content Type", 46 | "events.create": "Erstellen", 47 | "events.update": "Aktualisieren", 48 | "events.delete": "Löschen", 49 | "events.customNames": "Benutzerdefinierte Event-Namen verwenden", 50 | "events.includeRelations": "Relationen einbeziehen", 51 | "events.onlyPublished": "Nur veröffentlichte Inhalte", 52 | "permissions.title": "Rollen-Berechtigungen", 53 | "permissions.description": "Konfiguriere Socket.IO Berechtigungen pro Benutzerrolle", 54 | "permissions.role": "Rolle", 55 | "permissions.canConnect": "Kann verbinden", 56 | "permissions.blocked": "Blockiert", 57 | "permissions.contentTypesEnabled": "Content Types aktiviert", 58 | "permissions.allowConnection": "Verbindung erlauben", 59 | "permissions.allowConnectionHint": "Benutzer mit dieser Rolle können sich mit Socket.IO verbinden", 60 | "permissions.allowCredentials": "Credentials erlauben", 61 | "permissions.allowCredentialsHint": "Cookies und Auth-Header erlauben", 62 | "permissions.allowedMethods": "Erlaubte HTTP Methoden", 63 | "permissions.contentTypePermissions": "Content-Type-Berechtigungen", 64 | "permissions.noRoles": "Keine Rollen gefunden", 65 | "redis.title": "Redis Adapter", 66 | "redis.description": "Redis für Multi-Server Skalierung aktivieren", 67 | "redis.enable": "Redis Adapter aktivieren", 68 | "redis.url": "Redis URL", 69 | "namespaces.title": "Namespaces", 70 | "namespaces.description": "Separate Socket.IO Endpunkte erstellen", 71 | "namespaces.enable": "Namespaces aktivieren", 72 | "namespaces.list": "Namespaces", 73 | "namespaces.add": "Hinzufügen", 74 | "namespaces.hint": "Beispiele: admin, chat, notifications", 75 | "monitoring.title": "Monitoring & Logging", 76 | "monitoring.description": "Echtzeit-Verbindungs- und Event-Statistiken", 77 | "monitoring.connectionLogging": "Verbindungs-Logging", 78 | "monitoring.connectionLoggingHint": "Client-Verbindungen protokollieren", 79 | "monitoring.eventLogging": "Event-Logging", 80 | "monitoring.eventLoggingHint": "Alle Events zum Debuggen protokollieren", 81 | "monitoring.maxLogSize": "Max. Log-Größe", 82 | "monitoring.refresh": "Aktualisieren", 83 | "monitoring.reset": "Statistiken zurücksetzen", 84 | "monitoring.connectedClients": "Verbundene Clients", 85 | "monitoring.totalEvents": "Gesamt-Events", 86 | "monitoring.eventsPerSecond": "Events/Sekunde", 87 | "monitoring.rooms": "Aktive Räume", 88 | "monitoring.testEvent": "Test-Event senden", 89 | "monitoring.eventName": "Event-Name", 90 | "monitoring.eventData": "Event-Daten (JSON)", 91 | "monitoring.send": "Senden", 92 | "monitoring.connectedClientsList": "Verbundene Clients", 93 | "monitoring.noClients": "Keine Clients verbunden", 94 | "monitoring.eventLog": "Letzte Events", 95 | "monitoring.noEvents": "Noch keine Events protokolliert", 96 | "monitoring.invalidJson": "Ungültige JSON-Daten", 97 | "monitoring.testEventSent": "Test-Event erfolgreich gesendet!", 98 | "monitoring.testEventError": "Fehler beim Senden des Test-Events", 99 | "monitoring.statsReset": "Statistiken erfolgreich zurückgesetzt!", 100 | "monitoring.resetError": "Fehler beim Zurücksetzen der Statistiken" 101 | }; 102 | exports.default = de; 103 | -------------------------------------------------------------------------------- /test-entity-subscriptions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Test Client for Entity-Specific Subscriptions 4 | * Tests the new entity subscription feature 5 | * 6 | * Usage: 7 | * node test-entity-subscriptions.js 8 | */ 9 | 10 | const { io } = require('socket.io-client'); 11 | 12 | const SERVER_URL = 'http://localhost:1337'; 13 | const JWT_TOKEN = process.env.JWT_TOKEN || null; 14 | 15 | console.log('🧪 Entity Subscription Test Client\n'); 16 | console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); 17 | 18 | // Connect to server 19 | const socket = io(SERVER_URL, { 20 | auth: JWT_TOKEN ? { token: JWT_TOKEN } : undefined, 21 | transports: ['websocket', 'polling'] 22 | }); 23 | 24 | socket.on('connect', () => { 25 | console.log('✅ Connected to server'); 26 | console.log(` Socket ID: ${socket.id}\n`); 27 | 28 | runTests(); 29 | }); 30 | 31 | socket.on('connect_error', (error) => { 32 | console.error('❌ Connection error:', error.message); 33 | process.exit(1); 34 | }); 35 | 36 | socket.on('disconnect', (reason) => { 37 | console.log(`\n🔴 Disconnected: ${reason}`); 38 | }); 39 | 40 | // Test entity events 41 | socket.on('session:create', (data) => { 42 | console.log('\n📢 Received session:create event:'); 43 | console.log(JSON.stringify(data, null, 2)); 44 | }); 45 | 46 | socket.on('session:update', (data) => { 47 | console.log('\n📢 Received session:update event:'); 48 | console.log(JSON.stringify(data, null, 2)); 49 | }); 50 | 51 | socket.on('session:delete', (data) => { 52 | console.log('\n📢 Received session:delete event:'); 53 | console.log(JSON.stringify(data, null, 2)); 54 | }); 55 | 56 | async function runTests() { 57 | console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); 58 | console.log('📝 Running Entity Subscription Tests\n'); 59 | 60 | // Test 0: Subscribe to ALL session events (for create events) 61 | console.log('Test 0: Subscribe to ALL session events (to catch creates)'); 62 | socket.on('session:create', (data) => { 63 | console.log('\n🎉 Received session:create event!'); 64 | console.log(JSON.stringify(data, null, 2)); 65 | }); 66 | console.log('✅ Listening for session:create events\n'); 67 | 68 | await sleep(500); 69 | 70 | // Test 1: Subscribe to specific session 71 | console.log('Test 1: Subscribe to session with ID 94'); 72 | socket.emit('subscribe-entity', { uid: 'api::session.session', id: 94 }, (response) => { 73 | if (response.success) { 74 | console.log(`✅ Successfully subscribed to: ${response.room}`); 75 | } else { 76 | console.log(`❌ Subscription failed: ${response.error}`); 77 | } 78 | }); 79 | 80 | await sleep(1000); 81 | 82 | // Test 2: Subscribe to another session 83 | console.log('\nTest 2: Subscribe to session with ID 95'); 84 | socket.emit('subscribe-entity', { uid: 'api::session.session', id: 95 }, (response) => { 85 | if (response.success) { 86 | console.log(`✅ Successfully subscribed to: ${response.room}`); 87 | } else { 88 | console.log(`❌ Subscription failed: ${response.error}`); 89 | } 90 | }); 91 | 92 | await sleep(1000); 93 | 94 | // Test 3: Get current subscriptions 95 | console.log('\nTest 3: Get all entity subscriptions'); 96 | socket.emit('get-entity-subscriptions', (response) => { 97 | if (response.success) { 98 | console.log(`✅ Current subscriptions (${response.subscriptions.length}):`); 99 | response.subscriptions.forEach(sub => { 100 | console.log(` - ${sub.room} (uid: ${sub.uid}, id: ${sub.id})`); 101 | }); 102 | } else { 103 | console.log(`❌ Failed to get subscriptions: ${response.error}`); 104 | } 105 | }); 106 | 107 | await sleep(1000); 108 | 109 | // Test 4: Invalid subscription (should fail) 110 | console.log('\nTest 4: Try invalid subscription (should fail)'); 111 | socket.emit('subscribe-entity', { uid: 'invalid::format', id: 999 }, (response) => { 112 | if (response.success) { 113 | console.log(`⚠️ Unexpected success: ${response.room}`); 114 | } else { 115 | console.log(`✅ Correctly rejected: ${response.error}`); 116 | } 117 | }); 118 | 119 | await sleep(1000); 120 | 121 | // Test 5: Unsubscribe 122 | console.log('\nTest 5: Unsubscribe from session 94'); 123 | socket.emit('unsubscribe-entity', { uid: 'api::session.session', id: 94 }, (response) => { 124 | if (response.success) { 125 | console.log(`✅ Successfully unsubscribed from: ${response.room}`); 126 | } else { 127 | console.log(`❌ Unsubscribe failed: ${response.error}`); 128 | } 129 | }); 130 | 131 | await sleep(1000); 132 | 133 | // Test 6: Verify unsubscribe 134 | console.log('\nTest 6: Verify subscriptions after unsubscribe'); 135 | socket.emit('get-entity-subscriptions', (response) => { 136 | if (response.success) { 137 | console.log(`✅ Remaining subscriptions (${response.subscriptions.length}):`); 138 | if (response.subscriptions.length === 0) { 139 | console.log(' (none)'); 140 | } else { 141 | response.subscriptions.forEach(sub => { 142 | console.log(` - ${sub.room} (uid: ${sub.uid}, id: ${sub.id})`); 143 | }); 144 | } 145 | } 146 | }); 147 | 148 | await sleep(2000); 149 | 150 | console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); 151 | console.log('\n✅ Tests completed!'); 152 | console.log('\n📝 Next steps:'); 153 | console.log(' 1. 🆕 CREATE a new session → Will trigger session:create event'); 154 | console.log(' 2. ✏️ UPDATE session with ID 94 or 95 → Will trigger session:update event'); 155 | console.log(' 3. 🗑️ DELETE a session → Will trigger session:delete event'); 156 | console.log(' 4. Press Ctrl+C to exit\n'); 157 | } 158 | 159 | function sleep(ms) { 160 | return new Promise(resolve => setTimeout(resolve, ms)); 161 | } 162 | 163 | // Keep process alive 164 | process.on('SIGINT', () => { 165 | console.log('\n\n👋 Closing connection...'); 166 | socket.disconnect(); 167 | process.exit(0); 168 | }); 169 | 170 | -------------------------------------------------------------------------------- /dist/_chunks/en-B4_6Q0aQ.mjs: -------------------------------------------------------------------------------- 1 | const en = { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Socket.IO Stats", 4 | "widget.connections": "Active Connections", 5 | "widget.rooms": "Active Rooms", 6 | "widget.eventsPerSec": "Events/sec", 7 | "widget.totalEvents": "Total Events", 8 | "widget.viewMonitoring": "View Monitoring", 9 | "widget.live": "Live", 10 | "widget.offline": "Offline", 11 | "settings.title": "Settings", 12 | "settings.description": "Configure the Socket.IO connection for real-time events", 13 | "settings.save": "Save", 14 | "settings.saved": "Saved", 15 | "settings.saveAndApply": "Save & Apply", 16 | "settings.success": "Settings saved successfully!", 17 | "settings.error": "Error saving settings", 18 | "settings.loadError": "Error loading settings", 19 | "settings.noRestart": "Changes are applied immediately – no restart required!", 20 | "settings.export": "Export", 21 | "settings.import": "Import", 22 | "settings.exported": "Settings exported successfully!", 23 | "settings.imported": "Settings imported successfully!", 24 | "settings.importError": "Import failed", 25 | "settings.invalidJson": "Invalid settings file!", 26 | "cors.title": "CORS Settings", 27 | "cors.description": "Configure which frontends can connect", 28 | "cors.origins": "Allowed Origins", 29 | "cors.originsHint": "Add multiple frontend URLs that can connect", 30 | "cors.add": "Add", 31 | "cors.credentials": "Allow Credentials", 32 | "cors.methods": "Allowed HTTP Methods", 33 | "connection.title": "Connection Settings", 34 | "connection.description": "Configure connection limits and timeouts", 35 | "connection.maxConnections": "Max Connections", 36 | "connection.pingTimeout": "Ping Timeout (ms)", 37 | "connection.pingInterval": "Ping Interval (ms)", 38 | "connection.connectionTimeout": "Connection Timeout (ms)", 39 | "security.title": "Security Settings", 40 | "security.description": "Configure authentication and rate limiting", 41 | "security.requireAuth": "Require Authentication", 42 | "security.rateLimiting": "Enable Rate Limiting", 43 | "security.maxEventsPerSecond": "Max Events/Second", 44 | "events.title": "Real-time Events", 45 | "events.description": "Configure which events are sent for which content types", 46 | "events.enableAll": "Enable all", 47 | "events.disableAll": "Disable all", 48 | "events.noContentTypes": "No API content types found. Create content types in the Content-Type Builder first.", 49 | "events.contentType": "Content Type", 50 | "events.create": "Create", 51 | "events.update": "Update", 52 | "events.delete": "Delete", 53 | "events.customNames": "Use Custom Event Names", 54 | "events.includeRelations": "Include Relations", 55 | "events.onlyPublished": "Only Published Content", 56 | "permissions.title": "Role Permissions", 57 | "permissions.description": "Configure Socket.IO permissions per user role", 58 | "permissions.role": "Role", 59 | "permissions.canConnect": "Can Connect", 60 | "permissions.blocked": "Blocked", 61 | "permissions.contentTypesEnabled": "content types enabled", 62 | "permissions.allowConnection": "Allow Connection", 63 | "permissions.allowConnectionHint": "Users with this role can connect to Socket.IO", 64 | "permissions.allowCredentials": "Allow Credentials", 65 | "permissions.allowCredentialsHint": "Allow cookies and auth headers", 66 | "permissions.allowedMethods": "Allowed HTTP Methods", 67 | "permissions.contentTypePermissions": "Content Type Permissions", 68 | "permissions.noRoles": "No roles found", 69 | "permissions.enableAll": "Enable All", 70 | "permissions.disableAll": "Disable All", 71 | "redis.title": "Redis Adapter", 72 | "redis.description": "Enable Redis for multi-server scaling", 73 | "redis.enable": "Enable Redis Adapter", 74 | "redis.url": "Redis URL", 75 | "namespaces.title": "Namespaces", 76 | "namespaces.description": "Create separate Socket.IO endpoints", 77 | "namespaces.enable": "Enable Namespaces", 78 | "namespaces.list": "Namespaces", 79 | "namespaces.add": "Add", 80 | "namespaces.hint": "Examples: admin, chat, notifications", 81 | "namespaces.authRequired": "Auth required", 82 | "monitoring.title": "Monitoring & Logging", 83 | "monitoring.description": "Real-time connection and event statistics", 84 | "monitoring.connectionLogging": "Connection Logging", 85 | "monitoring.connectionLoggingHint": "Log client connections", 86 | "monitoring.eventLogging": "Event Logging", 87 | "monitoring.eventLoggingHint": "Log all events for debugging", 88 | "monitoring.maxLogSize": "Max Log Size", 89 | "monitoring.refresh": "Refresh", 90 | "monitoring.reset": "Reset Stats", 91 | "monitoring.connectedClients": "Connected Clients", 92 | "monitoring.totalEvents": "Total Events", 93 | "monitoring.eventsPerSecond": "Events/Second", 94 | "monitoring.rooms": "Active Rooms", 95 | "monitoring.testEvent": "Send Test Event", 96 | "monitoring.eventName": "Event Name", 97 | "monitoring.eventData": "Event Data (JSON)", 98 | "monitoring.send": "Send", 99 | "monitoring.connectedClientsList": "Connected Clients", 100 | "monitoring.noClients": "No clients connected", 101 | "monitoring.eventLog": "Recent Events", 102 | "monitoring.noEvents": "No events logged yet", 103 | "monitoring.noMatchingEvents": "No matching events", 104 | "monitoring.searchEvents": "Search events...", 105 | "monitoring.invalidJson": "Invalid JSON data", 106 | "monitoring.testEventSent": "Test event sent successfully!", 107 | "monitoring.testEventError": "Error sending test event", 108 | "monitoring.statsReset": "Statistics reset successfully!", 109 | "monitoring.resetError": "Error resetting statistics", 110 | "validation.errors": "Validation errors", 111 | "validation.invalidOrigin": "Invalid origin", 112 | "validation.pingTimeoutPositive": "Ping timeout must be positive", 113 | "validation.pingIntervalPositive": "Ping interval must be positive", 114 | "validation.connectionTimeoutPositive": "Connection timeout must be positive", 115 | "validation.maxConnectionsPositive": "Max connections must be positive", 116 | "validation.redisUrlRequired": "Redis URL is required when Redis is enabled" 117 | }; 118 | export { 119 | en as default 120 | }; 121 | -------------------------------------------------------------------------------- /admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Socket.IO", 3 | "widget.socket-stats.title": "Socket.IO Stats", 4 | "widget.connections": "Active Connections", 5 | "widget.rooms": "Active Rooms", 6 | "widget.eventsPerSec": "Events/sec", 7 | "widget.totalEvents": "Total Events", 8 | "widget.viewMonitoring": "View Monitoring", 9 | "widget.live": "Live", 10 | "widget.offline": "Offline", 11 | "settings.title": "Settings", 12 | "settings.description": "Configure the Socket.IO connection for real-time events", 13 | "settings.save": "Save", 14 | "settings.saved": "Saved", 15 | "settings.saveAndApply": "Save & Apply", 16 | "settings.success": "Settings saved successfully!", 17 | "settings.error": "Error saving settings", 18 | "settings.loadError": "Error loading settings", 19 | "settings.noRestart": "Changes are applied immediately – no restart required!", 20 | "settings.export": "Export", 21 | "settings.import": "Import", 22 | "settings.exported": "Settings exported successfully!", 23 | "settings.imported": "Settings imported successfully!", 24 | "settings.importError": "Import failed", 25 | "settings.invalidJson": "Invalid settings file!", 26 | 27 | "cors.title": "CORS Settings", 28 | "cors.description": "Configure which frontends can connect", 29 | "cors.origins": "Allowed Origins", 30 | "cors.originsHint": "Add multiple frontend URLs that can connect", 31 | "cors.add": "Add", 32 | "cors.credentials": "Allow Credentials", 33 | "cors.methods": "Allowed HTTP Methods", 34 | 35 | "connection.title": "Connection Settings", 36 | "connection.description": "Configure connection limits and timeouts", 37 | "connection.maxConnections": "Max Connections", 38 | "connection.pingTimeout": "Ping Timeout (ms)", 39 | "connection.pingInterval": "Ping Interval (ms)", 40 | "connection.connectionTimeout": "Connection Timeout (ms)", 41 | 42 | "security.title": "Security Settings", 43 | "security.description": "Configure authentication and rate limiting", 44 | "security.requireAuth": "Require Authentication", 45 | "security.rateLimiting": "Enable Rate Limiting", 46 | "security.maxEventsPerSecond": "Max Events/Second", 47 | 48 | "events.title": "Real-time Events", 49 | "events.description": "Configure which events are sent for which content types", 50 | "events.enableAll": "Enable all", 51 | "events.disableAll": "Disable all", 52 | "events.noContentTypes": "No API content types found. Create content types in the Content-Type Builder first.", 53 | "events.contentType": "Content Type", 54 | "events.create": "Create", 55 | "events.update": "Update", 56 | "events.delete": "Delete", 57 | "events.customNames": "Use Custom Event Names", 58 | "events.includeRelations": "Include Relations", 59 | "events.onlyPublished": "Only Published Content", 60 | 61 | "permissions.title": "Role Permissions", 62 | "permissions.description": "Configure Socket.IO permissions per user role", 63 | "permissions.role": "Role", 64 | "permissions.canConnect": "Can Connect", 65 | "permissions.blocked": "Blocked", 66 | "permissions.contentTypesEnabled": "content types enabled", 67 | "permissions.allowConnection": "Allow Connection", 68 | "permissions.allowConnectionHint": "Users with this role can connect to Socket.IO", 69 | "permissions.allowCredentials": "Allow Credentials", 70 | "permissions.allowCredentialsHint": "Allow cookies and auth headers", 71 | "permissions.allowedMethods": "Allowed HTTP Methods", 72 | "permissions.contentTypePermissions": "Content Type Permissions", 73 | "permissions.noRoles": "No roles found", 74 | "permissions.enableAll": "Enable All", 75 | "permissions.disableAll": "Disable All", 76 | 77 | "redis.title": "Redis Adapter", 78 | "redis.description": "Enable Redis for multi-server scaling", 79 | "redis.enable": "Enable Redis Adapter", 80 | "redis.url": "Redis URL", 81 | 82 | "namespaces.title": "Namespaces", 83 | "namespaces.description": "Create separate Socket.IO endpoints", 84 | "namespaces.enable": "Enable Namespaces", 85 | "namespaces.list": "Namespaces", 86 | "namespaces.add": "Add", 87 | "namespaces.hint": "Examples: admin, chat, notifications", 88 | "namespaces.authRequired": "Auth required", 89 | 90 | "monitoring.title": "Monitoring & Logging", 91 | "monitoring.description": "Real-time connection and event statistics", 92 | "monitoring.connectionLogging": "Connection Logging", 93 | "monitoring.connectionLoggingHint": "Log client connections", 94 | "monitoring.eventLogging": "Event Logging", 95 | "monitoring.eventLoggingHint": "Log all events for debugging", 96 | "monitoring.maxLogSize": "Max Log Size", 97 | "monitoring.refresh": "Refresh", 98 | "monitoring.reset": "Reset Stats", 99 | "monitoring.connectedClients": "Connected Clients", 100 | "monitoring.totalEvents": "Total Events", 101 | "monitoring.eventsPerSecond": "Events/Second", 102 | "monitoring.rooms": "Active Rooms", 103 | "monitoring.testEvent": "Send Test Event", 104 | "monitoring.eventName": "Event Name", 105 | "monitoring.eventData": "Event Data (JSON)", 106 | "monitoring.send": "Send", 107 | "monitoring.connectedClientsList": "Connected Clients", 108 | "monitoring.noClients": "No clients connected", 109 | "monitoring.eventLog": "Recent Events", 110 | "monitoring.noEvents": "No events logged yet", 111 | "monitoring.noMatchingEvents": "No matching events", 112 | "monitoring.searchEvents": "Search events...", 113 | "monitoring.invalidJson": "Invalid JSON data", 114 | "monitoring.testEventSent": "Test event sent successfully!", 115 | "monitoring.testEventError": "Error sending test event", 116 | "monitoring.statsReset": "Statistics reset successfully!", 117 | "monitoring.resetError": "Error resetting statistics", 118 | 119 | "validation.errors": "Validation errors", 120 | "validation.invalidOrigin": "Invalid origin", 121 | "validation.pingTimeoutPositive": "Ping timeout must be positive", 122 | "validation.pingIntervalPositive": "Ping interval must be positive", 123 | "validation.connectionTimeoutPositive": "Connection timeout must be positive", 124 | "validation.maxConnectionsPositive": "Max connections must be positive", 125 | "validation.redisUrlRequired": "Redis URL is required when Redis is enabled" 126 | } 127 | -------------------------------------------------------------------------------- /dist/_chunks/en-Bd2IKJzy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 3 | const en = { 4 | "plugin.name": "Socket.IO", 5 | "widget.socket-stats.title": "Socket.IO Stats", 6 | "widget.connections": "Active Connections", 7 | "widget.rooms": "Active Rooms", 8 | "widget.eventsPerSec": "Events/sec", 9 | "widget.totalEvents": "Total Events", 10 | "widget.viewMonitoring": "View Monitoring", 11 | "widget.live": "Live", 12 | "widget.offline": "Offline", 13 | "settings.title": "Settings", 14 | "settings.description": "Configure the Socket.IO connection for real-time events", 15 | "settings.save": "Save", 16 | "settings.saved": "Saved", 17 | "settings.saveAndApply": "Save & Apply", 18 | "settings.success": "Settings saved successfully!", 19 | "settings.error": "Error saving settings", 20 | "settings.loadError": "Error loading settings", 21 | "settings.noRestart": "Changes are applied immediately – no restart required!", 22 | "settings.export": "Export", 23 | "settings.import": "Import", 24 | "settings.exported": "Settings exported successfully!", 25 | "settings.imported": "Settings imported successfully!", 26 | "settings.importError": "Import failed", 27 | "settings.invalidJson": "Invalid settings file!", 28 | "cors.title": "CORS Settings", 29 | "cors.description": "Configure which frontends can connect", 30 | "cors.origins": "Allowed Origins", 31 | "cors.originsHint": "Add multiple frontend URLs that can connect", 32 | "cors.add": "Add", 33 | "cors.credentials": "Allow Credentials", 34 | "cors.methods": "Allowed HTTP Methods", 35 | "connection.title": "Connection Settings", 36 | "connection.description": "Configure connection limits and timeouts", 37 | "connection.maxConnections": "Max Connections", 38 | "connection.pingTimeout": "Ping Timeout (ms)", 39 | "connection.pingInterval": "Ping Interval (ms)", 40 | "connection.connectionTimeout": "Connection Timeout (ms)", 41 | "security.title": "Security Settings", 42 | "security.description": "Configure authentication and rate limiting", 43 | "security.requireAuth": "Require Authentication", 44 | "security.rateLimiting": "Enable Rate Limiting", 45 | "security.maxEventsPerSecond": "Max Events/Second", 46 | "events.title": "Real-time Events", 47 | "events.description": "Configure which events are sent for which content types", 48 | "events.enableAll": "Enable all", 49 | "events.disableAll": "Disable all", 50 | "events.noContentTypes": "No API content types found. Create content types in the Content-Type Builder first.", 51 | "events.contentType": "Content Type", 52 | "events.create": "Create", 53 | "events.update": "Update", 54 | "events.delete": "Delete", 55 | "events.customNames": "Use Custom Event Names", 56 | "events.includeRelations": "Include Relations", 57 | "events.onlyPublished": "Only Published Content", 58 | "permissions.title": "Role Permissions", 59 | "permissions.description": "Configure Socket.IO permissions per user role", 60 | "permissions.role": "Role", 61 | "permissions.canConnect": "Can Connect", 62 | "permissions.blocked": "Blocked", 63 | "permissions.contentTypesEnabled": "content types enabled", 64 | "permissions.allowConnection": "Allow Connection", 65 | "permissions.allowConnectionHint": "Users with this role can connect to Socket.IO", 66 | "permissions.allowCredentials": "Allow Credentials", 67 | "permissions.allowCredentialsHint": "Allow cookies and auth headers", 68 | "permissions.allowedMethods": "Allowed HTTP Methods", 69 | "permissions.contentTypePermissions": "Content Type Permissions", 70 | "permissions.noRoles": "No roles found", 71 | "permissions.enableAll": "Enable All", 72 | "permissions.disableAll": "Disable All", 73 | "redis.title": "Redis Adapter", 74 | "redis.description": "Enable Redis for multi-server scaling", 75 | "redis.enable": "Enable Redis Adapter", 76 | "redis.url": "Redis URL", 77 | "namespaces.title": "Namespaces", 78 | "namespaces.description": "Create separate Socket.IO endpoints", 79 | "namespaces.enable": "Enable Namespaces", 80 | "namespaces.list": "Namespaces", 81 | "namespaces.add": "Add", 82 | "namespaces.hint": "Examples: admin, chat, notifications", 83 | "namespaces.authRequired": "Auth required", 84 | "monitoring.title": "Monitoring & Logging", 85 | "monitoring.description": "Real-time connection and event statistics", 86 | "monitoring.connectionLogging": "Connection Logging", 87 | "monitoring.connectionLoggingHint": "Log client connections", 88 | "monitoring.eventLogging": "Event Logging", 89 | "monitoring.eventLoggingHint": "Log all events for debugging", 90 | "monitoring.maxLogSize": "Max Log Size", 91 | "monitoring.refresh": "Refresh", 92 | "monitoring.reset": "Reset Stats", 93 | "monitoring.connectedClients": "Connected Clients", 94 | "monitoring.totalEvents": "Total Events", 95 | "monitoring.eventsPerSecond": "Events/Second", 96 | "monitoring.rooms": "Active Rooms", 97 | "monitoring.testEvent": "Send Test Event", 98 | "monitoring.eventName": "Event Name", 99 | "monitoring.eventData": "Event Data (JSON)", 100 | "monitoring.send": "Send", 101 | "monitoring.connectedClientsList": "Connected Clients", 102 | "monitoring.noClients": "No clients connected", 103 | "monitoring.eventLog": "Recent Events", 104 | "monitoring.noEvents": "No events logged yet", 105 | "monitoring.noMatchingEvents": "No matching events", 106 | "monitoring.searchEvents": "Search events...", 107 | "monitoring.invalidJson": "Invalid JSON data", 108 | "monitoring.testEventSent": "Test event sent successfully!", 109 | "monitoring.testEventError": "Error sending test event", 110 | "monitoring.statsReset": "Statistics reset successfully!", 111 | "monitoring.resetError": "Error resetting statistics", 112 | "validation.errors": "Validation errors", 113 | "validation.invalidOrigin": "Invalid origin", 114 | "validation.pingTimeoutPositive": "Ping timeout must be positive", 115 | "validation.pingIntervalPositive": "Ping interval must be positive", 116 | "validation.connectionTimeoutPositive": "Connection timeout must be positive", 117 | "validation.maxConnectionsPositive": "Max connections must be positive", 118 | "validation.redisUrlRequired": "Redis URL is required when Redis is enabled" 119 | }; 120 | exports.default = en; 121 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Strapi Plugin IO - Documentation 2 | 3 | Modern, comprehensive documentation for the Socket.IO Plugin for Strapi v5, built with VitePress. 4 | 5 | --- 6 | 7 | ## 🚀 Quick Start 8 | 9 | ```bash 10 | # Install dependencies 11 | npm install 12 | 13 | # Start dev server 14 | npm run dev 15 | # Opens at http://localhost:5173 16 | 17 | # Build for production 18 | npm run build 19 | 20 | # Preview production build 21 | npm run preview 22 | ``` 23 | 24 | --- 25 | 26 | ## 📚 Documentation Structure 27 | 28 | ``` 29 | docs/ 30 | ├── index.md # Homepage 31 | ├── guide/ 32 | │ └── getting-started.md # Installation & setup guide 33 | ├── api/ 34 | │ ├── io-class.md # Core API reference 35 | │ └── plugin-config.md # Configuration options 36 | ├── examples/ 37 | │ ├── index.md # Examples overview 38 | │ ├── content-types.md # Content type examples 39 | │ ├── events.md # Custom events examples 40 | │ └── hooks.md # Lifecycle hooks examples 41 | ├── public/ 42 | │ └── logo.svg # Plugin logo 43 | └── .vitepress/ 44 | └── config.js # VitePress configuration 45 | ``` 46 | 47 | --- 48 | 49 | ## ✨ Features 50 | 51 | - 🎨 **Modern Design** - Clean, professional UI with dark mode 52 | - 🔍 **Local Search** - Fast, client-side search 53 | - 📱 **Mobile Responsive** - Works on all devices 54 | - 💻 **Code Highlighting** - Syntax highlighting for 40+ languages 55 | - 🔗 **Cross-References** - Easy navigation between related topics 56 | - 📖 **Rich Content** - Tips, warnings, code tabs, and more 57 | - ⚡ **Fast** - Static site generation for optimal performance 58 | 59 | --- 60 | 61 | ## 📖 Content Overview 62 | 63 | ### Getting Started 64 | - Requirements & compatibility 65 | - Installation instructions 66 | - Basic & advanced configuration 67 | - Authentication strategies 68 | - Admin panel setup 69 | - Troubleshooting 70 | 71 | ### API Reference 72 | - **IO Class**: Complete API with 7 helper functions 73 | - **Plugin Config**: All configuration options with examples 74 | 75 | ### Examples 76 | - **Content Types**: Automatic real-time events 77 | - **Events**: Custom event handlers 78 | - **Hooks**: Lifecycle hooks & adapters 79 | - **Use Cases**: 8 real-world implementations 80 | - **Frameworks**: React, Vue, Next.js examples 81 | 82 | --- 83 | 84 | ## 🛠️ Technology Stack 85 | 86 | - **VitePress**: v1.6.4 87 | - **Vue**: v3.5.13 88 | - **Node**: 18.0.0 - 22.x 89 | 90 | --- 91 | 92 | ## 📝 Writing Documentation 93 | 94 | ### Code Blocks 95 | 96 | ````markdown 97 | ```javascript 98 | const socket = io('http://localhost:1337'); 99 | ``` 100 | ```` 101 | 102 | ### Tips & Warnings 103 | 104 | ```markdown 105 | ::: tip 106 | This is a helpful tip! 107 | ::: 108 | 109 | ::: warning 110 | This is a warning! 111 | ::: 112 | 113 | ::: danger 114 | This is dangerous! 115 | ::: 116 | ``` 117 | 118 | ### Code Groups 119 | 120 | ````markdown 121 | ::: code-group 122 | 123 | ```javascript [JavaScript] 124 | const socket = io(); 125 | ``` 126 | 127 | ```typescript [TypeScript] 128 | const socket: Socket = io(); 129 | ``` 130 | 131 | ::: 132 | ```` 133 | 134 | --- 135 | 136 | ## 🔧 Configuration 137 | 138 | Edit `.vitepress/config.js` to customize: 139 | - Site title & description 140 | - Navigation & sidebar 141 | - Social links 142 | - Theme colors 143 | - Search settings 144 | - Footer content 145 | 146 | --- 147 | 148 | ## 🎨 Theming 149 | 150 | The documentation uses VitePress's default theme with custom branding: 151 | - **Primary Color**: `#4945ff` (Strapi blue) 152 | - **Dark Mode**: Automatic based on system preference 153 | - **Fonts**: System fonts for optimal performance 154 | 155 | --- 156 | 157 | ## 📊 Build Output 158 | 159 | ```bash 160 | npm run build 161 | ``` 162 | 163 | Generates static files in `.vitepress/dist/`: 164 | - Optimized HTML files 165 | - Minified CSS & JavaScript 166 | - Pre-rendered Vue components 167 | - Service worker for offline support 168 | 169 | --- 170 | 171 | ## 🚀 Deployment 172 | 173 | ### Netlify 174 | 175 | ```bash 176 | # Build command 177 | npm run build 178 | 179 | # Publish directory 180 | .vitepress/dist 181 | ``` 182 | 183 | ### Vercel 184 | 185 | ```bash 186 | # Build command 187 | npm run build 188 | 189 | # Output directory 190 | .vitepress/dist 191 | ``` 192 | 193 | ### GitHub Pages 194 | 195 | ```yaml 196 | # .github/workflows/deploy.yml 197 | name: Deploy docs 198 | 199 | on: 200 | push: 201 | branches: [main] 202 | 203 | jobs: 204 | deploy: 205 | runs-on: ubuntu-latest 206 | steps: 207 | - uses: actions/checkout@v3 208 | - uses: actions/setup-node@v3 209 | with: 210 | node-version: 18 211 | - run: npm install 212 | - run: npm run build 213 | - uses: peaceiris/actions-gh-pages@v3 214 | with: 215 | github_token: ${{ secrets.GITHUB_TOKEN }} 216 | publish_dir: .vitepress/dist 217 | ``` 218 | 219 | --- 220 | 221 | ## 🤝 Contributing 222 | 223 | ### Adding New Pages 224 | 225 | 1. Create markdown file in appropriate directory 226 | 2. Add to `.vitepress/config.js` sidebar 227 | 3. Link from relevant pages 228 | 4. Test locally with `npm run dev` 229 | 230 | ### Improving Content 231 | 232 | - Keep code examples up-to-date 233 | - Add practical use cases 234 | - Include TypeScript examples 235 | - Provide error handling examples 236 | - Add troubleshooting tips 237 | 238 | ### Style Guide 239 | 240 | - Use present tense 241 | - Keep sentences short 242 | - Include code examples 243 | - Add visual breaks (headings, lists) 244 | - Link to related content 245 | - Test all code examples 246 | 247 | --- 248 | 249 | ## 📄 License 250 | 251 | MIT License - Same as the plugin 252 | 253 | --- 254 | 255 | ## 👥 Credits 256 | 257 | **Original Plugin Authors:** 258 | - [@ComfortablyCoding](https://github.com/ComfortablyCoding) 259 | - [@hrdunn](https://github.com/hrdunn) 260 | 261 | **Documentation:** 262 | - Updated and enhanced by [@Schero94](https://github.com/Schero94) 263 | 264 | **Will be maintained till December 2026** 🚀 265 | 266 | --- 267 | 268 | ## 📞 Support 269 | 270 | - **Issues**: [GitHub Issues](https://github.com/strapi-community/strapi-plugin-io/issues) 271 | - **Discussions**: [GitHub Discussions](https://github.com/strapi-community/strapi-plugin-io/discussions) 272 | - **Documentation**: [strapi-plugin-io.netlify.app](https://strapi-plugin-io.netlify.app/) 273 | 274 | --- 275 | 276 | **Last Updated**: November 27, 2025 277 | **VitePress Version**: 1.6.4 278 | **Plugin Version**: 3.0.0 279 | 280 | -------------------------------------------------------------------------------- /server/services/strategies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { castArray, isNil, pipe, every } = require('lodash/fp'); 4 | const { differenceInHours, parseISO } = require('date-fns'); 5 | const { getService } = require('../utils/getService'); 6 | const { API_TOKEN_TYPE } = require('../utils/constants'); 7 | const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors; 8 | 9 | module.exports = ({ strapi }) => { 10 | const apiTokenService = getService({ type: 'admin', plugin: 'api-token' }); 11 | const jwtService = getService({ name: 'jwt', plugin: 'users-permissions' }); 12 | const userService = getService({ name: 'user', plugin: 'users-permissions' }); 13 | const role = { 14 | name: 'io-role', 15 | credentials: function (role) { 16 | return `${this.name}-${role.id}`; 17 | }, 18 | authenticate: async function (auth) { 19 | // adapted from https://github.com/strapi/strapi/blob/main/packages/plugins/users-permissions/server/strategies/users-permissions.js#L12 20 | const token = await jwtService.verify(auth.token); 21 | 22 | if (!token) { 23 | throw new UnauthorizedError('Invalid credentials'); 24 | } 25 | 26 | const { id } = token; 27 | 28 | // Invalid token 29 | if (id === undefined) { 30 | throw new UnauthorizedError('Invalid credentials'); 31 | } 32 | 33 | const user = await userService.fetchAuthenticatedUser(id); 34 | 35 | // No user associated to the token 36 | if (!user) { 37 | throw new UnauthorizedError('Invalid credentials'); 38 | } 39 | 40 | const advancedSettings = await strapi 41 | .store({ type: 'plugin', name: 'users-permissions' }) 42 | .get({ key: 'advanced' }); 43 | 44 | // User not confirmed 45 | if (advancedSettings.email_confirmation && !user.confirmed) { 46 | throw new UnauthorizedError('Invalid credentials'); 47 | } 48 | 49 | // User blocked 50 | if (user.blocked) { 51 | throw new UnauthorizedError('Invalid credentials'); 52 | } 53 | 54 | // Find role using Document Service API (Strapi v5) 55 | const roles = await strapi.documents('plugin::users-permissions.role').findMany({ 56 | filters: { id: user.role.id }, 57 | fields: ['id', 'name'], 58 | limit: 1, 59 | }); 60 | return roles.length > 0 ? roles[0] : null; 61 | }, 62 | verify: function (auth, config) { 63 | // adapted from https://github.com/strapi/strapi/blob/main/packages/plugins/users-permissions/server/strategies/users-permissions.js#L80 64 | const { ability } = auth; 65 | 66 | if (!ability) { 67 | throw new UnauthorizedError(); 68 | } 69 | 70 | const isAllowed = pipe( 71 | castArray, 72 | every((scope) => ability.can(scope)), 73 | )(config.scope); 74 | 75 | if (!isAllowed) { 76 | throw new ForbiddenError(); 77 | } 78 | }, 79 | getRoomName: function (role) { 80 | return `${this.name}-${role.name.toLowerCase()}`; 81 | }, 82 | getRooms: function () { 83 | // fetch all role types using Document Service API (Strapi v5) 84 | return strapi.documents('plugin::users-permissions.role').findMany({ 85 | fields: ['id', 'name'], 86 | populate: { permissions: true }, 87 | }); 88 | }, 89 | }; 90 | 91 | const token = { 92 | name: 'io-token', 93 | credentials: function (token) { 94 | return token; 95 | }, 96 | authenticate: async function (auth) { 97 | // adapted from https://github.com/strapi/strapi/blob/main/packages/core/admin/server/strategies/api-token.js#L30 98 | const token = auth.token; 99 | 100 | if (!token) { 101 | throw new UnauthorizedError('Invalid credentials'); 102 | } 103 | 104 | // Note: admin::api-token uses strapi.db.query() as it's a core admin entity 105 | // that doesn't follow the Document Service pattern 106 | const apiToken = await strapi.db.query('admin::api-token').findOne({ 107 | where: { accessKey: apiTokenService.hash(token) }, 108 | select: ['id', 'name', 'type', 'lastUsedAt', 'expiresAt'], 109 | populate: ['permissions'], 110 | }); 111 | 112 | // token not found 113 | if (!apiToken) { 114 | throw new UnauthorizedError('Invalid credentials'); 115 | } 116 | 117 | const currentDate = new Date(); 118 | if (!isNil(apiToken.expiresAt)) { 119 | const expirationDate = new Date(apiToken.expiresAt); 120 | // token has expired 121 | if (expirationDate < currentDate) { 122 | throw new UnauthorizedError('Token expired'); 123 | } 124 | } 125 | 126 | // update lastUsedAt if the token has not been used in the last hour 127 | if (!apiToken.lastUsedAt || differenceInHours(currentDate, parseISO(apiToken.lastUsedAt)) >= 1) { 128 | await strapi.db.query('admin::api-token').update({ 129 | where: { id: apiToken.id }, 130 | data: { lastUsedAt: currentDate }, 131 | }); 132 | } 133 | 134 | return apiToken; 135 | }, 136 | verify: function (auth, config) { 137 | // adapted from https://github.com/strapi/strapi/blob/main/packages/core/admin/server/strategies/api-token.js#L82 138 | const { credentials: apiToken, ability } = auth; 139 | if (!apiToken) { 140 | throw new UnauthorizedError('Token not found'); 141 | } 142 | 143 | if (!isNil(apiToken.expiresAt)) { 144 | const currentDate = new Date(); 145 | const expirationDate = new Date(apiToken.expiresAt); 146 | // token has expired 147 | if (expirationDate < currentDate) { 148 | throw new UnauthorizedError('Token expired'); 149 | } 150 | } 151 | 152 | if (apiToken.type === API_TOKEN_TYPE.FULL_ACCESS) { 153 | return; 154 | } else if (apiToken.type === API_TOKEN_TYPE.READ_ONLY) { 155 | const scopes = castArray(config.scope); 156 | 157 | if (config.scope && scopes.every(isReadScope)) { 158 | return; 159 | } 160 | } else if (apiToken.type === API_TOKEN_TYPE.CUSTOM) { 161 | if (!ability) { 162 | throw new ForbiddenError(); 163 | } 164 | 165 | const scopes = castArray(config.scope); 166 | 167 | const isAllowed = scopes.every((scope) => ability.can(scope)); 168 | 169 | if (isAllowed) { 170 | return; 171 | } 172 | } 173 | 174 | throw new ForbiddenError(); 175 | }, 176 | getRoomName: function (token) { 177 | return `${this.name}-${token.name.toLowerCase()}`; 178 | }, 179 | getRooms: function () { 180 | // fetch active token types 181 | // Note: admin::api-token uses strapi.db.query() as it's a core admin entity 182 | return strapi.db.query('admin::api-token').findMany({ 183 | select: ['id', 'type', 'name'], 184 | where: { 185 | $or: [ 186 | { 187 | expiresAt: { 188 | $gte: new Date(), 189 | }, 190 | }, 191 | { 192 | expiresAt: null, 193 | }, 194 | ], 195 | }, 196 | populate: ['permissions'], 197 | }); 198 | }, 199 | }; 200 | 201 | return { 202 | role, 203 | token, 204 | }; 205 | }; 206 | -------------------------------------------------------------------------------- /server/src/services/strategies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { castArray, isNil, pipe, every } = require('lodash/fp'); 4 | const { differenceInHours, parseISO } = require('date-fns'); 5 | const { getService } = require('../utils/getService'); 6 | const { API_TOKEN_TYPE } = require('../utils/constants'); 7 | const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors; 8 | 9 | module.exports = ({ strapi }) => { 10 | const apiTokenService = getService({ type: 'admin', plugin: 'api-token' }); 11 | const jwtService = getService({ name: 'jwt', plugin: 'users-permissions' }); 12 | const userService = getService({ name: 'user', plugin: 'users-permissions' }); 13 | const role = { 14 | name: 'io-role', 15 | credentials: function (role) { 16 | return `${this.name}-${role.id}`; 17 | }, 18 | authenticate: async function (auth) { 19 | // adapted from https://github.com/strapi/strapi/blob/main/packages/plugins/users-permissions/server/strategies/users-permissions.js#L12 20 | const token = await jwtService.verify(auth.token); 21 | 22 | if (!token) { 23 | throw new UnauthorizedError('Invalid credentials'); 24 | } 25 | 26 | const { id } = token; 27 | 28 | // Invalid token 29 | if (id === undefined) { 30 | throw new UnauthorizedError('Invalid credentials'); 31 | } 32 | 33 | const user = await userService.fetchAuthenticatedUser(id); 34 | 35 | // No user associated to the token 36 | if (!user) { 37 | throw new UnauthorizedError('Invalid credentials'); 38 | } 39 | 40 | const advancedSettings = await strapi 41 | .store({ type: 'plugin', name: 'users-permissions' }) 42 | .get({ key: 'advanced' }); 43 | 44 | // User not confirmed 45 | if (advancedSettings.email_confirmation && !user.confirmed) { 46 | throw new UnauthorizedError('Invalid credentials'); 47 | } 48 | 49 | // User blocked 50 | if (user.blocked) { 51 | throw new UnauthorizedError('Invalid credentials'); 52 | } 53 | 54 | // Find role using Document Service API (Strapi v5) 55 | const roles = await strapi.documents('plugin::users-permissions.role').findMany({ 56 | filters: { id: user.role.id }, 57 | fields: ['id', 'name'], 58 | limit: 1, 59 | }); 60 | return roles.length > 0 ? roles[0] : null; 61 | }, 62 | verify: function (auth, config) { 63 | // adapted from https://github.com/strapi/strapi/blob/main/packages/plugins/users-permissions/server/strategies/users-permissions.js#L80 64 | const { ability } = auth; 65 | 66 | if (!ability) { 67 | throw new UnauthorizedError(); 68 | } 69 | 70 | const isAllowed = pipe( 71 | castArray, 72 | every((scope) => ability.can(scope)), 73 | )(config.scope); 74 | 75 | if (!isAllowed) { 76 | throw new ForbiddenError(); 77 | } 78 | }, 79 | getRoomName: function (role) { 80 | return `${this.name}-${role.name.toLowerCase()}`; 81 | }, 82 | getRooms: function () { 83 | // fetch all role types using Document Service API (Strapi v5) 84 | return strapi.documents('plugin::users-permissions.role').findMany({ 85 | fields: ['id', 'name'], 86 | populate: { permissions: true }, 87 | }); 88 | }, 89 | }; 90 | 91 | const token = { 92 | name: 'io-token', 93 | credentials: function (token) { 94 | return token; 95 | }, 96 | authenticate: async function (auth) { 97 | // adapted from https://github.com/strapi/strapi/blob/main/packages/core/admin/server/strategies/api-token.js#L30 98 | const token = auth.token; 99 | 100 | if (!token) { 101 | throw new UnauthorizedError('Invalid credentials'); 102 | } 103 | 104 | // ⚠️ LEGITIMATE EXCEPTION: admin::api-token is a Strapi Core Admin entity 105 | // Official Strapi implementation uses strapi.db.query() for admin::api-token 106 | // Source: https://github.com/strapi/strapi/blob/main/packages/core/admin/server/strategies/api-token.js 107 | // This is NOT a mistake - Strapi Core itself uses Query Engine for admin entities 108 | const apiToken = await strapi.db.query('admin::api-token').findOne({ 109 | where: { accessKey: apiTokenService.hash(token) }, 110 | select: ['id', 'name', 'type', 'lastUsedAt', 'expiresAt'], 111 | populate: ['permissions'], 112 | }); 113 | 114 | // token not found 115 | if (!apiToken) { 116 | throw new UnauthorizedError('Invalid credentials'); 117 | } 118 | 119 | const currentDate = new Date(); 120 | if (!isNil(apiToken.expiresAt)) { 121 | const expirationDate = new Date(apiToken.expiresAt); 122 | // token has expired 123 | if (expirationDate < currentDate) { 124 | throw new UnauthorizedError('Token expired'); 125 | } 126 | } 127 | 128 | // Update lastUsedAt if the token has not been used in the last hour 129 | // ⚠️ LEGITIMATE EXCEPTION: Using Query Engine as per Strapi Core implementation 130 | if (!apiToken.lastUsedAt || differenceInHours(currentDate, parseISO(apiToken.lastUsedAt)) >= 1) { 131 | await strapi.db.query('admin::api-token').update({ 132 | where: { id: apiToken.id }, 133 | data: { lastUsedAt: currentDate }, 134 | }); 135 | } 136 | 137 | return apiToken; 138 | }, 139 | verify: function (auth, config) { 140 | // adapted from https://github.com/strapi/strapi/blob/main/packages/core/admin/server/strategies/api-token.js#L82 141 | const { credentials: apiToken, ability } = auth; 142 | if (!apiToken) { 143 | throw new UnauthorizedError('Token not found'); 144 | } 145 | 146 | if (!isNil(apiToken.expiresAt)) { 147 | const currentDate = new Date(); 148 | const expirationDate = new Date(apiToken.expiresAt); 149 | // token has expired 150 | if (expirationDate < currentDate) { 151 | throw new UnauthorizedError('Token expired'); 152 | } 153 | } 154 | 155 | if (apiToken.type === API_TOKEN_TYPE.FULL_ACCESS) { 156 | return; 157 | } else if (apiToken.type === API_TOKEN_TYPE.READ_ONLY) { 158 | const scopes = castArray(config.scope); 159 | 160 | if (config.scope && scopes.every(isReadScope)) { 161 | return; 162 | } 163 | } else if (apiToken.type === API_TOKEN_TYPE.CUSTOM) { 164 | if (!ability) { 165 | throw new ForbiddenError(); 166 | } 167 | 168 | const scopes = castArray(config.scope); 169 | 170 | const isAllowed = scopes.every((scope) => ability.can(scope)); 171 | 172 | if (isAllowed) { 173 | return; 174 | } 175 | } 176 | 177 | throw new ForbiddenError(); 178 | }, 179 | getRoomName: function (token) { 180 | return `${this.name}-${token.name.toLowerCase()}`; 181 | }, 182 | getRooms: function () { 183 | // Fetch active token types 184 | // ⚠️ LEGITIMATE EXCEPTION: Using Query Engine as per Strapi Core implementation 185 | return strapi.db.query('admin::api-token').findMany({ 186 | select: ['id', 'type', 'name'], 187 | where: { 188 | $or: [ 189 | { 190 | expiresAt: { 191 | $gte: new Date(), 192 | }, 193 | }, 194 | { 195 | expiresAt: null, 196 | }, 197 | ], 198 | }, 199 | populate: ['permissions'], 200 | }); 201 | }, 202 | }; 203 | 204 | return { 205 | role, 206 | token, 207 | }; 208 | }; 209 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Get up and running with Socket.IO in your Strapi v5 application in less than 5 minutes. 4 | 5 | ## Requirements 6 | 7 | - **Node.js**: 18.0.0 - 22.x 8 | - **Strapi**: v5.x 9 | - **npm**: 6.0.0 or higher 10 | 11 | ::: tip Compatibility 12 | This version (v5.x) is designed for **Strapi v5** only. For Strapi v4, use version 2.x of this plugin. 13 | ::: 14 | 15 | ## Installation 16 | 17 | Install the plugin in your Strapi project: 18 | 19 | ::: code-group 20 | 21 | ```bash [npm] 22 | npm install @strapi-community/plugin-io 23 | ``` 24 | 25 | ```bash [yarn] 26 | yarn add @strapi-community/plugin-io 27 | ``` 28 | 29 | ```bash [pnpm] 30 | pnpm add @strapi-community/plugin-io 31 | ``` 32 | 33 | ::: 34 | 35 | ## Basic Configuration 36 | 37 | Create or edit `config/plugins.js` (or `config/plugins.ts` for TypeScript): 38 | 39 | ::: code-group 40 | 41 | ```javascript [JavaScript] 42 | module.exports = ({ env }) => ({ 43 | io: { 44 | enabled: true, 45 | config: { 46 | // Enable automatic events for content types 47 | contentTypes: [ 48 | 'api::article.article', 49 | 'api::comment.comment' 50 | ], 51 | 52 | // Socket.IO server options 53 | socket: { 54 | serverOptions: { 55 | cors: { 56 | origin: env('CLIENT_URL', 'http://localhost:3000'), 57 | methods: ['GET', 'POST'] 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }); 64 | ``` 65 | 66 | ```typescript [TypeScript] 67 | export default ({ env }) => ({ 68 | io: { 69 | enabled: true, 70 | config: { 71 | contentTypes: [ 72 | 'api::article.article', 73 | 'api::comment.comment' 74 | ], 75 | 76 | socket: { 77 | serverOptions: { 78 | cors: { 79 | origin: env('CLIENT_URL', 'http://localhost:3000'), 80 | methods: ['GET', 'POST'] 81 | } 82 | } 83 | } 84 | } 85 | } 86 | }); 87 | ``` 88 | 89 | ::: 90 | 91 | ::: info 92 | The `plugins.js` file doesn't exist by default. Create it if this is a new project. 93 | ::: 94 | 95 | ## Start Your Server 96 | 97 | ```bash 98 | npm run develop 99 | ``` 100 | 101 | You should see in the console: 102 | 103 | ``` 104 | [io] ✅ Socket.IO initialized successfully 105 | [io] 🚀 Server listening on http://localhost:1337 106 | ``` 107 | 108 | ## Client Connection 109 | 110 | ### Frontend Setup 111 | 112 | Install Socket.IO client in your frontend project: 113 | 114 | ```bash 115 | npm install socket.io-client 116 | ``` 117 | 118 | ### Connect to Strapi 119 | 120 | ::: code-group 121 | 122 | ```javascript [Public Connection] 123 | import { io } from 'socket.io-client'; 124 | 125 | const socket = io('http://localhost:1337'); 126 | 127 | socket.on('connect', () => { 128 | console.log('✅ Connected:', socket.id); 129 | }); 130 | 131 | // Listen for article events 132 | socket.on('article:create', (data) => { 133 | console.log('New article created:', data); 134 | }); 135 | 136 | socket.on('article:update', (data) => { 137 | console.log('Article updated:', data); 138 | }); 139 | 140 | socket.on('article:delete', (data) => { 141 | console.log('Article deleted:', data); 142 | }); 143 | ``` 144 | 145 | ```javascript [JWT Authentication] 146 | import { io } from 'socket.io-client'; 147 | 148 | // Get JWT token after user login 149 | const jwtToken = 'your-jwt-token-here'; 150 | 151 | const socket = io('http://localhost:1337', { 152 | auth: { 153 | strategy: 'jwt', 154 | token: jwtToken 155 | } 156 | }); 157 | 158 | socket.on('connect', () => { 159 | console.log('✅ Authenticated connection:', socket.id); 160 | }); 161 | 162 | // Listen for events based on user role permissions 163 | socket.on('article:create', (data) => { 164 | console.log('New article:', data); 165 | }); 166 | ``` 167 | 168 | ```javascript [API Token] 169 | import { io } from 'socket.io-client'; 170 | 171 | // API Token from Strapi Admin Panel 172 | // Settings -> Global Settings -> API Tokens 173 | const apiToken = 'your-api-token-here'; 174 | 175 | const socket = io('http://localhost:1337', { 176 | auth: { 177 | strategy: 'api-token', 178 | token: apiToken 179 | } 180 | }); 181 | 182 | socket.on('connect', () => { 183 | console.log('✅ API Token authenticated:', socket.id); 184 | }); 185 | 186 | socket.on('article:create', (data) => { 187 | console.log('New article:', data); 188 | }); 189 | ``` 190 | 191 | ::: 192 | 193 | ## Authentication Strategies 194 | 195 | The plugin automatically handles authentication and places connections in rooms based on their role or token: 196 | 197 | | Strategy | Use Case | Room Assignment | 198 | |----------|----------|----------------| 199 | | **None** | Public access | `Public` role room | 200 | | **JWT** | User-Permissions plugin | User's role room (e.g., `Authenticated`) | 201 | | **API Token** | Server-to-server | Token's configured permissions | 202 | 203 | ::: tip Role-Based Events 204 | Users only receive events for content types their role has permission to access. This is enforced automatically! 205 | ::: 206 | 207 | ## Admin Panel Configuration 208 | 209 | After installation, configure the plugin visually: 210 | 211 | 1. Navigate to **Settings** → **Socket.IO** 212 | 2. Configure: 213 | - ✅ **CORS Origins** - Add your frontend URLs 214 | - ✅ **Content Types** - Enable real-time events 215 | - ✅ **Role Permissions** - Control access per role 216 | - ✅ **Security Settings** - Rate limiting, IP whitelisting 217 | - ✅ **Monitoring** - View live connections 218 | 219 | ![Socket.IO Settings Panel](/settings.png) 220 | 221 | *The visual settings panel makes configuration easy - no code required for most settings!* 222 | 223 | --- 224 | 225 | ## Dashboard Widget 226 | 227 | After installation, you'll see a live statistics widget on your admin home page: 228 | 229 | ![Socket.IO Dashboard Widget](/widget.png) 230 | 231 | **Widget Features:** 232 | - 🟢 Live connection status with pulsing indicator 233 | - 👥 Active connections count 234 | - 💬 Active rooms count 235 | - ⚡ Events per second 236 | - 📈 Total events processed 237 | - 🔄 Auto-updates every 5 seconds 238 | 239 | --- 240 | 241 | ## Quick Test 242 | 243 | Test your setup with this simple HTML file: 244 | 245 | ```html 246 | 247 | 248 | 249 | Socket.IO Test 250 | 251 | 252 | 253 |

Socket.IO Test

254 |
Connecting...
255 |
256 | 257 | 276 | 277 | 278 | ``` 279 | 280 | Open this file in your browser, then create an article in your Strapi admin panel. You should see the event appear in real-time! 281 | 282 | ## Next Steps 283 | 284 | - **[View Examples](/examples/)** - Learn common use cases 285 | - **[API Reference](/api/io-class)** - Explore all available methods 286 | - **[Configuration](/api/plugin-config)** - Advanced setup options 287 | - **[Helper Functions](/api/io-class#helper-functions)** - Utility methods 288 | 289 | ## Troubleshooting 290 | 291 | ### CORS Errors 292 | 293 | If you see CORS errors in the browser console: 294 | 295 | ```javascript 296 | config: { 297 | socket: { 298 | serverOptions: { 299 | cors: { 300 | origin: '*', // For development only! 301 | methods: ['GET', 'POST'] 302 | } 303 | } 304 | } 305 | } 306 | ``` 307 | 308 | ### Events Not Received 309 | 310 | 1. Check role permissions in **Settings** → **Socket.IO** 311 | 2. Verify content type is enabled in config 312 | 3. Ensure user has permission to access the content type 313 | 314 | ### Connection Fails 315 | 316 | 1. Verify Strapi is running 317 | 2. Check the URL (default: `http://localhost:1337`) 318 | 3. Look for firewall/network issues 319 | 320 | ### Migrating from Strapi v4? 321 | 322 | See our [Migration Guide](/guide/migration) for step-by-step instructions to upgrade from Strapi v4 to v5. 323 | 324 | ::: warning Data Transfer 325 | If using `strapi transfer` command, temporarily disable this plugin or run it on a different port. See [issue #76](https://github.com/strapi-community/strapi-plugin-io/issues/76) for details. 326 | ::: 327 | -------------------------------------------------------------------------------- /server/bootstrap/lifecycle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Lazy-load transaction context to avoid bundling issues 4 | let transactionCtx = null; 5 | function getTransactionCtx() { 6 | if (!transactionCtx) { 7 | try { 8 | transactionCtx = require('@strapi/database/dist/transaction-context').transactionCtx; 9 | } catch (error) { 10 | console.warn('[@strapi-community/plugin-io] Unable to access transaction context:', error.message); 11 | transactionCtx = { get: () => null, onCommit: () => {} }; // Fallback noop 12 | } 13 | } 14 | return transactionCtx; 15 | } 16 | 17 | const { pluginId } = require('../utils/pluginId'); 18 | 19 | /** 20 | * Run callback after the current transaction commits (if any). 21 | * Falls back to a plain setTimeout when no transaction is active. 22 | */ 23 | function scheduleAfterTransaction(callback, delay = 0) { 24 | const runner = () => setTimeout(callback, delay); 25 | const ctx = getTransactionCtx(); 26 | if (ctx.get()) { 27 | ctx.onCommit(runner); 28 | } else { 29 | runner(); 30 | } 31 | } 32 | 33 | /** 34 | * Check if action is enabled for a content type 35 | * Reads from strapi.$ioSettings which is updated on settings change 36 | */ 37 | function isActionEnabled(strapi, uid, action) { 38 | const settings = strapi.$ioSettings || {}; 39 | const rolePermissions = settings.rolePermissions || {}; 40 | 41 | // Check if ANY role has this action enabled for this content type 42 | for (const rolePerms of Object.values(rolePermissions)) { 43 | if (rolePerms.contentTypes?.[uid]?.[action] === true) { 44 | return true; 45 | } 46 | } 47 | return false; 48 | } 49 | 50 | /** 51 | * Bootstrap lifecycles 52 | * 53 | * @param {*} params 54 | * @param {*} params.strapi 55 | */ 56 | async function bootstrapLifecycles({ strapi }) { 57 | // Get content types from stored settings (set by bootstrapIO) 58 | const settings = strapi.$ioSettings || {}; 59 | const rolePermissions = settings.rolePermissions || {}; 60 | 61 | // Merge all role permissions to get all enabled content types 62 | const allContentTypes = {}; 63 | Object.values(rolePermissions).forEach((rolePerms) => { 64 | if (rolePerms.contentTypes) { 65 | Object.entries(rolePerms.contentTypes).forEach(([uid, actions]) => { 66 | if (!allContentTypes[uid]) { 67 | allContentTypes[uid] = { create: false, update: false, delete: false }; 68 | } 69 | // Enable action if ANY role has it enabled 70 | if (actions.create) allContentTypes[uid].create = true; 71 | if (actions.update) allContentTypes[uid].update = true; 72 | if (actions.delete) allContentTypes[uid].delete = true; 73 | }); 74 | } 75 | }); 76 | 77 | // Get all UIDs that have at least one action enabled 78 | const enabledUids = Object.entries(allContentTypes) 79 | .filter(([uid, actions]) => actions.create || actions.update || actions.delete) 80 | .map(([uid]) => uid); 81 | 82 | enabledUids.forEach((uid) => { 83 | const subscriber = { 84 | models: [uid], 85 | }; 86 | 87 | // CREATE - check dynamically if enabled 88 | subscriber.afterCreate = async (event) => { 89 | if (!isActionEnabled(strapi, uid, 'create')) return; 90 | // Skip if no result data 91 | if (!event.result) { 92 | strapi.log.debug(`socket.io: No result data in afterCreate for ${uid}`); 93 | return; 94 | } 95 | // Clone data to avoid transaction context issues 96 | try { 97 | const eventData = { 98 | event: 'create', 99 | schema: event.model, 100 | data: JSON.parse(JSON.stringify(event.result)), // Deep clone 101 | }; 102 | // Schedule emission after transaction commit 103 | scheduleAfterTransaction(() => { 104 | try { 105 | strapi.$io.emit(eventData); 106 | } catch (error) { 107 | strapi.log.error(`socket.io: Could not emit create event for ${uid}:`, error.message); 108 | } 109 | }); 110 | } catch (error) { 111 | strapi.log.error(`socket.io: Error cloning create event data for ${uid}:`, error.message); 112 | } 113 | }; 114 | subscriber.afterCreateMany = async (event) => { 115 | if (!isActionEnabled(strapi, uid, 'create')) return; 116 | const query = buildEventQuery({ event }); 117 | if (query.filters) { 118 | // Clone query to avoid transaction context issues 119 | const clonedQuery = JSON.parse(JSON.stringify(query)); 120 | const modelInfo = { singularName: event.model.singularName, uid: event.model.uid }; 121 | // Schedule query after transaction commit 122 | scheduleAfterTransaction(async () => { 123 | try { 124 | // Use Document Service API (Strapi v5) 125 | const records = await strapi.documents(uid).findMany(clonedQuery); 126 | records.forEach((r) => { 127 | strapi.$io.emit({ 128 | event: 'create', 129 | schema: { singularName: modelInfo.singularName, uid: modelInfo.uid }, 130 | data: r, 131 | }); 132 | }); 133 | } catch (error) { 134 | strapi.log.debug(`socket.io: Could not fetch records in afterCreateMany for ${uid}:`, error.message); 135 | } 136 | }, 50); 137 | } 138 | }; 139 | 140 | // UPDATE - check dynamically if enabled 141 | subscriber.afterUpdate = async (event) => { 142 | if (!isActionEnabled(strapi, uid, 'update')) return; 143 | // Clone data to avoid transaction context issues 144 | const eventData = { 145 | event: 'update', 146 | schema: event.model, 147 | data: JSON.parse(JSON.stringify(event.result)), // Deep clone 148 | }; 149 | // Schedule emission after transaction commit 150 | scheduleAfterTransaction(() => { 151 | try { 152 | strapi.$io.emit(eventData); 153 | } catch (error) { 154 | strapi.log.debug(`socket.io: Could not emit update event for ${uid}:`, error.message); 155 | } 156 | }); 157 | }; 158 | subscriber.beforeUpdateMany = async (event) => { 159 | // Don't do any queries in before* hooks to avoid transaction conflicts 160 | // Just store the params for use in afterUpdateMany 161 | if (!isActionEnabled(strapi, uid, 'update')) return; 162 | if (!event.state.io) { 163 | event.state.io = {}; 164 | } 165 | event.state.io.params = event.params; 166 | }; 167 | subscriber.afterUpdateMany = async (event) => { 168 | if (!isActionEnabled(strapi, uid, 'update')) return; 169 | // Fetch the updated records using params from beforeUpdateMany 170 | const params = event.state.io?.params; 171 | if (!params || !params.where) return; 172 | 173 | // Clone params to avoid transaction context issues 174 | const clonedWhere = JSON.parse(JSON.stringify(params.where)); 175 | const modelInfo = { singularName: event.model.singularName, uid: event.model.uid }; 176 | // Schedule query after transaction commit 177 | scheduleAfterTransaction(async () => { 178 | try { 179 | // Use Document Service API (Strapi v5) 180 | const records = await strapi.documents(uid).findMany({ 181 | filters: clonedWhere, 182 | }); 183 | records.forEach((r) => { 184 | strapi.$io.emit({ 185 | event: 'update', 186 | schema: { singularName: modelInfo.singularName, uid: modelInfo.uid }, 187 | data: r, 188 | }); 189 | }); 190 | } catch (error) { 191 | strapi.log.debug(`socket.io: Could not fetch records in afterUpdateMany for ${uid}:`, error.message); 192 | } 193 | }, 50); 194 | }; 195 | 196 | // DELETE - check dynamically if enabled 197 | subscriber.afterDelete = async (event) => { 198 | if (!isActionEnabled(strapi, uid, 'delete')) return; 199 | // Extract minimal data to avoid transaction context issues 200 | const deleteData = { 201 | id: event.result?.id || event.result?.documentId, 202 | documentId: event.result?.documentId || event.result?.id, 203 | }; 204 | const modelInfo = { 205 | singularName: event.model.singularName, 206 | uid: event.model.uid, 207 | }; 208 | 209 | // Use raw emit to avoid sanitization queries within transaction 210 | scheduleAfterTransaction(() => { 211 | try { 212 | const eventName = `${modelInfo.singularName}:delete`; 213 | strapi.$io.raw({ 214 | event: eventName, 215 | data: deleteData, 216 | }); 217 | } catch (error) { 218 | strapi.log.debug(`socket.io: Could not emit delete event for ${uid}:`, error.message); 219 | } 220 | }, 100); // Delay to ensure transaction is fully closed 221 | }; 222 | // Bulk delete events are intentionally disabled to prevent transaction conflicts 223 | 224 | // setup lifecycles 225 | strapi.db.lifecycles.subscribe(subscriber); 226 | }); 227 | 228 | // Log configured content types 229 | const configuredCount = enabledUids.length; 230 | if (configuredCount > 0) { 231 | strapi.log.info(`socket.io: Lifecycle hooks registered for ${configuredCount} content type(s)`); 232 | } 233 | } 234 | 235 | function buildEventQuery({ event }) { 236 | const query = {}; 237 | 238 | if (event.params.where) { 239 | query.filters = event.params.where; 240 | } 241 | 242 | if (event.result?.count) { 243 | query.limit = event.result.count; 244 | } else if (event.params.limit) { 245 | query.limit = event.params.limit; 246 | } 247 | 248 | if (event.action === 'afterCreateMany') { 249 | query.filters = { id: event.result.ids }; 250 | } else if (event.action === 'beforeUpdate') { 251 | query.fields = ['id']; 252 | } 253 | 254 | return query; 255 | } 256 | 257 | module.exports = { bootstrapLifecycles }; 258 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: 'Strapi Plugin IO' 6 | text: 'Socket.IO Integration for Strapi v5' 7 | tagline: 'Real-time WebSocket connections with authentication, monitoring, and advanced features' 8 | actions: 9 | - theme: brand 10 | text: Get Started 11 | link: /guide/getting-started 12 | - theme: alt 13 | text: View Examples 14 | link: /examples/ 15 | - theme: alt 16 | text: API Reference 17 | link: /api/io-class 18 | 19 | features: 20 | - icon: ⚡ 21 | title: Real-Time Events 22 | details: Automatic CRUD event broadcasting for all content types with role-based permissions 23 | 24 | - icon: 🔐 25 | title: Built-in Authentication 26 | details: JWT and API Token authentication with automatic role-based access control 27 | 28 | - icon: 📊 29 | title: Admin Dashboard 30 | details: Visual configuration panel with live monitoring, connection stats, and event logs 31 | 32 | - icon: 🚀 33 | title: Production Ready 34 | details: Redis adapter support, rate limiting, IP whitelisting, and optimized for 2500+ concurrent connections 35 | 36 | - icon: 🎯 37 | title: Developer Friendly 38 | details: Full TypeScript support, 7 helper functions, comprehensive documentation 39 | 40 | - icon: 🔧 41 | title: Highly Configurable 42 | details: Namespaces, rooms, custom events, hooks, and flexible CORS settings 43 | --- 44 | 45 | ## Quick Start 46 | 47 | Install the plugin: 48 | 49 | ```bash 50 | npm install @strapi-community/plugin-io 51 | ``` 52 | 53 | Enable in `config/plugins.js`: 54 | 55 | ```javascript 56 | module.exports = { 57 | 'io': { 58 | enabled: true, 59 | config: { 60 | contentTypes: ['api::article.article'], 61 | socket: { 62 | serverOptions: { 63 | cors: { 64 | origin: 'http://localhost:3000', 65 | methods: ['GET', 'POST'] 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }; 72 | ``` 73 | 74 | Connect from your frontend: 75 | 76 | ```javascript 77 | import { io } from 'socket.io-client'; 78 | 79 | const socket = io('http://localhost:1337', { 80 | auth: { 81 | strategy: 'jwt', 82 | token: 'your-jwt-token' 83 | } 84 | }); 85 | 86 | socket.on('article:create', (data) => { 87 | console.log('New article:', data); 88 | }); 89 | ``` 90 | 91 | ## What's New in v5.0 92 | 93 | **Strapi v5 Support** - Built for the latest Strapi version with full compatibility 94 | 95 | **Migration Made Easy** - [See migration guide](/guide/migration) - most projects migrate in under 1 hour! 96 | 97 | **Admin Dashboard** - Visual configuration with live monitoring widget on home page 98 | 99 | **Advanced Security** - Rate limiting, IP whitelisting, enhanced authentication 100 | 101 | **Performance Optimizations** - 90% reduction in DB queries, improved caching, parallel processing 102 | 103 | **Enhanced API** - 7 helper functions for common tasks, improved TypeScript definitions 104 | 105 | **Better Documentation** - Comprehensive guides, real-world examples, migration path 106 | 107 | ## Core Features 108 | 109 | ### 🎯 Automatic Content Type Events 110 | 111 | Enable any content type for real-time updates: 112 | 113 | ```javascript 114 | // Backend: config/plugins.js 115 | contentTypes: [ 116 | 'api::article.article', 117 | 'api::comment.comment', 118 | { 119 | uid: 'api::product.product', 120 | actions: ['create', 'update'] 121 | } 122 | ] 123 | ``` 124 | 125 | ```javascript 126 | // Frontend: Automatic events 127 | socket.on('article:create', (article) => { /* ... */ }); 128 | socket.on('article:update', (article) => { /* ... */ }); 129 | socket.on('article:delete', (article) => { /* ... */ }); 130 | ``` 131 | 132 | ### 🔐 Role-Based Access Control 133 | 134 | Permissions are automatically enforced based on user roles: 135 | 136 | ```javascript 137 | // Public users only receive events they have permission to see 138 | // Authenticated users receive events based on their role 139 | // API tokens work with their configured permissions 140 | ``` 141 | 142 | ### 📊 Real-Time Monitoring 143 | 144 | View live statistics in your admin panel: 145 | - 🟢 Active connections 146 | - 💬 Active rooms 147 | - ⚡ Events per second 148 | - 📈 Total events processed 149 | 150 | ### 🚀 Helper Functions 151 | 152 | Seven utility functions for common tasks: 153 | 154 | ```javascript 155 | // Join/leave rooms 156 | strapi.$io.joinRoom(socketId, 'premium-users'); 157 | strapi.$io.leaveRoom(socketId, 'premium-users'); 158 | 159 | // Private messages 160 | strapi.$io.sendPrivateMessage(socketId, 'notification', data); 161 | 162 | // Broadcast to all except sender 163 | strapi.$io.broadcast(socketId, 'user-joined', data); 164 | 165 | // Namespace management 166 | strapi.$io.emitToNamespace('admin', 'dashboard:update', stats); 167 | 168 | // Room info 169 | const sockets = await strapi.$io.getSocketsInRoom('chat-room'); 170 | 171 | // Disconnect 172 | strapi.$io.disconnectSocket(socketId, 'Kicked by admin'); 173 | ``` 174 | 175 | ## Use Cases 176 | 177 | ### Real-Time Blog 178 | Automatically notify readers when new articles are published 179 | 180 | ### Live Chat 181 | Build a chat system with rooms, private messages, and typing indicators 182 | 183 | ### E-Commerce 184 | Update product availability, prices, and inventory in real-time 185 | 186 | ### Collaborative Tools 187 | Synchronize state across multiple users editing the same document 188 | 189 | ### Dashboards 190 | Push live metrics, alerts, and system status to admin panels 191 | 192 | ### Gaming 193 | Handle player connections, game state, and multiplayer interactions 194 | 195 | ### IoT Integration 196 | Receive and broadcast sensor data and device status updates 197 | 198 | ## Dashboard Widget 199 | 200 | The plugin includes a **live statistics widget** on your Strapi admin home page: 201 | 202 | ![Socket.IO Dashboard Widget](/widget.png) 203 | 204 | 📊 **Widget Features:** 205 | - 🟢 Live connection status with pulsing indicator 206 | - 👥 Active connections count 207 | - 💬 Active rooms count 208 | - ⚡ Events per second 209 | - 📈 Total events processed 210 | - 🔄 Auto-updates every 5 seconds 211 | 212 | **See it in action:** Navigate to your admin home page after installing the plugin! 213 | 214 | --- 215 | 216 | ## Visual Configuration 217 | 218 | Configure everything visually in the admin panel: 219 | 220 | ![Socket.IO Settings](/settings.png) 221 | 222 | **Settings → Socket.IO** gives you full control over: 223 | - CORS origins 224 | - Content type monitoring 225 | - Role-based permissions 226 | - Security settings 227 | - Rate limiting 228 | 229 | ![Monitoring Dashboard](/monitoringSettings.png) 230 | 231 | **Real-time monitoring** shows: 232 | - Active connections with user details 233 | - Event logs 234 | - Performance metrics 235 | - Connection history 236 | 237 | --- 238 | 239 | ## Browser Support 240 | 241 | - ✅ Chrome/Edge 90+ 242 | - ✅ Firefox 88+ 243 | - ✅ Safari 14+ 244 | - ✅ iOS Safari 14+ 245 | - ✅ Android Chrome 90+ 246 | 247 | ## Requirements 248 | 249 | - Node.js 18.0.0 - 22.x 250 | - Strapi v5.x 251 | - Socket.IO 4.x 252 | 253 | ## Community & Support 254 | 255 | - 📖 [Full Documentation](https://strapi-plugin-io.netlify.app/) 256 | - 🐛 [Report Issues](https://github.com/strapi-community/strapi-plugin-io/issues) 257 | - 💬 [Discussions](https://github.com/strapi-community/strapi-plugin-io/discussions) 258 | - ⭐ [Star on GitHub](https://github.com/strapi-community/strapi-plugin-io) 259 | 260 | ## Next Steps 261 | 262 | - **[View Examples](/examples/)** - Learn common use cases 263 | - **[API Reference](/api/io-class)** - Explore all available methods 264 | - **[Configuration](/api/plugin-config)** - Advanced setup options 265 | - **[Dashboard Widget](/guide/widget)** - Learn about the monitoring widget 266 | - **[Migration Guide](/guide/migration)** - Upgrade from Strapi v4 267 | 268 | --- 269 | 270 | ## Related Plugins 271 | 272 | Check out other powerful Strapi v5 plugins by [@Schero94](https://github.com/Schero94) that complement Socket.IO perfectly: 273 | 274 | ### 📧 Magic-Mail 275 | Enterprise-grade multi-account email management with OAuth 2.0 support. Perfect for sending transactional emails triggered by real-time events. 276 | 277 | **Features**: Gmail/Microsoft/Yahoo OAuth, SendGrid, Mailgun, smart routing, rate limiting 278 | 279 | 🔗 **[View on GitHub](https://github.com/Schero94/Magic-Mail)** | 📦 **[NPM Package](https://www.npmjs.com/package/strapi-plugin-magic-mail-v5)** 280 | 281 | --- 282 | 283 | ### 🔐 Magic-Sessionmanager 284 | Advanced session tracking and monitoring for Strapi v5. Track Socket.IO connections, monitor active users, and analyze session patterns. 285 | 286 | **Features**: Real-time tracking, IP monitoring, active user dashboard, session analytics 287 | 288 | 🔗 **[View on GitHub](https://github.com/Schero94/Magic-Sessionmanager)** 289 | 290 | --- 291 | 292 | ### 🔖 Magicmark 293 | Powerful bookmark management system with real-time sync capabilities. Share bookmarks instantly with your team using Socket.IO integration. 294 | 295 | **Features**: Tag organization, team sharing, real-time sync, full REST API 296 | 297 | 🔗 **[View on GitHub](https://github.com/Schero94/Magicmark)** 298 | 299 | --- 300 | 301 | ## License 302 | 303 | MIT License - see [LICENSE](https://github.com/strapi-community/strapi-plugin-io/blob/master/LICENSE) for details 304 | 305 | --- 306 | 307 | Made with ❤️ by [@ComfortablyCoding](https://github.com/ComfortablyCoding) and [@hrdunn](https://github.com/hrdunn) 308 | 309 | Updated and made it better by: [@Schero94](https://github.com/Schero94) 310 | 311 | **Will be maintained till December 2026** 🚀 312 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | --- 9 | 10 | ## [5.0.0] - 2024-12-21 11 | 12 | ### Breaking Changes 13 | 14 | #### Package Rename 15 | - **BREAKING**: Package renamed from `strapi-plugin-io` to `@strapi-community/plugin-io` 16 | - Migration: Update your `package.json` and run `npm install @strapi-community/plugin-io` 17 | - Configuration stays the same - no code changes needed 18 | - Plugin ID remains `io` for backward compatibility 19 | 20 | #### Version Alignment 21 | - **BREAKING**: Version jumped from 3.0.0 to 5.0.0 to match Strapi v5 22 | - This makes it easier to identify which Strapi version is supported 23 | - v5.x = Strapi v5 24 | - v2.x = Strapi v4 25 | 26 | ### Added 27 | 28 | #### Documentation 29 | - Complete README modernization with better structure 30 | - Added Table of Contents 31 | - Added React and Vue 3 integration examples 32 | - Added TypeScript usage examples 33 | - Enhanced quick start guide 34 | - Added production configuration examples 35 | - Improved migration guide with detailed steps 36 | 37 | #### UI Improvements 38 | - Removed spinner buttons from number inputs for cleaner UI 39 | - Full dark mode support for monitoring page 40 | - Theme-aware components using Strapi's design system 41 | - Improved mobile responsiveness 42 | - Better hover and focus states 43 | 44 | #### Developer Experience 45 | - All components now use Strapi theme system 46 | - Better TypeScript support 47 | - Cleaner code with reduced complexity 48 | - Removed unnecessary CSS (52 lines of redundant styling) 49 | 50 | ### Changed 51 | 52 | #### Repository Migration 53 | - Migrated to `@strapi-community` organization 54 | - Updated all GitHub links to `strapi-community/strapi-plugin-io` 55 | - Updated NPM badges and shields 56 | - New repository URL: https://github.com/strapi-community/strapi-plugin-io 57 | 58 | #### Documentation Updates 59 | - All import paths updated to `@strapi-community/plugin-io` 60 | - Migration guide updated with v5 versioning 61 | - Getting started guide enhanced 62 | - API reference updated 63 | - Examples updated with new package name 64 | 65 | #### Code Quality 66 | - Replaced hardcoded colors with theme variables 67 | - Improved CSS specificity for better dark mode support 68 | - Better component isolation 69 | - Cleaner styled-components implementation 70 | 71 | ### Fixed 72 | 73 | - Fixed dark mode compatibility issues on monitoring page 74 | - Fixed white backgrounds not adapting to dark theme 75 | - Fixed text colors not visible in dark mode 76 | - Fixed select dropdown styling in dark mode 77 | - Fixed number input spinner buttons appearing on desktop 78 | - Fixed border colors not adapting to theme 79 | - Fixed shadow styles for better dark mode appearance 80 | 81 | ### Removed 82 | 83 | - Removed internal `DOCUMENTATION_UPDATE.md` file 84 | - Removed redundant CSS for number input spinners 85 | - Removed hardcoded color values 86 | - Removed unnecessary media queries for spinner buttons 87 | 88 | --- 89 | 90 | ## [3.0.0] - 2024-11-27 91 | 92 | ### Added 93 | 94 | #### Strapi v5 Support 95 | - Full compatibility with Strapi v5 96 | - Updated to use Strapi v5 Plugin SDK 97 | - New build system with optimized bundles 98 | - Both ESM and CJS exports 99 | 100 | #### Entity-Specific Subscriptions 101 | - Subscribe to individual entities for targeted updates 102 | - Client-side: `socket.emit('subscribe-entity', { uid, id })` 103 | - Server-side: `strapi.$io.emitToEntity(uid, id, event, data)` 104 | - Automatic permission checks 105 | - Configurable limits per socket 106 | 107 | #### Enhanced Admin Panel 108 | - Live dashboard widget on admin homepage 109 | - Real-time connection statistics 110 | - Events per second monitoring 111 | - Visual settings panel with tabs 112 | - Monitoring page with connection details 113 | 114 | #### Performance Optimizations 115 | - Intelligent caching (roles cached for 5 minutes) 116 | - 90% reduction in database queries 117 | - Debouncing for bulk operations 118 | - Parallel event processing 119 | - Support for 2500+ concurrent connections 120 | 121 | #### Documentation 122 | - Complete VitePress documentation site 123 | - API reference with TypeScript definitions 124 | - Usage guide with 8 real-world use cases 125 | - Security best practices guide 126 | - Performance optimization guide 127 | - Migration guide from v4 to v5 128 | 129 | #### Helper Functions 130 | - `joinRoom(socketId, roomName)` - Add socket to room 131 | - `leaveRoom(socketId, roomName)` - Remove socket from room 132 | - `getSocketsInRoom(roomName)` - Get all sockets in room 133 | - `sendPrivateMessage(socketId, event, data)` - Send to specific socket 134 | - `broadcast(socketId, event, data)` - Emit to all except sender 135 | - `emitToNamespace(namespace, event, data)` - Emit to namespace 136 | - `disconnectSocket(socketId, reason)` - Disconnect socket 137 | - `emitToEntity(uid, entityId, event, data)` - Emit to entity subscribers 138 | 139 | ### Changed 140 | 141 | #### Build System 142 | - Migrated to `@strapi/sdk-plugin` build tools 143 | - Optimized bundle sizes 144 | - Source maps for debugging 145 | - Modern build targets (Node 18+) 146 | 147 | #### API Improvements 148 | - Better error handling 149 | - Improved TypeScript definitions 150 | - More consistent API naming 151 | - Enhanced logging with structured messages 152 | 153 | #### Configuration 154 | - More flexible content type configuration 155 | - Better validation for settings 156 | - Environment variable support 157 | - Redis adapter configuration 158 | 159 | ### Fixed 160 | 161 | - Fixed permission checks for authenticated users 162 | - Fixed role-based room assignments 163 | - Fixed event emission to specific rooms 164 | - Fixed namespace handling 165 | - Fixed memory leaks in event listeners 166 | - Fixed connection cleanup on disconnect 167 | 168 | --- 169 | 170 | ## [2.0.0] - 2023-06-15 171 | 172 | ### Added 173 | 174 | #### Strapi v4 Support 175 | - Full rewrite for Strapi v4 compatibility 176 | - New admin panel integration 177 | - Updated dependencies 178 | 179 | #### Features 180 | - Basic Socket.IO integration 181 | - Content type event broadcasting 182 | - JWT authentication 183 | - API token support 184 | - Room management 185 | - Custom events 186 | - Namespace support 187 | 188 | #### Documentation 189 | - Basic README 190 | - API documentation 191 | - Usage examples 192 | 193 | ### Changed 194 | - Complete codebase modernization 195 | - Updated to Socket.IO v4 196 | - New configuration format 197 | 198 | ### Fixed 199 | - Various bug fixes from v1 200 | - Performance improvements 201 | - Memory leak fixes 202 | 203 | --- 204 | 205 | ## [1.0.0] - 2021-03-10 206 | 207 | ### Added 208 | - Initial release 209 | - Basic Socket.IO integration for Strapi v3 210 | - Simple event broadcasting 211 | - Basic authentication 212 | 213 | --- 214 | 215 | ## Version Support Matrix 216 | 217 | | Plugin Version | Strapi Version | Node.js | Socket.IO | Status | 218 | |----------------|----------------|---------|-----------|---------| 219 | | **v5.x** | v5.x | 18-22 | 4.8+ | Current | 220 | | v3.x | v5.x | 18-22 | 4.8+ | Deprecated | 221 | | v2.x | v4.x | 14-20 | 4.x | Legacy | 222 | | v1.x | v3.x | 12-14 | 3.x | Unsupported | 223 | 224 | --- 225 | 226 | ## Migration Paths 227 | 228 | ### From v2 (Strapi v4) to v5 (Strapi v5) 229 | 230 | 1. Update Strapi to v5: `npm install @strapi/strapi@5` 231 | 2. Uninstall old plugin: `npm uninstall strapi-plugin-io` 232 | 3. Install new plugin: `npm install @strapi-community/plugin-io@latest` 233 | 4. Restart server: `npm run develop` 234 | 235 | **Configuration compatibility**: 100% - no changes needed! 236 | 237 | See [Migration Guide](./docs/guide/migration.md) for detailed instructions. 238 | 239 | ### From v1 (Strapi v3) to v5 (Strapi v5) 240 | 241 | Not supported - please upgrade to Strapi v4 first, then to v5. 242 | 243 | --- 244 | 245 | ## Deprecation Notices 246 | 247 | ### v3.0.0 Package Name 248 | The package name `strapi-plugin-io` is deprecated. Use `@strapi-community/plugin-io` instead. 249 | 250 | ### Strapi v4 Support 251 | Strapi v4 support ended with version 2.x. Please upgrade to Strapi v5 and use v5.x of this plugin. 252 | 253 | --- 254 | 255 | ## Upcoming Features 256 | 257 | ### Planned for v5.1.0 258 | - [ ] Enhanced rate limiting with Redis backend 259 | - [ ] WebSocket compression support 260 | - [ ] Improved monitoring dashboard 261 | - [ ] Custom event validators 262 | - [ ] Advanced namespace routing 263 | 264 | ### Under Consideration 265 | - GraphQL subscription support 266 | - Message queue integration 267 | - Cluster mode improvements 268 | - Advanced analytics 269 | 270 | --- 271 | 272 | ## Support 273 | 274 | - **Documentation**: https://strapi-plugin-io.netlify.app/ 275 | - **Issues**: https://github.com/strapi-community/strapi-plugin-io/issues 276 | - **Discussions**: https://github.com/strapi-community/strapi-plugin-io/discussions 277 | - **Strapi Discord**: https://discord.strapi.io 278 | 279 | --- 280 | 281 | ## Credits 282 | 283 | **Original Authors:** 284 | - [@ComfortablyCoding](https://github.com/ComfortablyCoding) 285 | - [@hrdunn](https://github.com/hrdunn) 286 | 287 | **v5 Migration & Enhancements:** 288 | - [@Schero94](https://github.com/Schero94) 289 | 290 | **Maintained by:** Strapi Community 291 | 292 | **Maintained until:** December 2026 293 | 294 | --- 295 | 296 | ## License 297 | 298 | [MIT License](./LICENSE) 299 | 300 | --- 301 | 302 | *For older versions and legacy documentation, see [GitHub Releases](https://github.com/strapi-community/strapi-plugin-io/releases)* 303 | 304 | --------------------------------------------------------------------------------