├── .dockerignore ├── .gitignore ├── .prettierignore ├── .prettierrc.yaml ├── .prettierrc.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── backend ├── esbuild.js ├── media │ ├── logo-red.png │ └── logo.png ├── package.json ├── src │ ├── actions │ │ └── actions.js │ ├── broker │ │ └── BrokerManager.js │ ├── client │ │ ├── BaseMosquittoClient.js │ │ └── NodeMosquittoClient.js │ ├── config │ │ └── ConfigManager.js │ ├── errors │ │ └── NotAuthorizedError.js │ ├── http │ │ └── HTTPClient.js │ ├── license │ │ ├── License.js │ │ ├── LicenseChecker.js │ │ ├── LicenseContainer.js │ │ ├── LicenseKey.js │ │ ├── index.js │ │ └── utils │ │ │ ├── config │ │ │ └── public.pem │ │ │ ├── index.js │ │ │ ├── loadLicense.js │ │ │ └── readFile.js │ ├── middleware │ │ └── ContentTypeParser.js │ ├── plugins │ │ ├── BasePlugin.js │ │ ├── Errors.js │ │ ├── PluginManager.js │ │ ├── connect-disconnect │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Plugin.js │ │ │ │ ├── actions.js │ │ │ │ └── meta.json │ │ │ └── yarn.lock │ │ ├── login │ │ │ ├── component │ │ │ │ ├── images │ │ │ │ │ ├── cedalo.png │ │ │ │ │ ├── favicon-32x32.png │ │ │ │ │ ├── logo-red.png │ │ │ │ │ └── logo.png │ │ │ │ └── login.html │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Plugin.js │ │ │ │ ├── actions.js │ │ │ │ ├── meta.json │ │ │ │ └── swagger.js │ │ │ └── yarn.lock │ │ └── user-profile │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── Plugin.js │ │ │ ├── actions.js │ │ │ ├── meta.json │ │ │ └── swagger.js │ │ │ └── yarn.lock │ ├── security │ │ └── acl.js │ ├── settings │ │ └── SettingsManager.js │ ├── topictree │ │ └── TopicTreeManager.js │ ├── usage │ │ ├── InstallationManager.js │ │ └── UsageTracker.js │ └── utils │ │ ├── ConsumersRegistry.js │ │ ├── Logger.js │ │ ├── QueuedEmitter2.js │ │ ├── localhost.js │ │ ├── sessions.js │ │ ├── utils.js │ │ └── version.js ├── start.js ├── swagger.js ├── tests │ └── client.js └── views │ └── customError.ejs ├── build.sh ├── config └── config.json ├── docker-compose.yml ├── docker ├── config.json └── docker-entrypoint.sh ├── frontend ├── .gitignore ├── README.md ├── base │ ├── _BaseMosquittoProxyClient.js │ ├── main.js │ └── package.json ├── package.json ├── public │ ├── clients.png │ ├── disconnected.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── groups.png │ ├── index.html │ ├── inprogress.png │ ├── integration-logos │ │ ├── .DS_Store │ │ ├── alloydb.webp │ │ ├── cockroachdb.png │ │ ├── googlecloud.png │ │ ├── influxdb.png │ │ ├── jwt.svg │ │ ├── kafka.png │ │ ├── kubernetes.png │ │ ├── ldap.svg │ │ ├── mariadb.svg │ │ ├── mongodb.png │ │ ├── mongodbatlas.png │ │ ├── mssql.webp │ │ ├── mysql.svg │ │ ├── openshift.svg │ │ ├── oracledb.svg │ │ ├── postgres.png │ │ ├── prometheus.png │ │ ├── redshift.svg │ │ ├── snowflake.svg │ │ └── timescaledb.svg │ ├── manifest.json │ ├── onboarding-broker.png │ ├── onboarding-dashboard.png │ ├── onboarding-dynamic-security.png │ ├── onboarding-newsletter.png │ ├── onboarding-topic-tree.png │ ├── roles.png │ ├── security.png │ ├── settings.png │ ├── smilethink.png │ ├── status.png │ ├── streams.png │ ├── system.png │ └── topictree.png ├── src │ ├── App.js │ ├── AppRoutes.js │ ├── actions │ │ └── actions.js │ ├── admin │ │ ├── certificates │ │ │ └── components │ │ │ │ ├── AlertHint.js │ │ │ │ ├── CertificateDeploy.js │ │ │ │ ├── CertificateDetail.js │ │ │ │ ├── CertificateInfo.js │ │ │ │ ├── Certificates.js │ │ │ │ ├── ChipsList.js │ │ │ │ ├── ContentTable.js │ │ │ │ ├── UploadButton.js │ │ │ │ └── certutils.js │ │ ├── clusters │ │ │ ├── actions │ │ │ │ ├── ActionTypes.js │ │ │ │ └── actions.js │ │ │ ├── components │ │ │ │ ├── ClusterDetail.js │ │ │ │ ├── ClusterNew.js │ │ │ │ ├── Clusters.js │ │ │ │ ├── SelectNodeComponent.js │ │ │ │ ├── SelectNodeDialog.js │ │ │ │ └── clusterutils.js │ │ │ ├── reducers │ │ │ │ └── clustersReducer.js │ │ │ ├── utils.js │ │ │ └── validators.js │ │ ├── connections │ │ │ ├── components │ │ │ │ ├── ConnectionNew.js │ │ │ │ └── Connections.js │ │ │ └── utils.js │ │ ├── inspect │ │ │ ├── actions │ │ │ │ ├── ActionTypes.js │ │ │ │ └── actions.js │ │ │ ├── components │ │ │ │ └── InspectClients.js │ │ │ └── reducers │ │ │ │ └── inspectClientsReducer.js │ │ └── users │ │ │ ├── actions │ │ │ ├── ActionTypes.js │ │ │ └── actions.js │ │ │ ├── components │ │ │ ├── UserDetail.js │ │ │ ├── UserNew.js │ │ │ └── Users.js │ │ │ └── reducers │ │ │ ├── userGroupsReducer.js │ │ │ ├── userRolesReducer.js │ │ │ └── usersReducer.js │ ├── client │ │ ├── BaseMosquittoProxyClient.js │ │ ├── Constants.js │ │ ├── NodeMosquittoProxyClient.js │ │ └── WebMosquittoProxyClient.js │ ├── components │ │ ├── ACLTypesHelpDialog.js │ │ ├── AnonymousGroupSelect.js │ │ ├── ApplicationTokens.js │ │ ├── AutoSuggest.js │ │ ├── BrokerSelect.js │ │ ├── BrokerStatusIcon.js │ │ ├── ButtonWithLoadingProgress.js │ │ ├── Chart.js │ │ ├── ClientDetail.js │ │ ├── ClientNew.js │ │ ├── Clients.js │ │ ├── Configurations.js │ │ ├── ConnectedWarning.js │ │ ├── ConnectionDetail.js │ │ ├── ConnectionDetailComponent.js │ │ ├── ConnectionNewComponent.js │ │ ├── ContainerBox.js │ │ ├── ContainerBreadCrumbs.js │ │ ├── ContainerHeader.js │ │ ├── ContentContainer.js │ │ ├── CustomDrawer.js │ │ ├── DefaultACLAccess.js │ │ ├── DisconnectedDialog.js │ │ ├── FeedbackButton.js │ │ ├── FilterName.js │ │ ├── GroupDetail.js │ │ ├── GroupNew.js │ │ ├── Groups.js │ │ ├── HelpButtons.js │ │ ├── Info.js │ │ ├── InfoButton.js │ │ ├── InfoPage.js │ │ ├── Integrations.js │ │ ├── LicenseErrorDialog.js │ │ ├── LicenseTable.js │ │ ├── Logo.js │ │ ├── LogoutButton.js │ │ ├── MessagePage.js │ │ ├── NewsCard.js │ │ ├── NewsletterPopup.js │ │ ├── OnBoardingDialog.js │ │ ├── Plugins.js │ │ ├── PremiumFeatureDialog.js │ │ ├── ProfileButton.js │ │ ├── RoleDetail.js │ │ ├── RoleNew.js │ │ ├── Roles.js │ │ ├── SaveCancelButtons.js │ │ ├── SelectList.js │ │ ├── Settings.js │ │ ├── SnackbarCloseButton.js │ │ ├── SortableTablePage.js │ │ ├── Status.js │ │ ├── StreamDetail.js │ │ ├── StreamNew.js │ │ ├── Streams.js │ │ ├── Streamsheets.js │ │ ├── StyledTypography.js │ │ ├── Terminal.js │ │ ├── TestCollection.js │ │ ├── TestCollectionDetail.js │ │ ├── TestCollections.js │ │ ├── TestEdit.js │ │ ├── TopicTree.js │ │ ├── UpgradeButton.js │ │ ├── UserGroupDetail.js │ │ ├── UserGroupNew.js │ │ ├── UserGroups.js │ │ ├── UserProfile.js │ │ ├── WaitDialog.js │ │ ├── jsoneditor-fix.css │ │ └── streams │ │ │ └── ReplayStreamDialog.js │ ├── constants │ │ └── ActionTypes.js │ ├── helpers │ │ ├── useConfirmDialog.js │ │ ├── useFetch.js │ │ ├── useLocalStorage.js │ │ └── utils.js │ ├── index.js │ ├── reducers │ │ ├── applicationTokensReducer.js │ │ ├── backendParametersReducer.js │ │ ├── brokerConfigurationsReducer.js │ │ ├── brokerConnectionsReducer.js │ │ ├── brokerLicenseReducer.js │ │ ├── clientsReducer.js │ │ ├── groupsReducer.js │ │ ├── licenseReducer.js │ │ ├── loadingReducer.js │ │ ├── proxyConnectionReducer.js │ │ ├── rolesReducer.js │ │ ├── settingsReducer.js │ │ ├── streamsReducer.js │ │ ├── systemStatusReducer.js │ │ ├── testsReducer.js │ │ ├── topicTreeReducer.js │ │ ├── userProfileReducer.js │ │ └── versionsReducer.js │ ├── store.js │ ├── styles.js │ ├── theme-dark.js │ ├── theme.js │ ├── tutorial │ │ ├── admintour.js │ │ └── apptour.js │ ├── utils │ │ ├── Delayed.js │ │ ├── accessUtils │ │ │ └── access.js │ │ ├── connectionUtils │ │ │ └── connections.js │ │ └── utils.js │ └── websockets │ │ ├── WebSocket.js │ │ └── config.js ├── start.js └── tests │ ├── client.example.js │ ├── client.test.js │ └── multiple-client.example.js ├── package.json ├── start.sh ├── tests ├── conf │ └── mosquitto.conf ├── config │ └── mosquitto.conf ├── docker-compose.yml ├── mosquitto-mock-api │ ├── start-broker.js │ └── start-mock-api.js ├── start-broker.sh ├── start-brokers.sh └── utils │ └── send-messages.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | node_modules 3 | frontend/src 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | public-with-basepath 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | releases 110 | /config/management-center/* 111 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: true 2 | singleQuote: true 3 | tabWidth: 4 4 | trailingComma: es5 5 | printWidth: 120 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: 'none' 2 | tabWidth: 4 3 | semi: true 4 | singleQuote: true 5 | useTabs: true 6 | arrowParens: 'always' 7 | printWidth: 120 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | LABEL maintainer="philip.ackermann@cedalo.com" 3 | 4 | ARG CEDALO_MC_BUILD_DATE 5 | ENV CEDALO_MC_BUILD_DATE=${CEDALO_MC_BUILD_DATE} 6 | ARG CEDALO_MC_BUILD_NUMBER 7 | ENV CEDALO_MC_BUILD_NUMBER=${CEDALO_MC_BUILD_NUMBER} 8 | ENV CEDALO_MC_PROXY_CONFIG=/management-center/config/config.json 9 | ENV CEDALO_MC_PROXY_HOST=0.0.0.0 10 | ARG CEDALO_MC_PROXY_BASE_PATH 11 | ENV CEDALO_MC_PROXY_BASE_PATH=${CEDALO_MC_PROXY_BASE_PATH} 12 | 13 | WORKDIR /management-center 14 | 15 | COPY backend backend 16 | RUN cd backend && yarn install --prod --frozen-lockfile 17 | 18 | COPY frontend/build backend/public 19 | COPY docker/config.json ./config/ 20 | COPY docker/docker-entrypoint.sh backend 21 | 22 | WORKDIR /management-center/backend 23 | 24 | VOLUME /management-center/config 25 | 26 | EXPOSE 8088 27 | 28 | CMD [ "sh", "docker-entrypoint.sh" ] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Management Center 2 | 3 | The management center for Eclipse Mosquitto. See the [official documentation](https://docs.cedalo.com/) for additional details. 4 | 5 |
6 | 7 | ## Run in development mode 8 | 9 | Either go to [official documentation page](https://docs.cedalo.com/management-center/installation) and follow steps there or clone this repository and then: 10 | 11 | - Install `Docker` and `docker-compose` 12 | - Install `yarn` package manager 13 | 14 | - Run the Mosquitto broker: 15 | 16 | - Create `mosquitto` directory 17 | - Go inside this directory, create `config` and `data` directories 18 | - Go inside `config` directory and create config file `mosquitto.conf` 19 | - You can find an example of such file [here](https://github.com/eclipse/mosquitto/blob/master/mosquitto.conf). Be sure to uncomment or add the following lines to this file: 20 | 21 | ``` 22 | listener 1883 23 | allow_anonymous true 24 | ``` 25 | 26 | - Inside the `mosquitto` directory create a `docker-compose.yaml` file with the following content: 27 | 28 | ``` 29 | version: '3.8' 30 | 31 | services: 32 | mosquitto: 33 | image: eclipse-mosquitto:2 34 | ports: 35 | - 127.0.0.1:1883:1883 36 | - 127.0.0.1:8080:8080 37 | - 8883:8883 38 | volumes: 39 | - ./mosquitto/config:/mosquitto/config 40 | - ./mosquitto/data:/mosquitto/data 41 | networks: 42 | - mosquitto 43 | networks: 44 | mosquitto: 45 | name: mosquitto 46 | driver: bridge 47 | ``` 48 | 49 | - Inside `mosquitto` directory run the following command to start the broker: 50 | ``` 51 | docker-compose up 52 | ``` 53 | 54 | - Now, when Mosquitto broker is installed, go to the root directory of the Management Center and run: 55 | 56 | ``` 57 | yarn install 58 | ``` 59 | 60 | - Go to the `/frontend` folder and run: 61 | 62 | ``` 63 | yarn run build-without-base-path 64 | ``` 65 | 66 | - Set the following environmental variables (Note that `export` command works for Unix. For Windows use `set`): 67 | 68 | ``` 69 | export CEDALO_MC_BROKER_ID="mosquitto" \ 70 | export CEDALO_MC_BROKER_NAME="Mosquitto" \ 71 | export CEDALO_MC_BROKER_URL="mqtt://localhost:1883" \ 72 | export CEDALO_MC_BROKER_USERNAME="" \ 73 | export CEDALO_MC_BROKER_PASSWORD="" \ 74 | export CEDALO_MC_USERNAME="cedalo" \ 75 | export CEDALO_MC_PASSWORD="tests" \ 76 | export CEDALO_MC_PROXY_HOST="localhost" \ 77 | ``` 78 | 79 | - Go to the `backend` directory and run: 80 | 81 | ``` 82 | yarn start 83 | ``` 84 | 85 | - Go to `http://localhost:8088` to start working with the Management Center for Eclipse Mosquitto 86 | -------------------------------------------------------------------------------- /backend/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const indexHTML = fs.readFileSync(path.join(__dirname, 'public/index.html')).toString(); 6 | 7 | const ENTRYPOINT = path.relative('', path.join(__dirname, '../frontend/src/index.js')); 8 | 9 | const plugins = [ 10 | { 11 | name: 'my-plugin', 12 | setup(build) { 13 | build.onEnd((result) => { 14 | // console.log(Object.entries(result.metafile.outputs)); 15 | if (result.metafile && result.metafile.outputs) { 16 | const [mainBundle, { cssBundle }] = Object.entries(result.metafile.outputs).find( 17 | ([k, v]) => v.entryPoint === ENTRYPOINT.split(path.sep).join(path.posix.sep) 18 | ); 19 | // fs.writeFileSync('metafile.json', JSON.stringify(result.metafile)); 20 | 21 | if (result.errors.length === 0) { 22 | const updatedIndexHtml = indexHTML 23 | .replace( 24 | /` 29 | ) 30 | .replace( 31 | /` 36 | ); 37 | fs.writeFile(path.join(__dirname, 'public/index.html'), updatedIndexHtml, () => {}); 38 | } 39 | } 40 | }); 41 | }, 42 | }, 43 | ]; 44 | 45 | const define = { 46 | 'process.env.PUBLIC_URL': process.env.PUBLIC_URL || '""', 47 | 'process.env.CEDALO_MC_BROKER_CONNECTION_HOST_MAPPING': 48 | `"${process.env.CEDALO_MC_BROKER_CONNECTION_HOST_MAPPING}"` || '""', 49 | 'process.env.CEDALO_MC_BROKER_CONNECTION_MQTT_EXISTS_MAPPING': 50 | `"${process.env.CEDALO_MC_BROKER_CONNECTION_MQTT_EXISTS_MAPPING}"` || '""', 51 | 'process.env.CEDALO_MC_BROKER_CONNECTION_MQTTS_EXISTS_MAPPING': 52 | `"${process.env.CEDALO_MC_BROKER_CONNECTION_MQTTS_EXISTS_MAPPING}"` || '""', 53 | 'process.env.CEDALO_MC_BROKER_CONNECTION_WS_EXISTS_MAPPING': 54 | `"${process.env.CEDALO_MC_BROKER_CONNECTION_WS_EXISTS_MAPPING}"` || '""', 55 | 'process.env.MOSQUITTO_PROXY_URL': process.env.MOSQUITTO_PROXY_URL || '""', 56 | 'process.env.CEDALO_MC_DEV_CLUSTER_NODE_BROKER_PREFIX': process.env.CEDALO_MC_DEV_CLUSTER_NODE_BROKER_PREFIX 57 | ? `"${process.env.CEDALO_MC_DEV_CLUSTER_NODE_BROKER_PREFIX}"` 58 | : '""', 59 | 'process.env.CEDALO_MC_DEV_CLUSTER_NODE_ADDRESS_PREFIX': process.env.CEDALO_MC_DEV_CLUSTER_NODE_ADDRESS_PREFIX 60 | ? `"${process.env.CEDALO_MC_DEV_CLUSTER_NODE_ADDRESS_PREFIX}"` 61 | : '""', 62 | }; 63 | 64 | const run = async () => { 65 | const context = await esbuild.context({ 66 | entryPoints: [ENTRYPOINT], 67 | sourcemap: true, 68 | outdir: path.join(__dirname, 'public'), 69 | bundle: true, 70 | minify: true, 71 | entryNames: 'static/[ext]/main', 72 | assetNames: 'static/media/[name]', 73 | chunkNames: 'static/[ext]/[name]', 74 | target: ['chrome96', 'firefox96'], 75 | plugins, 76 | loader: { 77 | '.svg': 'file', 78 | '.js': 'jsx', 79 | }, 80 | define, 81 | metafile: true, 82 | // keepNames: true, 83 | }); 84 | context.watch(); 85 | }; 86 | 87 | run(); 88 | -------------------------------------------------------------------------------- /backend/media/logo-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/backend/media/logo-red.png -------------------------------------------------------------------------------- /backend/media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/backend/media/logo.png -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cedalo/management-center-backend", 3 | "version": "2.9.5", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node start.js", 9 | "start:dev": "node -r ./esbuild.js" 10 | }, 11 | "author": "Cedalo AG", 12 | "dependencies": { 13 | "async-mutex": "^0.4.0", 14 | "axios": "^0.27.2", 15 | "content-type-parser": "^1.0.2", 16 | "cors": "^2.8.5", 17 | "ejs": "^3.1.8", 18 | "eventemitter2": "^6.4.9", 19 | "express": "^4.18.1", 20 | "express-session": "^1.17.3", 21 | "jsonwebtoken": "^9.0.0", 22 | "lowdb": "^1.0.0", 23 | "mqtt": "^4.3.7", 24 | "node-cron": "^2.0.3", 25 | "passport": "^0.6.0", 26 | "passport-local": "^1.0.0", 27 | "swagger-ui-express": "^3.0.10", 28 | "uuid": "^8.3.2", 29 | "winston": "^3.6.0", 30 | "ws": "^8.6.0" 31 | }, 32 | "devDependencies": { 33 | "esbuild": "^0.17.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/broker/BrokerManager.js: -------------------------------------------------------------------------------- 1 | module.exports = class BrokerManager { 2 | constructor() { 3 | // TODO: merge _connection and _brokerConnection 4 | this._connection = null; 5 | this._brokerConnection = null; 6 | this._clientConnections = new Map(); 7 | } 8 | 9 | handleNewBrokerConnection(connection, brokerClient, system, topicTreeManager, proxyClient) { 10 | this._brokerClient = brokerClient; 11 | this._connection = connection; 12 | this._brokerConnection = { 13 | connection, 14 | name: connection.name, 15 | broker: brokerClient, 16 | system, 17 | topicTreeManager, 18 | proxyClient, 19 | }; 20 | } 21 | 22 | handleDeleteBrokerConnection(connection) { 23 | this._brokerClient = null; 24 | this._brokerConnection = null; 25 | this._connection = null; 26 | this._clientConnections = new Map(); 27 | } 28 | 29 | handleNewClientWebSocketConnection(ws) { 30 | this._clientConnections.set(ws, ws); 31 | } 32 | 33 | handleCloseClientWebSocketConnection(ws) { 34 | this._clientConnections.delete(ws); 35 | } 36 | 37 | getBrokerConnection(brokerName) { 38 | return this._brokerConnection; 39 | } 40 | 41 | getBrokerConnectionById(brokerId) { 42 | return this._brokerConnection; 43 | } 44 | 45 | getBrokerConnections() { 46 | return this._connection ? [this._connection] : []; 47 | } 48 | 49 | connectClient(client, broker) {} 50 | 51 | disconnectClient(client) {} 52 | 53 | getBroker(client) { 54 | return this._brokerClient; 55 | } 56 | 57 | getBrokerConnectionByClient(client) { 58 | return this._connection; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /backend/src/errors/NotAuthorizedError.js: -------------------------------------------------------------------------------- 1 | module.exports = class NotAuthorizedError extends Error { 2 | constructor() { 3 | super(`You don't have enough user rights to perform this operation`); 4 | this.name = 'NotAuthorizedError'; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /backend/src/http/HTTPClient.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const urlModule = require('url'); 3 | const axios = require('axios'); 4 | 5 | const version = require('../utils/version'); 6 | 7 | const CONFIG = {}; 8 | 9 | class HTTPClient { 10 | constructor(config) { 11 | this._init(config); 12 | } 13 | 14 | _init(config) {} 15 | 16 | _checkConfig(config) { 17 | if (!config.headers) { 18 | config.headers = {}; 19 | } 20 | if (!config.headers['User-Agent']) { 21 | config.headers['User-Agent'] = `${version.name}/${version.version} (${os.platform()} ${os.release()})`; 22 | } 23 | return config; 24 | } 25 | 26 | async request(body, headers, config = {}) { 27 | config.data = body; 28 | config.headers = headers; 29 | config = this._checkConfig(config); 30 | return axios.request(config); 31 | } 32 | 33 | async get(url, headers, config = {}) { 34 | config.headers = headers; 35 | config = this._checkConfig(config); 36 | return axios.get(url, config); 37 | } 38 | 39 | async delete(url, headers, config = {}) { 40 | config.headers = headers; 41 | config = this._checkConfig(config); 42 | return axios.delete(url, config); 43 | } 44 | 45 | async head(url, headers, config = {}) { 46 | config.headers = headers; 47 | config = this._checkConfig(config); 48 | return axios.head(url, config); 49 | } 50 | 51 | async options(url, headers, config = {}) { 52 | config.headers = headers; 53 | config = this._checkConfig(config); 54 | return axios.options(url, config); 55 | } 56 | 57 | async post(url, body, headers = {}, config = {}) { 58 | config.headers = headers; 59 | config = this._checkConfig(config); 60 | if (headers['Content-Type'] && headers['Content-Type'] === 'application/x-www-form-urlencoded') { 61 | const formParams = { 62 | ...body, 63 | }; 64 | try { 65 | const params = new urlModule.URLSearchParams(formParams); 66 | return axios.post(url, params.toString(), config); 67 | } catch (error) { 68 | console.error(error); 69 | } 70 | } 71 | return axios.post(url, body, config); 72 | } 73 | 74 | async put(url, body, headers, config = {}) { 75 | config.headers = headers; 76 | config = this._checkConfig(config); 77 | return axios.put(url, body, config); 78 | } 79 | 80 | async patch(url, body, headers, config = {}) { 81 | config.headers = headers; 82 | config = this._checkConfig(config); 83 | return axios.patch(url, body, config); 84 | } 85 | } 86 | 87 | let instance; 88 | const getInstance = () => { 89 | if (!instance) { 90 | instance = new HTTPClient(CONFIG); 91 | } 92 | return instance; 93 | }; 94 | 95 | module.exports = { 96 | getInstance, 97 | }; 98 | -------------------------------------------------------------------------------- /backend/src/license/License.js: -------------------------------------------------------------------------------- 1 | class License { 2 | static _setup(license, json) { 3 | return Object.entries(json).reduce((lic, [key, value]) => { 4 | if (value != null) lic[key] = value; 5 | return lic; 6 | }, license); 7 | } 8 | 9 | static _validate(license) { 10 | return Object.entries(license).reduce( 11 | (err, [key, value]) => err || (value == null ? `${key} not specified!` : undefined), 12 | undefined 13 | ); 14 | } 15 | 16 | static from(json = {}) { 17 | // const license = Object.assign(new License(), DEF, json); 18 | const license = License._setup(new License(), json); 19 | const error = License._validate(license); 20 | if (error) throw Error(error); 21 | return Object.freeze(license); 22 | } 23 | 24 | constructor() { 25 | this.edition = 'Personal'; 26 | this.issuedBy = 'Cedalo AG'; 27 | this.issuedTo = undefined; 28 | this.maxInstallations = -1; 29 | this.maxBrokerConnections = 1; 30 | this.validSince = Date.now(); 31 | this.validUntil = undefined; 32 | } 33 | 34 | toJSON() { 35 | return Object.assign({}, this); 36 | } 37 | 38 | get isValid() { 39 | const now = Date.now(); 40 | return this.validSince <= now && now <= this.validUntil; 41 | } 42 | 43 | // expiresIn() { 44 | // return this.validUntil - Date.now(); 45 | // } 46 | } 47 | 48 | class InvalidLicense extends License { 49 | constructor() { 50 | super(); 51 | this.validUntil = Date.now(); 52 | } 53 | 54 | get isValid() { 55 | return false; 56 | } 57 | } 58 | 59 | License.Invalid = Object.freeze(new InvalidLicense()); 60 | 61 | module.exports = License; 62 | -------------------------------------------------------------------------------- /backend/src/license/LicenseChecker.js: -------------------------------------------------------------------------------- 1 | const cron = require('node-cron'); 2 | const loadLicense = require('./utils/loadLicense'); 3 | 4 | const noop = () => {}; 5 | const pipe = 6 | (...fns) => 7 | (val) => 8 | fns.reduce((p, fn) => p.then(fn), Promise.resolve(val)); 9 | 10 | const toDate = (ms) => new Date(ms); 11 | const toCronTab = (date) => `${date.getMinutes()} ${date.getHours()} * * *`; 12 | const msToCronTab = pipe(toDate, toCronTab); 13 | const valideCronTab = (crontab) => { 14 | if (crontab && !cron.validate(crontab)) { 15 | crontab = undefined; 16 | } 17 | return crontab; 18 | }; 19 | 20 | // either run crontab job as per the passed crontab variable if it was passed and valid or take the license's valid until timestamp and run job then 21 | const getCronTab = async (crontab, license) => valideCronTab(crontab) || msToCronTab(license.validUntil); 22 | 23 | const notify = (cb) => (license) => { 24 | cb(null, license); 25 | return license; 26 | }; 27 | 28 | class LicenseChecker { 29 | constructor() { 30 | this._task = undefined; 31 | } 32 | 33 | async check(cb = noop) { 34 | this.checkLicense(cb)(); 35 | } 36 | 37 | checkLicense(cb) { 38 | return pipe(loadLicense, notify(cb)); 39 | } 40 | 41 | async schedule(cb = noop) { 42 | this.scheduleEvery('', cb); 43 | } 44 | 45 | async scheduleEvery(crontab = '', cb = noop) { 46 | if (!this._task) { 47 | const check = this.checkLicense(cb); 48 | const license = await check(); 49 | const every = await getCronTab(crontab, license); 50 | this._task = cron.schedule(every, check); 51 | } 52 | } 53 | 54 | stop() { 55 | if (this._task) { 56 | this._task.stop(); 57 | this._task = undefined; 58 | } 59 | } 60 | } 61 | 62 | // new LicenseChecker().check((license) => console.log(`License is valid: ${license.isValid}`)); 63 | 64 | module.exports = LicenseChecker; 65 | -------------------------------------------------------------------------------- /backend/src/license/LicenseContainer.js: -------------------------------------------------------------------------------- 1 | const devFeatures = (() => { 2 | try { 3 | const features = require(process.env.CEDALO_MC_DEV_FEATURES_FILE); 4 | return typeof features === 'object' && Object.keys(features).length === 0 ? [] : features; 5 | } catch (_) { 6 | /* ignore */ 7 | } 8 | return []; 9 | })(); 10 | 11 | // DEV PURPOSE ONLY 12 | class LicenseContainer { 13 | constructor() { 14 | this._license; 15 | } 16 | get license() { 17 | return this._license; 18 | } 19 | set license(license) { 20 | license?.features?.splice(license.features.length, 0, ...devFeatures); 21 | this._license = license; 22 | } 23 | } 24 | const licenseContainer = new LicenseContainer(); 25 | module.exports = licenseContainer; 26 | -------------------------------------------------------------------------------- /backend/src/license/LicenseKey.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const License = require('./License'); 3 | const readFile = require('./utils/readFile'); 4 | 5 | const generate = (license, privateKey) => 6 | new Promise((resolve, reject) => { 7 | if (!(license instanceof License)) reject(new Error('Invalid license object!')); 8 | jwt.sign(license.toJSON(), privateKey, { algorithm: 'RS256' }, (err, token) => { 9 | if (err) reject(err); 10 | else resolve(token); 11 | }); 12 | }); 13 | // TODO rename: 14 | const generateWithKeyFile = (license, filename) => 15 | readFile(filename).then((privateKey) => generate(license, privateKey)); 16 | 17 | const verify = (licenseKey, publicKey) => 18 | new Promise((resolve, reject) => { 19 | jwt.verify(licenseKey, publicKey, { algorithm: 'RS256' }, (err, json) => 20 | err ? reject(err) : resolve(License.from(json)) 21 | ); 22 | }); 23 | // TODO rename: 24 | const verifyLicenseKeyFile = async (licenseFile, keyFile) => { 25 | const publicKey = await readFile(keyFile); 26 | const licenseKey = await readFile(licenseFile); 27 | return verify(licenseKey.toString(), publicKey); 28 | }; 29 | 30 | module.exports = { 31 | generate, 32 | generateWithKeyFile, 33 | verify, 34 | verifyLicenseKeyFile, 35 | }; 36 | -------------------------------------------------------------------------------- /backend/src/license/index.js: -------------------------------------------------------------------------------- 1 | const License = require('./License'); 2 | const LicenseChecker = require('./LicenseChecker'); 3 | const LicenseKey = require('./LicenseKey'); 4 | 5 | module.exports = { 6 | License, 7 | LicenseChecker, 8 | LicenseKey, 9 | }; 10 | -------------------------------------------------------------------------------- /backend/src/license/utils/config/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzkhcHyHwrE1Kx+vInOrQ 3 | okC5KoQAz4hiUl1UzTCtb45wkFAyGwEAu8soHCsMW8EUUE5K+JPSARZVemyi4qns 4 | hSp6QcbV8AbNTZl1Me+N5yQKvu/eNtJazlTu8PpVHmXNnayY5RkyFtz4idyNvhkV 5 | P1Sg4i2VmK+gr+QeCccuQ89zWlL2POL8d+2mB+0tp0UJ05vkrrGbQ+2UhcHYh2TW 6 | uGkpVThis8uY9MiDXHl0k1mGoBQfSKL/qIbcbwp9Nz0RsY5HIGQoG2MOFrVGbcKJ 7 | DENnM9W2aB7SJ4IEgdKrcQH8mdr30FqN5jYS6a9Nq6JhvWa6uX5qcITBJ+Fd+Imf 8 | YwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /backend/src/license/utils/index.js: -------------------------------------------------------------------------------- 1 | const loadLicense = require('./loadLicense'); 2 | const readFile = require('./readFile'); 3 | 4 | module.exports = { 5 | loadLicense, 6 | readFile, 7 | }; 8 | -------------------------------------------------------------------------------- /backend/src/license/utils/loadLicense.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const License = require('../License'); 4 | const LicenseKey = require('../LicenseKey'); 5 | const readFile = require('./readFile'); 6 | 7 | let loaded = false; 8 | 9 | const loadPublicKey = () => 10 | readFile(process.env.CEDALO_MC_LICENSE_PUBLIC_KEY_PATH || path.join(__dirname, 'config', 'public.pem')); 11 | const loadLicenseKey = () => { 12 | if (process.env.CEDALO_LICENSE_KEY) { 13 | const licenseString = process.env.CEDALO_LICENSE_KEY; 14 | return Promise.resolve(licenseString.trim()); 15 | } else { 16 | const licensePath = 17 | process.env.CEDALO_LICENSE_FILE || 18 | process.env.CEDALO_MC_LICENSE_PATH || 19 | path.join(__dirname, 'config', 'license.lic'); 20 | if (!loaded) { 21 | console.log(`Loading license from ${licensePath} ...`); 22 | } 23 | return readFile(licensePath) 24 | .then((key) => key.toString().trim()) 25 | .catch((error) => { 26 | console.log('No license key found or provided.'); 27 | throw error; 28 | }) 29 | .finally(() => { 30 | loaded = true; 31 | }); 32 | } 33 | }; 34 | 35 | const loadLicense = async () => { 36 | let license; 37 | try { 38 | const publicKey = await loadPublicKey(); 39 | const licenseKey = await loadLicenseKey(); 40 | license = await LicenseKey.verify(licenseKey, publicKey); 41 | } catch (error) { 42 | console.error(error); 43 | license = License.Invalid; 44 | } 45 | return license; 46 | }; 47 | 48 | module.exports = loadLicense; 49 | -------------------------------------------------------------------------------- /backend/src/license/utils/readFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | // until we can finally use: const fs = require('fs').promises; 3 | 4 | const read = (file) => 5 | new Promise((resolve, reject) => fs.readFile(file, (err, data) => (err ? reject(err) : resolve(data)))); 6 | 7 | const fileExists = (filename = '') => 8 | new Promise((resolve, reject) => { 9 | if (!filename) reject(new Error('Please specify a file name to load!')); 10 | fs.access(filename, (err) => (err ? reject(err) : resolve(filename))); 11 | }); 12 | 13 | module.exports = (file) => fileExists(file).then(read); 14 | -------------------------------------------------------------------------------- /backend/src/middleware/ContentTypeParser.js: -------------------------------------------------------------------------------- 1 | const parse = require('content-type-parser'); 2 | 3 | module.exports = function contentTypeParser(request, response, next) { 4 | const contentType = parse(request?.headers?.accept); 5 | if (request) { 6 | request.contentType = contentType; 7 | } 8 | if (next) { 9 | next(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/plugins/BasePlugin.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const winston = require('winston'); 3 | 4 | const STATUS_ERROR = 'error'; 5 | const STATUS_UNLOADED = 'unloaded'; 6 | const STATUS_LOADED = 'loaded'; 7 | 8 | const LOG_DIR = process.env.CEDALO_MC_LOG_DIR || ''; 9 | 10 | module.exports = class BasePlugin { 11 | constructor(meta, options) { 12 | this._status = { 13 | type: STATUS_UNLOADED, 14 | }; 15 | const logger = winston.createLogger({ 16 | level: 'info', 17 | format: winston.format.json(), 18 | defaultMeta: { service: meta?.name }, 19 | transports: [new winston.transports.File({ filename: path.join(LOG_DIR, `plugin-${meta?.id}.log`) })], 20 | }); 21 | this._meta = meta; 22 | this._logger = logger; 23 | this._swagger = {}; 24 | this.options = {}; 25 | 26 | if (!options) { 27 | return; 28 | } else if (!(typeof option === 'object' && option !== null)) { 29 | throw new Error('options argument passed to BasePlugin is not of type "Object"'); 30 | } 31 | for (const option in options) { 32 | this.options[option] = options[option]; 33 | } 34 | } 35 | 36 | get logger() { 37 | return this._logger; 38 | } 39 | 40 | get meta() { 41 | return this._meta; 42 | } 43 | 44 | get featureId() { 45 | return this.meta.featureId; 46 | } 47 | 48 | get id() { 49 | return this.meta.id; 50 | } 51 | 52 | unload(context) { 53 | this.setUnloaded(); 54 | } 55 | 56 | load(context) { 57 | this.setLoaded(); 58 | } 59 | 60 | isLoaded() { 61 | return this._status.type === STATUS_LOADED; 62 | } 63 | 64 | setLoaded() { 65 | this._status = { 66 | type: STATUS_LOADED, 67 | }; 68 | } 69 | 70 | setUnloaded() { 71 | this._status = { 72 | type: STATUS_UNLOADED, 73 | message: 'Plugin not enabled', 74 | }; 75 | } 76 | 77 | setErrored(error) { 78 | this._status = { 79 | type: STATUS_ERROR, 80 | message: error, 81 | }; 82 | } 83 | 84 | get status() { 85 | return this._status; 86 | } 87 | 88 | get swagger() { 89 | return this._swagger; 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /backend/src/plugins/Errors.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_ERROR_MESSAGE = 'An error has occured'; 2 | 3 | const makeError = (message) => { 4 | return new Error(message || DEFAULT_ERROR_MESSAGE); 5 | }; 6 | 7 | const InputError = { 8 | conflict: (message = 'Conflict') => { 9 | const error = makeError(message); 10 | error.code = 'CONFLICT'; 11 | return error; 12 | }, 13 | invalid: (message = 'Bad request') => { 14 | const error = makeError(message); 15 | error.code = 'INVALID'; 16 | return error; 17 | }, 18 | notFound: (message = 'Not Found') => { 19 | const error = makeError(message); 20 | error.code = 'NOT_FOUND'; 21 | return error; 22 | }, 23 | gone: (message = 'Requestsed object does not exist') => { 24 | const error = makeError(message); 25 | error.code = 'GONE'; 26 | return error; 27 | }, 28 | }; 29 | 30 | const OtherError = { 31 | somethingWentWrong: (message = 'Something went wrong') => { 32 | const error = makeError(message); 33 | error.code = 'SOMETHING_WRONG'; 34 | return error; 35 | }, 36 | }; 37 | 38 | const InternalError = { 39 | unexpected: (message = 'Something went wrong!') => { 40 | const error = makeError(message); 41 | error.code = 'SOMETHING_WRONG'; 42 | return error; 43 | }, 44 | }; 45 | 46 | const AuthError = { 47 | notAllowed: (message = 'Not Allowed') => { 48 | const error = makeError(message); 49 | error.code = 'NOT_ALLOWED'; 50 | return error; 51 | }, 52 | }; 53 | 54 | module.exports = { 55 | InternalError, 56 | InputError, 57 | AuthError, 58 | OtherError, 59 | }; 60 | -------------------------------------------------------------------------------- /backend/src/plugins/connect-disconnect/index.js: -------------------------------------------------------------------------------- 1 | const Plugin = require('./src/Plugin'); 2 | 3 | module.exports = { 4 | Plugin, 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/plugins/connect-disconnect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cedalo/management-center-plugin-connect-disconnect", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "passport": "^0.4.1", 7 | "passport-local": "^1.0.0" 8 | }, 9 | "peerDependencies": { 10 | "express": "^4.17.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/plugins/connect-disconnect/src/Plugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require('../../BasePlugin'); 2 | const meta = require('./meta'); 3 | const { createActions } = require('./actions'); 4 | 5 | module.exports = class Plugin extends BasePlugin { 6 | constructor() { 7 | super(meta); 8 | } 9 | 10 | init(context) { 11 | this._context = context; 12 | const { requestHandlers } = context; 13 | requestHandlers.set('connectServerToBroker', 'connect-disconnect/connectToBroker'); 14 | requestHandlers.set('disconnectServerFromBroker', 'connect-disconnect/disconnectFromBroker'); 15 | const { connectServerToBrokerAction, disconnectServerFromBroker } = createActions(this); 16 | context.registerAction(connectServerToBrokerAction); 17 | context.registerAction(disconnectServerFromBroker); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/plugins/connect-disconnect/src/actions.js: -------------------------------------------------------------------------------- 1 | const { AuthError } = require('../../Errors'); 2 | const NOT_AUTHORIZED_ERROR_MESSAGE = `You don't have enough user rights to perform this operation`; 3 | 4 | const createActions = (plugin) => ({ 5 | connectServerToBrokerAction: { 6 | type: 'connect-disconnect/connectToBroker', 7 | isModifying: true, 8 | metainfo: { source: 'core' /*plugin.featureId*/, operation: 'connectServerToBroker', operationType: 'update' }, 9 | fn: async (context, { connectionId, oneshot = false }) => { 10 | const { user, security, configManager } = context; 11 | if (security.acl.isConnectionAuthorized(user, security.acl.atLeastAdmin, null, connectionId)) { 12 | const connection = configManager.getConnection(connectionId); 13 | await context.handleConnectServerToBroker(connection, user, oneshot); 14 | if (connection.status?.error) { 15 | throw new Error(connection.status?.error); 16 | } else { 17 | return configManager.connections; 18 | } 19 | } else { 20 | throw AuthError.notAllowed(NOT_AUTHORIZED_ERROR_MESSAGE); 21 | } 22 | }, 23 | }, 24 | disconnectServerFromBroker: { 25 | type: 'connect-disconnect/disconnectFromBroker', 26 | isModifying: true, 27 | metainfo: { 28 | source: 'core' /*plugin.featureId*/, 29 | operation: 'disconnectServerFromBroker', 30 | operationType: 'update', 31 | }, 32 | fn: async (context, { connectionId }) => { 33 | const { user, security, configManager } = context; 34 | if (security.acl.isConnectionAuthorized(user, security.acl.atLeastAdmin, null, connectionId)) { 35 | try { 36 | const connection = configManager.getConnection(connectionId); 37 | const isNormalDisconnect = true; 38 | await context.handleDisconnectServerFromBroker(connection, isNormalDisconnect); 39 | } catch (error) { 40 | throw error; 41 | } 42 | return configManager.connections; 43 | } else { 44 | throw AuthError.notAllowed(NOT_AUTHORIZED_ERROR_MESSAGE); 45 | } 46 | }, 47 | }, 48 | }); 49 | 50 | module.exports = { 51 | createActions, 52 | }; 53 | -------------------------------------------------------------------------------- /backend/src/plugins/connect-disconnect/src/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "connect-disconnect", 3 | "name": "Cedalo Connect / Disconnect", 4 | "version": "1.0", 5 | "description": "Connect and disconnect brokers", 6 | "feature": "Security", 7 | "featureId": "security", 8 | "type": "os", 9 | "actions": { 10 | "enable": false, 11 | "disable": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/plugins/connect-disconnect/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | passport-local@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" 8 | integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= 9 | dependencies: 10 | passport-strategy "1.x.x" 11 | 12 | passport-strategy@1.x.x: 13 | version "1.0.0" 14 | resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" 15 | integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= 16 | 17 | passport@^0.4.1: 18 | version "0.4.1" 19 | resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" 20 | integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== 21 | dependencies: 22 | passport-strategy "1.x.x" 23 | pause "0.0.1" 24 | 25 | pause@0.0.1: 26 | version "0.0.1" 27 | resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" 28 | integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= 29 | -------------------------------------------------------------------------------- /backend/src/plugins/login/component/images/cedalo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/backend/src/plugins/login/component/images/cedalo.png -------------------------------------------------------------------------------- /backend/src/plugins/login/component/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/backend/src/plugins/login/component/images/favicon-32x32.png -------------------------------------------------------------------------------- /backend/src/plugins/login/component/images/logo-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/backend/src/plugins/login/component/images/logo-red.png -------------------------------------------------------------------------------- /backend/src/plugins/login/component/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/backend/src/plugins/login/component/images/logo.png -------------------------------------------------------------------------------- /backend/src/plugins/login/index.js: -------------------------------------------------------------------------------- 1 | const Plugin = require('./src/Plugin'); 2 | 3 | module.exports = { 4 | Plugin, 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/plugins/login/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cedalo/management-center-plugin-login", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "passport": "^0.4.1", 7 | "passport-local": "^1.0.0" 8 | }, 9 | "peerDependencies": { 10 | "express": "^4.17.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/plugins/login/src/actions.js: -------------------------------------------------------------------------------- 1 | const { randomUUID } = require('crypto'); 2 | 3 | const USERNAME = process.env.CEDALO_MC_USERNAME || 'cedalo'; 4 | const PASSWORD = process.env.CEDALO_MC_PASSWORD || 'secret'; 5 | 6 | const addSessionId = (user) => user && { ...user, sessionId: randomUUID() }; 7 | 8 | const createActions = (plugin) => ({ 9 | loginAction: { 10 | type: 'user/login', 11 | isModifying: true, 12 | metainfo: { source: 'core' /* plugin.featureId*/, operation: 'login', operationType: 'update' }, 13 | fn: async (context, { username, password }) => { 14 | if (username === USERNAME && password === PASSWORD) { 15 | return addSessionId({ 16 | username, 17 | roles: ['admin'], 18 | }); 19 | } 20 | const valid = 21 | (username === USERNAME && password === PASSWORD) || 22 | (await context.security?.usersManager?.checkUser(username, password)); 23 | if (!valid) { 24 | throw new Error('Invalid credentials'); 25 | } 26 | if (!context.security.usersManagerEnabled) { 27 | throw new Error('UserManagement disabled'); 28 | } 29 | return addSessionId(context.security.usersManager.getUser(username)); 30 | }, 31 | filter: ({ password: _, ...toPublish }) => toPublish, 32 | }, 33 | logoutAction: { 34 | // Dummy Action to receive a logout event 35 | type: 'user/logout', 36 | isModifying: true, 37 | metainfo: { source: 'core' /* plugin.featureId*/, operation: 'logout', operationType: 'update' }, 38 | fn: (context, { username }) => { 39 | return true; 40 | }, 41 | }, 42 | }); 43 | module.exports = { createActions }; 44 | -------------------------------------------------------------------------------- /backend/src/plugins/login/src/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "login", 3 | "name": "Cedalo Login", 4 | "version": "1.0", 5 | "description": "Login for accessing the Management Center", 6 | "feature": "Security", 7 | "featureId": "security", 8 | "type": "os", 9 | "actions": { 10 | "enable": false, 11 | "disable": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/plugins/login/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | passport-local@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" 8 | integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= 9 | dependencies: 10 | passport-strategy "1.x.x" 11 | 12 | passport-strategy@1.x.x: 13 | version "1.0.0" 14 | resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" 15 | integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= 16 | 17 | passport@^0.4.1: 18 | version "0.4.1" 19 | resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" 20 | integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== 21 | dependencies: 22 | passport-strategy "1.x.x" 23 | pause "0.0.1" 24 | 25 | pause@0.0.1: 26 | version "0.0.1" 27 | resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" 28 | integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= 29 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/index.js: -------------------------------------------------------------------------------- 1 | const Plugin = require('./src/Plugin'); 2 | 3 | module.exports = { 4 | Plugin, 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cedalo/management-center-plugin-user-profile", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "passport": "^0.4.1", 7 | "passport-local": "^1.0.0" 8 | }, 9 | "peerDependencies": { 10 | "express": "^4.17.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/src/Plugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require('../../BasePlugin'); 2 | const { createActions } = require('./actions'); 3 | const meta = require('./meta'); 4 | const swagger = require('./swagger.js'); 5 | 6 | module.exports = class Plugin extends BasePlugin { 7 | constructor() { 8 | super(meta); 9 | this._swagger = swagger; 10 | } 11 | 12 | init(context) { 13 | const { router } = context; 14 | 15 | const { getProfileAction } = createActions(this); 16 | 17 | context.registerAction(getProfileAction); 18 | 19 | router.get( 20 | '/api/profile', 21 | // we need this wrapper becuse in some plugins like application-tokens we are going to redefine context.security.isLoggedIn, so we need it to be resolved dynamically via a wrapper 22 | (request, response, next) => context.security.isLoggedIn(request, response, next), 23 | context.middleware.isPluginLoaded(this), 24 | (request, response) => { 25 | const result = context.runAction(request.user, 'user-profile/get', null, { request }); 26 | response.send(result); 27 | } 28 | ); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/src/actions.js: -------------------------------------------------------------------------------- 1 | const createActions = (plugin) => ({ 2 | getProfileAction: { 3 | type: 'user-profile/get', 4 | metainfo: { source: 'core' /*plugin.featureId*/, operation: 'getUserProfile', operationType: 'read' }, 5 | fn: ({ user: { password: _, ...cleanUser } }) => cleanUser, 6 | }, 7 | }); 8 | 9 | module.exports = { 10 | createActions, 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/src/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "user-profile", 3 | "name": "Cedalo User Profile", 4 | "version": "1.0", 5 | "description": "Access to the user profile", 6 | "feature": "User Management", 7 | "featureId": "user-management", 8 | "type": "os", 9 | "actions": { 10 | "enable": false, 11 | "disable": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/src/swagger.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tags: [ 3 | { 4 | name: 'user-profile', 5 | description: 'User Profile API', 6 | }, 7 | ], 8 | paths: { 9 | '/api/profile': { 10 | get: { 11 | tags: ['user-profile'], 12 | summary: 'Returns the profile of the currently logged in user.', 13 | produces: ['application/json'], 14 | responses: { 15 | 200: { 16 | description: 'User profile object', 17 | }, 18 | }, 19 | }, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /backend/src/plugins/user-profile/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | passport-local@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" 8 | integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= 9 | dependencies: 10 | passport-strategy "1.x.x" 11 | 12 | passport-strategy@1.x.x: 13 | version "1.0.0" 14 | resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" 15 | integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= 16 | 17 | passport@^0.4.1: 18 | version "0.4.1" 19 | resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" 20 | integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== 21 | dependencies: 22 | passport-strategy "1.x.x" 23 | pause "0.0.1" 24 | 25 | pause@0.0.1: 26 | version "0.0.1" 27 | resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" 28 | integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= 29 | -------------------------------------------------------------------------------- /backend/src/security/acl.js: -------------------------------------------------------------------------------- 1 | // access control functions to be overriden in user-management plugin if said plugin is installed 2 | module.exports = { 3 | middleware: { 4 | isAdmin(request, response, next) { 5 | return next(); 6 | }, 7 | isEditor(request, response, next) { 8 | return next(); 9 | }, 10 | isViewer(request, response, next) { 11 | return next(); 12 | }, 13 | isConnectionManager(request, response, next) { 14 | return next(); 15 | }, 16 | isMonitoringViewer(request, response, next) { 17 | return next(); 18 | }, 19 | noRestrictedRoles(request, response, next) { 20 | return next(); 21 | }, 22 | allowConditions() { 23 | return (request, response, next) => { 24 | return next(); 25 | }; 26 | }, 27 | checkConnectionAuthorized() { 28 | return (request, response, next) => { 29 | return next(); 30 | }; 31 | }, 32 | }, 33 | isAdmin(user) { 34 | return true; 35 | }, 36 | isEditor(user) { 37 | return true; 38 | }, 39 | isViewer(user) { 40 | return true; 41 | }, 42 | isConnectionManager(user) { 43 | return true; 44 | }, 45 | isMonitoringViewer(user) { 46 | return true; 47 | }, 48 | noRestrictedRoles(user) { 49 | return true; 50 | }, 51 | filterAllowedConnections(connections, allowedConnections) { 52 | return connections; 53 | }, 54 | atLeastAdmin(user) { 55 | return true; 56 | }, 57 | atLeastEditor(user) { 58 | return true; 59 | }, 60 | atLeastViewer(user) { 61 | return true; 62 | }, 63 | atLeastConnectionManager(user) { 64 | return true; 65 | }, 66 | atLeastMonitoringViewer(user) { 67 | return true; 68 | }, 69 | isConnectionAuthorized(user) { 70 | return true; 71 | }, 72 | allowConditions() { 73 | return true; 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /backend/src/settings/SettingsManager.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const low = require('lowdb'); 3 | const FileSync = require('lowdb/adapters/FileSync'); 4 | const { getBaseDirectory } = require('../utils/utils'); 5 | 6 | const adapter = new FileSync( 7 | path.join(process.env.CEDALO_MC_DIRECTORY_SETTINGS || getBaseDirectory(__dirname), 'settings.json') 8 | ); 9 | const db = low(adapter); 10 | 11 | module.exports = class SettingsManager { 12 | constructor(context) { 13 | this._context = context; 14 | const defaultSettings = { 15 | allowTrackingUsageData: false, 16 | topicTreeEnabled: false, 17 | }; 18 | db.defaults({ 19 | settings: defaultSettings, 20 | }).write(); 21 | } 22 | 23 | get settings() { 24 | return db.get('settings').value(); 25 | } 26 | 27 | set settings(settings) { 28 | db.update('settings', (oldSettings) => settings).write(); 29 | } 30 | 31 | updateSettings(settings, brokerName) { 32 | const oldSettings = this.settings; 33 | 34 | this.settings = settings; 35 | 36 | this._context?.eventEmitter?.emit('settings-update', oldSettings, this.settings); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/usage/InstallationManager.js: -------------------------------------------------------------------------------- 1 | const HTTPClient = require('../http/HTTPClient'); 2 | 3 | const URL = 'https://api.cedalo.cloud/rest/request/mosquitto-ui/installation'; 4 | const CEDALO_MC_OFFLINE = process.env.CEDALO_MC_MODE === 'offline'; 5 | 6 | module.exports = class InstallationManager { 7 | constructor({ license, version, installation }) { 8 | this._license = license; 9 | this._version = version; 10 | this._installation = installation; 11 | } 12 | 13 | async send(data) { 14 | if (!CEDALO_MC_OFFLINE) { 15 | const sendData = { 16 | installation: this._installation, 17 | license: this._license, 18 | timestamp: Date.now(), 19 | version: this._version, 20 | }; 21 | try { 22 | const response = await HTTPClient.getInstance().post(URL, sendData); 23 | return response; 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | } 28 | } 29 | 30 | async verifyLicense() { 31 | const response = await this.send(); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /backend/src/usage/UsageTracker.js: -------------------------------------------------------------------------------- 1 | const HTTPClient = require('../http/HTTPClient'); 2 | 3 | const URL = 'https://api.cedalo.cloud/rest/request/mosquitto-ui/usage'; 4 | const CEDALO_MC_OFFLINE = process.env.CEDALO_MC_MODE === 'offline'; 5 | 6 | module.exports = class UsageTracker { 7 | constructor({ license, version, installation }) { 8 | this._license = license; 9 | this._version = version; 10 | this._installation = installation; 11 | } 12 | 13 | async send(data) { 14 | if (!CEDALO_MC_OFFLINE) { 15 | const sendData = { 16 | installation: this._installation, 17 | license: this._license, 18 | timestamp: Date.now(), 19 | version: this._version, 20 | ...data, 21 | }; 22 | try { 23 | const response = await HTTPClient.getInstance().post(URL, sendData); 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | } 28 | } 29 | 30 | async init() {} 31 | }; 32 | -------------------------------------------------------------------------------- /backend/src/utils/ConsumersRegistry.js: -------------------------------------------------------------------------------- 1 | class ConsumersRegistry { 2 | constructor(notifyFunction) { 3 | this._consumers = new Map(); 4 | this._notifyFunction = notifyFunction; 5 | } 6 | 7 | register(consumer) { 8 | this._consumers.set(consumer, false); 9 | } 10 | 11 | unregister(consumer) { 12 | this._consumers.delete(consumer); 13 | } 14 | 15 | clear() { 16 | this._consumers = new Map(); 17 | } 18 | 19 | areAllReady() { 20 | return Array.from(this._consumers.values()).every((value) => value); // if no consumear this will also return true 21 | } 22 | 23 | ready(consumer) { 24 | this._consumers.set(consumer, true); 25 | if (this.areAllReady()) { 26 | this._notifyFunction(); 27 | } 28 | } 29 | 30 | get consumers() { 31 | return this._consumers; 32 | } 33 | } 34 | 35 | module.exports = ConsumersRegistry; 36 | -------------------------------------------------------------------------------- /backend/src/utils/Logger.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | constructor(logger, silentMode = false) { 3 | this.logger = logger; 4 | this.silentMode = silentMode; 5 | } 6 | 7 | formatTimestamp() { 8 | const date = new Date(); 9 | return date.toISOString() + ' (' + date.getTime() + '): '; 10 | } 11 | 12 | log() { 13 | if (!this.silentMode) { 14 | const timestamp = this.formatTimestamp(); 15 | this.logger.log(timestamp, ...arguments); 16 | } 17 | } 18 | 19 | error() { 20 | if (!this.silentMode) { 21 | const timestamp = this.formatTimestamp(); 22 | this.logger.error(timestamp, ...arguments); 23 | } 24 | } 25 | 26 | trace() { 27 | this.logger.log('trace not implemented'); // trace will anyways not give a full stacktrace 28 | } 29 | } 30 | 31 | module.exports = Logger; 32 | -------------------------------------------------------------------------------- /backend/src/utils/QueuedEmitter2.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const EventEmitter2 = require('eventemitter2'); 3 | const ConsumersRegistry = require('./ConsumersRegistry'); 4 | 5 | class QueuedEmitter2 extends EventEmitter2 { 6 | constructor() { 7 | super(); 8 | this.queue = []; 9 | this.ready = false; 10 | this._eventEmitter = new EventEmitter(); 11 | this.consumers = new ConsumersRegistry(() => this._eventEmitter.emit('consumers-registry/consumers-ready')); 12 | 13 | this._eventEmitter.on('consumers-registry/consumers-ready', () => { 14 | this.processQueue(); 15 | }); 16 | } 17 | 18 | emit(event, ...args) { 19 | if (this.ready) { 20 | // If ready, emit the event immediately 21 | super.emit(event, ...args); 22 | } else { 23 | // If not ready, queue the event 24 | this.queue.push({ event, args }); 25 | } 26 | } 27 | 28 | processQueue() { 29 | this.ready = true; 30 | while (this.queue.length > 0) { 31 | const { event, args } = this.queue.shift(); 32 | super.emit(event, ...args); 33 | } 34 | } 35 | } 36 | 37 | module.exports = QueuedEmitter2; 38 | -------------------------------------------------------------------------------- /backend/src/utils/localhost.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | const CEDALO_HOST_NAME = process.env.CEDALO_HOST_NAME; 4 | 5 | const toAddress = ({ address }) => address; 6 | const isPublicIP = 7 | (ipFamily) => 8 | ({ family, internal }) => 9 | family === ipFamily && !internal; 10 | 11 | module.exports = { 12 | // in case we are running inside a docker container, default hostname will be container's hostname not actual hostname. Thath's why in such cases we have to pass it as an env var to docker-compose environment secion 13 | hostname: CEDALO_HOST_NAME || os.hostname(), 14 | hostIPs: { 15 | // get all external v4 ip addresses and v6 addresses as arrays 16 | v4: Object.values(os.networkInterfaces()).flat().filter(isPublicIP('IPv4')).map(toAddress), 17 | v6: Object.values(os.networkInterfaces()).flat().filter(isPublicIP('IPv6')).map(toAddress), 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/utils/sessions.js: -------------------------------------------------------------------------------- 1 | const destroySession = (sessionStore) => (sessionId) => 2 | new Promise((resolve) => sessionStore.destroy(sessionId, resolve)); 3 | 4 | const getAllSessions = (sessionStore) => new Promise((resolve) => sessionStore.all((_, sessions) => resolve(sessions))); 5 | 6 | const collectOtherSessions = 7 | (sessionID, username) => 8 | (all, [id, session]) => { 9 | if (id !== sessionID && session.passport?.user?.username === username) all.push(id); 10 | return all; 11 | }; 12 | 13 | const collectDestroyedSessions = (logger, sessions, username) => (all, res, index) => { 14 | if (res.status === 'fulfilled') all.push(sessions[index]); 15 | else logger.error(`Failed to destroy session for user: ${username}(${sessions[index]})`, res.reason); 16 | return all; 17 | }; 18 | 19 | const destroyUserSessions = (plugin) => async (username, request) => { 20 | const { sessionStore, sessionID } = request; 21 | const sessions = await getAllSessions(sessionStore).catch((err) => 22 | plugin.logger.error('Failed to get sessions!', err) 23 | ); 24 | if (sessions) { 25 | const otherSessions = Object.entries(sessions).reduce(collectOtherSessions(sessionID, username), []); 26 | const results = await Promise.allSettled(otherSessions.map(destroySession(sessionStore))); 27 | return results.reduce(collectDestroyedSessions(plugin.logger, otherSessions, username), []); 28 | } 29 | return []; 30 | }; 31 | 32 | const hasSession = (request) => { 33 | const { sessionStore, sessionID } = request; 34 | return !!sessionStore.sessions[sessionID]; 35 | }; 36 | 37 | const createSessionsDestroyedMessage = (sessionIDs) => ({ 38 | type: 'event', 39 | event: { 40 | type: 'sessions-destroyed', 41 | payload: sessionIDs, 42 | }, 43 | }); 44 | 45 | module.exports = { 46 | createSessionsDestroyedMessage, 47 | destroyUserSessions, 48 | hasSession, 49 | }; 50 | -------------------------------------------------------------------------------- /backend/src/utils/version.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid'); 2 | 3 | const packageJSON = require('../../package.json'); 4 | 5 | const version = { 6 | name: process.env.CEDALO_MC_NAME || 'Cedalo Management Center', 7 | version: process.env.CEDALO_MC_VERSION || packageJSON.version, 8 | buildNumber: process.env.TRAVIS_BUILD_NUMBER || process.env.CEDALO_MC_BUILD_NUMBER || 'Build Number unknown', 9 | buildDate: process.env.CEDALO_MC_BUILD_DATE || Date.now(), 10 | }; 11 | 12 | module.exports = version; 13 | -------------------------------------------------------------------------------- /backend/swagger.js: -------------------------------------------------------------------------------- 1 | const CEDALO_MC_PROXY_PORT = process.env.CEDALO_MC_PROXY_PORT || 8088; 2 | const CEDALO_MC_PROXY_HOST = process.env.HOSTNAME || process.env.CEDALO_MC_PROXY_HOST || 'localhost'; 3 | 4 | module.exports = { 5 | openapi: '3.0.3', 6 | // "swagger": "2.0", 7 | info: { 8 | title: 'Management Center REST API', 9 | description: 10 | 'API description for the Management Center. Set the Accept header to "application/json;version=" to access a correct version of the api', 11 | version: '2.0.0', 12 | }, 13 | servers: [ 14 | { url: `http://${CEDALO_MC_PROXY_HOST}:${CEDALO_MC_PROXY_PORT}` }, 15 | { url: `https://${CEDALO_MC_PROXY_HOST}:${CEDALO_MC_PROXY_PORT}` }, 16 | ], 17 | basePath: '/', 18 | paths: {}, 19 | components: { 20 | schemas: {}, 21 | statuses: {}, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/tests/client.js: -------------------------------------------------------------------------------- 1 | const NodeMosquittoClient = require('../src/client/NodeMosquittoClient'); 2 | 3 | const MOSQUITTO_URL = process.env.MOSQUITTO_URL || 'mqtt://localhost'; 4 | const MOSQUITTO_PORT = process.env.MOSQUITTO_PORT || 1888; 5 | 6 | (async () => { 7 | const client = new NodeMosquittoClient({ 8 | /* logger: console */ 9 | }); 10 | try { 11 | await client.connect({ 12 | mqttEndpointURL: `${MOSQUITTO_URL}:${MOSQUITTO_PORT}`, 13 | }); 14 | const feature = 'user-management'; 15 | const commandMessage = { 16 | command: 'createUser', 17 | username: 'user_one', 18 | password: 'password', 19 | clientid: 'cid', 20 | rolename: '', 21 | groups: [ 22 | { 23 | name: 'admins', 24 | priority: 0, 25 | }, 26 | ], 27 | }; 28 | const result = await client.sendCommandMessage(feature, commandMessage); 29 | console.log(result); 30 | 31 | const commandMessage2 = { 32 | command: 'listUsers', 33 | }; 34 | const users = await client.sendCommandMessage(feature, commandMessage2); 35 | console.log(JSON.stringify(users, null, 2)); 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | yarn workspace @cedalo/management-center-frontend run build 3 | docker build --build-arg CEDALO_MC_BUILD_DATE="$(date)" --build-arg CEDALO_MC_BUILD_NUMBER="$(date '+%s')" -t cedalo/management-center:dev . 4 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "connections": [ 3 | { 4 | "id": "mosquitto-2-preview", 5 | "name": "Mosquitto 2.0 Instance", 6 | "url": "mqtt://localhost:1883", 7 | "credentials": { 8 | "username": "cedalo", 9 | "password": "eAkX29UnAs" 10 | } 11 | } 12 | ], 13 | "tools": { 14 | "streamsheets": { 15 | "instances": [ 16 | { 17 | "id": "streamsheets", 18 | "name": "Streamsheets", 19 | "description": "Streamsheets running locally", 20 | "url": "http://localhost:9000" 21 | } 22 | ] 23 | } 24 | }, 25 | "plugins": [] 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mosquitto: 5 | image: cedalo/mosquitto:streams 6 | container_name: mosquitto 7 | ports: 8 | - 1883:1883 9 | - 9002:9001 10 | expose: 11 | - 1883 12 | - 9002 13 | # volumes: 14 | # - ./data:/mosquitto/data 15 | # - ./conf:/mosquitto/config 16 | networks: 17 | - mosquitto 18 | management-center: 19 | image: cedalo/management-center:dev 20 | container_name: management-center 21 | environment: 22 | # Do not change these settings for the moment! 23 | CEDALO_MC_BROKER_ID: mosquitto-2.0 24 | CEDALO_MC_BROKER_NAME: Mosquitto 2.0 25 | CEDALO_MC_BROKER_URL: mqtt://mosquitto:1883 26 | CEDALO_MC_BROKER_USERNAME: cedalo 27 | CEDALO_MC_BROKER_PASSWORD: eAkX29UnAs 28 | CEDALO_MC_PROXY_CONFIG_DIR: /management-center/config/config.json 29 | ports: 30 | - 8088:8088 31 | expose: 32 | - 8088 33 | depends_on: 34 | - mosquitto 35 | # volumes: 36 | # - ./config:/management-center/backend/config 37 | networks: 38 | - mosquitto 39 | networks: 40 | mosquitto: 41 | name: mosquitto 42 | driver: bridge 43 | -------------------------------------------------------------------------------- /docker/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "connections": [], 3 | "tools": { 4 | "streamsheets": { 5 | "instances": [] 6 | } 7 | }, 8 | "plugins": [] 9 | } 10 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | NOCOLOR='\033[0m' 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | CYAN='\033[0;36m' 7 | ORANGE='\033[0;33m' 8 | YELLOW='\033[1;33m' 9 | 10 | echo -e "${GREEN}Starting Management Center for Eclipse Mosquitto${NOCOLOR}" 11 | 12 | node start.js 13 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # IDEs and editors 15 | /.idea 16 | /.vscode 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Create React App example 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/mui-org/material-ui): 6 | 7 | ```sh 8 | curl https://codeload.github.com/mui-org/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/create-react-app 9 | cd create-react-app 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```sh 15 | npm install 16 | npm start 17 | ``` 18 | 19 | or: 20 | 21 | [![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mui-org/material-ui/tree/master/examples/create-react-app) 22 | 23 | ## The idea behind the example 24 | 25 | This example demonstrates how you can use [Create React App](https://github.com/facebookincubator/create-react-app). 26 | -------------------------------------------------------------------------------- /frontend/base/main.js: -------------------------------------------------------------------------------- 1 | const _BaseMosquittoProxyClient = require('./_BaseMosquittoProxyClient'); 2 | 3 | module.exports = { 4 | BaseMosquittoProxyClient: _BaseMosquittoProxyClient, 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base-mc-classes", 3 | "description": "", 4 | "version": "1.0.0", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": {}, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^0.27.2", 15 | "uuid": "^8.3.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cedalo/management-center-frontend", 3 | "version": "2.9.5", 4 | "private": true, 5 | "author": "Cedalo AG", 6 | "homepage": ".", 7 | "scripts": { 8 | "start": "react-scripts start", 9 | "build": "cross-env SKIP_PREFLIGHT_CHECK=true PUBLIC_URL=$CEDALO_MC_PROXY_BASE_PATH react-scripts build", 10 | "build-without-base-path": "cross-env SKIP_PREFLIGHT_CHECK=true run-script-os", 11 | "build-without-base-path:win32": "(if exist ..\\backend\\public rmdir ..\\backend\\public /s /q) && cross-env PUBLIC_URL=/ react-scripts build && move build ../backend/public", 12 | "build-without-base-path:darwin:linux": "rm -rf ../backend/public && cross-env PUBLIC_URL=/ react-scripts build && mv build ../backend/public", 13 | "build-with-base-path": "cross-env SKIP_PREFLIGHT_CHECK=true run-script-os", 14 | "build-with-base-path:win32": "(if exist ..\\backend\\public rmdir ..\\backend\\public /s /q) && cross-env PUBLIC_URL=/mosquitto-management-center react-scripts build --verbose && move build ../backend/public", 15 | "build-with-base-path:darwin:linux": "rm -rf ../backend/public && cross-env PUBLIC_URL=/mosquitto-management-center react-scripts build --verbose && mv build ../backend/public", 16 | "test": "react-scripts test", 17 | "test-client": "npx jest tests/client.test.js --detectOpenHandles", 18 | "eject": "react-scripts eject", 19 | "import-sort": "npx import-sort-cli --write src/**/*.js" 20 | }, 21 | "dependencies": { 22 | "@emotion/react": "11.7.1", 23 | "@material-ui/core": "4.12.4", 24 | "@material-ui/icons": "^4.11.3", 25 | "@material-ui/lab": "^4.0.0-alpha.61", 26 | "@reactour/tour": "^3.3.0", 27 | "@reactour/utils": "^0.4.7", 28 | "ajv": "^7.1.1", 29 | "base-mc-classes": "file:./base", 30 | "brace": "^0.11.1", 31 | "buffer": "^6.0.3", 32 | "express": "^4.18.1", 33 | "file-saver": "^2.0.5", 34 | "jsoneditor": "^9.7.4", 35 | "jsoneditor-react": "^3.1.2", 36 | "material-ui-confirm": "^2.1.1", 37 | "notistack": "1.0.10", 38 | "react": "^18.0.1", 39 | "react-chartjs-2": "^2.10.0", 40 | "react-d3-speedometer": "^2.1.0-rc.0", 41 | "react-dom": "^18.0.1", 42 | "react-redux": "^7.2.1", 43 | "react-router-dom": "^5.2.0", 44 | "react-scripts": "^5.0.1", 45 | "react-select": "^3.1.0", 46 | "redux": "^4.2.0", 47 | "styled-components": "^5.3.5", 48 | "sweetalert2": "^11.4.14", 49 | "sweetalert2-react-content": "^5.0.0", 50 | "terminal-in-react": "^4.3.1" 51 | }, 52 | "devDependencies": { 53 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 54 | "chart.js": "^2.9.3", 55 | "clsx": "latest", 56 | "cross-env": "^7.0.3", 57 | "form-data": "^4.0.0", 58 | "jest": "^29.1.2", 59 | "run-script-os": "^1.1.6" 60 | }, 61 | "browserslist": { 62 | "production": [ 63 | ">0.2%", 64 | "not dead", 65 | "not op_mini all" 66 | ], 67 | "development": [ 68 | "last 1 chrome version", 69 | "last 1 firefox version", 70 | "last 1 safari version" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/public/clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/clients.png -------------------------------------------------------------------------------- /frontend/public/disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/disconnected.png -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/groups.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Management Center 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | -------------------------------------------------------------------------------- /frontend/public/inprogress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/inprogress.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/.DS_Store -------------------------------------------------------------------------------- /frontend/public/integration-logos/alloydb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/alloydb.webp -------------------------------------------------------------------------------- /frontend/public/integration-logos/cockroachdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/cockroachdb.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/googlecloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/googlecloud.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/influxdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/influxdb.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/jwt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/integration-logos/kafka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/kafka.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/kubernetes.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/mariadb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MDB-VLogo_Black 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/public/integration-logos/mongodb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/mongodb.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/mongodbatlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/mongodbatlas.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/mssql.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/mssql.webp -------------------------------------------------------------------------------- /frontend/public/integration-logos/oracledb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/integration-logos/postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/postgres.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/integration-logos/prometheus.png -------------------------------------------------------------------------------- /frontend/public/integration-logos/redshift.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/integration-logos/snowflake.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/integration-logos/timescaledb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Your Orders", 3 | "name": "Your Orders", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/public/onboarding-broker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/onboarding-broker.png -------------------------------------------------------------------------------- /frontend/public/onboarding-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/onboarding-dashboard.png -------------------------------------------------------------------------------- /frontend/public/onboarding-dynamic-security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/onboarding-dynamic-security.png -------------------------------------------------------------------------------- /frontend/public/onboarding-newsletter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/onboarding-newsletter.png -------------------------------------------------------------------------------- /frontend/public/onboarding-topic-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/onboarding-topic-tree.png -------------------------------------------------------------------------------- /frontend/public/roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/roles.png -------------------------------------------------------------------------------- /frontend/public/security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/security.png -------------------------------------------------------------------------------- /frontend/public/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/settings.png -------------------------------------------------------------------------------- /frontend/public/smilethink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/smilethink.png -------------------------------------------------------------------------------- /frontend/public/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/status.png -------------------------------------------------------------------------------- /frontend/public/streams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/streams.png -------------------------------------------------------------------------------- /frontend/public/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/system.png -------------------------------------------------------------------------------- /frontend/public/topictree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/frontend/public/topictree.png -------------------------------------------------------------------------------- /frontend/src/admin/certificates/components/AlertHint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, AlertTitle } from '@material-ui/lab'; 3 | 4 | const AlertHint = ({ message, severity, title }) => ( 5 | <> 6 |
7 | 8 | {title} 9 | {message} 10 | 11 | 12 | ); 13 | 14 | export const InfoHint = ({ title, message }) => AlertHint({ title, message, severity: 'info' }); 15 | export const ErrorHint = ({ title, message }) => AlertHint({ title, message, severity: 'error' }); 16 | export const WarningHint = ({ title, message }) => AlertHint({ title, message, severity: 'warning' }); 17 | -------------------------------------------------------------------------------- /frontend/src/admin/certificates/components/ChipsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import { Chip, Grid } from '@material-ui/core'; 4 | 5 | const byLabel = (a, b) => { 6 | if (a.label < b.label) return -1; 7 | return a.label > b.label ? 1 : 0; 8 | }; 9 | const ChipsGrid = (values) => ( 10 | 11 | {values.sort(byLabel).map((broker) => ( 12 | 13 | 14 | 15 | ))} 16 | 17 | ); 18 | 19 | const ChipList = ({ component, values }) => { 20 | return ( 21 | 33 | ); 34 | }; 35 | 36 | export default ChipList; 37 | -------------------------------------------------------------------------------- /frontend/src/admin/certificates/components/ContentTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core'; 3 | 4 | const ContentTable = ({ children, columns }) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | {columns.map((column) => ( 11 | 18 | {column.key} 19 | 20 | ))} 21 | 22 | 23 | {children} 24 |
25 |
26 | ); 27 | }; 28 | export default ContentTable; 29 | -------------------------------------------------------------------------------- /frontend/src/admin/certificates/components/UploadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@material-ui/core'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import CloudUpload from '@material-ui/icons/CloudUpload'; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | button: { 8 | margin: theme.spacing(1), 9 | width: '20%', 10 | }, 11 | restrictButtonHeight: { 12 | maxHeight: '27px', 13 | }, 14 | })); 15 | 16 | const loadFile = (file, onUpload) => { 17 | const reader = new FileReader(); 18 | reader.onload = (ev) => { 19 | try { 20 | const res = ev.target.result; 21 | const parts = res ? res.split(':') : []; 22 | onUpload({ file, data: parts.length > 1 ? parts[1] : parts[0] }); 23 | } catch (error) { 24 | onUpload({ file, error }); 25 | } 26 | }; 27 | reader.readAsText(file); 28 | }; 29 | 30 | const onChange = (onUpload) => (event) => { 31 | const { files } = event.target; 32 | if (files && files.length) { 33 | loadFile(files[0], onUpload); 34 | } 35 | }; 36 | 37 | const UploadButton = ({ disabled, onUpload }) => { 38 | const classes = useStyles(); 39 | return ( 40 | 54 | ); 55 | }; 56 | export default UploadButton; 57 | -------------------------------------------------------------------------------- /frontend/src/admin/certificates/components/certutils.js: -------------------------------------------------------------------------------- 1 | const normalize = (host = '') => { 2 | if (host.startsWith('//')) return host.substring(2); 3 | if (host.startsWith('/')) return host.substring(1); 4 | return host; 5 | }; 6 | const getConnectionInfo = (connection) => { 7 | const { id, name, url = '' } = connection; 8 | const parts = url.split(':'); 9 | return { id, name, protocol: parts[0], host: normalize(parts[1]), port: parts[2] }; 10 | }; 11 | 12 | const mapById = (all, val) => { 13 | all[val.id] = val; 14 | return all; 15 | }; 16 | const getUsedConnections = (availableConnections = [], cert) => { 17 | const connections = availableConnections.reduce(mapById, {}); 18 | const ids = Object.keys(cert.usedBy); 19 | return ids.filter((id) => !!connections[id]).map((id) => connections[id]); 20 | }; 21 | 22 | const mapSubjectKeys = { 23 | CN: 'Common Name', 24 | L: 'Locality', 25 | ST: 'State Or Province', 26 | O: 'Organization', 27 | OU: 'Organization Unit', 28 | C: 'Country Code', 29 | E: 'E-Mail', 30 | STREET: 'Street', 31 | emailAddress: 'E-Mail', 32 | // DC: 'Domain' 33 | // UID: 'User ID' 34 | }; 35 | const toObj = (delimiter, mapKey) => (obj, str) => { 36 | const [key, value] = str.split(delimiter); 37 | obj[mapKey(key)] = value; 38 | return obj; 39 | }; 40 | const identity = (v) => v; 41 | const mapSubjectKey = (key) => mapSubjectKeys[key] || key; 42 | const parseSubjectInfo = (str, mapKey = identity) => (str ? str.split('\n').reduce(toObj('=', mapKey), {}) : {}); 43 | 44 | const isValid = ({ cert, id, filename }) => cert || (id && filename); 45 | const loadCertificateInfo = async (certificate, client) => { 46 | if (isValid(certificate)) { 47 | try { 48 | const { data: info } = await client.getCertificateInfo(certificate); 49 | return { info }; 50 | } catch (error) { 51 | return { error }; 52 | } 53 | } 54 | return { info: undefined }; 55 | }; 56 | 57 | export { getConnectionInfo, getUsedConnections, mapSubjectKey, parseSubjectInfo, loadCertificateInfo }; 58 | -------------------------------------------------------------------------------- /frontend/src/admin/clusters/actions/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_CLUSTER = 'UPDATE_CLUSTER'; 2 | export const UPDATE_CLUSTERS = 'UPDATE_CLUSTERS'; 3 | export const UPDATE_CLUSTER_DETAILS = 'UPDATE_CLUSTER_DETAILS'; 4 | -------------------------------------------------------------------------------- /frontend/src/admin/clusters/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './ActionTypes'; 2 | 3 | export function updateCluster(update) { 4 | return { 5 | type: ActionTypes.UPDATE_CLUSTER, 6 | update, 7 | }; 8 | } 9 | 10 | export function updateClusters(update) { 11 | return { 12 | type: ActionTypes.UPDATE_CLUSTERS, 13 | update, 14 | }; 15 | } 16 | 17 | export function updateClusterDetails(update) { 18 | return { 19 | type: ActionTypes.UPDATE_CLUSTER_DETAILS, 20 | update, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/admin/clusters/components/clusterutils.js: -------------------------------------------------------------------------------- 1 | const BROKER_PREFIX = undefined; // process.env.CEDALO_MC_DEV_CLUSTER_NODE_BROKER_PREFIX; 2 | const ADDRESS_PREFIX = undefined; // process.env.CEDALO_MC_DEV_CLUSTER_NODE_ADDRESS_PREFIX; 3 | const concat = (prefix, suffix) => (prefix ? `${prefix}${suffix}` : undefined); 4 | 5 | const SYNCMODES = Object.freeze([ 6 | { 7 | label: 'Full Sync', 8 | value: 'full', 9 | }, 10 | { 11 | label: 'Dynamic Security Sync', 12 | value: 'dynsec', 13 | }, 14 | ]); 15 | 16 | const getSyncModes = () => SYNCMODES; 17 | 18 | const getSyncModeLabel = (syncmode) => { 19 | const modeidx = syncmode === 'dynsec' ? 1 : 0; 20 | return SYNCMODES[modeidx].label; 21 | }; 22 | 23 | const defaultNodeBroker = (nodeid) => concat(BROKER_PREFIX, nodeid); 24 | 25 | const defaultNodeAddress = (nodeid) => concat(ADDRESS_PREFIX, nodeid); 26 | 27 | const generateClusterDetails = async (client, clusters) => { 28 | const clusterDetails = {}; 29 | // it's not ideal to make requests for every cluster, but that's what we currently have 30 | if (clusters && Array.isArray(clusters)) { 31 | for (const cluster of clusters) { 32 | clusterDetails[cluster.clustername] = await client.getCluster(cluster.clustername); 33 | } 34 | } 35 | 36 | return clusterDetails; 37 | }; 38 | 39 | export { getSyncModes, getSyncModeLabel, defaultNodeBroker, defaultNodeAddress, generateClusterDetails }; 40 | -------------------------------------------------------------------------------- /frontend/src/admin/clusters/reducers/clustersReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/ActionTypes'; 2 | 3 | export default function clusters(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_CLUSTER: 7 | newState.cluster = action.update; 8 | break; 9 | case ActionTypes.UPDATE_CLUSTERS: 10 | newState.clusters = action.update; 11 | break; 12 | case ActionTypes.UPDATE_CLUSTER_DETAILS: 13 | newState.clusterDetails = action.update; 14 | break; 15 | default: 16 | } 17 | return newState; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/admin/clusters/utils.js: -------------------------------------------------------------------------------- 1 | export const toClusterConnectionEntries = (clusterDetails) => { 2 | const clusterConnections = {}; 3 | // clusterDetails is a dict of cluternames and their details 4 | // clusterConnections is a dict of brokernames and their respective clusternodes 5 | clusterDetails && 6 | Object.keys(clusterDetails).forEach((clustername) => { 7 | const clusterDetail = clusterDetails[clustername]; 8 | 9 | clusterDetail?.nodes.forEach((node) => { 10 | clusterConnections[node.broker] = { clustername, isLeader: node.leader }; 11 | }); 12 | }); 13 | 14 | return clusterConnections; 15 | }; 16 | 17 | export const allClustersHaveLeaders = (clusterDetails) => { 18 | for (const clusterName in clusterDetails) { 19 | const cluster = clusterDetails[clusterName]; 20 | if (!cluster.nodes.some((node) => node.leader)) { 21 | // if none have preperty leeader set to true 22 | return false; 23 | } 24 | } 25 | return true; 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/admin/clusters/validators.js: -------------------------------------------------------------------------------- 1 | const getNodeIdsUniqueValidator = (nodes) => { 2 | const listOfNodeIds = nodes.map((node) => node.nodeid); 3 | 4 | const areNodeIdsUnique = () => { 5 | return new Set(listOfNodeIds).size === listOfNodeIds.length; 6 | }; 7 | 8 | return areNodeIdsUnique; 9 | }; 10 | 11 | const getPrivateAddressesPresentValidator = (nodes) => { 12 | const listOfNodeAddresses = nodes.map((node) => node.address); 13 | 14 | const arePrivateAddressesPresent = () => { 15 | return listOfNodeAddresses.every((el) => !!el); 16 | }; 17 | 18 | return arePrivateAddressesPresent; 19 | }; 20 | 21 | const getPrivateAddressesUniqueValidator = (nodes) => { 22 | const listOfNodeAddresses = nodes.map((node) => node.address); 23 | 24 | const arePrivateAddressesUnique = () => { 25 | return new Set(listOfNodeAddresses).size === listOfNodeAddresses.length; 26 | }; 27 | 28 | return arePrivateAddressesUnique; 29 | }; 30 | 31 | const getBrokersPresentValidator = (nodes) => { 32 | const listOfNodeBrokers = nodes.map((node) => node.broker); 33 | 34 | const areBrokersPresent = () => { 35 | return listOfNodeBrokers.every((el) => !!el); 36 | }; 37 | 38 | return areBrokersPresent; 39 | }; 40 | 41 | export { 42 | getNodeIdsUniqueValidator, 43 | getPrivateAddressesPresentValidator, 44 | getPrivateAddressesUniqueValidator, 45 | getBrokersPresentValidator, 46 | }; 47 | -------------------------------------------------------------------------------- /frontend/src/admin/connections/components/ConnectionNew.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ConnectionNewComponent from '../../../components/ConnectionNewComponent'; 4 | import ContainerBox from '../../../components/ContainerBox'; 5 | import ContainerBreadCrumbs from '../../../components/ContainerBreadCrumbs'; 6 | import ContainerHeader from '../../../components/ContainerHeader'; 7 | import ContentContainer from '../../../components/ContentContainer'; 8 | 9 | const ConnectionNew = () => { 10 | return ( 11 | 20 | } 21 | > 22 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | connections: state.brokerConnections?.brokerConnections, 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps)(ConnectionNew); 38 | -------------------------------------------------------------------------------- /frontend/src/admin/connections/utils.js: -------------------------------------------------------------------------------- 1 | export const getChangedOrNewConnectionIds = (oldConnections, newConnections) => { 2 | const changedOrNewConnectionIds = []; 3 | newConnections.forEach((newConnection) => { 4 | const oldConnection = oldConnections.find((oldConnection) => oldConnection.id === newConnection.id); 5 | if (!oldConnection || oldConnection.status?.connected !== newConnection.status?.connected) { 6 | changedOrNewConnectionIds.push(newConnection.id); 7 | } 8 | }); 9 | return changedOrNewConnectionIds; 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/src/admin/inspect/actions/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_INSPECT_CLIENT = 'UPDATE_INSPECT_CLIENT'; 2 | export const UPDATE_INSPECT_CLIENTS = 'UPDATE_INSPECT_CLIENTS'; 3 | -------------------------------------------------------------------------------- /frontend/src/admin/inspect/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './ActionTypes'; 2 | 3 | export function updateInspectClient(update) { 4 | return { 5 | type: ActionTypes.UPDATE_INSPECT_CLIENT, 6 | update, 7 | }; 8 | } 9 | 10 | export function updateInspectClients(update) { 11 | return { 12 | type: ActionTypes.UPDATE_INSPECT_CLIENTS, 13 | update, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/admin/inspect/reducers/inspectClientsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/ActionTypes'; 2 | 3 | export default function inspectClients(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_INSPECT_CLIENT: 7 | newState.client = action.update; 8 | break; 9 | case ActionTypes.UPDATE_INSPECT_CLIENTS: 10 | newState.clients = action.update; 11 | break; 12 | default: 13 | } 14 | return newState; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/admin/users/actions/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_USER = 'UPDATE_USER'; 2 | export const UPDATE_USERS = 'UPDATE_USERS'; 3 | export const UPDATE_USER_ROLES = 'UPDATE_USER_ROLES'; 4 | export const UPDATE_USER_GROUPS = 'UPDATE_USER_GROUPS'; 5 | export const UPDATE_USER_GROUP = 'UPDATE_USER_GROUP'; 6 | -------------------------------------------------------------------------------- /frontend/src/admin/users/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './ActionTypes'; 2 | 3 | export function updateUserRoles(update) { 4 | return { 5 | type: ActionTypes.UPDATE_USER_ROLES, 6 | update, 7 | }; 8 | } 9 | 10 | export function updateUser(update) { 11 | return { 12 | type: ActionTypes.UPDATE_USER, 13 | update, 14 | }; 15 | } 16 | 17 | export function updateUsers(update) { 18 | return { 19 | type: ActionTypes.UPDATE_USERS, 20 | update, 21 | }; 22 | } 23 | 24 | export function updateUserGroups(update) { 25 | return { 26 | type: ActionTypes.UPDATE_USER_GROUPS, 27 | update, 28 | }; 29 | } 30 | 31 | export function updateUserGroup(update) { 32 | return { 33 | type: ActionTypes.UPDATE_USER_GROUP, 34 | update, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/admin/users/reducers/userGroupsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/ActionTypes'; 2 | 3 | export default function userGroups(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_USER_GROUPS: 7 | newState.userGroups = action.update; 8 | break; 9 | case ActionTypes.UPDATE_USER_GROUP: 10 | newState.userGroup = action.update; 11 | break; 12 | default: 13 | } 14 | 15 | return newState; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/admin/users/reducers/userRolesReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/ActionTypes'; 2 | 3 | export default function userRoles(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_USER_ROLES: 7 | newState.userRoles = action.update; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/admin/users/reducers/usersReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/ActionTypes'; 2 | 3 | export default function users(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_USER: 7 | newState.user = action.update; 8 | break; 9 | case ActionTypes.UPDATE_USERS: 10 | newState.users = action.update; 11 | break; 12 | default: 13 | } 14 | return newState; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/client/BaseMosquittoProxyClient.js: -------------------------------------------------------------------------------- 1 | const BaseMosquittoProxyClient = require('base-mc-classes').BaseMosquittoProxyClient; 2 | // const { BaseMosquittoProxyClient } = require('../../base/main'); 3 | 4 | console.log('BaseMosquittoProxyClient:', BaseMosquittoProxyClient); 5 | 6 | export default BaseMosquittoProxyClient; 7 | -------------------------------------------------------------------------------- /frontend/src/client/Constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | CREATE_USER: 'create_user', 3 | GET_USER: 'read_user', 4 | UPDATE_USER: 'update_user', 5 | DELETE_USER: 'delete_user', 6 | CREATE_USER_GROUP: 'create_user_group', 7 | GET_USER_GROUP: 'read_user', 8 | UPDATE_USER_GROUP: 'update_user_group', 9 | DELETE_USER_GROUP: 'delete_user_group', 10 | CREATE_ROLE: 'create_role', 11 | GET_ROLE: 'get_role', 12 | UPDATE_ROLE: 'update_role', 13 | DELETE_ROLE: 'delete_role', 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/client/NodeMosquittoProxyClient.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const BaseMosquittoProxyClient = require('base-mc-classes').BaseMosquittoProxyClient; 3 | 4 | module.exports = class NodeMosquittoProxyClient extends BaseMosquittoProxyClient { 5 | constructor( 6 | { name = 'Node Mosquitto Proxy Client', logger } = {}, 7 | { socketEndpointURL, httpEndpointURL } = {}, 8 | headers = undefined 9 | ) { 10 | super({ name, logger }, { socketEndpointURL, httpEndpointURL }, headers); 11 | } 12 | 13 | _connectSocketServer(url, sid = undefined) { 14 | return new Promise((resolve) => { 15 | const ws = new WebSocket(url, [], sid ? { headers: { Cookie: sid } } : undefined); 16 | ws.on('open', () => this._handleOpenedSocketConnection().then(() => resolve(ws))); 17 | ws.on('message', (message) => this._handleSocketMessage(message)); 18 | ws.on('error', (event) => this._handleSocketError(event)); 19 | ws.on('close', (event) => this._handleSocketClose(event)); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/src/client/WebMosquittoProxyClient.js: -------------------------------------------------------------------------------- 1 | import BaseMosquittoProxyClient from './BaseMosquittoProxyClient'; 2 | 3 | export default class WebMosquittoProxyClient extends BaseMosquittoProxyClient { 4 | constructor( 5 | { name = 'Web Mosquitto Proxy Client', logger, defaultListener } = {}, 6 | { socketEndpointURL, httpEndpointURL } = {}, 7 | headers = undefined 8 | ) { 9 | super({ name, logger, defaultListener }, { socketEndpointURL, httpEndpointURL }, headers); 10 | } 11 | 12 | _connectSocketServer(url) { 13 | return new Promise((resolve, reject) => { 14 | const ws = new WebSocket(url); 15 | ws.onopen = () => { 16 | this._handleOpenedSocketConnection().then(() => resolve(ws)); 17 | }; 18 | ws.onmessage = (event) => this._handleSocketMessage(event.data); 19 | ws.onerror = (event) => { 20 | this._handleSocketError(event); 21 | reject(event); 22 | }; 23 | ws.onclose = (event) => this._handleSocketClose(event); 24 | }).catch((error) => { 25 | // this._handleSocketError(error); 26 | throw error; 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/components/ButtonWithLoadingProgress.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import CircularProgress from '@material-ui/core/CircularProgress'; 5 | import { green } from '@material-ui/core/colors'; 6 | import Button from '@material-ui/core/Button'; 7 | import SaveIcon from '@material-ui/icons/Save'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | root: { 11 | display: 'flex', 12 | alignItems: 'center', 13 | }, 14 | wrapper: { 15 | margin: theme.spacing(0.5), 16 | position: 'relative', 17 | '& > *': { 18 | marginRight: theme.spacing(1), 19 | }, 20 | }, 21 | buttonSuccess: { 22 | backgroundColor: green[500], 23 | '&:hover': { 24 | backgroundColor: green[700], 25 | }, 26 | }, 27 | buttonProgress: { 28 | color: green[500], 29 | position: 'absolute', 30 | top: '50%', 31 | left: '25%', 32 | marginTop: -12, 33 | marginLeft: -12, 34 | }, 35 | })); 36 | 37 | const ButtonWithLoadingProgress = (props) => { 38 | const classes = useStyles(); 39 | const [loading, setLoading] = React.useState(false); 40 | const [success, setSuccess] = React.useState(false); 41 | const timer = React.useRef(); 42 | const { onClick, saveDisabled, buttonText } = props; 43 | 44 | const buttonClassname = clsx({ 45 | [classes.buttonSuccess]: success, 46 | }); 47 | 48 | React.useEffect(() => { 49 | return () => { 50 | clearTimeout(timer.current); 51 | }; 52 | }, []); 53 | 54 | const handleButtonClick = async () => { 55 | if (!loading) { 56 | setSuccess(false); 57 | setLoading(true); 58 | try { 59 | await onClick(); 60 | setSuccess(true); 61 | setLoading(false); 62 | } catch (error) { 63 | setSuccess(false); 64 | setLoading(false); 65 | } 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |
72 | 82 | {loading && } 83 |
84 |
85 | ); 86 | }; 87 | 88 | export default ButtonWithLoadingProgress; 89 | -------------------------------------------------------------------------------- /frontend/src/components/Chart.js: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Card, 4 | CardContent, 5 | CardHeader, 6 | Divider, 7 | Typography, 8 | colors, 9 | makeStyles, 10 | useTheme, 11 | } from '@material-ui/core'; 12 | 13 | import { Doughnut } from 'react-chartjs-2'; 14 | import LaptopMacIcon from '@material-ui/icons/LaptopMac'; 15 | import PhoneIcon from '@material-ui/icons/Phone'; 16 | import PropTypes from 'prop-types'; 17 | import React from 'react'; 18 | import TabletIcon from '@material-ui/icons/Tablet'; 19 | import clsx from 'clsx'; 20 | 21 | const useStyles = makeStyles(() => ({ 22 | root: { 23 | height: '100%', 24 | }, 25 | })); 26 | 27 | const Chart = ({ className, title, data, labels, dataDescriptions, ...rest }) => { 28 | const classes = useStyles(); 29 | const theme = useTheme(); 30 | 31 | const options = { 32 | animation: false, 33 | cutoutPercentage: 80, 34 | layout: { padding: 0 }, 35 | legend: { 36 | display: false, 37 | }, 38 | maintainAspectRatio: false, 39 | responsive: true, 40 | tooltips: { 41 | backgroundColor: theme.palette.background.default, 42 | bodyFontColor: theme.palette.text.secondary, 43 | borderColor: theme.palette.divider, 44 | borderWidth: 1, 45 | enabled: true, 46 | footerFontColor: theme.palette.text.secondary, 47 | intersect: false, 48 | mode: 'index', 49 | titleFontColor: theme.palette.text.primary, 50 | }, 51 | }; 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {dataDescriptions.map(({ color, icon: Icon, title, value }) => ( 63 | 64 | 65 | 66 | {title} 67 | 68 | 69 | {value}% 70 | 71 | 72 | ))} 73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | Chart.propTypes = { 80 | className: PropTypes.string, 81 | }; 82 | 83 | export default Chart; 84 | -------------------------------------------------------------------------------- /frontend/src/components/ConnectedWarning.js: -------------------------------------------------------------------------------- 1 | import { Alert, AlertTitle } from '@material-ui/lab'; 2 | import React from 'react'; 3 | import { Link as RouterLink } from 'react-router-dom'; 4 | import Delayed from '../utils/Delayed'; 5 | import { useTheme } from '@material-ui/core/styles'; 6 | 7 | const ConnectedWarning = ({ connected }) => { 8 | const theme = useTheme(); 9 | 10 | if (connected) { 11 | return null; 12 | } 13 | 14 | return ( 15 | 16 | 17 | System information not accessible! 18 | The selected broker connection is not active. Please connect the current broker in the 19 | 20 | Connections List 21 | 22 | or select a connected broker from the connection selection on the title bar. 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default ConnectedWarning; 29 | -------------------------------------------------------------------------------- /frontend/src/components/ConnectionDetail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Redirect } from 'react-router-dom'; 4 | import ConnectionDetailComponent from './ConnectionDetailComponent'; 5 | import ContainerBox from './ContainerBox'; 6 | import ContainerBreadCrumbs from './ContainerBreadCrumbs'; 7 | import ContainerHeader from './ContainerHeader'; 8 | import ContentContainer from './ContentContainer'; 9 | 10 | const ConnectionDetail = (props) => { 11 | const { selectedConnectionToEdit: connection } = props; 12 | 13 | return connection ? ( 14 | 23 | } 24 | > 25 | 29 | 30 | 31 | ) : ( 32 | 33 | ); 34 | }; 35 | 36 | const mapStateToProps = (state) => { 37 | return { 38 | selectedConnectionToEdit: state.brokerConnections?.selectedConnectionToEdit, 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps)(ConnectionDetail); 43 | -------------------------------------------------------------------------------- /frontend/src/components/ContainerBox.js: -------------------------------------------------------------------------------- 1 | import Box from '@material-ui/core/Box'; 2 | import { useTheme } from '@material-ui/core/styles'; 3 | import React from 'react'; 4 | 5 | const ContainerBox = ({ children, dataTour }) => { 6 | const theme = useTheme(); 7 | 8 | return ( 9 | 16 | 23 | {children} 24 | 25 | 26 | ); 27 | }; 28 | export default ContainerBox; 29 | -------------------------------------------------------------------------------- /frontend/src/components/ContainerBreadCrumbs.js: -------------------------------------------------------------------------------- 1 | import Breadcrumbs from '@material-ui/core/Breadcrumbs'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import React from 'react'; 4 | import { Link as RouterLink } from 'react-router-dom'; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | breadcrumbItem: theme.palette.breadcrumbItem, 8 | breadcrumbLink: theme.palette.breadcrumbLink, 9 | })); 10 | 11 | export default function ConnectionBreadCrumbs({ links, title }) { 12 | const classes = useStyles(); 13 | 14 | return ( 15 |
16 | 17 | {links && 18 | links.map((link) => ( 19 | 20 | {link.name} 21 | 22 | ))} 23 | {title} 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/ContainerHeader.js: -------------------------------------------------------------------------------- 1 | import CircularProgress from '@material-ui/core/CircularProgress'; 2 | import { useTheme } from '@material-ui/core/styles'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 5 | import { Alert, AlertTitle } from '@material-ui/lab'; 6 | import React from 'react'; 7 | import ConnectedWarning from './ConnectedWarning'; 8 | 9 | export default function ContainerHeader(props) { 10 | const small = useMediaQuery((theme) => theme.breakpoints.down('xs')); 11 | const theme = useTheme(); 12 | 13 | return ( 14 |
15 | 16 | {props.title} 17 | 18 |
19 | {small ? null : {props.subTitle}} 20 | {props.children ? ( 21 |
29 | {props.children} 30 |
31 | ) : null} 32 |
33 | {props.connectedWarning ? : null} 34 | {props.brokerFeatureWarning ? ( 35 | 36 | Feature is not available 37 | Make sure that the connected broker has {props.brokerFeatureWarning} enabled. 38 | 39 | ) : null} 40 | {props.featureWarning ? ( 41 | 42 | Premium feature 43 | {props.featureWarning} is a premium feature. For more information visit{' '} 44 | 45 | cedalo.com 46 | {' '} 47 | or contact us at{' '} 48 | 49 | info@cedalo.com 50 | 51 | . 52 | 53 | ) : null} 54 | {props.warnings && 55 | !props.featureWarning && 56 | props.warnings()?.map((warning) => ( 57 | 58 | {warning.title} 59 | {warning.error} 60 | 61 | ))} 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/components/ContentContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContainerBox from './ContainerBox'; 3 | import ContainerBreadCrumbs from './ContainerBreadCrumbs'; 4 | 5 | const getHeaderContent = (children) => { 6 | children = children.length ? children : children.props && children.props.children; 7 | const [header, content] = children?.length ? children : [children]; 8 | return { header: content && header, content: content || header }; 9 | }; 10 | const ContentContainer = ({ children, breadCrumbs, dataTour, overFlowX, overFlowY = 'auto' }) => { 11 | // expecting header and content 12 | const { header, content } = getHeaderContent(children); 13 | 14 | return ( 15 | 16 | {breadCrumbs} 17 |
18 |
19 | {header} 20 |
{content}
21 |
22 |
23 |
24 | ); 25 | }; 26 | export default ContentContainer; 27 | -------------------------------------------------------------------------------- /frontend/src/components/FeedbackButton.js: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@material-ui/core'; 2 | import Button from '@material-ui/core/Button'; 3 | import { indigo } from '@material-ui/core/colors'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 7 | import FeedbackIcon from '@material-ui/icons/Feedback'; 8 | import React, { useState, useEffect } from 'react'; 9 | import { connect } from 'react-redux'; 10 | 11 | const ColorButton = withStyles((theme) => ({ 12 | root: { 13 | marginTop: '2px', 14 | // color: 'black', 15 | // backgroundColor: 'white', 16 | '&:hover': { 17 | backgroundColor: 'rgba(0, 0, 0, 0.04)', 18 | }, 19 | }, 20 | }))(IconButton); 21 | 22 | const FeedbackButton = ({ backendParameters }) => { 23 | const [displayFeedback, setDisplayFeedback] = useState(backendParameters.showFeedbackForm); 24 | const small = useMediaQuery((theme) => theme.breakpoints.down('xs')); 25 | 26 | useEffect(() => { 27 | setDisplayFeedback(backendParameters?.showFeedbackForm); 28 | }, [backendParameters]); 29 | 30 | const formPageAddress = 'https://majy33976q6.typeform.com/to/aeRoINk0'; 31 | 32 | return !small && displayFeedback ? ( 33 | 34 | 35 | 36 | 37 | 38 | ) : null; 39 | }; 40 | 41 | const mapStateToProps = (state) => { 42 | return { 43 | backendParameters: state.backendParameters?.backendParameters, 44 | version: state.version?.version, 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps)(FeedbackButton); 49 | -------------------------------------------------------------------------------- /frontend/src/components/FilterName.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2020 Cedalo GmbH 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | * 10 | ********************************************************************************/ 11 | // import PropTypes from 'prop-types'; 12 | import React from 'react'; 13 | import IconSearch from '@material-ui/icons/Search'; 14 | import InputAdornment from '@material-ui/core/InputAdornment'; 15 | import Input from '@material-ui/core/Input'; 16 | 17 | export default class FilterName extends React.Component { 18 | render() { 19 | return ( 20 | this.props.onUpdateFilter(event.target.value)} 25 | startAdornment={ 26 | 27 | `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})`, 31 | } 32 | } 33 | /> 34 | 35 | } 36 | placeholder="Filter" 37 | // sx={{ 38 | // width: '90%', 39 | // color: (theme) => `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})`, 40 | // '&.MuiInput-root': { 41 | // '&:hover': { 42 | // borderWidth: '1px', 43 | // borderColor: (theme) => 44 | // `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})` 45 | // }, 46 | // '&:hover:not(.Mui-disabled):before': { 47 | // borderColor: (theme) => 48 | // `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})` 49 | // } 50 | // }, 51 | // '&.MuiInput-underline:before': { 52 | // // borderColor: (theme) => `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})`, 53 | // // borderWidth: '1px', 54 | // border: 'none' 55 | // }, 56 | // '&.MuiInput-underline:after': { 57 | // borderWidth: '1px', 58 | // borderColor: (theme) => 59 | // `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})` 60 | // }, 61 | // '&.Mui-focused': { 62 | // backgroundColor: (theme) => 63 | // `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor}, 0.1)`, 64 | // borderColor: (theme) => 65 | // `${theme.components.MuiAppBar.styleOverrides.colorIcon.backgroundColor})` 66 | // } 67 | // }} 68 | /> 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/components/InfoButton.js: -------------------------------------------------------------------------------- 1 | import IconButton from '@material-ui/core/IconButton'; 2 | import InfoIcon from '@material-ui/icons/Info'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import Tooltip from '@material-ui/core/Tooltip'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import { useHistory } from 'react-router-dom'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | toolbarButton: { 11 | marginTop: '2px', 12 | color: theme.palette.type === 'dark' ? 'white' : 'rgba(117, 117, 117)', 13 | 14 | // marginBottom: theme.spacing(0.2) 15 | }, 16 | })); 17 | 18 | const InfoButton = () => { 19 | const classes = useStyles(); 20 | const history = useHistory(); 21 | 22 | const onClickInfo = () => { 23 | history.push('/info'); 24 | }; 25 | 26 | return ( 27 | 28 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | InfoButton.propTypes = { 44 | className: PropTypes.string, 45 | }; 46 | 47 | export default InfoButton; 48 | -------------------------------------------------------------------------------- /frontend/src/components/LicenseErrorDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | 10 | const LicenseErrorDialog = ({ license, backendParameters }) => { 11 | const handleClose = () => { 12 | // setOpen(false); 13 | }; 14 | 15 | let error = null; 16 | if (license && license.error) { 17 | error = license.error; 18 | } else if (license && license.integrations?.error) { 19 | error = license.integrations.error; 20 | } else if (license && license.isValid === false) { 21 | if (backendParameters.isPremium) { 22 | error = { 23 | type: 'Invalid License', 24 | message: 'License is invalid, expired, or unavailable', 25 | }; 26 | } 27 | } 28 | 29 | return ( 30 | 36 | 37 | {error?.type} 38 | 39 | 40 | {error?.message} 41 | 42 | 43 | ); 44 | }; 45 | 46 | const mapStateToProps = (state) => { 47 | return { 48 | license: state.license?.license, 49 | backendParameters: state.backendParameters?.backendParameters, 50 | }; 51 | }; 52 | 53 | export default connect(mapStateToProps)(LicenseErrorDialog); 54 | -------------------------------------------------------------------------------- /frontend/src/components/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | 4 | function Logo() { 5 | return ( 6 | 7 | powered by Cedalo AG 8 | 9 | ); 10 | } 11 | 12 | export default Logo; 13 | -------------------------------------------------------------------------------- /frontend/src/components/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { connect, useDispatch } from 'react-redux'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import Tooltip from '@material-ui/core/Tooltip'; 5 | import LogoutIcon from '@material-ui/icons/ExitToApp'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import { useConfirm } from 'material-ui-confirm'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | toolbarButton: { 11 | marginTop: '2px', 12 | color: theme.palette.type === 'dark' ? 'white' : 'rgba(117, 117, 117)', 13 | // marginBottom: theme.spacing(0.2) 14 | }, 15 | })); 16 | 17 | const LogoutButton = (props) => { 18 | const classes = useStyles(); 19 | const confirm = useConfirm(); 20 | 21 | const handleLogout = async () => { 22 | await confirm({ 23 | title: 'Confirm logout', 24 | description: `Do you really want to logout?`, 25 | }); 26 | window.location.href = `${process.env.PUBLIC_URL || ''}/logout`; 27 | }; 28 | 29 | return ( 30 | 31 | handleLogout()} 38 | color="inherit" 39 | className={classes.toolbarButton} 40 | > 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | const mapStateToProps = (state) => { 48 | return {}; 49 | }; 50 | 51 | export default connect(mapStateToProps)(LogoutButton); 52 | -------------------------------------------------------------------------------- /frontend/src/components/MessagePage.js: -------------------------------------------------------------------------------- 1 | import Button from '@material-ui/core/Button'; 2 | import DownloadIcon from '@material-ui/icons/GetApp'; 3 | import Grid from '@material-ui/core/Grid'; 4 | import Paper from '@material-ui/core/Paper'; 5 | import React from 'react'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import { amber } from '@material-ui/core/colors'; 8 | import { connect } from 'react-redux'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | button: { 13 | margin: theme.spacing(1), 14 | }, 15 | updateButton: { 16 | marginLeft: '20px', 17 | }, 18 | badges: { 19 | '& > *': { 20 | margin: theme.spacing(0.3), 21 | }, 22 | }, 23 | breadcrumbItem: theme.palette.breadcrumbItem, 24 | breadcrumbLink: theme.palette.breadcrumbLink, 25 | })); 26 | 27 | const MessagePage = ({ message, buttonText, buttonIcon, callToAction, image = '/smilethink.png' }) => { 28 | const classes = useStyles(); 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | {message} 37 | 38 | 39 | 40 | {buttonText ? ( 41 | 54 | ) : ( 55 | '' 56 | )} 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | const mapStateToProps = (state) => { 64 | return {}; 65 | }; 66 | 67 | export default connect(mapStateToProps)(MessagePage); 68 | -------------------------------------------------------------------------------- /frontend/src/components/NewsCard.js: -------------------------------------------------------------------------------- 1 | import Button from '@material-ui/core/Link'; 2 | import Card from '@material-ui/core/Card'; 3 | import CardActionArea from '@material-ui/core/CardActionArea'; 4 | import CardActions from '@material-ui/core/CardActions'; 5 | import CardContent from '@material-ui/core/CardContent'; 6 | import CardMedia from '@material-ui/core/CardMedia'; 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | const useStyles = makeStyles({ 12 | root: { 13 | maxWidth: 345, 14 | }, 15 | }); 16 | 17 | const NewsCard = ({ title, description, image, link, ...rest }) => { 18 | const classes = useStyles(); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | {title} 27 | 28 | 29 | {description} 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default NewsCard; 43 | -------------------------------------------------------------------------------- /frontend/src/components/NewsletterPopup.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import Swal from 'sweetalert2'; 3 | import withReactContent from 'sweetalert2-react-content'; 4 | import { connect } from 'react-redux'; 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import useFetch from '../helpers/useFetch'; 7 | 8 | import useLocalStorage from '../helpers/useLocalStorage'; 9 | 10 | const NEWSLETTER_POPUP_DELAY = 30000; 11 | 12 | const MySwal = withReactContent(Swal); 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | button: { 16 | margin: theme.spacing(1), 17 | }, 18 | updateButton: { 19 | marginLeft: '20px', 20 | }, 21 | badges: { 22 | '& > *': { 23 | margin: theme.spacing(0.3), 24 | }, 25 | }, 26 | breadcrumbItem: theme.palette.breadcrumbItem, 27 | breadcrumbLink: theme.palette.breadcrumbLink, 28 | })); 29 | 30 | const NewsletterPopup = () => { 31 | const classes = useStyles(); 32 | const [subscribed, setSubscribed] = useLocalStorage('cedalo.managementcenter.subscribedToNewsletter'); 33 | const [showNewsletterPopup, setShowNewsletterPopup] = useLocalStorage( 34 | 'cedalo.managementcenter.showNewsletterPopup' 35 | ); 36 | const [newsletterEndpointResponse, loading, hasError] = useFetch( 37 | `${process.env.PUBLIC_URL}/api/newsletter/subscribe` 38 | ); 39 | 40 | const subscribeNewsletter = async (email) => { 41 | try { 42 | const response = await fetch(`/api/newsletter/subscribe`, { 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | }, 47 | body: JSON.stringify({ 48 | email, 49 | }), 50 | }); 51 | setSubscribed('true'); 52 | } catch (error) { 53 | // TODO: add error handling 54 | console.error(error); 55 | } 56 | }; 57 | 58 | useEffect(() => { 59 | const timer = setTimeout(async () => { 60 | if (newsletterEndpointResponse?.newsletterEndpointAvailable) { 61 | if (subscribed !== 'true' && showNewsletterPopup !== 'false') { 62 | const { value: email, isConfirmed } = await MySwal.fire({ 63 | position: 'bottom-end', 64 | title: 'Want to get all the news?', 65 | input: 'email', 66 | inputLabel: 'Subscribe to our newsletter!', 67 | inputPlaceholder: 'Enter your email address here', 68 | showCancelButton: true, 69 | width: 500, 70 | }); 71 | if (isConfirmed) { 72 | await subscribeNewsletter(email); 73 | setShowNewsletterPopup('false'); 74 | MySwal.fire({ 75 | position: 'bottom-end', 76 | title: 'That worked!', 77 | text: 'You now get all the news for Mosquitto, MQTT and Streamsheets.', 78 | icon: 'success', 79 | width: 500, 80 | }); 81 | } else { 82 | setShowNewsletterPopup('false'); 83 | } 84 | } 85 | } 86 | }, NEWSLETTER_POPUP_DELAY); 87 | return () => clearTimeout(timer); 88 | }, [newsletterEndpointResponse]); 89 | return null; 90 | }; 91 | 92 | const mapStateToProps = (state) => { 93 | return {}; 94 | }; 95 | 96 | export default connect(mapStateToProps)(NewsletterPopup); 97 | -------------------------------------------------------------------------------- /frontend/src/components/ProfileButton.js: -------------------------------------------------------------------------------- 1 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 2 | import React, { useContext, useState } from 'react'; 3 | import { connect, useDispatch } from 'react-redux'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import Tooltip from '@material-ui/core/Tooltip'; 6 | import ProfileIcon from '@material-ui/icons/Person'; 7 | import { makeStyles } from '@material-ui/core/styles'; 8 | import { useConfirm } from 'material-ui-confirm'; 9 | import { useHistory } from 'react-router-dom'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | toolbarButton: { 13 | marginTop: '2px', // theme.spacing(0.8), 14 | color: theme.palette.type === 'dark' ? 'white' : 'rgba(117, 117, 117)', 15 | 16 | // marginBottom: theme.spacing(0.2) 17 | }, 18 | })); 19 | 20 | const ProfileButton = (props) => { 21 | const history = useHistory(); 22 | const classes = useStyles(); 23 | const medium = useMediaQuery((theme) => theme.breakpoints.between('sm', 'sm')); 24 | const small = useMediaQuery((theme) => theme.breakpoints.down('xs')); 25 | 26 | const handleProfile = async () => { 27 | history.push('/profile'); 28 | }; 29 | 30 | if (small || medium) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 36 | handleProfile()} 42 | color="inherit" 43 | id="profile-button" 44 | className={classes.toolbarButton} 45 | > 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | const mapStateToProps = (state) => { 53 | return {}; 54 | }; 55 | 56 | export default connect(mapStateToProps)(ProfileButton); 57 | -------------------------------------------------------------------------------- /frontend/src/components/SaveCancelButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import CircularProgress from '@material-ui/core/CircularProgress'; 5 | import { green } from '@material-ui/core/colors'; 6 | import Button from '@material-ui/core/Button'; 7 | import Fab from '@material-ui/core/Fab'; 8 | import CheckIcon from '@material-ui/icons/Check'; 9 | import SaveIcon from '@material-ui/icons/Save'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | root: { 13 | display: 'flex', 14 | alignItems: 'center', 15 | }, 16 | wrapper: { 17 | marginTop: '15px', 18 | position: 'relative', 19 | '& > *': { 20 | marginRight: theme.spacing(1), 21 | }, 22 | }, 23 | buttonSuccess: { 24 | backgroundColor: green[500], 25 | '&:hover': { 26 | backgroundColor: green[700], 27 | }, 28 | }, 29 | buttonProgress: { 30 | color: green[500], 31 | position: 'absolute', 32 | top: '50%', 33 | left: '25%', 34 | marginTop: -12, 35 | marginLeft: -12, 36 | }, 37 | })); 38 | 39 | const SaveButton = (props) => { 40 | const classes = useStyles(); 41 | const [loading, setLoading] = React.useState(false); 42 | const [success, setSuccess] = React.useState(false); 43 | const timer = React.useRef(); 44 | const { onSave, saveDisabled, onCancel, saveCaption = 'Save' } = props; 45 | 46 | const buttonClassname = clsx({ 47 | [classes.buttonSuccess]: success, 48 | }); 49 | 50 | React.useEffect(() => { 51 | return () => { 52 | clearTimeout(timer.current); 53 | }; 54 | }, []); 55 | 56 | const handleButtonClick = async () => { 57 | if (!loading) { 58 | setSuccess(false); 59 | setLoading(true); 60 | try { 61 | await onSave(); 62 | setSuccess(true); 63 | setLoading(false); 64 | } catch (error) { 65 | setSuccess(false); 66 | setLoading(false); 67 | } 68 | } 69 | }; 70 | 71 | return ( 72 |
73 |
74 | 86 | 96 | {loading && } 97 |
98 |
99 | ); 100 | }; 101 | 102 | export default SaveButton; 103 | -------------------------------------------------------------------------------- /frontend/src/components/SelectList.js: -------------------------------------------------------------------------------- 1 | import Autocomplete from '@material-ui/lab/Autocomplete'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import Checkbox from '@material-ui/core/Checkbox'; 4 | import CheckBoxIcon from '@material-ui/icons/CheckBox'; 5 | import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import React, { useState } from 'react'; 8 | import createStyles from '@material-ui/core/styles/createStyles'; 9 | import makeStyles from '@material-ui/core/styles/makeStyles'; 10 | 11 | const checkedIcon = ; 12 | const icon = ; 13 | 14 | const useStyles = makeStyles((theme) => 15 | createStyles({ 16 | disabled: { 17 | color: theme.palette.text.disabled, 18 | }, 19 | tagSizeSmall: {}, 20 | }) 21 | ); 22 | export default function SelectList({ 23 | values, 24 | onChange, 25 | disabled, 26 | suggestions, 27 | getValue, 28 | getLabel, 29 | variant, 30 | label, 31 | className, 32 | }) { 33 | const [inputValueClients, setInputValueClients] = useState(''); 34 | const classes = useStyles(); 35 | 36 | return ( 37 | (option ? option.label : '')} 47 | getOptionSelected={(option, value) => 48 | option.value === value.value && values.find((val) => value.value === getValue(val)) 49 | } 50 | value={values.map((value) => ({ 51 | label: getLabel ? getLabel(value) : getValue(value), 52 | value: getValue(value), 53 | }))} 54 | onChange={(ev, selection, ...args) => { 55 | selection = selection.filter((option) => !option.disabled); 56 | onChange(ev, selection, ...args); 57 | }} 58 | inputValue={inputValueClients} 59 | onInputChange={(event, newInputValue, reason) => { 60 | if (reason !== 'reset') { 61 | setInputValueClients(newInputValue); 62 | } 63 | }} 64 | renderOption={(option, { selected }) => ( 65 | 66 | option.value === getValue(value))} 72 | disabled={option.disabled} 73 | /> 74 | {option.label} 75 | 76 | )} 77 | renderInput={(params) => ( 78 | 86 | )} 87 | /> 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /frontend/src/components/SnackbarCloseButton.js: -------------------------------------------------------------------------------- 1 | import IconButton from '@material-ui/core/IconButton'; 2 | import CloseIcon from '@material-ui/icons/Close'; 3 | import { useSnackbar } from 'notistack'; 4 | 5 | // used for closing snackbar 6 | 7 | function SnackbarCloseButton({ snackbarKey }) { 8 | const { closeSnackbar } = useSnackbar(); 9 | 10 | return ( 11 | closeSnackbar(snackbarKey)}> 12 | 13 | 14 | ); 15 | } 16 | 17 | export default SnackbarCloseButton; 18 | -------------------------------------------------------------------------------- /frontend/src/components/SortableTablePage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const SortableTablePage = ({ Component, filter }) => { 4 | const [sortDirection, setSortDirection] = useState('asc'); 5 | const [sortBy, setSortBy] = useState(''); 6 | 7 | // const onSort = (property) => { 8 | // const isAsc = sortBy === property && sortDirection === 'asc'; 9 | // setSortDirection(isAsc ? 'desc' : 'asc'); 10 | // setSortBy(property); 11 | // }; 12 | const onSort = (columnId) => { 13 | if (sortBy === columnId) { 14 | if (sortDirection === 'asc') { 15 | setSortDirection('desc'); 16 | } else { 17 | setSortBy(''); 18 | setSortDirection('asc'); 19 | } 20 | } else { 21 | setSortBy(columnId); 22 | setSortDirection('asc'); 23 | } 24 | }; 25 | 26 | const descendingComparator = (a, b, orderByFunc) => { 27 | if (orderByFunc(b) < orderByFunc(a)) { 28 | return -1; 29 | } 30 | if (orderByFunc(b) > orderByFunc(a)) { 31 | return 1; 32 | } 33 | return 0; 34 | }; 35 | 36 | const getComparator = (order, orderByFunc) => { 37 | return order === 'desc' 38 | ? (a, b) => descendingComparator(a, b, orderByFunc) 39 | : (a, b) => -descendingComparator(a, b, orderByFunc); 40 | }; 41 | 42 | const doSort = (list, order, orderByFunc) => { 43 | return list.sort(getComparator(order, orderByFunc)); 44 | }; 45 | 46 | const disableSort = () => { 47 | setSortBy(''); 48 | }; 49 | 50 | return ( 51 | <> 52 | 60 | 61 | ); 62 | }; 63 | 64 | export default SortableTablePage; 65 | -------------------------------------------------------------------------------- /frontend/src/components/StyledTypography.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import createStyles from '@material-ui/core/styles/createStyles'; 4 | import makeStyles from '@material-ui/core/styles/makeStyles'; 5 | 6 | const useStyles = makeStyles((theme) => 7 | createStyles({ 8 | disabled: { 9 | color: theme.palette.text.disabled, 10 | }, 11 | }) 12 | ); 13 | 14 | // currently only disabled is supported... 15 | const StyledTypography = ({ disabled, text = '' }) => { 16 | const classes = useStyles(); 17 | return {text}; 18 | }; 19 | export default StyledTypography; 20 | 21 | // adds disabled class name to children: ... 22 | // export const Disable = ({ disabled, children }) => { 23 | // const classes = useStyles(); 24 | // const disabledClassName = disabled ? classes.disabled : ''; 25 | // return React.Children.map(children, (child) => { 26 | // const className = `${child.props.className} ${disabledClassName}`; 27 | // const props = { ...child.props, className }; 28 | // return React.cloneElement(child, props); 29 | // }) 30 | // }; 31 | -------------------------------------------------------------------------------- /frontend/src/components/UpgradeButton.js: -------------------------------------------------------------------------------- 1 | import Button from '@material-ui/core/Button'; 2 | import { indigo } from '@material-ui/core/colors'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 5 | import UpgradeIcon from '@material-ui/icons/NewReleases'; 6 | import React, { useState, useEffect } from 'react'; 7 | import { connect } from 'react-redux'; 8 | 9 | const ColorButton = withStyles((theme) => ({ 10 | root: { 11 | color: 'black', 12 | backgroundColor: 'white', 13 | '&:hover': { 14 | backgroundColor: indigo[100], 15 | }, 16 | }, 17 | }))(Button); 18 | 19 | const UpgradeButton = ({ license }) => { 20 | const [isTrial, setIsTrial] = useState(license?.plan === 'trial'); 21 | const small = useMediaQuery((theme) => theme.breakpoints.down('xs')); 22 | 23 | useEffect(() => { 24 | setIsTrial(license?.plan === 'trial'); 25 | }, [license]); 26 | 27 | const pricingPageAddress = 28 | 'https://cedalo.com/mqtt-broker-pro-mosquitto/pricing/?product=mosquitto&premises=hosted&billing=annually¤cy=eur&sHA=no_ha&mHA=no_ha&lHA=no_ha&xlHA=no_ha'; 29 | 30 | return !small && isTrial ? ( 31 | <> 32 | } 36 | size="small" 37 | target="_blank" 38 | href={pricingPageAddress} 39 | > 40 | Upgrade Now! 41 | 42 | 43 | ) : ( 44 | <> 45 | ); 46 | }; 47 | 48 | const mapStateToProps = (state) => { 49 | return { 50 | license: state.license?.license, 51 | version: state.version?.version, 52 | }; 53 | }; 54 | 55 | export default connect(mapStateToProps)(UpgradeButton); 56 | -------------------------------------------------------------------------------- /frontend/src/components/WaitDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import Grid from '@material-ui/core/Grid'; 8 | 9 | const WaitDialog = ({ open, title, message, handleClose }) => { 10 | return ( 11 | 17 | 18 | {title} 19 | 20 | 21 | 22 | 23 | {message} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default WaitDialog; 35 | -------------------------------------------------------------------------------- /frontend/src/components/jsoneditor-fix.css: -------------------------------------------------------------------------------- 1 | .jsoneditor { 2 | height: 500px; 3 | border: 1px solid grey; 4 | } 5 | 6 | .jsoneditor-menu { 7 | background-color: grey; 8 | border-bottom: 1px solid grey; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const DELETED_USER = 'DELETED_USER'; 2 | export const UPDATE_BROKER_CONFIGURATIONS = 'UPDATE_BROKER_CONFIGURATIONS'; 3 | export const UPDATE_BROKER_CONNECTED = 'UPDATE_BROKER_CONNECTED'; 4 | export const UPDATE_BROKER_CONNECTIONS = 'UPDATE_BROKER_CONNECTIONS'; 5 | export const UPDATE_PROXY_CONNECTED = 'UPDATE_PROXY_CONNECTED'; 6 | export const UPDATE_WEBSOCKET_CLIENTS = 'UPDATE_WEBSOCKET_CLIENTS'; 7 | export const UPDATE_WEBSOCKET_CLIENT_CONNECTED = 'UPDATE_WEBSOCKET_CLIENT_CONNECTED'; 8 | export const UPDATE_WEBSOCKET_CLIENT_DISCONNECTED = 'UPDATE_WEBSOCKET_CLIENT_DISCONNECTED'; 9 | export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; 10 | export const UPDATE_FEATURES = 'UPDATE_FEATURES'; 11 | export const UPDATE_ANONYMOUS_GROUP = 'UPDATE_ANONYMOUS_GROUP'; 12 | export const UPDATE_GROUP = 'UPDATE_GROUP'; 13 | export const UPDATE_GROUPS = 'UPDATE_GROUPS'; 14 | export const UPDATE_GROUPS_PAGE = 'UPDATE_GRPOUPS_PAGE'; 15 | export const UPDATE_GROUPS_ROWS_PER_PAGE = 'UPDATE_GROUPS_ROWS_PER_PAGE'; 16 | export const UPDATE_GROUPS_ALL = 'UPDATE_GROUPS_ALL'; 17 | export const UPDATE_DEFAULT_ACL_ACESS = 'UPDATE_DEFAULT_ACL_ACESS'; 18 | export const UPDATE_ROLE = 'UPDATE_ROLE'; 19 | export const UPDATE_ROLES = 'UPDATE_ROLES'; 20 | export const UPDATE_ROLES_PAGE = 'UPDATE_ROLES_PAGE'; 21 | export const UPDATE_ROLES_ROWS_PER_PAGE = 'UPDATE_ROLES_ROWS_PER_PAGE'; 22 | export const UPDATE_ROLES_ALL = 'UPDATE_ROLES_ALL'; 23 | export const UPDATE_LICENSE = 'UPDATE_LICENSE'; 24 | export const UPDATE_VERSION = 'UPDATE_VERSION'; 25 | export const UPDATE_SYSTEM_STATUS = 'UPDATE_SYSTEM_STATUS'; 26 | export const UPDATE_TOPIC_TREE = 'UPDATE_TOPIC_TREE'; 27 | export const UPDATE_LICENSE_STATUS = 'UPDATE_LICENSE_STATUS'; 28 | export const UPDATE_CLIENT = 'UPDATE_CLIENT'; 29 | export const UPDATE_CLIENTS = 'UPDATE_CLIENTS'; 30 | export const UPDATE_CLIENTS_PAGE = 'UPDATE_CLIENTS_PAGE'; 31 | export const UPDATE_CLIENTS_ROWS_PER_PAGE = 'UPDATE_CLIENTS_ROWS_PER_PAGE'; 32 | export const UPDATE_CLIENTS_ALL = 'UPDATE_CLIENTS_ALL'; 33 | export const UPDATE_STREAM = 'UPDATE_STREAM'; 34 | export const UPDATE_STREAMS = 'UPDATE_STREAMS'; 35 | export const UPDATE_EDIT_DEFAULT_CLIENT = 'UPDATE_EDIT_DEFAULT_CLIENT'; 36 | export const UPDATE_SELECTED_CONNECTION = 'UPDATE_SELECTED_CONNECTION'; 37 | export const UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE'; 38 | export const UPDATE_BROKER_LICENSE_INFORMATION = 'UPDATE_BROKER_LICENSE_INFORMATION'; 39 | export const UPDATE_TESTCOLLECTION = 'UPDATE_TESTCOLLECTION'; 40 | export const UPDATE_TESTCOLLECTIONS = 'UPDATE_TESTCOLLECTIONS'; 41 | export const UPDATE_TEST = 'UPDATE_TEST'; 42 | export const UPDATE_TESTS = 'UPDATE_TESTS'; 43 | export const UPDATE_APPLICATION_TOKENS = 'UPDATE_APPLICATION_TOKENS'; 44 | export const UPDATE_LOADING = 'UPDATE_LOADING'; 45 | export const UPDATE_BACKEND_PARAMETERS = 'UPDATE_BACKEND_PARAMETERS'; 46 | -------------------------------------------------------------------------------- /frontend/src/helpers/useConfirmDialog.js: -------------------------------------------------------------------------------- 1 | import { useConfirm } from 'material-ui-confirm'; 2 | 3 | const CANCEL_CONFIRM_OPTS = { 4 | confirmationText: 'Yes, cancel', 5 | cancellationText: 'No', 6 | }; 7 | 8 | // simple wrapper around useConfirm to create specialized confirm dialogs 9 | // NOTE: use to set global confirm options 10 | export const useConfirmDialog = (defOpts) => { 11 | const confirm = useConfirm(); 12 | return (opts) => confirm({ ...defOpts, ...opts }); 13 | }; 14 | 15 | export const useConfirmCancel = () => useConfirmDialog(CANCEL_CONFIRM_OPTS); 16 | -------------------------------------------------------------------------------- /frontend/src/helpers/useFetch.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const LOGIN_ENDPOINT = '/login?error=session-expired'; 4 | 5 | export default function useFetch(url, opts) { 6 | const [response, setResponse] = useState(null); 7 | const [loading, setLoading] = useState(false); 8 | const [hasError, setHasError] = useState(false); 9 | 10 | useEffect(() => { 11 | setLoading(true); 12 | fetch(url, opts) 13 | .then((response) => { 14 | if (!response.ok) { 15 | throw response; 16 | } 17 | return response; 18 | }) 19 | .then(async (response) => { 20 | const json = await response.json(); 21 | setResponse(json); 22 | setLoading(false); 23 | }) 24 | .catch((errorResponse) => { 25 | errorResponse 26 | .json() 27 | .then((errorData) => { 28 | if ( 29 | errorResponse.status === 401 && 30 | errorData.code === 'UNAUTHORIZED' && 31 | !errorData.data?.session 32 | ) { 33 | // If the session has expired, redirect to login 34 | console.error('Session has expired or is invalid'); 35 | window.location.href = (process.env.PUBLIC_URL || '') + LOGIN_ENDPOINT; 36 | } else { 37 | console.error(errorData); 38 | } 39 | }) 40 | .catch((error) => { 41 | console.error('useFetch:', error); 42 | }); 43 | setHasError(true); 44 | setLoading(false); 45 | }); 46 | }, [url]); 47 | return [response, loading, hasError]; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/helpers/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { Dispatch, useCallback, useEffect, useState } from 'react'; 2 | 3 | export default function useLocalStorage(key, initialValue = '') { 4 | const [value, setValue] = useState(() => window.localStorage.getItem(key) || initialValue); 5 | 6 | const setItem = (newValue) => { 7 | setValue(newValue); 8 | window.localStorage.setItem(key, newValue); 9 | }; 10 | 11 | useEffect(() => { 12 | const newValue = window.localStorage.getItem(key); 13 | if (value !== newValue) { 14 | setValue(newValue || initialValue); 15 | } 16 | }); 17 | 18 | const handleStorage = useCallback( 19 | (event) => { 20 | if (event.key === key && event.newValue !== value) { 21 | setValue(event.newValue || initialValue); 22 | } 23 | }, 24 | [value] 25 | ); 26 | 27 | useEffect(() => { 28 | window.addEventListener('storage', handleStorage); 29 | return () => window.removeEventListener('storage', handleStorage); 30 | }, [handleStorage]); 31 | 32 | return [value, setItem]; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/helpers/utils.js: -------------------------------------------------------------------------------- 1 | const getBrokerById = (brokerConnections, id) => 2 | brokerConnections.find((brokerConnection) => brokerConnection.id === id); 3 | 4 | const getIsAdminClient = (defaultClient) => { 5 | const defClientUsername = defaultClient?.username; 6 | // default client or its username not always defined 7 | return (client) => (defClientUsername ? defClientUsername === client.username : client.username === 'admin'); 8 | }; 9 | 10 | const getAdminRoles = (defaultClient, clients) => { 11 | const isAdmin = getIsAdminClient(defaultClient); 12 | clients = clients || []; 13 | const adminClient = clients.find(isAdmin); 14 | return adminClient ? adminClient.roles.map((r) => r.rolename) : []; 15 | }; 16 | 17 | export { getIsAdminClient, getAdminRoles, getBrokerById }; 18 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | // import CssBaseline from '@material-ui/core/CssBaseline'; 5 | // import { ThemeProvider } from '@material-ui/core/styles'; 6 | import App from './App'; 7 | // import theme from './theme'; 8 | 9 | ReactDOM.render(, document.querySelector('#root')); 10 | -------------------------------------------------------------------------------- /frontend/src/reducers/applicationTokensReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function applicationTokens(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_APPLICATION_TOKENS: 7 | newState.tokens = action.update; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reducers/backendParametersReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function packendParameters(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_BACKEND_PARAMETERS: 7 | newState.backendParameters = action.update; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reducers/brokerConfigurationsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function brokerConfigurations(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_BROKER_CONFIGURATIONS: 7 | newState.brokerConfigurations = action.update; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reducers/brokerConnectionsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function brokerConnections(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_BROKER_CONNECTED: 7 | newState.connected = action.update.connected; 8 | newState.currentConnectionName = action.update.connectionName; 9 | break; 10 | case ActionTypes.UPDATE_BROKER_CONNECTIONS: 11 | newState.brokerConnections = action.update; 12 | 13 | // TODO: figure out if it is necessary to also update selectedConnectionToEdit when updating all broker conenctions 14 | // TODO contunue: because technically a user can have a tab where only selectedConnectionToEdit is mapped to props open when update of all the connections is initiated (I belive this happens in connectionDetailComponent) 15 | // if (newState.selectedConnectionToEdit) { 16 | // newState.brokerConnections.forEach(el => { 17 | // if (newState.selectedConnectionToEdit.id === el.id) { 18 | // newState.selectedConnectionToEdit = JSON.parse(JSON.stringify(el)); 19 | // } 20 | // }) 21 | // } 22 | 23 | break; 24 | case ActionTypes.UPDATE_EDIT_DEFAULT_CLIENT: 25 | newState.editDefaultClient = action.edit; 26 | break; 27 | case ActionTypes.UPDATE_SELECTED_CONNECTION: 28 | newState.selectedConnectionToEdit = action.update; 29 | break; 30 | default: 31 | // console.log('DEFAULT called!!!', action.type); 32 | } 33 | // only fire this whenever it's one of the following actions. because this function is normally invoked on every state change 34 | // and it would be wasteful to change current broker connection every time. 35 | if ( 36 | [ 37 | ActionTypes.UPDATE_BROKER_CONNECTED, 38 | ActionTypes.UPDATE_EDIT_DEFAULT_CLIENT, 39 | ActionTypes.UPDATE_SELECTED_CONNECTION, 40 | ].includes(action.type) 41 | ) { 42 | if (newState.currentConnectionName && newState.brokerConnections) { 43 | newState.currentConnection = newState.brokerConnections?.find((brokerConnection) => { 44 | return brokerConnection.name === newState.currentConnectionName; 45 | }); 46 | newState.defaultClient = { 47 | username: newState.currentConnection?.credentials?.username, 48 | }; 49 | } 50 | } 51 | 52 | return newState; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/reducers/brokerLicenseReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function brokerLicense(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_BROKER_LICENSE_INFORMATION: 7 | newState.license = action.update; 8 | if (newState.license) { 9 | newState.isLoading = false; 10 | } else { 11 | newState.isLoading = true; 12 | } 13 | break; 14 | default: 15 | } 16 | return newState; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/reducers/clientsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function clients(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_CLIENT: 7 | newState.client = action.update; 8 | break; 9 | case ActionTypes.UPDATE_CLIENTS: 10 | newState.clients = action.update; 11 | break; 12 | case ActionTypes.UPDATE_CLIENTS_ALL: 13 | newState.clientsAll = action.update; 14 | break; 15 | case ActionTypes.UPDATE_CLIENTS_ROWS_PER_PAGE: 16 | newState.rowsPerPage = action.update; 17 | break; 18 | case ActionTypes.UPDATE_CLIENTS_PAGE: 19 | newState.page = action.update; 20 | break; 21 | default: 22 | } 23 | return newState; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/reducers/groupsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function groups(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_ANONYMOUS_GROUP: 7 | newState.anonymousGroup = action.update; 8 | break; 9 | case ActionTypes.UPDATE_GROUP: 10 | newState.group = action.update; 11 | break; 12 | case ActionTypes.UPDATE_GROUPS: 13 | newState.groups = action.update; 14 | break; 15 | case ActionTypes.UPDATE_GROUPS_ALL: 16 | newState.groupsAll = action.update; 17 | break; 18 | case ActionTypes.UPDATE_GROUPS_ROWS_PER_PAGE: 19 | newState.rowsPerPage = action.update; 20 | break; 21 | case ActionTypes.UPDATE_GROUPS_PAGE: 22 | newState.page = action.update; 23 | break; 24 | default: 25 | } 26 | return newState; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/reducers/licenseReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function license(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_LICENSE: 7 | newState.license = action.update; 8 | break; 9 | case ActionTypes.UPDATE_LICENSE_STATUS: 10 | newState.licenseStatus = action.update; 11 | break; 12 | default: 13 | } 14 | return newState; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/reducers/loadingReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function loading(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_LOADING: 7 | newState.loadingStatus = action.update.loadingStatus; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reducers/proxyConnectionReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function proxyConnection(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_PROXY_CONNECTED: 7 | newState.connected = action.update.connected; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reducers/rolesReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function roles(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_DEFAULT_ACL_ACESS: 7 | newState.defaultACLAccess = action.update; 8 | break; 9 | case ActionTypes.UPDATE_ROLE: 10 | newState.role = action.update; 11 | break; 12 | case ActionTypes.UPDATE_ROLES: 13 | newState.roles = action.update; 14 | break; 15 | case ActionTypes.UPDATE_ROLES_ALL: 16 | newState.rolesAll = action.update; 17 | break; 18 | case ActionTypes.UPDATE_ROLES_ROWS_PER_PAGE: 19 | newState.rowsPerPage = action.update; 20 | break; 21 | case ActionTypes.UPDATE_ROLES_PAGE: 22 | newState.page = action.update; 23 | break; 24 | default: 25 | } 26 | return newState; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/reducers/settingsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function settings(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_SETTINGS: 7 | newState.settings = action.update; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reducers/streamsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function streams(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_STREAM: 7 | newState.stream = action.update; 8 | break; 9 | case ActionTypes.UPDATE_STREAMS: 10 | newState.streams = action.update; 11 | break; 12 | default: 13 | } 14 | return newState; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/reducers/systemStatusReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | // TODO: quick fix, remove if this is fixed on server-side 4 | let currentTopicTreeConnectionName; 5 | 6 | export default function systemStatus(state = {}, action) { 7 | const newState = { ...state }; 8 | switch (action.type) { 9 | case ActionTypes.UPDATE_SYSTEM_STATUS: 10 | // Quick fix: only update if selected topic tree is the same 11 | if (state.systemStatus === undefined || currentTopicTreeConnectionName === action.update._name) { 12 | newState.systemStatus = action.update; 13 | newState.lastUpdated = Date.now(); 14 | } 15 | break; 16 | case ActionTypes.UPDATE_BROKER_CONNECTED: 17 | currentTopicTreeConnectionName = action.update.connectionName; 18 | break; 19 | case ActionTypes.UPDATE_FEATURES: 20 | newState.features = newState.features || {}; 21 | 22 | newState.features[action.update.feature] = { 23 | // TODO: Quick hack to detect whether feature is supported 24 | supported: 25 | action.update.status?.message?.includes('Client: Timeout') || 26 | action.update.status?.includes('Client: Timeout') || 27 | action.update.error 28 | ? false 29 | : true, 30 | error: action.update.error, 31 | }; 32 | break; 33 | default: 34 | } 35 | return newState; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/reducers/testsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function tests(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_TESTCOLLECTION: 7 | newState.testCollection = action.update; 8 | break; 9 | case ActionTypes.UPDATE_TESTCOLLECTIONS: 10 | newState.testCollections = action.update; 11 | break; 12 | case ActionTypes.UPDATE_TEST: 13 | newState.test = action.update; 14 | break; 15 | case ActionTypes.UPDATE_TESTS: 16 | newState.tests = action.update; 17 | break; 18 | default: 19 | } 20 | return newState; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/reducers/topicTreeReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | // TODO: quick fix, remove if this is fixed on server-side 4 | let currentTopicTreeConnectionName; 5 | 6 | export default function topicTreeReducer(state = {}, action) { 7 | const newState = { ...state }; 8 | switch (action.type) { 9 | case ActionTypes.UPDATE_TOPIC_TREE: 10 | // Quick fix: only update if selected topic tree is the same 11 | 12 | if (state.topicTree === undefined || currentTopicTreeConnectionName === action.update?._name) { 13 | newState.topicTree = action.update; 14 | newState.lastUpdated = Date.now(); 15 | } 16 | break; 17 | case ActionTypes.UPDATE_BROKER_CONNECTED: 18 | currentTopicTreeConnectionName = action.update.connectionName; 19 | break; 20 | default: 21 | } 22 | return newState; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/reducers/userProfileReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | const ADMIN_ROLE = 'admin'; 4 | const EDITOR_ROLE = 'editor'; 5 | const VIEW_ROLE = 'viewer'; 6 | 7 | export default function userProfileReducer(state = {}, action) { 8 | const newState = { ...state }; 9 | switch (action.type) { 10 | case ActionTypes.UPDATE_USER_PROFILE: 11 | newState.userProfile = action.update; 12 | newState.userProfile.isAdmin = newState.userProfile.roles.includes(ADMIN_ROLE); 13 | newState.userProfile.isEditor = newState.userProfile.roles.includes(EDITOR_ROLE); 14 | newState.userProfile.isViewer = newState.userProfile.roles.includes(VIEW_ROLE); 15 | 16 | if (!newState.userProfile.connections) { 17 | break; 18 | } 19 | 20 | for (const connection of newState.userProfile.connections) { 21 | connection.isAdmin = connection.role === ADMIN_ROLE; 22 | connection.isEditor = connection.role === EDITOR_ROLE; 23 | connection.isViewer = connection.role === VIEW_ROLE; 24 | } 25 | break; 26 | default: 27 | } 28 | return newState; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/reducers/versionsReducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default function version(state = {}, action) { 4 | const newState = { ...state }; 5 | switch (action.type) { 6 | case ActionTypes.UPDATE_VERSION: 7 | newState.version = action.update; 8 | break; 9 | default: 10 | } 11 | return newState; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/store.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { createStore, combineReducers } from 'redux'; 3 | import brokerConfigurationsReducer from './reducers/brokerConfigurationsReducer'; 4 | import brokerConnectionsReducer from './reducers/brokerConnectionsReducer'; 5 | import proxyConnectionReducer from './reducers/proxyConnectionReducer'; 6 | import groupsReducer from './reducers/groupsReducer'; 7 | import licenseReducer from './reducers/licenseReducer'; 8 | import versionsReducer from './reducers/versionsReducer'; 9 | import rolesReducer from './reducers/rolesReducer'; 10 | import streamsReducer from './reducers/streamsReducer'; 11 | import systemStatusReducer from './reducers/systemStatusReducer'; 12 | import topicTreeReducer from './reducers/topicTreeReducer'; 13 | import clientsReducer from './reducers/clientsReducer'; 14 | import userProfileReducer from './reducers/userProfileReducer'; 15 | import settingsReducer from './reducers/settingsReducer'; 16 | import brokerLicenseReducer from './reducers/brokerLicenseReducer'; 17 | import testsReducer from './reducers/testsReducer'; 18 | import applicationTokensReducer from './reducers/applicationTokensReducer'; 19 | import backendParametersReducer from './reducers/backendParametersReducer'; 20 | import loadingReducer from './reducers/loadingReducer'; 21 | 22 | import userGroupsReducer from './admin/users/reducers/userGroupsReducer'; 23 | import userRolesReducer from './admin/users/reducers/userRolesReducer'; 24 | import usersReducer from './admin/users/reducers/usersReducer'; 25 | import clustersReducer from './admin/clusters/reducers/clustersReducer'; 26 | import inspectClientsReducer from './admin/inspect/reducers/inspectClientsReducer'; 27 | // import bridgesReducer from './admin/cloud/reducers/bridgesReducer'; 28 | 29 | const store = createStore( 30 | combineReducers({ 31 | brokerConfigurations: brokerConfigurationsReducer, 32 | brokerConnections: brokerConnectionsReducer, 33 | proxyConnection: proxyConnectionReducer, 34 | groups: groupsReducer, 35 | userGroups: userGroupsReducer, 36 | license: licenseReducer, 37 | version: versionsReducer, 38 | roles: rolesReducer, 39 | settings: settingsReducer, 40 | streams: streamsReducer, 41 | systemStatus: systemStatusReducer, 42 | topicTree: topicTreeReducer, 43 | clients: clientsReducer, 44 | userRoles: userRolesReducer, 45 | userProfile: userProfileReducer, 46 | users: usersReducer, 47 | clusters: clustersReducer, 48 | inspectClients: inspectClientsReducer, 49 | brokerLicense: brokerLicenseReducer, 50 | tests: testsReducer, 51 | tokens: applicationTokensReducer, 52 | loading: loadingReducer, 53 | backendParameters: backendParametersReducer, 54 | // bridges: bridgesReducer 55 | }) 56 | ); 57 | 58 | export default store; 59 | -------------------------------------------------------------------------------- /frontend/src/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | 3 | export const useFormStyles = makeStyles((theme) => ({ 4 | textField: { 5 | maxWidth: '75ch', 6 | }, 7 | autoComplete: { 8 | maxWidth: '75ch', 9 | }, 10 | buttonTop: { 11 | marginTop: '15px', 12 | }, 13 | buttonTopRight: { 14 | marginTop: '15px', 15 | marginRight: '15px', 16 | }, 17 | })); 18 | -------------------------------------------------------------------------------- /frontend/src/theme-dark.js: -------------------------------------------------------------------------------- 1 | import { red } from '@material-ui/core/colors'; 2 | import { createTheme } from '@material-ui/core/styles'; 3 | 4 | const theme = createTheme({}); 5 | 6 | export default theme; 7 | -------------------------------------------------------------------------------- /frontend/src/utils/Delayed.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const Delayed = ({ children, waitBeforeShow = 500 }) => { 4 | const [isShown, setIsShown] = useState(false); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setIsShown(true); 9 | }, waitBeforeShow); 10 | return () => clearTimeout(timer); 11 | }, [waitBeforeShow]); 12 | 13 | return isShown ? children : null; 14 | }; 15 | 16 | export default Delayed; 17 | -------------------------------------------------------------------------------- /frontend/src/utils/accessUtils/access.js: -------------------------------------------------------------------------------- 1 | const adminOrHigher = (x) => x?.isAdmin; 2 | const editorOrHigher = (x) => x?.isAdmin || x?.isEditor; 3 | const viewerOrHigher = (x) => x?.isAdmin || x?.isEditor || x?.isViewer; 4 | 5 | export const isConnectionAllowed = (userProfile, currentConnectionName, permissionFunction) => { 6 | if (!userProfile || !currentConnectionName) return undefined; 7 | if (typeof userProfile === 'object' && Object.keys(userProfile).length === 0) return undefined; // check if userProfile is an empty object 8 | if (typeof currentConnectionName === 'object' && Object.keys(currentConnectionName).length === 0) return undefined; 9 | 10 | if (!userProfile.connections) { 11 | // if license not present, connections array is not injected into user object 12 | return undefined; 13 | } 14 | 15 | for (const connection of userProfile.connections) { 16 | if (connection.name === currentConnectionName) { 17 | return permissionFunction(connection); 18 | } 19 | } 20 | 21 | return undefined; 22 | }; 23 | 24 | export const atLeastAdmin = (userProfile, currentConnectionName) => { 25 | const connectionAllowed = isConnectionAllowed(userProfile, currentConnectionName, adminOrHigher); 26 | 27 | return connectionAllowed === undefined ? adminOrHigher(userProfile) : connectionAllowed; 28 | }; 29 | 30 | export const atLeastEditor = (userProfile, currentConnectionName) => { 31 | const connectionAllowed = isConnectionAllowed(userProfile, currentConnectionName, editorOrHigher); 32 | 33 | return connectionAllowed === undefined ? editorOrHigher(userProfile) : connectionAllowed; 34 | }; 35 | 36 | export const atLeastViewer = (userProfile, currentConnectionName) => { 37 | const connectionAllowed = isConnectionAllowed(userProfile, currentConnectionName, viewerOrHigher); 38 | 39 | return connectionAllowed === undefined ? viewerOrHigher(userProfile) : connectionAllowed; 40 | }; 41 | 42 | export const isGroupMember = (userProfile) => { 43 | return ( 44 | userProfile?.connections && 45 | typeof userProfile.connections === 'object' && 46 | Object.keys(userProfile.connections).length !== 0 47 | ); 48 | }; 49 | 50 | // export const isMemberOfAdminGroups = (userProfile) => { 51 | // if (isGroupMember(userProfile)) { 52 | // for (const connection of userProfile.connections) { 53 | // if (adminOrHigher(connection)) { 54 | // return true; 55 | // } 56 | // } 57 | // } 58 | // return false; 59 | // }; 60 | -------------------------------------------------------------------------------- /frontend/src/utils/utils.js: -------------------------------------------------------------------------------- 1 | export const trimString = (value) => { 2 | return typeof value === 'string' ? value.trim() : value; 3 | }; 4 | 5 | export const isAdminOpen = () => { 6 | return ( 7 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/user-groups`) || 8 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/tokens`) || 9 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/info`) || 10 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/users`) || 11 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/settings`) 12 | ); 13 | }; 14 | export const showConnections = () => { 15 | return !( 16 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/user-groups`) || 17 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/tokens`) || 18 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/info`) || 19 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/users`) || 20 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/certs`) || 21 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/connections`) || 22 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/clusters`) || 23 | location.pathname.startsWith(`${process.env.PUBLIC_URL || ''}/settings`) 24 | ); 25 | }; 26 | 27 | export const getHelpBasePath = () => { 28 | return 'https://docs.cedalo.com/'; 29 | }; 30 | 31 | const splitByFirstOccurence = (text, separator) => { 32 | const index = text.indexOf(separator); 33 | if (index === -1) { 34 | return [text]; 35 | } 36 | return [text.slice(0, index), text.slice(index + 1)]; 37 | }; 38 | 39 | export const parseUrl = (url) => { 40 | if (typeof url !== 'string') { 41 | throw new Error('Invalid URL'); 42 | } 43 | if (url === '') { 44 | throw new Error('Invalid URL'); 45 | } 46 | const parsedUrl = {}; 47 | 48 | let parts = url.split('://'); 49 | 50 | if (parts.length === 2) { 51 | parsedUrl.protocol = parts[0]; 52 | } else if (parts.length === 1) { 53 | parsedUrl.protocol = ''; 54 | } else { 55 | throw new Error('Invalid URL'); 56 | } 57 | let rest = parts[1] || parts[0]; 58 | 59 | parts = splitByFirstOccurence(rest, '/'); 60 | 61 | if (parts.length === 2) { 62 | parsedUrl.host = parts[0]; 63 | parsedUrl.path = parts[1]; 64 | } else { 65 | parsedUrl.host = parts[0]; 66 | parsedUrl.path = ''; 67 | } 68 | 69 | parts = parsedUrl.path.split('?'); 70 | 71 | if (parts.length === 2) { 72 | parsedUrl.path = parts[0]; 73 | parsedUrl.query = parts[1]; 74 | } else if (parts.length === 1) { 75 | parsedUrl.query = ''; 76 | } else { 77 | throw new Error('Invalid URL'); 78 | } 79 | 80 | parts = parsedUrl.host.split(':'); 81 | 82 | if (parts.length === 2) { 83 | parsedUrl.host = parts[0]; 84 | parsedUrl.port = parts[1]; 85 | if ( 86 | !parseInt(parsedUrl.port) || 87 | parseInt(parsedUrl.port) < 0 || 88 | parseInt(parsedUrl.port) > 65535 || 89 | parsedUrl.port.length > ('' + parseInt(parsedUrl.port)).length 90 | ) { 91 | throw new Error('Invalid URL'); 92 | } 93 | } else if (parts.length === 1) { 94 | parsedUrl.port = ''; 95 | } else { 96 | throw new Error('Invalid URL'); 97 | } 98 | 99 | return parsedUrl; 100 | }; 101 | -------------------------------------------------------------------------------- /frontend/src/websockets/config.js: -------------------------------------------------------------------------------- 1 | const { protocol } = window.location; 2 | const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:'; 3 | const path = `${process.env.PUBLIC_URL && process.env.PUBLIC_URL !== '.' ? process.env.PUBLIC_URL : ''}`; 4 | 5 | export default { 6 | // url: `${wsProtocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}${process.env.PUBLIC_URL ? process.env.PUBLIC_URL : ''}` 7 | url: `${wsProtocol}//${window.location.host}${path}`, 8 | urlHTTP: `${protocol}//${window.location.host}${path}`, 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/start.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | 5 | app.use(express.static(path.join(__dirname, 'build'))); 6 | 7 | app.get('/api/config', (request, response) => { 8 | response.sendFile(path.join(__dirname, '..', 'config', 'config.json')); 9 | }); 10 | 11 | app.get('/', (request, response) => { 12 | response.sendFile(path.join(__dirname, 'build', 'index.html')); 13 | }); 14 | 15 | app.get('/*', (request, response) => { 16 | response.sendFile(path.join(__dirname, 'build', 'index.html')); 17 | }); 18 | 19 | app.listen(9000); 20 | -------------------------------------------------------------------------------- /frontend/tests/client.example.js: -------------------------------------------------------------------------------- 1 | const NodeMosquittoProxyClient = require('../src/client/NodeMosquittoProxyClient'); 2 | 3 | const MOSQUITTO_PROXY_URL = process.env.MOSQUITTO_PROXY_URL || 'ws://localhost'; 4 | const MOSQUITTO_PROXY_PORT = process.env.MOSQUITTO_PROXY_PORT || 8088; 5 | 6 | (async () => { 7 | const client = new NodeMosquittoProxyClient({ 8 | /* logger: console */ 9 | }); 10 | try { 11 | await client.connect({ socketEndpointURL: `${MOSQUITTO_PROXY_URL}:${MOSQUITTO_PROXY_PORT}` }); 12 | await client.connectToBroker('Mosquitto 2.0 Preview'); 13 | // await client.connectToBroker('Mosquitto 2.0 Mock API'); 14 | console.log('connected'); 15 | const usersBefore = await client.listUsers(); 16 | console.log(usersBefore); 17 | const groupsBefore = await client.listGroups(); 18 | console.log(groupsBefore); 19 | // process.exit(0) 20 | 21 | try { 22 | const response = await client.addUser( 23 | 'streamsheets', 24 | 'secret', 25 | 'streamsheets', 26 | '', 27 | 'Cedalo Sheets', 28 | 'The best software for integrating things.' 29 | ); 30 | console.log(response); 31 | console.log('connected'); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | await client.addGroup('software', '', 'Software', 'Software connected to Mosquitto.'); 36 | await client.addGroup('sensors', '', 'Sensors', 'Sensors connected to Mosquitto.'); 37 | await client.addGroup('hall1', '', 'Factory hall 1', 'Sensors in factory hall one.'); 38 | await client.addGroup('hall2', '', 'Factory hall 2', 'Sensors in factory hall two.'); 39 | await client.addGroup('hall3', '', 'Factory hall 3', 'Sensors in factory hall three.'); 40 | 41 | await client.addUser('node-red', 'secret', 'nodered', '', 'Node-RED', 'A software for integrating things.'); 42 | await client.addUser('n8n', 'secret', 'n8n', '', 'n8n.io', 'A software for integrating things.'); 43 | await client.addUser('temp-1', 'secret', 'sensor_1', '', ' Temperature Sensor', 'A sensor for temperature.'); 44 | await client.addUser('hum-1', 'secret', 'sensor_2', '', 'Humidity Sensor', 'A sensor for humidity.'); 45 | await client.addUser( 46 | 'temp-2', 47 | 'secret', 48 | 'sensor_3', 49 | '', 50 | 'Temperature Sensor', 51 | 'Another sensor for temperature.' 52 | ); 53 | 54 | await client.addUserToGroup('streamsheets', 'software'); 55 | await client.addUserToGroup('node-red', 'software'); 56 | await client.addUserToGroup('n8n', 'software'); 57 | 58 | await client.addUserToGroup('temp-1', 'sensors'); 59 | await client.addUserToGroup('temp-1', 'hall1'); 60 | await client.addUserToGroup('hum-1', 'sensors'); 61 | await client.addUserToGroup('hum-1', 'hall2'); 62 | await client.addUserToGroup('temp-2', 'sensors'); 63 | await client.addUserToGroup('temp-2', 'hall3'); 64 | 65 | // await client.deleteUserFromGroup('user1', 'sensors'); 66 | // await client.setUserPassword('user5', 'secretNew'); 67 | // await client.deleteUser('user2'); 68 | const users = await client.listUsers(); 69 | console.log(users); 70 | const groups = await client.listGroups(); 71 | console.log(groups); 72 | // const addGroupResponse = await client.addGroup('default'); 73 | // console.log('added group'); 74 | } catch (error) { 75 | console.error(error); 76 | } 77 | })(); 78 | -------------------------------------------------------------------------------- /frontend/tests/multiple-client.example.js: -------------------------------------------------------------------------------- 1 | const NodeMosquittoProxyClient = require('../src/client/NodeMosquittoProxyClient'); 2 | 3 | const MOSQUITTO_PROXY_URL = process.env.MOSQUITTO_PROXY_URL || 'ws://localhost'; 4 | const MOSQUITTO_PROXY_PORT = process.env.MOSQUITTO_PROXY_PORT || 8088; 5 | 6 | async () => { 7 | const client1 = new NodeMosquittoProxyClient({ 8 | /* logger: console */ 9 | }); 10 | const client2 = new NodeMosquittoProxyClient({ 11 | /* logger: console */ 12 | }); 13 | const client3 = new NodeMosquittoProxyClient({ 14 | /* logger: console */ 15 | }); 16 | try { 17 | await client1.connect({ socketEndpointURL: `${MOSQUITTO_PROXY_URL}:${MOSQUITTO_PROXY_PORT}` }); 18 | await client1.connectToBroker('Mosquitto 1'); 19 | // await client.disconnectFromBroker('Mosquitto 1'); 20 | 21 | // client1.on('system_status', (message) => { 22 | // console.log(message); 23 | // }); 24 | // client1.on('topic_tree', (message) => { 25 | // console.log('Client 1'); 26 | // console.log(message); 27 | // }); 28 | const addUserResponse = await client1.addUser('maxmustermann', 'secret', '1234567'); 29 | const addGroupResponse = await client1.addGroup('default'); 30 | const connections = await client1.getBrokerConnections(); 31 | console.log(connections); 32 | 33 | await client2.connect({ socketEndpointURL: `${MOSQUITTO_PROXY_URL}:${MOSQUITTO_PROXY_PORT}` }); 34 | await client2.connectToBroker('Mosquitto 2'); 35 | // client2.on('topic_tree', (message) => { 36 | // console.log('Client 2'); 37 | // console.log(message); 38 | // }); 39 | 40 | await client3.connect({ socketEndpointURL: `${MOSQUITTO_PROXY_URL}:${MOSQUITTO_PROXY_PORT}` }); 41 | await client3.connectToBroker('Mosquitto 3'); 42 | // client3.on('topic_tree', (message) => { 43 | // console.log('Client 3'); 44 | // console.log(message); 45 | // }); 46 | } catch (error) { 47 | console.error(error); 48 | } 49 | }; 50 | 51 | (async () => { 52 | const client = new NodeMosquittoProxyClient({ 53 | /* logger: console */ 54 | }); 55 | try { 56 | await client.connect({ socketEndpointURL: `${MOSQUITTO_PROXY_URL}:${MOSQUITTO_PROXY_PORT}` }); 57 | await client.connectToBroker('Mosquitto Mock API'); 58 | await client.addUser('maxmustermann', 'secret', '1234567'); 59 | const users = await client.listUsers(); 60 | console.log(users); 61 | const connections = await client.getBrokerConnections(); 62 | } catch (error) { 63 | console.error(error); 64 | } 65 | })(); 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "prettier": "npx prettier --write ." 5 | }, 6 | "workspaces": [ 7 | "frontend", 8 | "backend" 9 | ], 10 | "version": "2.8.2" 11 | } 12 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | docker run -p 8088:8088 -v $(pwd)/config-docker:/management-center/config cedalo/management-center 2 | -------------------------------------------------------------------------------- /tests/conf/mosquitto.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cedalo/management-center/e7472ca2b0c4dd709082e8bc6590447226c2e179/tests/conf/mosquitto.conf -------------------------------------------------------------------------------- /tests/config/mosquitto.conf: -------------------------------------------------------------------------------- 1 | sys_interval 5 -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mosquitto-instance1: 5 | image: eclipse-mosquitto 6 | container_name: mosquitto-instance1 7 | ports: 8 | - 1885:1883 9 | - 9001:9001 10 | expose: 11 | - 1885 12 | - 9001 13 | volumes: 14 | - ./conf:/mosquitto/config 15 | mosquitto-instance2: 16 | image: eclipse-mosquitto 17 | container_name: mosquitto-instance2 18 | ports: 19 | - 1886:1883 20 | - 9002:9001 21 | expose: 22 | - 1886 23 | - 9002 24 | volumes: 25 | - ./conf:/mosquitto/config 26 | mosquitto-instance3: 27 | image: eclipse-mosquitto 28 | container_name: mosquitto-instance3 29 | ports: 30 | - 1887:1883 31 | - 9003:9001 32 | expose: 33 | - 1887 34 | - 9003 35 | volumes: 36 | - ./conf:/mosquitto/config 37 | mosquitto-instance4: 38 | image: ralight/mosquitto-dynsec 39 | container_name: mosquitto-instance4 40 | ports: 41 | - 1888:1883 42 | - 9004:9001 43 | expose: 44 | - 1888 45 | - 9004 46 | volumes: 47 | - ./conf:/mosquitto/config 48 | -------------------------------------------------------------------------------- /tests/mosquitto-mock-api/start-broker.js: -------------------------------------------------------------------------------- 1 | const mosca = require('mosca'); 2 | const mqtt = require('mqtt'); 3 | 4 | const settings = { 5 | port: 1889, 6 | }; 7 | 8 | let mockAPI = null; 9 | 10 | const setup = () => { 11 | console.log('Mosquitto Mock API server is up and running'); 12 | mockAPI = mqtt.connect('mqtt://localhost:1888'); 13 | mockAPI.on('connect', () => { 14 | console.log('publish'); 15 | mockAPI.publish('$SYS/broker/clients/total', '5'); 16 | }); 17 | }; 18 | 19 | const server = new mosca.Server(settings); 20 | 21 | server.on('clientConnected', (client) => { 22 | console.log('client connected', client.id); 23 | }); 24 | 25 | server.on('published', (packet, client) => { 26 | console.log('Published', packet.payload); 27 | }); 28 | 29 | server.on('ready', setup); 30 | -------------------------------------------------------------------------------- /tests/start-broker.sh: -------------------------------------------------------------------------------- 1 | docker run -it -p 1885:1883 -p 9001:9001 -v $(pwd)/config:/mosquitto/config eclipse-mosquitto -------------------------------------------------------------------------------- /tests/start-brokers.sh: -------------------------------------------------------------------------------- 1 | docker-compose up 2 | -------------------------------------------------------------------------------- /tests/utils/send-messages.js: -------------------------------------------------------------------------------- 1 | const mqtt = require('mqtt'); 2 | 3 | const URL = 'localhost:1883'; 4 | const USERNAME = 'topictree'; 5 | const PASSWORD = 'topictree'; 6 | 7 | const brokerClient = mqtt.connect(URL, { 8 | username: USERNAME, 9 | password: PASSWORD, 10 | }); 11 | 12 | brokerClient.on('connect', () => { 13 | console.log(`Connected to ${URL}`); 14 | let counter = 0; 15 | setInterval(() => { 16 | counter += 5; 17 | const sensor = { 18 | id: 'some sensor', 19 | value: counter, 20 | }; 21 | brokerClient.publish('sensors/example', JSON.stringify(sensor)); 22 | console.log(JSON.stringify(sensor)); 23 | const sensor2 = { 24 | id: 'some other sensor', 25 | value: counter, 26 | }; 27 | brokerClient.publish('sensors/example2', JSON.stringify(sensor2)); 28 | console.log(JSON.stringify(sensor2)); 29 | brokerClient.publish('sensors/nonJSON', `${counter}`); 30 | }, 1000); 31 | }); 32 | --------------------------------------------------------------------------------