├── .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 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------