Version: 2.3.2
https://www.mashroom-server.com
(c) 2022 nonblocking.at gmbh
Mashroom Server is a Node.js based Microfrontend Integration Platform. It supports the integration of Express webapps on the server side and composing pages from multiple Single Page Applications on the client side (Browser). It also provides common infrastructure such as security, communication (publish/subscribe), theming, i18n, storage, and logging out of the box and supports custom middleware and services via plugins.
Mashroom Server allows it to implemented SPAs (and express webapps) completely independent and without a vendor lock-in, and to use it on arbitrary pages with different configurations and even multiple times on the same page. It also allows it to restrict the access to resources (Pages, Apps) based on user roles.
From a technical point of view the core of Mashroom Server is a plugin loader that scans npm packages for plugin definitions (package.json, mashroom.json) and loads them at runtime. Such a plugin could be an Express webapp or a SPA or more generally all kind of code it knows how to load, which is determined by the available plugin loaders. Plugin loaders itself are also just plugins, so it is possible to add any type of custom plugin type.
Supported | |
---|---|
Operating Systems | Linux, MacOS, Windows |
Node.js | 14.x, 16.x, 18.x |
HTTP | 1.0, 1.1, 2 + TLS 1.1, 1.2, 1.3 |
Authentication | LDAP (Active Directory), OpenID Connect/OAuth2, local user database (JSON file) |
Authorization | Role based; ACL (URL and HTTP method, based on roles and/or IP address); Resource permissions (Page, App instance, Topic, …) |
Security | CSRF protection, Helmet integration |
Storage | MongoDB, Plain JSON Files |
Memory Cache | Local Memory, Redis |
Messaging | MQTT (3.1, 3.1.1/4.0, 5.0), AMQP (1.0) |
Session Storage | Local Memory (no Cluster support), shared Filesystem, Redis, MongoDB |
API Proxy | HTTP, HTTPS, SSE, WebSocket |
CDN | Any that can be configured as caching proxy |
Clustering | yes (tested with PM2) |
Monitoring | CPU, Heap, Requests + Plugin Metrics; Exporter for Prometheus |
Desktop Browsers | Chrome (latest), Firefox (latest), Safari (latest), Edge (latest) |
Mobile Browsers | Chrome (latest), Safari (latest) |
A plugin definition consists of two parts:
A package.json with a Mashroom Server plugin definition looks like this:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-packagejson-extension.json",
"name": "my-webapp",
"version": "1.0.0",
"dependencies": {
"express": "4.16.4"
},
"devDependencies": {
},
"scripts": {
"build": "babel src -d dist"
},
"mashroom": {
"devModeBuildScript": "build",
"plugins": [
{
"name": "My Webapp",
"type": "web-app",
"bootstrap": "./dist/mashroom-bootstrap.js",
"requires": [
"A special service plugin"
],
"defaultConfig": {
"path": "/my/webapp"
}
}
]
}
}
The same in a separate mashroom.json
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-plugins.json",
"devModeBuildScript": "build",
"plugins": [
{
"name": "My Webapp",
"type": "web-app",
"bootstrap": "./dist/mashroom-bootstrap.js",
"requires": [
"A special service plugin"
],
"defaultConfig": {
"path": "/my/webapp"
}
}
]
}
Multiple plugins can be defined within a single npm package.
The type element determines which plugin loader will be used to load the plugin. The optional requires defines plugins that must be loaded before this plugin can be loaded. The content defaultConfig differs per plugin type. It can be overwritten in the plugins section of the server configuration.
The devModeBuildScript property is optional. If present npm run <devModeBuildScript> is executed in devMode after every change in the package.
The bootstrap script for this case might look like this:
import webapp from './my-express-webapp';
import {MashroomWebAppPluginBootstrapFunction} from 'mashroom/type-definitions';
const bootstrap: MashroomWebAppPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
return webapp;
};
export default bootstrap;
The context element allows access to the server configuration, the logger factory and all services.
The plugin context allows access to the logger factory und all services. The plugin context is available via:
Examples:
import type {MashroomLogger} from '@mashroom/mashroom/type-definitions';
import type {MashroomStorageService} from '@mashroom/mashroom-storage/type-definitions';
const bootstrap: MashroomWebAppPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
const pluginContext = pluginContextHolder.getPluginContext();
const logger: MashroomLogger = pluginContext.loggerFactory('my.log.category');
const storageService: MashroomStorageService = pluginContext.services.storage.service;
//...
};
app.get('/', (req, res) => {
const pluginContext = req.pluginContext;
const logger: MashroomLogger = pluginContext.loggerFactory('my.log.category');
const storageService: MashroomStorageService = pluginContext.services.storage.service;
//...
});
NOTE: Never store the pluginContext outside a bootstrap or request handler because service references my change over time when plugins are reloaded. But it save to store the pluginContextHolder instance.
Just checkout the mashroom-portal-quickstart repo for a typical portal setup. Or mashroom-quickstart if you don't need the Portal plugin.
A single package.json is enough to set up a server instance. Plugins are just npm dependencies.
The configuration files are expected in the folder where mashroom is executed. Alternatively you can pass the root folder as argument:
mashroom <path_to_config_files>
The following config files are loaded and merged together if present (in this order):
The typical configuration could look like this:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-server-config.json",
"name": "Mashroom Test Server 1",
"port": 8080,
"indexPage": "/portal",
"xPowerByHeader": "Mashroom Server",
"tmpFolder": "/tmp",
"pluginPackageFolders": [{
"path": "./node_modules/@mashroom"
}, {
"path": "./my-plugin-packages",
"watch": true,
"devMode": true
}],
"ignorePlugins": [],
"plugins": {
"Mashroom Session Middleware": {
"provider": "Mashroom Session Filestore Provider",
"session": {
}
},
"Mashroom Session Filestore Provider": {
"path": "./data/sessions",
"ttl": 1200
},
"Mashroom Security Services": {
"provider": "Mashroom Security Simple Provider",
"acl": "./acl.json"
},
"Mashroom Security Simple Provider": {
"users": "./users.json",
"loginPage": "/login"
},
"Mashroom Storage Services": {
"provider": "Mashroom Storage Filestore Provider"
},
"Mashroom Storage Filestore Provider": {
"dataFolder": "./data/storage"
},
"Mashroom Internationalization Services": {
"availableLanguages": ["en", "de"],
"defaultLanguage": "en"
},
"Mashroom Http Proxy Services": {
"rejectUnauthorized": false,
"poolMaxSockets": 10
}
}
}
The same as a Javascript file:
module.exports = {
name: "Mashroom Test Server 1",
port: 8080,
indexPage: "/portal",
xPowerByHeader: "Mashroom Server",
tmpFolder: "/tmp",
pluginPackageFolders: [{
path: "./node_modules/@mashroom"
}, {
path: "./my-plugin-packages",
devMode: true
}],
ignorePlugins: [],
plugins: {
}
}
Since version 1.3 the property values can also contain string templates and the environment variables are accessible via env object:
{
"name": "${env.USER}'s Mashroom Server",
"port": 5050
}
To enable HTTPS and/or HTTP2 you would add:
{
"httpsPort": 5443,
"tlsOptions": {
"key": "./certs/key.pem",
"cert": "./certs/cert.pem"
},
"enableHttp2": true
}
To enable security you have to add the mashroom-security package and a provider package such as mashroom-security-provider-simple.
The security package provides access control lists based on URLs and a Service for managing and checking resource permissions manually.
NOTE: The security in the Portal and for Portal Apps (SPA) is described below in Mashroom Portal -> Security.
You can secure every URL in Mashroom with the ACL, based on user role or IP. For example:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-security-acl.json",
"/portal/**": {
"*": {
"allow": {
"roles": ["Authenticated"]
}
}
},
"/mashroom/**": {
"*": {
"allow": {
"roles": ["Administrator"],
"ips": ["127.0.0.1", "::1"]
}
}
}
}
NOTE: For more details check the mashroom-security documentation below.
The SecurityService allows it to define and check resource permissions based on a permission key and the user roles. For example:
import type {MashroomSecurityService} from '@mashroom/mashroom-security/type-definitions';
export default async (req: Request, res: Response) => {
const securityService: MashroomSecurityService = req.pluginContext.services.security.service;
// Create a permission
await securityService.updateResourcePermission(req, {
type: 'Page',
key: pageId,
permissions: [{
permissions: ['View'],
roles: ['Role1', 'Role2']
}]
});
// Check a permission
const mayAccess = await securityService.checkResourcePermission(req, 'Page', pageId, 'View');
// ...
}
This mechanism is used by the Portal for Site, Page and Portal App permissions.
NOTE: For more details check the mashroom-security documentation below.
Use the mashroom-helmet plugin to add the Helmet middleware, which adds a bunch of HTTP headers to prevent XSS and other attacks.
The mashroom-csrf plugin adds middleware that checks every POST, PUT and DELETE request for a CSRF token in the HTTP headers or the query. You need to use the MashroomCSRFService to get the current token.
NOTE: The default Portal theme automatically adds the current CSRF token in a meta tag with the name csrf-token.
The logger is currently backed by log4js.
The configuration is expected to be in the same folder as mashroom.cfg. Possible config files are:
The first config file found from this list will be used. A file logger would be configured like this:
{
"appenders": {
"file1": {"type": "file", "filename": "log/mashroom.log", "maxLogSize": 10485760, "numBackups": 3},
"file2": {
"type": "file", "filename": "log/my-stuff.log", "maxLogSize": 10485760, "numBackups": 3,
"layout": {
"type": "pattern",
"pattern": "%d %p %X{sessionID} %X{clientIP} %X{username} %c - %m"
}
},
"console": {"type": "console"}
},
"categories": {
"default": {"appenders": ["file1", "console"], "level": "debug"},
"my-stuff": {"appenders": ["file2"], "level": "info"}
}
}
The following built in context properties can be used with %X{
You can use logger.withContext() or logger.addContext() to add context information.
For configuration details and possible appenders see log4js-node Homepage.
To push the logs to logstash you can use the logstash-http package:
"dependencies": {
"@log4js-node/logstash-http": "^1.0.0"
}
And configure log4js like this:
{
"appenders": {
"logstash": {
"type": "@log4js-node/logstash-http",
"url": "http://elasticsearch:9200/_bulk",
"application": "your-index"
}
},
"categories": {
"default": {
"appenders": [ "logstash" ],
"level": "info"
}
}
}
The mashroom-i18n plugin provides a simple service to lookup messages on the file system based on the current user language.
You can use it like this from where ever the pluginContext is available:
import type {MashroomI18NService} from '@mashroom/mashroom-i18n/type-definitions';
export default (req: Request, res: Response) => {
const i18nService: MashroomI18NService = req.pluginContext.services.i18n.service;
const currentLang = i18nService.getLanguage(req);
const message = i18nService.getMessage('username', 'de');
// message will be 'Benutzernamen'
// ...
}
One the client-side (in Portal Apps) you can use an arbitrary i18n framework (such as intl-messageformat). The current Portal locale will be passed to the Apps with the portalAppSetup:
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup, clientServices) => {
const { lang } = portalAppSetup;
// lang will be 'en' or 'fr' or whatever
const { messageBus, portalAppService } = clientServices;
// ...
};
Mashroom tries to automatically use the most efficient caching mechanisms. All you need to do is to add the appropriate plugins.
Add mashroom-memory-cache plugin and optionally mashroom-memory-cache-provider-redis if you want to use Redis instead of the local memory. Many other plugins (such as mashroom-storage and mashroom-portal) will automatically detect it and use it (see their documentation for more details).
The mashroom-browser-cache plugin provides a service to set Cache-Control headers based on a policy. For example:
import type {MashroomCacheControlService} from '@mashroom/mashroom-browser-cache/type-definitions';
export default async (req: Request, res: Response) => {
const cacheControlService: MashroomCacheControlService = req.pluginContext.services.browserCache.cacheControl;
await cacheControlService.addCacheControlHeader('ONLY_FOR_ANONYMOUS_USERS', req, res);
// ..
};
Other plugins (such as mashroom-portal) will automatically use it if present.
NOTE: This plugin will always set no-cache header in devMode.
Since v2 Mashroom ships a CDN plugin that will automatically be used by mashroom-portal and other plugins to deliver static assets.
Basically, mashroom-cdn consists of a list of CDN hosts and a service to get the next host to use:
import type {MashroomCDNService} from '@mashroom/mashroom-cdn/type-definitions';
export default async (req: Request, res: Response) => {
const cdnService: MashroomCDNService = req.pluginContext.services.cdn.service;
const cdnHost = cdnService.getCDNHost();
const resourceUrl = `${cdnHost}/<the-actual-path>`;
// ..
};
NOTE: The mashroom-cdn plugin requires a CDN that works like a transparent proxy, which forwards an identical request to the origin (in this case Mashroom) if does not exist yet.
The mashroom-vhost-path-mapper plugin can be used to map frontend paths to internal paths, based on virtual hosts.
So, lets say you want to map the Mashroom Portal site site1, reachable under http://localhost:5050/portal/site1, to www.my-company.com. In that case you would configure the plugin like this:
{
"plugins": {
"Mashroom VHost Path Mapper Middleware": {
"hosts": {
"www.my-company.com": {
"mapping": {
"/login": "/login",
"/": "/portal/site1"
}
}
}
}
}
}
If your frontend base path is different, e.g. www.my-company.com/foo, you would also have to set the frontendBasePath in the configuration.
NOTE: All other plugins will only deal with the rewritten paths, keep that in mind especially when defining ACLs.
If you're going to run Mashroom Server in a cluster you should keep in mind:
A cluster safe log configuration could look like this:
const NODE_ID = process.env.pm_id || process.pid;
module.exports = {
appenders: {
file: {'type': 'file', 'filename': `log/mashroom.${NODE_ID}.log`, 'maxLogSize': 10485760, 'numBackups': 3},
console: {'type': 'console'}
},
categories: {
default: {appenders: ['file', 'console'], 'level': 'info'}
},
disableClustering: true
};
Mashroom Server gathers a lot of internal metrics that can be exposed in different formats. Currently, there are two exporters available:
To enable the metrics, just add @mashroom/mashroom-monitoring-metrics-collector and one of the exporter plugins.
The Prometheus metrics will be available at /metrics. An example Grafana Dashboard can be found in the Mashroom repo.
Here how it looks:
There are a few integrated health checks available that can be used as probes for monitoring tools or to check the readiness/liveness of Kubernetes pods.
An overview of the health checks is available under http://<host>:<port>/mashroom/health
The Mashroom Server Admin UI is available under http://<host>:<port>/mashroom/admin
It contains:
Every Portal App (SPA) has to expose a global bootstrap method. The Portal Client fetches the app metadata from the server, loads all required resources and calls then the bootstrap with the configuration object and the DOM element where the app should run in.
The built-in proxy allows the Portal App to access internal APIs. It supports HTTP, Websocket and SSE.
You have to define proxies like this in the plugin definition:
"defaultConfig": {
"proxies": {
"myApi": {
"targetUri": "http://localhost:1234/api/v1"
}
}
}
To be able to use it in the boostrap of your App like this:
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup) => {
const {lang, restProxyPaths} = portalAppSetup;
fetch(`${restProxyPaths.myApi}/foo/bar?q=xxx`, {
credentials: 'same-origin',
}).then( /* ... */ );
// ...
};
Plugins of type http-proxy-interceptor can be used to intercept all proxy calls and to check, enrich or manipulate requests or responses.
A typical use case is to add some security headers (e.g. Bearer) or do some redirect or caching.
Checkout out the mashroom-http-proxy documentation for details.
Portal Apps (SPA) can reside on a remote server and automatically be registered. Currently, there are two different remote registries that can also be combined:
Here, how it works:
Here for example with mashroom-portal-remote-app-registry.
Open /mashroom/admin/ext/remote-portal-apps, paste the URL into the input and lick Add:
After that you can add the new Portal App via Drag'n'Drop where ever you want:
Since v2 the Portal can render the whole Portal page on the server-side and even include the initial HTML of Portal Apps, capable of server-side rendering.
NOTE: If an App support SSR it should provide its style in separate CSS file, because in that case the Portal will try to inline all styles so everything gets rendered correctly without any extra resources.
To enable SSR in an App you have to:
The ssrBootstrap could look like this in React:
import React from 'react';
import {renderToString} from 'react-dom/server';
import App from './App';
import type {MashroomPortalAppPluginSSRBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalAppPluginSSRBootstrapFunction = (portalAppSetup) => {
const {appConfig, restProxyPaths, lang} = portalAppSetup;
const dummyMessageBus: any = {};
return renderToString(<App appConfig={appConfig} messageBus={dummyMessageBus}/>);
};
export default bootstrap;
NOTE: The server-side bootstrap will receive the same portalAppSetup like on the client-side, but no client services. If you need the messageBus or other services make sure the App renders without them or with a mock implementation.
On the client-side you would do:
import React from 'react';
import {render, hydrate, unmountComponentAtNode} from 'react-dom';
import App from './App';
import type {MashroomPortalAppPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (element, portalAppSetup, clientServices) => {
const {appConfig, restProxyPaths, lang} = portalAppSetup;
const {messageBus} = clientServices;
const ssrHost = element.querySelector('[data-ssr-host="true"]');
if (ssrHost) {
hydrate(<App appConfig={appConfig} messageBus={messageBus}/>, ssrHost);
} else {
render(<App appConfig={appConfig} messageBus={messageBus}/>, element);
}
};
global.startMyApp = bootstrap;
Remote Apps would basically work the same, but the server-side bootstrap needs to be exposed as route that receives a POST with the portalAppSetup in the content. Checkout the Mashroom Demo SSR Portal App.
All put together rendering a page works like this:
NOTE: Every App should support client-side rendering, because they can be added dynamically to a page. If you don't want to render on the client (e.g. for security reasons) at least render an error message on the client-side.
SSR improves page performance and SEO heavily. To improve it further we recommend:
The Portal supports dynamically replacing the content of a page (that's the layout + all Apps) by the content of another page. This can be used to avoid full page loads and let the Portal behave like an SPA itself, so it basically can switch to client-side rendering.
The client-side rendering needs to be implemented in the Theme, which needs to the following during page navigation:
<!-- in the template -->
<a class="nav-link active"
href="{{@root.siteBasePath}}{{friendlyUrl}}"
onclick="return replacePageContent('{{pageId}}', '{{@root.siteBasePath}}{{friendlyUrl}}')"
data-mr-page-id="{{pageId}}">
{{title}}
</a>
import type {MashroomPortalClientServices, MashroomPortalPageContent} from '@mashroom/mashroom-portal/type-definitions';
const clientServices: MashroomPortalClientServices | undefined = (global as any).MashroomPortalServices;
if (!clientServices) {
return;
}
(global as any).replacePageContent = (pageId: string, pageUrl: string): boolean => {
showPageLoadingIndicator(true);
clientServices.portalPageService.getPageContent(pageId).then(
(content: MashroomPortalPageContent) => {
if (content.fullPageLoadRequired || !content.pageContent) {
// Full page load required!
document.location.replace(pageUrl);
} else {
contentEl.innerHTML = content.pageContent;
// Execute page scripts
eval(content.evalScript);
highlightPageIdInNavigation(pageId);
window.history.pushState({ pageId }, '', pageUrl);
showPageLoadingIndicator(false);
}
},
(error) => {
// If an error occurs we do a full page load
console.error('Dynamically replacing the page content failed!', error);
document.location.replace(pageUrl);
}
);
return false;
}
NOTE: The Portal will automatically detect if the requested page is not compatible to the initial loaded one
(because the Theme oder Page Enhancements differ). In that case it will return fullPageLoadRequired: true
.
A Composite App is a higher order App, which uses other Portal Apps (SPAs) as building blocks. So, its App in App, or SPA in SPA, or picture in picture ;-) Such a Composite App can itself be used as building block for another Composite App, which can be continued infinite.
This approach takes advantage of Mashroom Portal's ability to load any registered App into any DOM element. Basically, you just render a div element in your SPA with a unique (random) ID and load the App like so:
// Get the portalAppService in the bootstrap
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (element, portalAppSetup, clientServices) => {
const {portalAppService} = clientServices;
//...
}
// Load the App
const loadedApp = await portalAppService.loadApp(domElID, 'My App', null, /* position */ null, /* appConfig */ {
someProp: 'foo',
});
And unload it like this:
await portalAppService.unloadApp(loadedApp.id);
Make sure you call unloadApp() if you want to remove/replace an App and not just remove the host DOM node, because like that the resources are not properly removed from the browser.
Here for example the mashroom-portal-demo-composite-app:
NOTE: A Composite App only runs in Mashroom Portal and leads to a vendor lock-in. At very least other integration hosts need to provide a compatible implementation of MashroomPortalAppService.
One key feature of Mashroom Portal is the possibility to create dynamic pages (or cockpits) based:
This is quite similar to a Composite App but in this case the page loads a single (static) App, which takes over the page and manages loading and unloading other Apps outside of itself. Typically, the names of the Apps that get loaded are not pre-defined, but determined from some metaInfo of the available Apps. So, such a cockpit can be dynamically extended by just adding new Apps with some capabilities (at runtime).
To find Apps you would use the portalAppService like this:
const availableApps = await portalAppService.getAvailableApps();
const appToShowWhatever = availableApps.filter(({metaInfo}) => metaInfo?.canShow === 'whatever');
Have a look at this Demo Dynamic Cockpit, it consist of a central search bar and tries to load found data with Apps with apropriate metaInfo. E.g. this App could load customers:
{
"plugins": [
{
"name": "Mashroom Dynamic Cockpit Demo Customer Details App",
// ...
"defaultConfig": {
"metaInfo": {
"demoCockpit": {
"viewType": "Details",
"entity": "Customer"
}
},
// ...
}
}
]
}
A Mashroom Portal Theme can be written with any template engine that works with Express.
You need to implement three templates:
NOTE: If you want to have a type-safe template we recommend using React. Have a look at mashroom-portal-demo-alternative-theme for an example.
Obviously, theming only works properly if all the Apps on a page regard the currently applied theme and in particular do not:
Here some best practices:
The Portal comes with a client-side MessageBus that can be used by Portal Apps to communicate with each other locally.
If server-side messaging (mashroom-messaging) and Websocket support (mashroom-websocket) is installed, the MessageBus is automatically connected to the server-side messaging facility and like this Portal Apps can communicate with Apps in other browsers and even with 3rd party systems (when a external messaging system such as MQTT is connected).
Page enhancement plugins allow to add some extra script or style to a page. Either to any page or based on some rules (e.g. page URL or the user agent).
Checkout the mashroom-portal documentation for details.
Portal app enhancement plugins can be used to modify the portalAppSetup of some (or any) Apps before loading. It can also be used to add custom services on the client side (passed with clientServices to every App).
A typical use case would be to add some extra data to the user or to augment the appConfig.
Checkout the mashroom-portal documentation for details.
The Mashroom Portal uses the security plugin to control the access to pages and Portal Apps. It also introduced a concept of fine grain permissions (mapped to roles) which can be checked in Portal Apps and in backends (passed via HTTP header by the API Proxy).
If a user requires specific roles to be able to load an App (dynamically) you have to set the defaultRestrictViewToRoles in the plugin definition. Otherwise, any user, which is able to access the Portal API (which can be controlled via ACL), will be able to load it.
NOTE: Users with the role Administrator are able to load all Apps, regardless of defaultRestrictViewToRoles. They are also able to override/redefine the view permission when adding Apps to a page.
Within Portal Apps you should not work with roles but with abstract permission keys such as "mayDeleteCustomer". In the plugin definition this keys can then be mapped to roles like this:
"defaultConfig": {
"rolePermissions": {
"mayDeleteCustomer": ["Role1", "Role2"]
}
}
And the permission can be checked like this:
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup, clientServices) => {
const {appConfig, user: {permissions, username, displayName, email}} = portalAppSetup;
// True if user has role "Role1" OR "Role2"
if (permissions.mayDeleteCustomer) {
// ...
}
}
Proxy access can be restricted by adding a restrictToRole property in the plugin definition:
"defaultConfig": {
"proxies": {
"myApi": {
"targetUri": "http://localhost:1234/api/v1",
"sendPermissionsHeader": false,
"restrictToRoles": ["Role1"]
}
}
}
NOTE: Not even users with the Administrator role can override that restriction. So, even if they can load a restricted App they will not be able to access the backend.
Furthermore, it is possible to pass the Portal App security context to the backend via HTTP headers. This is useful if you want to check some fine grain permissions there as well.
Headers that can be passed to the Backend:
Sites and Pages can be secured by:
Both approaches can be combined.
The theme is responsible for rendering the page. It defines where the main content is. The portal adds the selected layout to the main content and the configured Portal Apps within the app areas of this layout.
As an Administrator you can add Portal Apps via Admin Toolbar: Add Apps
After adding an App you can click on the Configure icon to edit the appConfig and the permissions:
Instead of the default JSON editor you can define a custom editor App for you appConfig. The custom editor is itself a plain Portal App (SPA) which gets an extra appConfig property editorTarget of type MashroomPortalConfigEditorTarget that can be used to communicate back with the Admin Toolbar:
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup, clientServices) => {
const {appConfig: {editorTarget}} = portalAppSetup;
if (!editorTarget || !editorTarget.pluginName) {
throw new Error('This app can only be started as App Config Editor!');
}
const currentAppConfig = editorTarget.appConfig;
// ...
// When the user is done:
editorTarget.updateAppConfig(updatedAppConfig);
editorTarget.close();
//...
};
Here for example the mashroom-portal-demo-react-app2 plugin which has a custom editor:
The default portal theme allows to show all Portal App versions by clicking on App versions. You can enable it like this in the Mashroom config file:
{
"plugins": {
"Mashroom Portal Default Theme": {
"showEnvAndVersions": true
}
}
}
As an Administrator you can add a new Page from the Admin Toolbar: Create -> Create New Page:
After that you can start to place Portal Apps via Add Apps.
As an Administrator you can add a new Site from the Admin Toolbar: Create -> Create New Page:
After that you can start to add additional pages.
Accessible through pluginContext.services.core.pluginService
Interface:
export interface MashroomPluginService {
/**
* The currently known plugin loaders
*/
getPluginLoaders(): Readonly<MashroomPluginLoaderMap>;
/**
* Get all currently known plugins
*/
getPlugins(): Readonly<Array<MashroomPlugin>>;
/**
* Get all currently known plugin packages
*/
getPluginPackages(): Readonly<Array<MashroomPluginPackage>>;
/**
* Register for the next loaded event of given plugin (fired AFTER the plugin has been loaded).
*/
onLoadedOnce(pluginName: string, listener: () => void): void;
/**
* Register for the next unload event of given plugin (fired BEFORE the plugin is going to be unloaded).
*/
onUnloadOnce(pluginName: string, listener: () => void): void;
}
Accessible through pluginContext.services.core.middlewareStackService
Interface:
export interface MashroomMiddlewareStackService {
/**
* Check if the stack has given plugin
*/
has(pluginName: string): boolean;
/**
* Execute the given middleware.
* Throws an exception if it doesn't exists
*/
apply(
pluginName: string,
req: Request,
res: Response,
): Promise<void>;
/**
* Get the ordered list of middleware plugin (first in the list is executed first)
*/
getStack(): Array<{pluginName: string; order: number}>;
}
Accessible through pluginContext.services.core.websocketUpgradeService
Interface:
/**
* A services to add and remove HTTP/1 upgrade listeners
*/
export interface MashroomHttpUpgradeService {
/**
* Register an upgrade handler for given path
*/
registerUpgradeHandler(handler: MashroomHttpUpgradeHandler, pathExpression: string | RegExp): void;
/**
* Unregister an upgrade handler
*/
unregisterUpgradeHandler(handler: MashroomHttpUpgradeHandler): void;
}
A service that allows it plugins to register health probes. If a probe fails the server state goes to unready.
/**
* A services to obtain all available health probes
*/
export interface MashroomHealthProbeService {
/**
* Register a new health probe for given plugin
*/
registerProbe(forPlugin: string, probe: MashroomHealthProbe): void;
/**
* Unregister a health probe for given plugin
*/
unregisterProbe(forPlugin: string): void;
/**
* Get all registered probes
*/
getProbes(): Readonly<Array<MashroomHealthProbe>>;
}
You can use it like this in your plugin bootstrap:
const bootstrap: MashroomStoragePluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
const {services: {core: {pluginService, healthProbeService}}} = pluginContextHolder.getPluginContext();
healthProbeService.registerProbe(pluginName, healthProbe);
pluginService.onUnloadOnce(pluginName, () => {
healthProbeService.unregisterProbe(pluginName);
});
// ...
};
A plugin-loader plugin adds support for a custom plugin type.
To register a new plugin-loader add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Custom Plugin Loader",
"type": "plugin-loader",
"bootstrap": "./dist/mashroom-bootstrap",
"loads": "my-custom-type",
"defaultConfig": {
"myProperty": "foo"
}
}
]
}
}
After that all plugins of type my-custom-type will be passed to your custom loader instantiated by the bootstrap script:
import type {
MashroomPluginLoader, MashroomPlugin, MashroomPluginConfig, MashroomPluginContext,
MashroomPluginLoaderPluginBootstrapFunction
} from 'mashroom/type-definitions';
class MyPluginLoader implements MashroomPluginLoader {
get name(): string {
return 'My Plugin Loader';
}
generateMinimumConfig(plugin: MashroomPlugin) {
return {};
}
async load(plugin: MashroomPlugin, config: MashroomPluginConfig, context: MashroomPluginContext) {
// TODO
}
async unload(plugin: MashroomPlugin) {
// TODO
}
}
const myPluginLoaderPlugin: MashroomPluginLoaderPluginBootstrapFunction = (pluginName, pluginConfig, pluginContextHolder) => {
return new MyPluginLoader();
};
export default myPluginLoaderPlugin;
Registers a Express webapp that will be available at a given path.
To register a web-app plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Webapp",
"type": "web-app",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"path": "/my/webapp",
"myProperty": "foo"
}
}
]
}
}
And the bootstrap just returns the Express webapp:
import webapp from './webapp';
import type {MashroomWebAppPluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';
const bootstrap: MashroomWebAppPluginBootstrapFunction = async () => {
return webapp;
};
export default bootstrap;
Additional handlers
It is also possible to return handlers in the bootstrap. Currently there is only one:
Example:
const bootstrap: MashroomWebAppPluginBootstrapFunction = async () => {
return {
expressApp: webapp,
upgradeHandler: (request: IncomingMessageWithContext, socket: Socket, head: Buffer) => {
// TODO
},
};
};
Registers a Express Router (a REST API) and makes it available at a given path.
To register a API plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My REST API",
"type": "api",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"path": "/my/api",
"myProperty": "foo"
}
}
]
}
}
And the bootstrap just returns the Express router:
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
// ...
});
import type {MashroomApiPluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';
const bootstrap: MashroomApiPluginBootstrapFunction = async () => {
return router;
};
export default bootstrap;
Registers a Express middleware and adds it to the global middleware stack.
To register a middleware plugin add this to package.json:
{
"mashroom": {
"plugins": [{
"name": "My Middleware",
"type": "middleware",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"order": 500,
"myProperty": "foo"
}
}]
}
}
And the bootstrap just returns the Express middleware:
import MyMiddleware from './MyMiddleware';
import type {MashroomMiddlewarePluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';
const bootstrap: MashroomMiddlewarePluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
const pluginContext = pluginContextHolder.getPluginContext();
const middleware = new MyMiddleware(pluginConfig.myProperty, pluginContext.loggerFactory);
return middleware.middleware();
};
export default bootstrap;
Registers some static resources and exposes it at a given path (via Express static).
To register a static plugin add this to package.json:
{
"mashroom": {
"plugins": [{
"name": "My Documents",
"type": "static",
"documentRoot": "./my-documents",
"defaultConfig": {
"path": "/my/docs"
}
}]
}
}
Used to load arbitrary shared code that can be loaded via pluginContext.
To register a service plugin add this to package.json:
{
"mashroom": {
"plugins": [{
"name": "My Services",
"type": "services",
"namespace": "myNamespace",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
}
}]
}
}
The bootstrap will just return an object with a bunch of services:
import MyService from './MyService';
import type {MashroomServicesPluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';
const bootstrap: MashroomServicesPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
const pluginContext = pluginContextHolder.getPluginContext();
const service = new MyService(pluginContext.loggerFactory);
return {
service,
};
};
export default bootstrap;
A simple plugin to register an arbitrary web-app or static plugin as panel in the Admin UI.
To register an admin-ui-integration plugin add this to package.json:
{
"mashroom": {
"plugins": [{
"name": "My Admin Panel Integration",
"type": "admin-ui-integration",
"requires": [
"My Admin Panel"
],
"target": "My Admin Panel",
"defaultConfig": {
"menuTitle": "My Admin Panel",
"path": "/my-admin-panel",
"height": "80vh",
"weight": 10000
}
}]
}
}
parent.postMessage({ height: contentHeight + 20 }, "*");
This plugin adds role based security to the Mashroom Server.
It comes with the following mechanisms:
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Security Services": {
"provider": "Mashroom Security Simple Provider",
"forwardQueryHintsToProvider": [],
"acl": "./acl.json"
}
}
}
A typical ACL configuration looks like this:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-security-acl.json",
"/portal/**": {
"*": {
"allow": {
"roles": ["Authenticated"]
}
}
},
"/mashroom/**": {
"*": {
"allow": {
"roles": ["Administrator"],
"ips": ["127.0.0.1", "::1"]
}
}
}
}
The general structure is:
"/my/path/**": {
"*|GET|POST|PUT|DELETE|PATCH|OPTIONS": {
"allow": "any"|<array of roles>|<object with optional properties roles and ips>
"deny": "any"|<array of roles>|<object with optional properties roles and ips>
}
}
Example: Allow all users except the ones that come from an IP address starting with 12:
{
"/my-app/**": {
"*": {
"allow": {
"roles": ["Authenticated"]
},
"deny": {
"ips": ["12.**"]
}
}
}
}
Example: Restrict the Portal to authenticated users but make a specific site public:
{
"/portal/public-site/**": {
"*": {
"allow": "any"
}
},
"/portal/**": {
"*": {
"allow": {
"roles": ["Authenticated"]
}
}
}
}
Adding and checking a resource permission (e.g. for a Page) works like this:
import type {MashroomSecurityService} from '@mashroom/mashroom-security/type-definitions';
export default async (req: Request, res: Response) => {
const securityService: MashroomSecurityService = req.pluginContext.services.security.service;
// Create a permission
await securityService.updateResourcePermission(req, {
type: 'Page',
key: pageId,
permissions: [{
permissions: ['View'],
roles: ['Role1', 'Role2']
}]
});
// Check a permission
const mayAccess = await securityService.checkResourcePermission(req, 'Page', pageId, 'View');
// ...
}
The exposed service is accessible through pluginContext.services.security.service
Interface:
export interface MashroomSecurityService {
/**
* Get the current user or null if the user is not authenticated
*/
getUser(request: Request): MashroomSecurityUser | null | undefined;
/**
* Checks if user != null
*/
isAuthenticated(request: Request): boolean;
/**
* Check if the currently authenticated user has given role
*/
isInRole(request: Request, roleName: string): boolean;
/**
* Check if the currently authenticated user is an admin (has the role Administrator)
*/
isAdmin(request: Request): boolean;
/**
* Check the request against the ACL
*/
checkACL(request: Request): Promise<boolean>;
/**
* Check if given abstract "resource" is permitted for currently authenticated user.
* The permission has to be defined with updateResourcePermission() first, otherwise the allowIfNoResourceDefinitionFound flag defines the outcome.
*/
checkResourcePermission(request: Request, resourceType: MashroomSecurityResourceType, resourceKey: string, permission: MashroomSecurityPermission, allowIfNoResourceDefinitionFound?: boolean): Promise<boolean>;
/**
* Set a resource permission for given abstract resource.
* A resource could be: {type: 'Page', key: 'home', permissions: [{ roles: ['User'], permissions: ['VIEW'] }]}
*
* If you pass a permission with an empty roles array it actually gets removed from the storage.
*/
updateResourcePermission(request: Request, resource: MashroomSecurityProtectedResource): Promise<void>;
/**
* Get the permission definition for given resource, if any.
*/
getResourcePermissions(request: Request, resourceType: MashroomSecurityResourceType, resourceKey: string): Promise<MashroomSecurityProtectedResource | null | undefined>;
/**
* Add a role definition
*/
addRoleDefinition(request: Request, roleDefinition: MashroomSecurityRoleDefinition): Promise<void>;
/**
* Get all known roles. Returns all roles added with addRoleDefinition() or implicitly added bei updateResourcePermission().
*/
getExistingRoles(request: Request): Promise<Array<MashroomSecurityRoleDefinition>>;
/**
* Check if an auto login would be possible.
*/
canAuthenticateWithoutUserInteraction(request: Request): Promise<boolean>;
/**
* Start authentication process
*/
authenticate(request: Request, response: Response): Promise<MashroomSecurityAuthenticationResult>;
/**
* Check the existing authentication (if any)
*/
checkAuthentication(request: Request): Promise<void>;
/**
* Get the authentication expiration time in unix time ms
*/
getAuthenticationExpiration(request: Request): number | null | undefined;
/**
* Revoke the current authentication
*/
revokeAuthentication(request: Request): Promise<void>;
/**
* Login user with given credentials (for form login).
*/
login(request: Request, username: string, password: string): Promise<MashroomSecurityLoginResult>;
/**
* Find a security provider by name.
* Useful if you want to dispatch the authentication to a different provider.
*/
getSecurityProvider(name: string): MashroomSecurityProvider | null | undefined;
}
This plugin type is responsible for the actual authentication and for creating a user object with a list of roles.
To register your custom security-provider plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Custom Security Provider",
"type": "security-provider",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"myProperty": "foo"
}
}
]
}
}
The bootstrap returns the provider:
import type {MashroomSecurityProviderPluginBootstrapFunction} from '@mashroom/mashroom-security/type-definitions';
const bootstrap: MashroomSecurityProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
return new MySecurityProvider(/* ... */);
};
export default bootstrap;
The provider has to implement the following interface:
export interface MashroomSecurityProvider {
/**
* Check if an auto login would be possible.
* This is used for public pages when an authentication is optional but nevertheless desirable.
* It is safe to always return false.
*/
canAuthenticateWithoutUserInteraction(request: Request): Promise<boolean>;
/**
* Start authentication process.
* This typically means to redirect to the login page, then you should return status: 'deferred'.
* This method could also automatically login the user, then you should return status: 'authenticated'.
*/
authenticate(request: Request, response: Response, authenticationHints?: any): Promise<MashroomSecurityAuthenticationResult>;
/**
* Check the existing authentication (if any).
* Use this to extend the authentication expiration or to periodically refresh the access token.
*
* This method gets called for almost every request, so do nothing expensive here.
*/
checkAuthentication(request: Request): Promise<void>;
/**
* Get the authentication expiration time in unix time ms. Return null/undefined if there is no authentication.
*/
getAuthenticationExpiration(request: Request): number | null | undefined;
/**
* Revoke the current authentication.
* That typically means to remove the user object from the session.
*/
revokeAuthentication(request: Request): Promise<void>;
/**
* Programmatically login user with given credentials (optional, but necessary if you use the default login page)
*/
login(request: Request, username: string, password: string): Promise<MashroomSecurityLoginResult>;
/**
* Get the current user or null if the user is not authenticated
*/
getUser(request: Request): MashroomSecurityUser | null | undefined;
}
This plugin adds a simple, JSON file based security provider.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-simple as dependency.
To activate this provider configure the Mashroom Security plugin like this:
{
"plugins": {
"Mashroom Security Services": {
"provider": "Mashroom Security Simple Provider"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Security Simple Provider": {
"users": "./users.json",
"loginPage": "/login",
"authenticationTimeoutSec": 1200
}
}
}
users: The path to the JSON file with user and role definitions (Default: ./users.json)
loginPage: The path to redirect to if a restricted resource is requested but the user not logged in yet (Default: /login)
authenticationTimeoutSec: The inactivity time after that the authentication expires. Since this plugin uses the session to store make sure the session cookie.maxAge is greater than this value (Default: 1200)
The content of the JSON file might look like this.
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-security-simple-provider-users.json",
"users": [
{
"username": "admin",
"displayName": "Administrator",
"email": "xxxxx@xxxx.com",
"pictureUrl": "xxxx",
"passwordHash": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"extraData": {
"firstName": "John",
"lastName": "Do"
},
"roles": [
"Administrator"
],
"secrets": {
"token": "xxxxxxx"
}
},
{
"username": "john",
"displayName": "John Do",
"passwordHash": "96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a",
"roles": [
"User",
"PowerUser"
]
}
]
}
The passwordHash is the SHA256 hash of the password. displayName, email and pictureUrl are optional. extraData will be mapped to user.extraData and secrets will be mapped to user.secrets.
This plugin adds a LDAP security provider.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-ldap as dependency.
To activate this provider, configure the Mashroom Security plugin like this:
{
"plugins": {
"Mashroom Security Services": {
"provider": "Mashroom LDAP Security Provider"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom LDAP Security Provider": {
"loginPage": "/login",
"serverUrl": "ldap://my-ldap-server:636",
"ldapConnectTimeout": 3000,
"ldapTimeout": 5000,
"bindDN": "uid=mashroom,dc=nonblocking,dc=at",
"bindCredentials": "secret",
"baseDN": "ou=users,dc=nonblocking,dc=at",
"userSearchFilter": "(&(objectClass=person)(uid=@username@))",
"groupSearchFilter": "(objectClass=group)",
"extraDataMapping": {
"mobile": "mobile",
"address": "postalAddress"
},
"secretsMapping": {
"internalUserId": "uid"
},
"groupToRoleMapping": "./groupToRoleMapping.json",
"userToRoleMapping": "./userToRoleMapping.json",
"authenticationTimeoutSec": 1200
}
}
}
For a server that requires TLS you have to provide a tlsOptions object:
{
"plugins": {
"Mashroom LDAP Security Provider": {
"serverUrl": "ldaps://my-ldap-server:636",
"tlsOptions": {
"cert": "./server-cert.pem",
// Necessary only if the server requires client certificate authentication.
//"key": "./client-key.pem",
// Necessary only if the server uses a self-signed certificate.
// "rejectUnauthorized": false,
// "ca": [ "./server-cert.pem" ],
}
}
}
}
The groupToRoleMapping file has to following simple structure:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-security-ldap-provider-group-to-role-mapping.json",
"LDAP_GROUP1": [
"ROLE1",
"ROLE2"
]
}
And the userToRoleMapping file:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-security-ldap-provider-user-to-role-mapping.json",
"username": [
"ROLE1",
"ROLE2"
]
}
This plugin adds an OpenID Connect/OAuth2 security provider that can be used to integrate Mashroom Server with almost all Identity Providers or Identity Platforms.
Tested with:
Should work with (among others):
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-ldap as dependency.
To activate this provider, configure the Mashroom Security plugin like this:
{
"plugins": {
"Mashroom Security Services": {
"provider": "Mashroom OpenID Connect Security Provider"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom OpenID Connect Security Provider": {
"mode": "OIDC",
"issuerDiscoveryUrl": "http://localhost:8080/.well-known/openid-configuration",
"issuerMetadata": null,
"scope": "openid email profile",
"clientId": "mashroom",
"clientSecret": "your-client-secret",
"redirectUrl": "http://localhost:5050/openid-connect-cb",
"responseType": "code",
"usePKCE": false,
"extraAuthParams": {},
"extraDataMapping": {
"phone": "phone",
"birthdate": "birthdate",
"updatedAt": "updated_at"
},
"rolesClaimName": "roles",
"adminRoles": [
"mashroom-admin"
],
"httpRequestTimeoutMs": 3500
},
"Mashroom OpenID Connect Security Provider Callback": {
"path": "/openid-connect-cb"
}
}
}
Since the authorization mechanism relies on user roles it is necessary to configure your identity provider to map the user roles to a scope (which means we can get it as claim). See Example Configurations below.
The plugin maps the ID/JWT Token to user.secrets.idToken so it can for example be used in a Http Proxy Interceptor to set the Bearer for backend calls.
The implementation automatically extends the authentication via refresh token every view seconds (as long as the user is active). So, if the authentication session gets revoked in the identity provider the user is signed out almost immediately.
The expiration time of the access token defines after which time the user is automatically signed out due to inactivity. And the expiration time of the refresh token defines how long the user can work without signing in again.
Setup:
You'll find more details about the configuration here: https://www.keycloak.org/documentation.html
If your Keycloak runs on localhost, the Realm name is test amd the client name mashroom, then the config would look like this:
{
"plugins": {
"Mashroom OpenID Connect Security Provider": {
"issuerDiscoveryUrl": "http://localhost:8080/auth/realms/test/.well-known/uma2-configuration",
"clientId": "mashroom",
"clientSecret": "xxxxxxxxxxxxxxxx",
"redirectUrl": "http://localhost:5050/openid-connect-cb",
"rolesClaim": "roles",
"adminRoles": [
"mashroom-admin"
]
}
}
}
Setup:
You'll find more details about the configuration here: https://backstage.forgerock.com/docs/openam/13.5/admin-guide
If your OpenAM server runs on localhost, the Realm name is Test amd the client name mashroom, then the config would look like this:
{
"plugins": {
"Mashroom OpenID Connect Security Provider": {
"issuerDiscoveryUrl": "http://localhost:8080/openam/oauth2/Test/.well-known/openid-configuration",
"scope": "openid email profile",
"clientId": "mashroom",
"clientSecret": "mashroom",
"redirectUrl": "http://localhost:5050/openid-connect-cb",
"rolesClaim": "roles",
"adminRoles": [
"mashroom-admin"
]
}
}
}
Setup:
Possible config:
{
"plugins": {
"Mashroom OpenID Connect Security Provider": {
"issuerDiscoveryUrl": "https://accounts.google.com/.well-known/openid-configuration",
"scope": "openid email profile",
"clientId": "xxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"clientSecret": "xxxxxxxxxxxxxxxx",
"redirectUrl": "http://localhost:5050/openid-connect-cb",
"extraAuthParams": {
"access_type": "offline"
},
"usePKCE": true
}
}
}
The access_type=offline parameter is necessary to get a refresh token.
Since Google users don't have authorization roles there is no way to make some users Administrator.
Setup:
Possible config:
{
"plugins": {
"Mashroom OpenID Connect Security Provider": {
"mode": "OAuth2",
"issuerMetadata": {
"issuer": "GitHub",
"authorization_endpoint": "https://github.com/login/oauth/authorize",
"token_endpoint": "https://github.com/login/oauth/access_token",
"userinfo_endpoint": "https://api.github.com/user",
"end_session_endpoint": null
},
"scope": "openid email profile",
"clientId": "xxxxxxxxxxxxxxxx",
"clientSecret": "xxxxxxxxxxxxxxxx",
"redirectUrl": "http://localhost:5050/openid-connect-cb"
}
}
}
Since GitHub uses pure OAuth2 the users don't have permission roles and there is no way to make some users Administrator. It also supports no userinfo endpoint, so it actually makes not much sense to use it with Mashroom.
This plugin adds support for Basic authentication to any other security provider that implements login() properly. This can be useful when you need to access some APIs on the server from an external system or for test purposes.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-basic-wrapper as dependency.
To activate this provider configure the Mashroom Security plugin like this:
{
"plugins": {
"Mashroom Security Services": {
"provider": "Mashroom Basic Wrapper Security Provider"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Basic Wrapper Security Provider": {
"targetSecurityProvider": "Mashroom Security Simple Provider",
"onlyPreemptive": true,
"realm": "mashroom"
}
}
}
This plugin adds a default login webapp which can be used for security providers that require a login page.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-default-login-webapp as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Security Default Login Webapp": {
"path": "/login",
"pageTitle": "My fancy website",
"loginFormTitle": "Login",
"styleFile": "./login_style.css"
}
}
}
If you add this plugin all updating HTTP methods (such as POST, PUT and DELETE) must contain a CSRF token automatically generated for the session. Otherwise, the request will be rejected.
There are two ways to pass the token:
You can use the MashroomCSRFService to get the current token.
Mashroom Portal automatically uses this plugin to secure all requests if available.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-csrf-protection as dependency.
After that you can use the service like this:
import type {MashroomCacheControlService} from '@mashroom/mashroom-csrf-protection/type-definitions';
export default (req: Request, res: Response) => {
const csrfService: MashroomCacheControlService = req.pluginContext.services.csrf.service;
const token = csrfService.getCSRFToken(req);
// ...
}
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom CSRF Middleware": {
"safeMethods": ["GET", "HEAD", "OPTIONS"]
},
"Mashroom CSRF Services": {
"saltLength": 8,
"secretLength": 18
}
}
}
The exposed service is accessible through pluginContext.services.csrf.service
Interface:
export interface MashroomCSRFService {
/**
* Get the current CSRF token for this session
*/
getCSRFToken(request: Request): string;
/**
* Check if the given token is valid
*/
isValidCSRFToken(request: Request, token: string): boolean;
}
This plugin adds the Helmet middleware which sets a bunch of protective HTTP headers on each response.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-helmet as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Helmet Middleware": {
"helmet": {
"contentSecurityPolicy": false,
"crossOriginEmbedderPolicy": false,
"crossOriginOpenerPolicy": {
"policy": "same-origin"
},
"crossOriginResourcePolicy": {
"policy": "same-site"
},
"expectCt": false,
"referrerPolicy": false,
"hsts": {
"maxAge": 31536000
},
"noSniff": true,
"originAgentCluster": false,
"dnsPrefetchControl": {
"allow": false
},
"frameguard": {
"action": "sameorigin"
},
"permittedCrossDomainPolicies": {
"permittedPolicies": "none"
},
"hidePoweredBy": false,
"xssFilter": true
}
}
}
}
NOTE: You shouldn't enable the noCache module because this would significantly decrease the performance of the Mashroom Portal.
This plugin allows it to show proper HTML pages for arbitrary HTTP status codes. It delivers error pages only if the request accept header contains text/html. So, typically not for AJAX requests.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-error-pages as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Error Pages Middleware": {
"mapping": {
"404": "./pages/404.html",
"403": "./pages/403.html",
"400": "http://my.server-com/bad_request.html",
"500": "/some/server/path/500.html",
"default": "./pages/default.html"
}
}
}
}
This plugin adds a storage service abstraction that delegates to a provider plugin.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-storage as dependency.
Then use the storage service like this:
import type {MashroomStorageService} from '@mashroom/mashroom-storage/type-definitions';
export default async (req: Request, res: ExpressResponse) => {
const storageService: MashroomStorageService = req.pluginContext.services.storage.service;
const customerCollection = await storageService.getCollection('my-collection');
const customer = await customerCollection.findOne({customerNr: 1234567});
const customers = await customerCollection.find({ $and: [{ name: { $regex: 'jo.*' } }, { visits: { $gt: 10 } }], 20, 10, { visits: 'desc' });
// ...
}
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Storage Services": {
"provider": "Mashroom Storage Filestore Provider",
"memoryCache": {
"enabled": false,
"ttlSec": 120,
"invalidateOnUpdate": true,
"collections": {
"mashroom-portal-pages": {
"enabled": true,
"ttlSec": 300
}
}
}
}
}
}
The exposed service is accessible through pluginContext.services.storage.service
Interface:
export interface MashroomStorageService {
/**
* Get (or create) the MashroomStorageCollection with given name.
*/
getCollection<T extends StorageRecord>(name: string): Promise<MashroomStorageCollection<T>>;
}
export interface MashroomStorageCollection<T extends MashroomStorageRecord> {
/**
* Find all items that match given filter. The filter supports a subset of Mongo's filter operations (like $gt, $regex, ...).
*/
find(filter?: MashroomStorageObjectFilter<T>, limit?: number, skip?: number, sort?: MashroomStorageSort<T>): Promise<MashroomStorageSearchResult<T>>;
/**
* Return the first item that matches the given filter or null otherwise.
*/
findOne(filter: MashroomStorageObjectFilter<T>): Promise<MashroomStorageObject<T> | null | undefined>;
/**
* Insert one item
*/
insertOne(item: T): Promise<MashroomStorageObject<T>>;
/**
* Update the first item that matches the given filter.
*/
updateOne(filter: MashroomStorageObjectFilter<T>, propertiesToUpdate: Partial<MashroomStorageObject<T>>): Promise<MashroomStorageUpdateResult>;
/**
* Update multiple entries
*/
updateMany(filter: MashroomStorageObjectFilter<T>, propertiesToUpdate: Partial<MashroomStorageObject<T>>): Promise<MashroomStorageUpdateResult>;
/**
* Replace the first item that matches the given filter.
*/
replaceOne(filter: MashroomStorageObjectFilter<T>, newItem: T): Promise<MashroomStorageUpdateResult>;
/**
* Delete the first item that matches the given filter.
*/
deleteOne(filter: MashroomStorageObjectFilter<T>): Promise<MashroomStorageDeleteResult>;
/**
* Delete all items that match the given filter.
*/
deleteMany(filter: MashroomStorageObjectFilter<T>): Promise<MashroomStorageDeleteResult>;
}
This plugin type adds a a new storage implementation that can be used by this plugin.
To register a new storage-provider plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Storage Provider",
"type": "storage-provider",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"myProperty": "test"
}
}
]
}
}
The bootstrap returns the provider:
import MyStorage from './MyStorage';
import type {MashroomStoragePluginBootstrapFunction} from '@mashroom/mashroom-storage/type-definitions';
const bootstrap: MashroomStoragePluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
return new MyStorage(/* .... */);
};
export default bootstrap;
The plugin has to implement the following interfaces:
export interface MashroomStorage {
/**
* Get (or create) the MashroomStorageCollection with given name.
*/
getCollection<T extends StorageRecord>(
name: string,
): Promise<MashroomStorageCollection<T>>;
}
This plugin adds a simple but cluster-safe, JSON based storage provider.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-storage-provider-filestore as dependency.
To activate this provider configure the Mashroom Security plugin like this:
{
"plugins": {
"Mashroom Storage Services": {
"provider": "Mashroom Storage Filestore Provider"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Storage Filestore Provider": {
"dataFolder": "/var/mashroom/data/storage",
"checkExternalChangePeriodMs": 2000,
"prettyPrintJson": true
}
}
}
This plugin adds a MongoDB based storage provider.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-storage-provider-mongodb as dependency.
To activate this provider, configure the Mashroom Security plugin like this:
{
"plugins": {
"Mashroom Storage Services": {
"provider": "Mashroom Storage MongoDB Provider"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Storage MongoDB Provider": {
"uri": "mongodb://user:xxxxx@localhost:27017/mashroom_storage_db",
"connectionOptions": {
"poolSize": 5,
"useUnifiedTopology": true,
"useNewUrlParser": true
}
}
}
}
This plugin adds Express session as middleware.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Session Middleware": {
"order": -100,
"provider": "Mashroom Session Filestore Provider",
"session": {
"secret": "EWhQ5hvETGkqvPDA",
"resave": false,
"saveUninitialized": false,
"cookie": {
"maxAge": 7200000,
"httpOnly": true,
"secure": false,
"sameSite": false
}
}
}
}
}
Security hints:
This plugin type adds a session store that can be used by this plugin.
To register a custom session-store-provider plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Session Provider",
"type": "session-store-provider",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"myProperty": "test"
}
}
]
}
}
The bootstrap returns the express session store (here for example the file store):
import sessionFileStore from 'session-file-store';
import type {MashroomSessionStoreProviderPluginBootstrapFunction} from '@mashroom/mashroom-session/type-definitions';
const bootstrap: MashroomSessionStoreProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder, expressSession) => {
const options = {...pluginConfig};
const FileStore = sessionFileStore(expressSession);
return new FileStore(options);
};
export default bootstrap;
This plugin adds a file based session store that can be used by Mashroom Session. Actually this is just a wrapper for the session-file-store package.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session-provider-filestore as dependency.
Activate this session provider in your Mashroom config file like this:
{
"plugins": {
"Mashroom Session Middleware": {
"provider": "Mashroom Session Filestore Provider"
}
}
}
And to change the default config of this plugin add:
{
"plugins": {
"Mashroom Session Filestore Provider": {
"path": "../../data/sessions"
}
}
}
NOTE: The base for relative paths is the Mashroom config file.
All config options are passed to the session-file-store. See session-file-store for available options.
This plugin adds a Redis session store that can be used by Mashroom Session. Actually this is just a wrapper for the connect-redis package.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session-provider-redis as dependency.
Activate this session provider in your Mashroom config file like this:
{
"plugins": {
"Mashroom Session Redis Provider": {
"provider": "Mashroom Session MongoDB Provider"
}
}
}
And to change the default config of this plugin add:
{
"plugins": {
"Mashroom Session Redis Provider": {
"client": {
"redisOptions": {
"host": "localhost",
"port": "6379",
}
},
"prefix": "mashroom:sess:",
"ttl": 86400
}
}
}
NOTE: Don't set client.redisOptions.keyPrefix because then the session metrics will not work properly.
For a high availability cluster with Sentinel the configuration would look like this:
{
"plugins": {
"Mashroom Session Redis Provider": {
"client": {
"redisOptions": {
"sentinels": [
{ "host": "localhost", "port": 26379 },
{ "host": "localhost", "port": 26380 }
],
"name": "myMaster",
"keyPrefix": "mashroom:sess:"
}
}
}
}
}
Checkout out the Sentinel section of the ioredis documentation for all available options.
For a sharding cluster configure the plugin like this:
{
"plugins": {
"Mashroom Session Redis Provider": {
"client": {
"cluster": true,
"clusterNodes": [
{
"host": "redis-node1",
"port": "6379"
},
{
"host": "redis-node2",
"port": "6379"
}
],
"clusterOptions": {
"maxRedirections": 3
},
"redisOptions": {
"keyPrefix": "mashroom:sess:"
}
}
}
}
}
Checkout out the Cluster section of the ioredis documentation for all available options.
This plugin adds a mongoDB session store that can be used by Mashroom Session. Actually this is just a wrapper for the connect-mongo package.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session-provider-mongodb as dependency.
Activate this session provider in your Mashroom config file like this:
{
"plugins": {
"Mashroom Session Middleware": {
"provider": "Mashroom Session MongoDB Provider"
}
}
}
And to change the default config of this plugin add:
{
"plugins": {
"Mashroom Session MongoDB Provider": {
"client": {
"uri": "mongodb://localhost:27017/mashroom_session_db?connectTimeoutMS=1000&socketTimeoutMS=2500",
"connectionOptions": {
"minPoolSize": 5,
"serverSelectionTimeoutMS": 3000
}
},
"collectionName": "mashroom-sessions",
"ttl": 86400,
"autoRemove": "native",
"autoRemoveInterval": 10,
"touchAfter": 0,
"crypto": {
"secret": false
}
}
}
}
This plugin adds a service for forwarding requests to a target URI. It supports HTTP, HTTPS and WebSockets (only the default/nodeHttpProxy implementation, see below).
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-http-proxy as dependency.
After that you can use the service like this:
import type {MashroomHttpProxyService} from '@mashroom/mashroom-http-proxy/type-definitions';
export default async (req: Request, res: Response) => {
const httpProxyService: MashroomHttpProxyService = req.pluginContext.services.proxy.service;
const targetURI = 'http://foo.bar/api/test';
const additionalHeaders = {};
await httpProxyService.forward(req, res, targetURI, additionalHeaders);
}
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Http Proxy Services": {
"forwardMethods": [
"GET",
"POST",
"PUT",
"DELETE"
],
"forwardHeaders": [
"accept",
"accept-*",
"range",
"expires",
"cache-control",
"last-modified",
"content-*",
"uber-trace-id",
"uberctx-",
"b3",
"x-b3-*",
"trace*"
],
"rejectUnauthorized": true,
"poolMaxSockets": 10,
"socketTimeoutMs": 60000,
"keepAlive": true,
"proxyImpl": "default"
}
}
}
The exposed service is accessible through pluginContext.services.proxy.service
Interface:
export interface MashroomHttpProxyService {
/**
* Forwards the given request to the targetUri and passes the response from the target to the response object.
* The Promise will always resolve, you have to check response.statusCode to see if the transfer was successful or not.
* The Promise will resolve as soon as the whole response was sent to the client.
*/
forward(req: Request, res: Response, targetUri: string, additionalHeaders?: HttpHeaders): Promise<void>;
/**
* Forwards a WebSocket request (ws or wss).
* The passed additional headers are only available at the upgrade/handshake request (most WS frameworks allow you to access it).
*/
forwardWs(req: IncomingMessageWithContext, socket: Socket, head: Buffer, targetUri: string, additionalHeaders?: HttpHeaders): Promise<void>;
}
This plugin type can be used to intercept http proxy calls and to add for example authentication headers to backend calls.
To register your custom http-proxy-interceptor plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Custom Http Proxy Interceptor",
"type": "http-proxy-interceptor",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"order": 500,
"myProperty": "foo"
}
}
]
}
}
The bootstrap returns the interceptor:
import type {MashroomHttpProxyInterceptorPluginBootstrapFunction} from '@mashroom/mashroom-http-proxy/type-definitions';
const bootstrap: MashroomHttpProxyInterceptorPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
return new MyInterceptor(/* ... */);
};
export default bootstrap;
The provider has to implement the following interface:
interface MashroomHttpProxyInterceptor {
/**
* Intercept request to given targetUri.
*
* The existingHeaders contain the original request headers, headers added by the MashroomHttpProxyService client and the ones already added by other interceptors.
* The existingQueryParams contain query parameters from the request and the ones already added by other interceptors.
*
* clientRequest is the request that shall be forwarded. DO NOT MANIPULATE IT. Just use it to access "method" and "pluginContext".
*
* Return null or undefined if you don't want to interfere with a call.
*/
interceptRequest?(targetUri: string, existingHeaders: Readonly<HttpHeaders>, existingQueryParams: Readonly<QueryParams>,
clientRequest: Request, clientResponse: Response):
Promise<MashroomHttpProxyRequestInterceptorResult | undefined | null>;
/**
* Intercept WebSocket request to given targetUri.
*
* The existingHeaders contain the original request headers, headers added by the MashroomHttpProxyService client and the ones already added by other interceptors.
*
* The changes are ONLY applied to the upgrade request, not to WebSocket messages.
*
* Return null or undefined if you don't want to interfere with a call.
*/
interceptWsRequest?(targetUri: string, existingHeaders: Readonly<HttpHeaders>, clientRequest: IncomingMessageWithContext):
Promise<MashroomWsProxyRequestInterceptorResult | undefined | null>;
/**
* Intercept response from given targetUri.
*
* The existingHeaders contain the original request header and the ones already added by other interceptors.
* targetResponse is the response that shall be forwarded to the client. DO NOT MANIPULATE IT. Just use it to access "statusCode" an such.
*
* Return null or undefined if you don't want to interfere with a call.
*/
interceptResponse?(targetUri: string, existingHeaders: Readonly<HttpHeaders>, targetResponse: IncomingMessage,
clientRequest: Request, clientResponse: Response):
Promise<MashroomHttpProxyResponseInterceptorResult | undefined | null>;
}
As an example you could add a Bearer token to each request like this (implemented like this in the mashroom-http-proxy-add-id-token module):
export default class MyInterceptor implements MashroomHttpProxyInterceptor {
async interceptRequest(targetUri: string, existingHeaders: Readonly<HttpHeaders>, existingQueryParams: Readonly<QueryParams>,
clientRequest: Request, clientResponse: Response) {
const logger = clientRequest.pluginContext.loggerFactory('test.http.interceptor');
const securityService = clientRequest.pluginContext.services.security && clientRequest.pluginContext.services.security.service;
const user = securityService.getUser(clientRequest);
if (!user) {
return;
}
return {
addHeaders: {
Authorization: `Bearer ${user.secrets.idToken}`
}
};
}
}
Or return forbidden for some reason:
export default class MyInterceptor implements MashroomHttpProxyInterceptor {
async interceptRequest(targetUri: string, existingHeaders: Readonly<HttpHeaders>, existingQueryParams: Readonly<QueryParams>,
clientRequest: Request, clientResponse: Response) {
clientResponse.sendStatus(403);
return {
responseHandled: true
};
}
}
Or even manipulate the response:
export default class MyInterceptor implements MashroomHttpProxyInterceptor {
async interceptResponse(targetUri: string, existingHeaders: Readonly<HttpHeaders>, targetResponse: IncomingMessage, clientRequest: ExpressRequest, clientResponse: ExpressResponse) {
let body = [];
targetResponse.on('data', function (chunk) {
body.push(chunk);
});
targetResponse.on('end', function () {
body = Buffer.concat(body).toString();
console.log("Response from proxied server:", body);
clientResponse.json({ success: true });
});
// NOTE: if you "await" the end event you have to call targetResponse.resume() here
// because the interceptor pauses the stream from the target until all interceptors are done
return {
responseHandled: true
};
}
}
If you add this plugin it will add HTTP headers with user information to all proxy backend calls. By default, it adds:
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-csrf-protection as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Http Proxy Add User Headers Interceptor": {
"userNameHeader": "X-USER-NAME",
"displayNameHeader": "X-USER-DISPLAY-NAME",
"emailHeader": "X-USER-EMAIL",
"extraDataHeaders": {},
"targetUris": [".*"]
}
}
}
If you add this plugin it will add the access token from the OpenId Connect plugin to every backend call.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-csrf-protection as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Http Proxy Add Access Token Interceptor": {
"addBearer": false,
"accessTokenHeader": "X-USER-ACCESS-TOKEN",
"targetUris": [".*"]
}
}
}
This plugin adds WebSocket support to the Mashroom Server. It exposes a new service that can be used to interact with clients that connect at /websocket/*.
NOTE: This implementation only allows authenticated users to connect.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-websocket as dependency.
And you can use the security service like this:
import type {MashroomWebSocketService} from '@mashroom/mashroom-websocket/type-definitions';
export default async (req: Request, res: Response) => {
const webSocketService: MashroomWebSocketService = req.pluginContext.services.websocket.service;
webSocketService.addMessageListener((path) => path === '/whatever', async (message, client) => {
// ...
await webSocketService.sendMessage(client, {
message: 'Hello there'
});
});
}
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom WebSocket Webapp": {
"path": "/websocket",
"reconnectMessageBufferFolder": null,
"reconnectTimeoutSec": 5,
"restrictToRoles": ["WebSocketRole"],
"enableKeepAlive": true,
"keepAliveIntervalSec": 15,
"maxConnections": 2000
}
}
}
There will also be a test page available under: /websocket/test
When you connect with a client you will receive a message with your clientId from the server:
{
"type": "setClientId",
"payload": "abcdef"
}
When you get disconnected you should reconnect with the query parameter ?clientId=abcdef to get all messages you missed meanwhile.
This only works if reconnectMessageBufferFolder is set properly.
The exposed service is accessible through pluginContext.services.websocket.service
Interface:
export interface MashroomWebSocketService {
/**
* Add a listener for message.
* The matcher defines which messages the listener receives. The match can be based on the connect path
* (which is the sub path where the client connected, e.g. if it connected on /websocket/test the connect path would be /test)
* or be based on the message content or both.
*/
addMessageListener(
matcher: MashroomWebSocketMatcher,
listener: MashroomWebSocketMessageListener,
): void;
/**
* Remove a message listener
*/
removeMessageListener(
matcher: MashroomWebSocketMatcher,
listener: MashroomWebSocketMessageListener,
): void;
/**
* Add a listener for disconnects
*/
addDisconnectListener(listener: MashroomWebSocketDisconnectListener): void;
/**
* Remove a disconnect listener
*/
removeDisconnectListener(
listener: MashroomWebSocketDisconnectListener,
): void;
/**
* Send a (JSON) message to given client.
*/
sendMessage(client: MashroomWebSocketClient, message: any): Promise<void>;
/**
* Get all clients on given connect path
*/
getClientsOnPath(connectPath: string): Array<MashroomWebSocketClient>;
/**
* Get all clients for a specific username
*/
getClientsOfUser(username: string): Array<MashroomWebSocketClient>;
/**
* Get the number of connected clients
*/
getClientCount(): number;
/**
* Close client connection (this will also trigger disconnect listeners)
*/
close(client: MashroomWebSocketClient): void;
/**
* The base path where clients can connect
*/
readonly basePath: string;
}
This plugin adds server side messaging support to Mashroom Server. If an external provider plugin (e.g. MQTT) is configured the messages can also be sent across multiple nodes (cluster support!) and to 3rd party systems.
Optionally it supports sending and receiving messages via WebSocket (Requires mashroom-websocket).
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging as dependency.
And you can use the messaging service like this:
import type {MashroomMessagingService} from '@mashroom/mashroom-messaging/type-definitions';
export default async (req: Request, res: Response) => {
const messagingService: MashroomMessagingService = req.pluginContext.services.messaging.service;
// Subscribe
await messagingService.subscribe(req, 'my/topic', (data) => {
// Do something with data
});
// Publish
await messagingService.publish(req, 'other/topic', {
item: 'Beer',
quantity: 1,
});
// ...
}
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Messaging Services": {
"externalProvider": null,
"externalTopics": [],
"userPrivateBaseTopic": "user",
"enableWebSockets": true,
"topicACL": "./topicACL.json"
}
}
}
With a config like that you can place a file topicacl.json_ in your server config with a content like this:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-security-topic-acl.json",
"my/topic": {
"allow": ["Role1"]
},
"foo/bar/#": {
"allow": "any"
"deny": ["NotSoTrustedRole"]
}
}
The general structure is:
"/my/+/topic/#": {
"allow": "any"|<array of roles>
"deny": "any"|<array of roles>
}
You can use here + or * as a wildcard for a single level and # for multiple levels.
If enableWebSockets is true you can connect to the messaging system on
After a successful connection you can use the following commands:
Subscribe
{
messageId: 'ABCD',
command: 'subscribe',
topic: 'foo/bar',
}
The messageId should be unique. You will get a response message like this when the operation succeeds:
{
messageId: 'ABCD',
success: true,
}
Otherwise a error message like this:
{
messageId: 'ABCD',
error: true,
message: 'The error message'
}
Unsubscribe
{
messageId: 'ABCD',
command: 'unsubscribe',
topic: 'foo/bar',
}
Success and error response messages are the same as above.
Publish
{
messageId: 'ABCD',
command: 'publish',
topic: 'foo/bar',
message: {
foo: 'bar'
}
}
Success and error response messages are the same as above.
And the server will push the following if a message for a subscribed topic arrives:
{
remoteMessage: true,
topic: 'foo/bar',
message: {
what: 'ever'
}
}
The exposed service is accessible through pluginContext.services.messaging.service
Interface:
export interface MashroomMessagingService {
/**
* Subscribe to given topic.
* Topics can be hierarchical and also can contain wildcards. Supported wildcards are + for a single level
* and # for multiple levels. E.g. foo/+/bar or foo/#
*
* Throws an exception if there is no authenticated user
*/
subscribe(
req: Request,
topic: string,
callback: MashroomMessagingSubscriberCallback,
): Promise<void>;
/**
* Unsubscribe from topic
*/
unsubscribe(
topic: string,
callback: MashroomMessagingSubscriberCallback,
): Promise<void>;
/**
* Publish to a specific topic
*
* Throws an exception if there is no authenticated user
*/
publish(req: Request, topic: string, data: any): Promise<void>;
/**
* The private topic only the current user can access.
* E.g. if the value is user/john the user john can access to user/john/whatever
* but not to user/otheruser/foo
*
* Throws an exception if there is no authenticated user
*/
getUserPrivateTopic(req: Request): string;
/**
* The connect path to send publish or subscribe via WebSocket.
* Only available if enableWebSockets is true and mashroom-websocket is preset.
*/
getWebSocketConnectPath(req: Request): string | null | undefined;
}
This plugin type connects the messaging system to an external provider such as MQTT or AMQP. It also adds cluster support to the messaging system.
To register your custom external-messaging-provider plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Custom External Messaging Provider",
"type": "external-messaging-provider",
"bootstrap": "./dist/mashroom-bootstrap",
"defaultConfig": {
"myProperty": "foo"
}
}
]
}
}
The bootstrap returns the provider:
import type {MashroomExternalMessagingProviderPluginBootstrapFunction} from '@mashroom/mashroom-messaging/type-definitions';
const bootstrap: MashroomExternalMessagingProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
return new MyExternalMessagingProvider(/* ... */);
};
export default bootstrap;
The provider has to implement the following interface:
export interface MashroomMessagingExternalProvider {
/**
* Add a message listener
* The message must be a JSON object.
*/
addMessageListener(listener: MashroomExternalMessageListener): void;
/**
* Remove an existing listener
*/
removeMessageListener(listener: MashroomExternalMessageListener): void;
/**
* Send a message to given external topic.
* The passed topic must be prefixed with the topic the provider is listening to.
* E.g. if the passed topic is foo/bar and the provider is listening to mashroom/# the message must be
* sent to mashroom/foo/bars.
*
* The message will be a JSON object.
*/
sendInternalMessage(topic: string, message: any): Promise<void>;
/**
* Send a message to given external topic.
* The message will be a JSON object.
*/
sendExternalMessage(topic: string, message: any): Promise<void>;
}
This plugin allows to use a MQTT server as external messaging provider for server side messaging. This enables cluster support for server side messaging and also allows communication with 3rd party systems.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging-external-provider-mqtt as dependency.
To activate this provider configure the Mashroom Messaging plugin like this:
{
"plugins": {
"Mashroom Messaging Services": {
"externalProvider": "Mashroom Messaging External Provider MQTT"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Messaging External Provider MQTT": {
"internalTopic": "mashroom",
"mqttConnectUrl": "mqtt://localhost:1883",
"mqttProtocolVersion": 4,
"mqttQoS": 1,
"mqttUser": null,
"mqttPassword": null,
"rejectUnauthorized": true
}
}
}
This plugin allows to use an AMQP 1.0 compliant broker as external messaging provider for server side messaging. This enables cluster support for server side messaging and also allows communication with 3rd party systems.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging-external-provider-amqp as dependency.
To activate this provider configure the Mashroom Messaging plugin like this:
{
"plugins": {
"Mashroom Messaging Services": {
"externalProvider": "Mashroom Messaging External Provider AMQP"
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Messaging External Provider MQTT": {
"internalRoutingKey": "mashroom",
"brokerTopicExchangePrefix": "/topic/",
"brokerTopicMatchAny": "#",
"brokerHost": "localhost",
"brokerPort": 5672,
"brokerUsername": null,
"brokerPassword": null
}
}
}
RabbitMQ
"brokerTopicExchangePrefix": "/topic/",
"brokerTopicMatchAny": "#",
ActiveMQ
"brokerTopicExchangePrefix": "topic://",
"brokerTopicMatchAny": ">",
Qpid Broker
"brokerTopicExchangePrefix": "amq.topic/",
"brokerTopicMatchAny": "#",
This plugin adds a general purpose memory cache service. Some other plugins will automatically use it if present, for example mashroom-storage.
The cache service provides multiple regions with the possibility to clear single regions. It comes with a built-in provider that uses the local Node.js memory, which is not ideal for clusters. But it can also be configured to use another provider, e.g. an implementation based on Redis.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-memory-cache as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Memory Cache Services": {
"provider": "local",
"defaultTTLSec": 300
}
}
}
The exposed service is accessible through pluginContext.services.cache.service
Interface:
export interface MashroomMemoryCacheService {
/**
* Get a cache entry from given region
*/
get(region: string, key: CacheKey): Promise<CacheValue | undefined>;
/**
* Set a cache entry in given region
*/
set(region: string, key: CacheKey, value: CacheValue, ttlSec?: number): Promise<void>;
/**
* Delete an entry in given region
*/
del(region: string, key: CacheKey): Promise<void>;
/**
* Clear the entire region
* This might be an expensive operation, depending on the provider
*/
clear(region: string): Promise<void>;
/**
* Get the number of entries in this region (if possible)
* This might be an expensive operation, depending on the provider
*/
getEntryCount(region: string): Promise<number | undefined>;
}
This plugin type adds a memory cache provider that can be used by this plugin.
To register a custom memory-cache-provider plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Cache Store Provider",
"type": "memory-cache-provider",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"myProperty": "test"
}
}
]
}
}
The bootstrap returns the provider:
import type {MashroomMemoryCacheProviderPluginBootstrapFunction} from '@mashroom/mashroom-memory-cache/type-definitions';
const bootstrap: MashroomMemoryCacheProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
return new MyCacheStore();
};
export default bootstrap;
And the provider has to implement the following interface:
export interface MashroomMemoryCacheProvider {
/**
* Get a cache entry from given region
*/
get(region: string, key: CacheKey): Promise<CacheValue | undefined>;
/**
* Set a cache entry in given region
*/
set(region: string, key: CacheKey, value: CacheValue, ttlSec: number): Promise<void>;
/**
* Delete an entry in given region
*/
del(region: string, key: CacheKey): Promise<void>;
/**
* Clear the entire region
*/
clear(region: string): Promise<void>;
/**
* Get the number of entries in this region (if possible)
*/
getEntryCount(region: string): Promise<number | undefined>;
}
This plugin adds a Redis based provider for the mashroom-memory-cache.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-memory-cache-provider-redis as dependency.
To activate this provider configure the Mashroom Memory Cache plugin like this:
{
"plugins": {
"Mashroom Memory Cache Redis Provider": {
"provider": "Mashroom Memory Cache Redis Provider",
"defaultTTLSec": 10
}
}
}
And configure this plugin like this in the Mashroom config file:
{
"plugins": {
"Mashroom Memory Cache Redis Provider": {
"redisOptions": {
"host": "localhost",
"port": "6379",
"keyPrefix": "mashroom:cache:"
}
}
}
}
Checkout out the ioredis documentation for all available options.
For a high availability cluster with Sentinel the configuration would look like this:
{
"plugins": {
"Mashroom Memory Cache Redis Provider": {
"redisOptions": {
"sentinels": [
{ "host": "localhost", "port": 26379 },
{ "host": "localhost", "port": 26380 }
],
"name": "myMaster",
"keyPrefix": "mashroom:cache:"
}
}
}
}
Checkout out the Sentinel section of the ioredis documentation for all available options.
For a sharding cluster configure the plugin like this:
{
"plugins": {
"Mashroom Memory Cache Redis Provider": {
"cluster": true,
"clusterNodes": [
{
"host": "redis-node1",
"port": "6379"
},
{
"host": "redis-node2",
"port": "6379"
}
],
"clusterOptions": {
"maxRedirections": 3
},
"redisOptions": {
"keyPrefix": "mashroom:cache:"
}
}
}
}
Checkout out the Cluster section of the ioredis documentation for all available options.
This plugin adds a service for internationalization. It determines the language from the HTTP headers and supports translation of messages.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-i18n as dependency.
After that you can use the service like this:
import type {MashroomI18NService} from '@mashroom/mashroom-i18n/type-definitions';
export default (req: Request, res: Response) => {
const i18nService: MashroomI18NService = req.pluginContext.services.i18n.service;
const currentLang = i18nService.getLanguage(req);
const message = i18nService.getMessage('username', 'de');
// message will be 'Benutzernamen'
// ...
}
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Internationalization Services": {
"availableLanguages": ["en", "de", "fr"],
"defaultLanguage": "en",
"messages": "./messages"
}
}
}
The lookup for message files works like this:
And a messages file (e.g. messages.de.json) looks like this:
{
"message_key": "Die Nachricht"
}
The exposed service is accessible through pluginContext.services.i18n.service
Interface:
export interface MashroomI18NService {
/**
* Get the currently set language (for current session)
*/
getLanguage(req: Request): string;
/**
* Set session language
*/
setLanguage(language: string, req: Request): void;
/**
* Get the message for given key and language
*/
getMessage(key: string, language: string): string;
/**
* Get plain string in the current users language from a I18NString
*/
translate(req: Request, str: I18NString): string;
/**
* Get available languages
*/
readonly availableLanguages: Readonly<Array<string>>;
/**
* Get the default languages
*/
readonly defaultLanguage: string;
}
This plugin adds a background job scheduler to the Mashroom Server that supports cron expressions. It is possible to add background jobs via service or as custom plugin.
If you don't provide a cron expression the job is only executed now (immediately), this is useful if you need some code that should be executed once during server startup.
This plugin also comes with an Admin UI extension (/mashroom/admin/ext/background-jobs) that can be used to check the jobs.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-background-jobs as dependency.
After that you can use the service like this:
import type {MashroomBackgroundJobService} from '@mashroom/mashroom-background-jobs/type-definitions';
export default async (req: Request, res: Response) => {
const backgroundJobsService: MashroomBackgroundJobService = req.pluginContext.services.backgroundJobs.service;
backgroundJobsService.schedule('Test Job', '0/5 * * * *', () => {
// Job implementation
});
// ...
}
NOTE: Despite its name a job is started in the main thread and therefore blocks the event loop. So, if you do CPU intensive work you need to spawn a Worker Thread yourself.
The exposed service is accessible through pluginContext.services.backgroundJobs.service
Interface:
export interface MashroomBackgroundJobService {
/**
* Schedule a job.
* If cronSchedule is not defined the job is executed once (immediately).
* Throws an error if the cron expression is invalid.
*/
scheduleJob(name: string, cronSchedule: string | undefined | null, callback: MashroomBackgroundJobCallback): MashroomBackgroundJob;
/**
* Unschedule an existing job
*/
unscheduleJob(name: string): void;
readonly jobs: Readonly<Array<MashroomBackgroundJob>>;
}
This plugin type allows it to schedule a background job.
To register your custom background-job plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My background job",
"type": "background-job",
"bootstrap": "./dist/mashroom-bootstrap.js",
"defaultConfig": {
"cronSchedule": "0/1 * * * *",
"yourConfigProp": "whatever"
}
}
]
}
}
The bootstrap returns the job callback:
import type {MashroomBackgroundJobPluginBootstrapFunction} from '@mashroom/mashroom-background-jobs/type-definitions';
const bootstrap: MashroomBackgroundJobPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
const {yourConfigProp} = pluginConfig;
return (pluginContect) => {
// Job impl
};
};
export default bootstrap;
This plugin adds a Service to manage cache control headers. It also allows to disable the cache globally.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-browser-cache as dependency.
After that you can use the service like this:
import type {MashroomCacheControlService} from '@mashroom/mashroom-browser-cache/type-definitions';
export default async (req: Request, res: Response) => {
const cacheControlService: MashroomCacheControlService = req.pluginContext.services.browserCache.cacheControl;
await cacheControlService.addCacheControlHeader('ONLY_FOR_ANONYMOUS_USERS', req, res);
// ..
};
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Cache Control Services": {
"disabled": false,
"maxAgeSec": 31536000
}
}
}
The Cache Control service is accessible through pluginContext.services.browserCache.cacheControl
Interface:
export interface MashroomCacheControlService {
/**
* Add the Cache-Control header based on the policy and authentication status.
*/
addCacheControlHeader(cachingPolicy: CachingPolicy, request: Request, response: Response): void;
/**
* Remove a previously set Cache-Control header
*/
removeCacheControlHeader(response: Response): void;
}
Caching Policies are:
export type CachingPolicy = 'SHARED' | 'PRIVATE_IF_AUTHENTICATED' | 'NEVER' | 'ONLY_FOR_ANONYMOUS_USERS';
This plugin adds a Service to manage CDN hosts. It basically just returns a host from a configurable list, which can be used to access an asset via CDN.
NOTE: The mashroom-cdn plugin requires a CDN that works like a transparent proxy, which forwards an identical request to the origin (in this case Mashroom) if does not exist yet.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-cdn as dependency.
After that you can use the service like this:
import type {MashroomCDNService} from '@mashroom/mashroom-cdn/type-definitions';
export default async (req: Request, res: Response) => {
const cdnService: MashroomCDNService = req.pluginContext.services.cdn.service;
const cdnHost = cdnService.getCDNHost();
const resourceUrl = `${cdnHost}/<the-actual-path>`;
// ..
};
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom CDN Services": {
"cdnHosts": [
"//cdn1.myhost.com",
"//cdn2.myhost.com"
]
}
}
}
The CDN service is accessible through pluginContext.services.cdn.cacheControl
Interface:
export interface MashroomCDNService {
/**
* Return a CDN host or null if there is none configured.
*/
getCDNHost(): string | null;
}
This plugin adds a middleware that exposes a robots.txt file for search engines.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-robots as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Robots Middleware": {
"robots.txt": "./robots.txt"
}
}
}
This plugin adds the possibility to map external paths to internal ones based on virtual host. This is required for web-apps that need to know the actual "base path" to generate URLs (in that case rewriting via reverse proxy won't work).
For example Mashroom Portal can use this to move Sites to different paths but keep the ability to generate absolute paths for resources and API calls. Which is useful if you want to expose specific Sites via a virtual host.
NOTE: All other plugins will only deal with the rewritten paths, keep that in mind especially when defining ACLs.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-vhost-path-mapper as dependency.
To map for example a portal site to www.my-company.com/web configure the reverse proxy like this:
www.my-company.com/web -> <portal-host>:<portal-port>/
and the plugin like this:
{
"plugins": {
"Mashroom VHost Path Mapper Middleware": {
"considerHttpHeaders": ["x-my-custom-host-header", "x-forwarded-host"],
"hosts": {
"www.my-company.com": {
"frontendBasePath": "/web",
"mapping": {
"/login": "/login",
"/": "/portal/public-site"
}
},
"localhost:8080": {
"mapping": {
"/": "/local-test"
}
}
}
}
}
}
That means if someone accesses Mashroom Server via https://www.my-company.com/web/test the request will hit the path /portal/public-site/test.
It also works the other way round. If the server redirects to /login it would be changed to /web/login (in this example).
Port based virtual hosts (like localhost:8080) are also possible but only if the request still contains the original host header (and no X-Forwarded-Host different from the host header).
The mapping rules do not support regular expressions.
The frontendBasePath is optional and / by default.
The considerHttpHeaders property is also optional and can be used to detect the host based on some custom header. The first header that is present will be used (so the order in the list specifies the priority).
The exposed service is accessible through pluginContext.services.vhostPathMapper.service
Interface:
export interface MashroomVHostPathMapperService {
/**
* Reverse map the given server url to the url as seen by the user (browser).
* The given URL must not contain host, only path with query params and so on.
*/
getFrontendUrl(req: Request, url: string): string;
/**
* Get the details if the url of the current path has been rewritten
*/
getMappingInfo(req: Request): RequestVHostMappingInfo | undefined;
}
This plugin provides a service to add metrics to the monitoring system that can be used by plugins. It also adds a middleware that collects request metrics like duration and HTTP status.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-monitoring-stats-collector as dependency.
You can change the default configuration in your Mashroom config file like this:
{
"plugins": {
"Mashroom Monitoring Metrics Collector Services": {
"disableMetrics": [],
"defaultHistogramBuckets": [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
"customHistogramBucketConfig": {
"mashroom_http_request_duration_seconds": [0.1, 1, 10]
},
"defaultSummaryQuantiles": [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999],
"customSummaryQuantileConfig": {
}
}
}
}
The exposed service is accessible through pluginContext.services.metrics.service
Interface:
/**
* Mashroom Monitoring Metrics Collector Service
*
* It uses the metric types defined here: https://prometheus.io/docs/concepts/metric_types
* and supports also *labels* which can be used to differentiate the characteristics of the thing that is being measured;
* e.g. to group requests total by the HTTP error code.
*
* The AggregationHint can be used if you need to aggregate metrics, e.g. in a Node.js cluster.
*/
export interface MashroomMonitoringMetricsCollectorService {
/**
* A counter is a cumulative metric that represents a single monotonically increasing counter
* whose value can only increase.
* If though the returned Counter has a set() method, the new value must always be higher than the current.
*/
counter(name: string, help: string, aggregationHint?: AggregationHint): Counter;
/**
* A gauge is a metric that represents a single numerical value that can arbitrarily go up and down.
*/
gauge(name: string, help: string, aggregationHint?: AggregationHint): Gauge;
/**
* A histogram samples observations (usually things like request durations or response sizes)
* and counts them in configurable buckets.It also provides a sum of all observed values.
*/
histogram(name: string, help: string, buckets?: number[], aggregationHint?: AggregationHint): Histogram;
/**
* Similar to a histogram, a summary samples observations. While it also provides a total count of
* observations and a sum of all observed values, it calculates configurable quantiles..
*/
summary(name: string, help: string, quantiles?: number[], aggregationHint?: AggregationHint): Summary;
/**
* Get the collected metrics
*/
getMetrics(): MetricDataMap;
}
NOTE: Don't keep a reference to the returned metrics objects. Instead, use the service like this:
const collectorService: MashroomMonitoringMetricsCollectorService = req.pluginContext.services.metrics.service;
collectorService.counter('http_request_counter', 'HTTP Request Counter').inc();
This plugin exports the following metrics to the Prometheus monitoring system:
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-monitoring-prometheus-exporter as dependency.
You can change the default configuration in your Mashroom config file like this:
{
"plugins": {
"Mashroom Monitoring Prometheus Exporter Webapp": {
"path": "/myMetricsPath",
"enableGcStats": true
}
}
}
The examples assume the Prometheus job to scrape the metrics adds the label service:Mashroom
Request rate:
sum(rate(mashroom_http_requests_total{service="Mashroom"}[5m]))
Requests with HTTP 500 response rate:
sum(rate(mashroom_http_requests_total{status="500", service="Mashroom"}[5m]))
95% of requests served within seconds:
histogram_quantile(0.95, sum(rate(mashroom_http_request_duration_seconds_bucket{service="Mashroom"}[5m])) by (le)) * 1000
Heap total in MB:
nodejs_heap_size_total_bytes{service="Mashroom"} / 1024 / 1024
Heap used in MB:
nodejs_heap_size_used_bytes{service="Mashroom"} / 1024 / 1024
CPU usage total in %:
avg(irate(process_cpu_seconds_total{service="Mashroom"}[5m])) * 100
GC pauses rate:
sum(rate(nodejs_gc_pause_seconds_total{service="Mashroom"}[5m]))
GC pauses 95% quantile
histogram_quantile(0.95, sum(rate(nodejs_gc_duration_seconds_bucket[5m])) by (le))
User sessions:
mashroom_sessions_total{service="Mashroom"}
Active HTTP proxy connections (e.g. Portal App REST calls):
mashroom_http_proxy_active_connections_total{service="Mashroom"}
Idle HTTP proxy connections:
mashroom_http_proxy_idle_connections_total{service="Mashroom"}
Plugins total:
mashroom_plugins_total{service="Mashroom"}
Plugins loaded:
mashroom_plugins_loaded_total{service="Mashroom"}
Plugins in error state:
mashroom_plugins_error_total{service="Mashroom"}
Remote portal apps total:
mashroom_remote_apps_total{service="Mashroom"}
Remote portal apps in error state:
mashroom_remote_apps_error_total{service="Mashroom"}
Remote portal apps with connection timeouts:
mashroom_remote_apps_connection_timeout_total{service="Mashroom"}
Kubernetes remote portal apps total:
mashroom_remote_apps_k8s_total{service="Mashroom"}
Kubernetes remote portal apps in error state:
mashroom_remote_apps_k8s_error_total{service="Mashroom"}
Kubernetes remote portal apps with connection timeouts:
mashroom_remote_apps_k8s_connection_timeout_total{service="Mashroom"}
Memory cache hit ratio:
mashroom_memory_cache_hit_ratio{service="Mashroom"}
Redis session store provider connected:
mashroom_sessions_redis_nodes_connected{service="Mashroom"}
Redis memory cache provider connected:
mashroom_memory_cache_redis_nodes_connected{service="Mashroom"}
MongoDB storage provider connected:
mashroom_storage_mongodb_connected{service="Mashroom"}
MQTT messaging system connected:
mashroom_messaging_mqtt_connected{service="Mashroom"}
If you run a Node.js cluster you have to launch a separate server in the master process and gather the metrics like this:
import {AggregatorRegistry} from 'prom-client';
const aggregatorRegistry = new AggregatorRegistry();
metricsServer.get('/metrics', async (req, res) => {
const metrics = await aggregatorRegistry.clusterMetrics();
res.set('Content-Type', aggregatorRegistry.contentType);
res.send(metrics);
});
If you use PM2 this won't work because it occupies the master process. There you need to launch a separate app which communicates with the other workers to gather the metrics. Like this:
Create a script metrics.js:
const pm2 = require('pm2');
const promClient = require('prom-client');
const Express = require('express');
const metricsServer = Express();
const dummyRegistry = new promClient.Registry();
const metrics = {};
const metricsServerPort = 15050;
metricsServer.get('/metrics/:id', async (req, res) => {
const id = req.params.id;
const slice = metrics[id];
if (!slice) {
console.error(`No metrics found for ID ${id}. Known node IDs:`, Object.keys(metrics));
res.sendStatus(404);
return;
}
const response = promClient.AggregatorRegistry.aggregate([slice]);
res.set('Content-Type', dummyRegistry.contentType);
res.send(await response.metrics());
});
metricsServer.get('/metrics', async (req, res) => {
const response = promClient.AggregatorRegistry.aggregate(
Object.values(metrics).map((o) => o),
);
res.set('Content-Type', dummyRegistry.contentType);
res.send(await response.metrics());
});
metricsServer.listen(metricsServerPort, '0.0.0.0', () => {
console.debug(`Prometheus cluster metrics are available at http://localhost:${metricsServerPort}/metrics`);
});
setInterval(() => {
pm2.connect(() => {
pm2.describe('mashroom', (describeError, processInfo) => {
if (!describeError) {
Promise.all(processInfo.map((processData) => {
console.debug(`Asking process ${processData.pm_id} for metrics`);
return new Promise((resolve) => {
pm2.sendDataToProcessId(
processData.pm_id,
{
data: null,
topic: 'getMetrics',
from: process.env.pm_id,
},
(err, res) => {
if (err) {
console.error('Error sending data via PM2 intercom', err);
}
resolve();
},
);
});
})).finally(() => {
pm2.disconnect();
});
} else {
pm2.disconnect();
}
});
});
}, 10000);
process.on('message', (msg) => {
if (msg.from !== process.env.pm_id && msg.topic === 'returnMetrics') {
console.debug(`Received metrics from process ${msg.from}`);
metrics[msg.from] = msg.data;
}
});
And a pm2 config like this:
{
"apps": [
{
"name": "mashroom",
"instances": 4,
"max_restarts": 3,
"exec_mode": "cluster",
"script": "starter.js",
"env": {
"NODE_ENV": "production"
}
},
{
"name": "metrics_collector",
"instances": 1,
"exec_mode": "fork",
"script": "metrics.js"
}
]
}
Now the cluster metrics will be available under http:/localhost:15050/metrics.
NOTE: In a real world application it might not be ideal to rely on prom-client aggregation. Instead you should expose the metrics for each worker node separately and do the aggregation in your monitoring tool. In this example metrics for a single node will be available at http:/localhost:15050/metrics/<pm2_pid>.
In case you want to get the metrics separately, the Prometheus scrape config could look like this:
- job_name: 'mashroom'
static_configs:
- targets:
- localhost:15050/metrics/0
- localhost:15050/metrics/2
- localhost:15050/metrics/3
- localhost:15050/metrics/4
labels:
service: 'Mashroom'
relabel_configs:
- source_labels: [__address__]
regex: '[^/]+(/.*)'
target_label: __metrics_path__
- source_labels: [__address__]
regex: '[^/]+/[^/]+/(.*)'
target_label: node
- source_labels: [__address__]
regex: '([^/]+)/.*'
target_label: __address__
On Kubernetes the metrics are scraped separately for each container. So, you have to do the aggregation in the query.
For example, the overall request rate would still be:
sum(rate(mashroom_http_requests_total{namespace="my-namespace"}[5m]))
But the request rate per pod:
sum by (kubernetes_pod_name) (rate(mashroom_http_requests_total{namespace="my-namespace"}[5m]))
Or the Session count per pod:
mashroom_sessions_total{namespace="my-namespace"} by (kubernetes_pod_name)
In the last two examples you typically would use {{kubernetespodname}} in the legend.
You can find a demo Grafana Dashboard here: https://github.com/nonblocking/mashroom/tree/master/packages/plugin-packages/mashroom-monitoring-prometheus-exporter/test/grafana-test/grafana/provisioning/dashboards/Mashroom%20Dashboard.json
This plugin exports metrics to the PM2 via pm2/io. Which is useful if you use the PM2 process manager to run Mashroom Server.
It activates the pm2/io default metrics like v8, runtime, network, http (configurable). And it exports Mashroom plugin metrics like session count, memory cache stats, MongoDB/Redis connection stats, …
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-monitoring-pm2-exporter as dependency.
You can change the default configuration in your Mashroom config file like this:
"plugins": {
"Mashroom Monitoring PM2 Exporter": {
"pmxMetrics": {
"v8": true,
"runtime": true,
"network": {
"upload": true,
"download": true
},
"http": true,
"eventLoop": true
},
"mashroomMetrics": [
"mashroom_plugins_total",
"mashroom_plugins_loaded_total",
"mashroom_plugins_error_total",
"mashroom_remote_apps_total",
"mashroom_remote_apps_error_total",
"mashroom_remote_apps_connection_timeout_total",
"mashroom_sessions_total",
"mashroom_websocket_connections_total",
"mashroom_https_proxy_active_connections_total",
"mashroom_https_proxy_idle_connections_total",
"mashroom_https_proxy_waiting_requests_total",
"mashroom_sessions_mongodb_connected",
"mashroom_sessions_redis_nodes_connected",
"mashroom_storage_mongodb_connected",
"mashroom_memory_cache_entries_added_total",
"mashroom_memory_cache_hit_ratio",
"mashroom_memory_cache_redis_nodes_connected",
"mashroom_messaging_amqp_connected",
"mashroom_messaging_mqtt_connected"
]
}
}
}
NOTE: Currently only counter and gauge metrics can be exported! For a full list install the mashroom-monitoring-prometheus-exporter and check the output of /metrics
After starting the server with pm2 you can see the metrics in the "Custom metrics" pane when you start:
pm2 monit
Or you can get it as JSON (axm_monitor property) if you execute
pm2 prettylist
This plugin adds a Portal component which allows composing pages from Single Page Applications (SPAs).
Registered SPAs (Portal Apps) can be placed on arbitrary pages via Drag'n'Drop. Each instance receives a config object during startup and a bunch of client services, which for example allow access to the message bus. The config is basically an arbitrary JSON object, which can be edited via Admin Toolbar. A Portal App can also bring its own config editor App, which again is just a simple SPA.
One of the provided client services allow Portal Apps to load any other App (known by name) into any existing DOM node. This can be used to:
The Portal supports hybrid rendering for both the Portal pages and SPAs. So, if an SPA supports server side rendering the initial HTML can be incorporated into the initial HTML page. Navigating to another page dynamically replaces the SPAs in the content area via client side rendering (needs to be supported by the Theme).
The Portal also supports i18n, theming, role based security, a client-side message bus which can be connected to a server-side broker and a registry for Remote Apps on a separate server or container.
Since this plugin requires a lot of other plugins the easiest way to use it is to clone this quickstart repository: mashroom-portal-quickstart
You can find a full documentation of Mashroom Server and this portal plugin with a setup and configuration guide here: https://www.mashroom-server.com/documentation
The plugin allows the following configuration properties:
{
"plugins": {
"Mashroom Portal WebApp": {
"path": "/portal",
"adminApp": "Mashroom Portal Admin App",
"defaultTheme": "Mashroom Portal Default Theme",
"defaultLayout": "Mashroom Portal Default Layouts 1 Column",
"warnBeforeAuthenticationExpiresSec": 60,
"autoExtendAuthentication": false,
"ignoreMissingAppsOnPages": false,
"versionHashSalt": null,
"defaultProxyConfig": {
"sendPermissionsHeader": false,
"restrictToRoles": ["ROLE_X"]
},
"ssrConfig": {
"ssrEnable": true,
"renderTimoutMs": 2000,
"cacheEnable": true,
"cacheTTLSec": 300,
"inlineStyles": true
},
"addDemoPages": true
}
}
}
The Portal supports only modern Browsers and requires ES6.
The exposed service is accessible through pluginContext.services.portal.service
Interface:
export interface MashroomPortalService {
/**
* Get all registered portal apps
*/
getPortalApps(): Readonly<Array<MashroomPortalApp>>;
/**
* Get all registered theme plugins
*/
getThemes(): Readonly<Array<MashroomPortalTheme>>;
/**
* Get all registered layout plugins
*/
getLayouts(): Readonly<Array<MashroomPortalLayout>>;
/**
* Get all registered page enhancement plugins
*/
getPortalPageEnhancements(): Readonly<Array<MashroomPortalPageEnhancement>>;
/**
* Get all registered app enhancement plugins
*/
getPortalAppEnhancements(): Readonly<Array<MashroomPortalAppEnhancement>>;
/**
* Get all sites
*/
getSites(limit?: number): Promise<Array<MashroomPortalSite>>;
/**
* Get the site with the given id
*/
getSite(siteId: string): Promise<MashroomPortalSite | null | undefined>;
/**
* Find the site with given path
*/
findSiteByPath(path: string): Promise<MashroomPortalSite | null | undefined>;
/**
* Insert new site
*/
insertSite(site: MashroomPortalSite): Promise<void>;
/**
* Update site
*/
updateSite(site: MashroomPortalSite): Promise<void>;
/**
* Delete site
*/
deleteSite(req: Request, siteId: string): Promise<void>;
/**
* Get page with given id
*/
getPage(pageId: string): Promise<MashroomPortalPage | null | undefined>;
/**
* Find the page ref within a site with given friendly URL
*/
findPageRefByFriendlyUrl(site: MashroomPortalSite, friendlyUrl: string): Promise<MashroomPortalPageRef | null | undefined>;
/**
* Find the page ref within a site by the given pageId
*/
findPageRefByPageId(site: MashroomPortalSite, pageId: string): Promise<MashroomPortalPageRef | null | undefined>;
/**
* Insert new page
*/
insertPage(page: MashroomPortalPage): Promise<void>;
/**
* Update page
*/
updatePage(page: MashroomPortalPage): Promise<void>;
/**
* Insert new page
*/
deletePage(req: Request, pageId: string): Promise<void>;
/**
* GetPortal App instance
*/
getPortalAppInstance(pluginName: string, instanceId: string | null | undefined): Promise<MashroomPortalAppInstance | null | undefined>;
/**
* Insert a new Portal App instance
*/
insertPortalAppInstance(portalAppInstance: MashroomPortalAppInstance): Promise<void>;
/**
* Update given Portal App instance
*/
updatePortalAppInstance(portalAppInstance: MashroomPortalAppInstance): Promise<void>;
/**
* Delete given portal Portal App instance
*/
deletePortalAppInstance(req: Request, pluginName: string, instanceId: string | null | undefined): Promise<void>;
}
Deprecated since Mashroom v2, please use portal-app2.
This plugin type makes a Single Page Application (SPA) available in the Portal.
To register a new portal-app plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Single Page App",
"type": "portal-app2",
"clientBootstrap": "startMyApp",
"resources": {
"js": [
"bundle.js"
]
},
"local": {
"resourcesRoot": "./dist",
"ssrBootstrap": "./dist/renderToString.js"
},
"defaultConfig": {
"appConfig": {
"myProperty": "foo"
}
}
}
]
}
}
A full config with all optional properties would look like this:
{
"mashroom": {
"plugins": [
{
"name": "My Single Page App",
"type": "portal-app2",
"clientBootstrap": "startMyApp",
"resources": {
"js": [
"bundle.js"
],
"css": []
},
"sharedResources": {
"js": []
},
"screenshots": [
"screenshot1.png"
],
"local": {
"resourcesRoot": "./dist",
"ssrBootstrap": "/dist/renderToString.js"
},
"remote": {
"resourcesRoot": "/public",
"ssrInitialHtmlPath": "/ssr"
},
"defaultConfig": {
"title": {
"en": "My Single Page App",
"de": "Meine Single Page App"
},
"category": "My Category",
"tags": ["my", "stuff"],
"description": {
"en": "Here the english description",
"de": "Hier die deutsche Beschreibung"
},
"metaInfo": {
"capabilities": ["foo"]
},
"defaultRestrictViewToRoles": ["Role1"],
"rolePermissions": {
"doSomethingSpecial": ["Role2", "Role3"]
},
"caching": {
"ssrHtml": "same-config-and-user"
},
"editor": {
"editorPortalApp": "Demo Config Editor",
"position": "in-place",
"appConfig": {
}
},
"proxies": {
"spaceXApi": {
"targetUri": "https://api.spacexdata.com/v3",
"sendPermissionsHeader": false,
"restrictToRoles": ["Role1"]
}
},
"appConfig": {
"myProperty": "foo"
}
}
}
]
}
}
The clientBootstrap is in this case a global function that starts the App within the given host element. Here for example a React app:
import React from 'react';
import {render, hydrate, unmountComponentAtNode} from 'react-dom';
import App from './App';
import type {MashroomPortalAppPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (element, portalAppSetup, clientServices) => {
const {appConfig, restProxyPaths, lang} = portalAppSetup;
const {messageBus} = clientServices;
// Check if the Apps has been rendered in the server-side, if this is a Hybrid App and a ssrBootstrap is configured
//const ssrHost = element.querySelector('[data-ssr-host="true"]');
//if (ssrHost) {
// hydrate(<App appConfig={appConfig} messageBus={messageBus}/>, ssrHost);
//} else {
// CSR
render(<App appConfig={appConfig} messageBus={messageBus}/>, element);
//}
return {
willBeRemoved: () => {
unmountComponentAtNode(portalAppHostElement);
},
updateAppConfig: (appConfig) => {
// Implement if dynamic app config should be possible
}
};
};
global.startMyApp = bootstrap;
And for an Angular app:
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {MashroomPortalAppPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
import {AppModule} from './app/app.module';
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (hostElement, portalAppSetup, portalClientServices) => {
return platformBrowserDynamic([
{provide: 'host.element', useValue: hostElement },
{provide: 'app.setup', useValue: portalAppSetup},
{provide: 'client.services', useValue: portalClientServices}
]).bootstrapModule(AppModule).then(
(module) => {
return {
willBeRemoved: () => {
console.info('Destroying Angular module');
module.destroy();
}
};
}
);
};
global.startAngularDemoApp = bootstrap;
In case of a Hybrid App which supports Server Side Rendering (SSR) the server side bootstrap would look like this:
import React from 'react';
import {renderToString} from 'react-dom/server';
import App from './App';
import type {MashroomPortalAppPluginSSRBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalAppPluginSSRBootstrapFunction = (portalAppSetup, req) => {
const {appConfig, restProxyPaths, lang} = portalAppSetup;
const dummyMessageBus: any = {};
const html = renderToString(<App appConfig={appConfig} messageBus={dummyMessageBus}/>);
return html;
// Alternatively (supports Composite Apps)
/*
return {
html,
embeddedApps: [],
}
*/
};
export default bootstrap;
The portalAppSetup has the following structure:
export type MashroomPortalAppSetup = {
readonly appId: string;
readonly title: string | null | undefined;
readonly proxyPaths: MashroomPortalProxyPaths;
// Legacy, will be removed in Mashroom v3
readonly restProxyPaths: MashroomPortalProxyPaths;
readonly resourcesBasePath: string;
readonly globalLaunchFunction: string;
readonly lang: string;
readonly user: MashroomPortalAppUser;
readonly appConfig: MashroomPluginConfig;
}
export type MashroomPortalAppUser = {
readonly guest: boolean;
readonly username: string;
readonly displayName: string;
readonly email: string | null;
readonly permissions: MashroomPortalAppUserPermissions;
readonly [customProp: string]: any;
}
The clientServices argument contains the client services, see below.
Client Services
The following client side services are available for all portal apps:
MashroomPortalMessageBus
export interface MashroomPortalMessageBus {
/**
* Subscribe to given topic.
* Topics starting with getRemotePrefix() will be subscribed server side via WebSocket (if available).
* Remote topics can also contain wildcards: # for multiple levels and + or * for a single level
* (e.g. remote:/foo/+/bar)
*/
subscribe(topic: string, callback: MashroomPortalMessageBusSubscriberCallback): Promise<void>;
/**
* Subscribe once to given topic. The handler will be removed after the first message has been received.
* Remote topics are accepted.
*/
subscribeOnce(topic: string, callback: MashroomPortalMessageBusSubscriberCallback): Promise<void>;
/**
* Unsubscribe from given topic.
* Remote topics are accepted.
*/
unsubscribe(topic: string, callback: MashroomPortalMessageBusSubscriberCallback): Promise<void>;
/**
* Publish to given topic.
* Remote topics are accepted.
*/
publish(topic: string, data: any): Promise<void>;
/**
* Get the private user topic for the given user or the currently authenticated user if no argument given.
* You can subscribe to "sub" topics as well, e.g. <private_topic>/foo
*/
getRemoteUserPrivateTopic(username?: string): string | null | undefined;
/**
* The prefix for remote topics
*/
getRemotePrefix(): string;
/**
* Register a message interceptor.
* An interceptor can be useful for debugging or to manipulate the messages.
* It can change the data of an event by return a different value or block messages
* by calling cancelMessage() from the interceptor arguments.
*/
registerMessageInterceptor(interceptor: MashroomPortalMessageBusInterceptor): void;
/**
* Unregister a message interceptor.
*/
unregisterMessageInterceptor(interceptor: MashroomPortalMessageBusInterceptor): void;
}
MashroomPortalStateService
export interface MashroomPortalStateService {
/**
* Get a property from state.
* It will be looked up in the URL (query param or encoded) and in the local and session storage
*/
getStateProperty(key: string): any | null | undefined;
/**
* Add given key value pair into the URL (encoded)
*/
setUrlStateProperty(key: string, value: any | null | undefined): void;
/**
* Add given key value pair to the session storage
*/
setSessionStateProperty(key: string, value: any): void;
/**
* Add given key value pair to the local storage
*/
setLocalStoreStateProperty(key: string, value: any): void;
}
MashroomPortalAppService
export interface MashroomPortalAppService {
/**
* Get all existing apps
*/
getAvailableApps(): Promise<Array<MashroomAvailablePortalApp>>;
/**
* Load portal app to given host element at given position (or at the end if position is not set)
*
* The returned promise will always resolve! If there was a loading error the MashroomPortalLoadedPortalApp.error property will be true.
*/
loadApp(appAreaId: string, pluginName: string, instanceId: string | null | undefined, position?: number | null | undefined, overrideAppConfig?: any | null | undefined): Promise<MashroomPortalLoadedPortalApp>;
/**
* Load portal app into a modal overlay.
*
* The returned promise will always resolve! If there was a loading error the MashroomPortalLoadedPortalApp.error property will be true.
*/
loadAppModal(pluginName: string, title?: string | null | undefined, overrideAppConfig?: any | null | undefined, onClose?: ModalAppCloseCallback | null | undefined): Promise<MashroomPortalLoadedPortalApp>;
/**
* Reload given portal app
*
* The returned promise will always resolve!
* If there was a loading error the MashroomPortalLoadedPortalApp.error property will be true.
*/
reloadApp(id: string, overrideAppConfig?: any | null | undefined): Promise<MashroomPortalLoadedPortalApp>;
/**
* Unload given portal app
*/
unloadApp(id: string): void;
/**
* Move a loaded app to another area (to another host element within the DOM)
*/
moveApp(id: string, newAppAreaId: string, newPosition?: number): void;
/**
* Show the name and version for all currently loaded apps in a overlay (for debug purposes)
*/
showAppInfos(customize?: (portalApp: MashroomPortalLoadedPortalApp, overlay: HTMLDivElement) => void): void;
/**
* Hide all app info overlays
*/
hideAppInfos(): void;
/**
* Add listener for load events (fired after an app has been loaded an attached to the page)
*/
registerAppLoadedListener(listener: MashroomPortalAppLoadListener): void;
/**
* Remove listener for load events
*/
unregisterAppLoadedListener(listener: MashroomPortalAppLoadListener): void;
/**
* Add listener for unload events (fired before an app will been detached from the page)
*/
registerAppAboutToUnloadListener(listener: MashroomPortalAppLoadListener,): void;
/**
* Remove listener for unload events
*/
unregisterAppAboutToUnloadListener(listener: MashroomPortalAppLoadListener,): void;
/**
* Load the setup for given app/plugin name on the current page
*/
loadAppSetup(pluginName: string, instanceId: string | null | undefined): Promise<MashroomPortalAppSetup>;
/**
* Get some stats about a loaded App
*/
getAppStats(pluginName: string): MashroomPortalLoadedPortalAppStats | null;
/**
* Check if some loaded Portal Apps have been update (and have a different version on the server).
* This can be used to check if the user should refresh the current page.
*
* Returns the list of upgraded Apps.
*/
checkLoadedPortalAppsUpdated(): Promise<Array<string>>;
/**
* Prefetch resources of given app/plugin. This is useful if you know which apps you will have to load
* in the future and want to minimize the loading time.
*/
prefetchResources(pluginName: string): Promise<void>;
readonly loadedPortalApps: Array<MashroomPortalLoadedPortalApp>;
}
MashroomPortalUserService
export interface MashroomPortalUserService {
/**
* Get the authentication expiration time in unix time ms.
* Returns null if the check fails and "0" if the check returns 403.
*/
getAuthenticationExpiration(): Promise<number | null>;
/**
* Extend the authentication.
* Can be used to update the authentication when no server interaction has occurred for a while and the authentication is about to expire.
*/
extendAuthentication(): void;
/**
* Logout the current user
*/
logout(): Promise<void>;
/**
* Get the current user's language
*/
getUserLanguage(): string;
/**
* Set the new user language
*/
setUserLanguage(lang: string): Promise<void>;
/**
* Get all available languages (e.g. en, de)
*/
getAvailableLanguages(): Promise<Array<string>>;
/**
* Get the configured default language
*/
getDefaultLanguage(): Promise<string>;
}
MashroomPortalSiteService
export interface MashroomPortalSiteService {
/**
* Get the base url for the current site
*/
getCurrentSiteUrl(): string;
/**
* Get a list with all sites
*/
getSites(): Promise<Array<MashroomPortalSiteLinkLocalized>>;
/**
* Get the page tree for given site
*/
getPageTree(siteId: string): Promise<Array<MashroomPortalPageRefLocalized>>;
}
MashroomPortalPageService
export interface MashroomPortalPageService {
/**
* Get current pageId
*/
getCurrentPageId(): string;
/**
* Get the page friendlyUrl from given URL (e.g. /portal/web/test?x=1 -> /test)
*/
getPageFriendlyUrl(pageUrl: string): string;
/**
* Find the pageId for given URL (can be a page friendlyUrl or a full URL as seen by the client).
*/
getPageId(pageUrl: string): Promise<string | undefined>;
/**
* Get the content for given pageId.
* It also calculates if the correct theme and all necessary page enhancements for the requested page
* are already loaded. Otherwise fullPageLoadRequired is going to be true and no content returned.
*/
getPageContent(pageId: string): Promise<MashroomPortalPageContent>;
}
MashroomPortalRemoteLogger
export interface MashroomPortalRemoteLogger {
/**
* Send a client error to the server log
*/
error(msg: string, error?: Error): void;
/**
* Send a client warning to the server log
*/
warn(msg: string, error?: Error): void;
/**
* Send a client info to the server log
*/
info(msg: string): void;
}
MashroomPortalAdminService
export interface MashroomPortalAdminService {
/**
* Get all existing themes
*/
getAvailableThemes(): Promise<Array<MashroomAvailablePortalTheme>>;
/**
* Get all existing layouts
*/
getAvailableLayouts(): Promise<Array<MashroomAvailablePortalLayout>>;
/**
* Get all currently existing roles
*/
getExistingRoles(): Promise<Array<RoleDefinition>>;
/**
* Get all app instances on current page
*/
getAppInstances(): Promise<Array<MashroomPagePortalAppInstance>>;
/**
* Add an app to the current page.
*/
addAppInstance(pluginName: string, areaId: string, position?: number, appConfig?: any): Promise<MashroomPagePortalAppInstance>;
/**
* Update given app instance config or position
*/
updateAppInstance(pluginName: string, instanceId: string, areaId: string | null | undefined, position: number | null | undefined, appConfig: any | null | undefined): Promise<void>;
/**
* Remove given app instance from page
*/
removeAppInstance(pluginName: string, instanceId: string): Promise<void>;
/**
* Get roles that are permitted to view the app (no roles means everyone is permitted)
*/
getAppInstancePermittedRoles(pluginName: string, instanceId: string): Promise<string[] | null | undefined>;
/**
* Update roles that are permitted to view the app (undefined or null means everyone is permitted)
*/
updateAppInstancePermittedRoles(pluginName: string, instanceId: string, roles: string[] | null | undefined): Promise<void>;
/**
* Get current pageId
*/
getCurrentPageId(): string;
/**
* Get page data
*/
getPage(pageId: string): Promise<MashroomPortalPage>;
/**
* Add new page
*/
addPage(page: MashroomPortalPage): Promise<MashroomPortalPage>;
/**
* Update an existing page
*/
updatePage(page: MashroomPortalPage): Promise<void>;
/**
* Delete the given page
*/
deletePage(pageId: string): Promise<void>;
/**
* Get roles that are permitted to view the page (no roles means everyone is permitted)
*/
getPagePermittedRoles(pageId: string): Promise<string[] | null | undefined>;
/**
* Update roles that are permitted to view the page (undefined or null means everyone is permitted)
*/
updatePagePermittedRoles(pageId: string, roles: string[] | null | undefined): Promise<void>;
/**
* Get current siteId
*/
getCurrentSiteId(): string;
/**
* Get site with given id
*/
getSite(siteId: string): Promise<MashroomPortalSite>;
/**
* Add new site
*/
addSite(site: MashroomPortalSite): Promise<MashroomPortalSite>;
/**
* Update existing site
*/
updateSite(site: MashroomPortalSite): Promise<void>;
/**
* Delete the given site
*/
deleteSite(siteId: string): Promise<void>;
/**
* Get roles that are permitted to view the site (no roles means everyone is permitted)
*/
getSitePermittedRoles(siteId: string): Promise<string[] | null | undefined>;
/**
* Update roles that are permitted to view the site (undefined or null means everyone is permitted)
*/
updateSitePermittedRoles(siteId: string, roles: string[] | null | undefined): Promise<void>;
}
This plugin types adds a theme to the Portal.
To register a new portal-theme plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Theme",
"type": "portal-theme",
"bootstrap": "./dist/mashroom-bootstrap.js",
"resourcesRoot": "./dist",
"views": "./views",
"defaultConfig": {
"param1": true
}
}
]
}
}
Since Mashroom Portal uses the Express render mechanism all Template Engines supported by Express can be used to define the template. The bootstrap returns the template engine and the engine name like so:
import {engine} from 'express-handlebars';
import path from 'path';
import type {MashroomPortalThemePluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalThemePluginBootstrapFunction = async () => {
return {
engineName: 'handlebars',
engineFactory: () => {
return engine({
partialsDir: path.resolve(__dirname, '../views/partials/'),
});
},
};
};
export default bootstrap;
NOTE: Even if Express.js could automatically load the template engine (like for Pug) you have to provide the engineFactory here, otherwise plugin local
modules can not be loaded. In that case define the engineFactory like this:
engineFactory: () => require('pug').__express
The theme can contain the following views:
A typical portal view with Handlebars might look like this:
<!doctype html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="{{model.page.description}}">
<meta name="keywords" content="{{model.page.keywords}}">
{{#if csrfToken}}
<meta name="csrf-token" content="{{csrfToken}}">
{{/if}}
<title>{{site.title}} - {{page.title}}</title>
<link rel="stylesheet" type="text/css" href="{{resourcesBasePath}}/style.css">
{{{portalResourcesHeader}}}
{{#if page.extraCss}}
<style>
{{{page.extraCss}}}
</style>
{{/if}}
</head>
<body>
<div id="mashroom-portal-admin-app-container">
<!-- Admin app goes here -->
</div>
<header>
<div class="site-name">
<h1>{{site.title}}</h1>
</div>
</header>
<main>
{{> navigation}}
<div id="portal-page-content" class="mashroom-portal-apps-container container-fluid">
{{{pageContent}}}
</div>
</main>
<div id="mashroom-portal-modal-overlay">
<div class="mashroom-portal-modal-overlay-wrapper">
<div class="mashroom-portal-modal-overlay-header">
<div id="mashroom-portal-modal-overlay-title">Title</div>
<div id="mashroom-portal-modal-overlay-close" class="close-button"></div>
</div>
<div class="mashroom-portal-modal-overlay-content">
<div id="mashroom-portal-modal-overlay-app">
<!-- Modal apps go here -->
</div>
</div>
</div>
</div>
<div id="mashroom-portal-auth-expires-warning">
<div class="mashroom-portal-auth-expires-warning-message">
{{{__ messages "authenticationExpiresWarning"}}}
</div>
</div>
{{{portalResourcesFooter}}}
</body>
</html>
The pageContent variable contains the actual content with the Portal layout (see below) and the Apps.
Here all available variables:
export type MashroomPortalPageRenderModel = {
readonly portalName: string;
readonly siteBasePath: string;
readonly apiBasePath: string;
readonly resourcesBasePath: string | null | undefined;
readonly site: MashroomPortalSiteLocalized;
readonly page: MashroomPortalPage & MashroomPortalPageRefLocalized;
readonly portalResourcesHeader: string;
readonly portalResourcesFooter: string;
readonly pageContent: string;
// @Deprecated, use pageContent; will be removed in 3.0
readonly portalLayout: string;
readonly lang: string;
readonly availableLanguages: Readonly<Array<string>>;
readonly messages: (key: string) => string;
readonly user: MashroomPortalUser;
readonly csrfToken: string | null | undefined;
readonly userAgent: UserAgent;
readonly lastThemeReloadTs: number;
readonly themeVersionHash: string;
}
This plugin type adds portal layouts to the portal. A layout defines a areas where portal-apps can be placed.
To register a new portal-layouts plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Layouts",
"type": "portal-layouts",
"layouts": {
"1 Column": "./layouts/1column.html",
"2 Columns": "./layouts/2columns.html",
"2 Columns 70/30": "./layouts/2columns_70_30.html",
"2 Columns with 1 Column Header": "./layouts/2columnsWith1columnHeader.html"
}
}
]
}
}
A layout looks like this:
<div class="row">
<div class="col-md-8 mashroom-portal-app-area" id="app-area1">
<!-- Portal apps go here -->
</div>
<div class="col-md-4 mashroom-portal-app-area" id="app-area2">
<!-- Portal apps go here -->
</div>
</div>
Important is the class mashroom-portal-app-area and a unique id element.
This plugin type adds a registry for remote portal-apps to the Portal.
To register a new remote-portal-app-registry plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "Mashroom Portal Remote App Registry",
"type": "remote-portal-app-registry",
"bootstrap": "./dist/registry/mashroom-bootstrap-remote-portal-app-registry.js",
"defaultConfig": {
"priority": 100
}
}
]
}
}
And the bootstrap must return an implementation of RemotePortalAppRegistry:
import {MyRegistry} from './MyRegistry';
import type {MashroomRemotePortalAppRegistryBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomRemotePortalAppRegistryBootstrapFunction = async (pluginName, pluginConfig, pluginContext) => {
return new MyRegistry();
};
export default bootstrap;
The plugin must implement the following interface:
export interface MashroomRemotePortalAppRegistry {
readonly portalApps: Readonly<Array<MashroomPortalApp>>;
}
h3.
This plugin type allows it to add extra resources (JavaScript and CSS) to a Portal page based on some (optional) rules. This can be used to add polyfills or some analytics stuff without the need to change a theme.
To register a new portal-page-enhancement plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Portal Page Enhancement",
"type": "portal-page-enhancement",
"bootstrap": "./dist/mashroom-bootstrap.js",
"pageResources": {
"js": [{
"path": "my-extra-scripts.js",
"rule": "includeExtraScript",
"location": "header",
"inline": false
}, {
"dynamicResource": "myScript",
"location": "header"
}],
"css": []
},
"defaultConfig": {
"order": 100,
"resourcesRoot": "./dist/public"
}
}
]
}
}
The bootstrap returns a map of rules and could look like this:
import type {MashroomPortalPageEnhancementPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalPageEnhancementPluginBootstrapFunction = () => {
return {
dynamicResources: {
myScript: () => `console.info('My Script loaded');`,
},
rules: {
// Example rule: Show only for IE
includeExtraScript: (sitePath, pageFriendlyUrl, lang, userAgent) => userAgent.browser.name === 'IE',
}
}
};
export default bootstrap;
The JavaScript or CSS resource can also be generated dynamically by the plugin. In that case it will always be inlined. To use this state a dynamicResource name instead of a path and include the function that actually generates the content to the object returned by the bootstrap:
{
"mashroom": {
"plugins": [
{
"name": "My Portal Page Enhancement",
"type": "portal-page-enhancement",
"bootstrap": "./dist/mashroom-bootstrap.js",
"pageResources": {
"js": [{
"dynamicResource": "extraScript",
"location": "header"
}],
"css": []
}
}
]
}
}
import type {MashroomPortalPageEnhancementPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalPageEnhancementPluginBootstrapFunction = () => {
return {
dynamicResources: {
extraScript: (sitePath, pageFriendlyUrl, lang, userAgent) => `console.info('test');`,
}
}
};
export default bootstrap;
This plugin type allows it to update or rewrite the portalAppSetup that is passed to Portal Apps at startup. This can be used to add extra config or user properties from a context. Additionally, this plugin allows it to pass extra clientServices to Portal Apps or replace one of the default ones.
To register a new portal-app-enhancement plugin add this to package.json:
{
"mashroom": {
"plugins": [
{
"name": "My Portal App Enhancement",
"type": "portal-app-enhancement",
"bootstrap": "./dist/mashroom-bootstrap.js",
"portalCustomClientServices": {
"customService": "MY_CUSTOM_SERVICE"
}
}
]
}
}
The bootstrap returns the actual enhancer plugin:
import MyPortalAppEnhancementPlugin from './MyPortalAppEnhancementPlugin';
import type {MashroomPortalAppEnhancementPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
const bootstrap: MashroomPortalAppEnhancementPluginBootstrapFunction = () => {
return new MyPortalAppEnhancementPlugin();
};
export default bootstrap;
The plugin has to implement the following interface:
export interface MashroomPortalAppEnhancementPlugin {
/**
* Enhance the portalAppSetup object passed as the first argument (if necessary)
*/
enhancePortalAppSetup: (portalAppSetup: MashroomPortalAppSetup, portalApp: MashroomPortalApp, request: Request) => Promise<MashroomPortalAppSetup>;
}
This plugin adds some default layouts to the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-default-layouts as dependency.
This plugin copies the property *extraData" from the server user object to the Portal App user. This is useful if the security provider adds some extra information (such as the phone number) and you want to use it in a Portal App.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-app-user-extradata as dependency.
This plugins adds the default theme for the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-default-theme as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Portal Default Theme": {
"spaMode": true,
"showPortalAppHeaders": true,
"showEnvAndVersions": true
}
}
}
This plugin contains the default Admin Toolbar for the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-admin-app as dependency.
To enable it add the following to the Mashroom Portal config:
{
"plugins": {
"Plugin: Mashroom Portal WebApp": {
"adminApp": "Mashroom Portal Admin App"
}
}
}
This Mashroom Portal App turns any app area where it is placed automatically into a tabbed container.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-tabify-app as dependency.
After placing it on a page use the Portal Admin Toolbar to set the following properties:
Updates via MessageBus
It is possible to change the App/Title mapping and to show a specific Tab via MessageBus. This is especially useful for dynamic cockpits where you load Apps programmatically via MashroomPortalAppService.
Available topics:
tabify-add-plugin-name-title-mapping expect a message like this:
{
pluginName: 'My App',
title: 'Another title'
}
tabify-add-app-id-title-mapping expect a message like this:
{
appId: '1234123',
title: 'Another title'
}
And tabify-focus-app expects just an id:
{
appId: '1234123'
}
Adds a (responsive) iFrame Portal App to the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-iframe-app as dependency.
After placing it on a page use the Portal Admin Toolbar to set the following properties:
To make the iFrame responsive, the embedded page has to post its height via message like so:
var lastContentHeight = null;
function sendHeight() {
var contentHeight = document.getElementById('content-wrapper').offsetHeight;
if (lastContentHeight !== contentHeight) {
parent.postMessage({
height: contentHeight + /* margin */ 20
}, "*");
lastContentHeight = contentHeight;
}
}
setInterval(sendHeight, 1000);
This plugin adds a remote app registry to Mashroom Portal, which scans periodically a list of remote servers for Portal Apps. It expects the package.json and optionally an external plugin config file (default mashroom.json) to be exposed at /. It also expects a remote config in the plugin definition, like this:
{
"name": "My Single Page App",
"remote": {
"resourcesRoot": "/public",
"ssrInitialHtmlPath": "/ssr"
}
}
You can find an example remote app here: Mashroom Demo Remote Portal App.
This plugin also comes with an Admin UI extension (/mashroom/admin/ext/remote-portal-apps) and a REST API to add and remote URL's. The Admin UI allows adding a URL temporary only for the current session.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-remote-app-registry as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Portal Remote App Background Job": {
"cronSchedule": "0/1 * * * *",
"socketTimeoutSec": 3,
"registrationRefreshIntervalSec": 600,
"unregisterAppsAfterScanErrors": -1
},
"Mashroom Portal Remote App Registry": {
"remotePortalAppUrls": "./remotePortalApps.json"
},
"Mashroom Portal Remote App Registry Admin Webapp": {
"showAddRemoteAppForm": true
}
}
}
The config file contains just a list of URLs:
{
"$schema": "https://www.mashroom-server.com/schemas/mashroom-portal-remote-apps.json",
"remotePortalApps": [
"http://demo-remote-app.mashroom-server.com"
]
}
The Service can be used like this:
import type {MashroomPortalRemoteAppEndpointService} from '@mashroom/mashroom-portal-remote-app-registry/type-definitions';
export default async (req: Request, res: Response) => {
const remoteAppService: MashroomPortalRemoteAppEndpointService = req.pluginContext.services.remotePortalAppEndpoint.service;
const remoteApps = await remoteAppService.findAll();
// ...
}
The REST API can be used like this:
Available at /portal-remote-app-registry/api. Methods:
json
{
"url": "http://my-server.com/app1",
"sessionOnly": false
}
The exposed service is accessible through pluginContext.services.remotePortalAppEndpoint.service
Interface:
export interface MashroomPortalRemoteAppEndpointService {
/**
* Register a new Remote App URL
*/
registerRemoteAppUrl(url: string): Promise<void>;
/**
* Register a Remote App URL only for the current session (useful for testing)
*/
synchronousRegisterRemoteAppUrlInSession(
url: string,
request: Request,
): Promise<void>;
/**
* Unregister a Remote App
*/
unregisterRemoteAppUrl(url: string): Promise<void>;
/**
* Find Remote App by URL
*/
findRemotePortalAppByUrl(
url: string,
): Promise<RemotePortalAppEndpoint | null | undefined>;
/**
* Return all known Remote App endpoints
*/
findAll(): Promise<Readonly<Array<RemotePortalAppEndpoint>>>;
/**
* Update an existing Remote App endpoint
*/
updateRemotePortalAppEndpoint(
remotePortalAppEndpoint: RemotePortalAppEndpoint,
): Promise<void>;
/**
* Refresh (fetch new metadata) from given endpoint
*/
refreshEndpointRegistration(
remotePortalAppEndpoint: RemotePortalAppEndpoint,
): Promise<void>;
}
Adds a remote app registry to Mashroom Portal which periodically scans Kubernetes services that expose Remote Portal Apps. It expects the package.json and optionally an external plugin config file (default mashroom.json) to be exposed at /. It also expects a remote config in the plugin definition, like this:
{
"name": "My Single Page App",
"remote": {
"resourcesRoot": "/public",
"ssrInitialHtmlPath": "/ssr"
}
}
You can find an example remote app here: Mashroom Demo Remote Portal App.
This plugin also comes with an Admin UI extension (/mashroom/admin/ext/remote-portal-apps-k8s) that can be used to check all registered Apps.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-remote-app-registry-k8s as dependency.
You can override the default config in your Mashroom config file like this:
{
"plugins": {
"Mashroom Portal Remote App Kubernetes Background Job": {
"cronSchedule": "0/1 * * * *",
"k8sNamespacesLabelSelector": null,
"k8sNamespaces": ["default"],
"k8sServiceLabelSelector": null,
"serviceNameFilter": "(microfrontend-|widget-)",
"socketTimeoutSec": 3,
"refreshIntervalSec": 600,
"unregisterAppsAfterScanErrors": -1,
"accessViaClusterIP": false
}
}
}
The list of successful registered services will be available on http://<host>:<port>/portal-remote-app-registry-kubernetes
A more complex example
Select all services with label microfrontend=true and not label channel=alpha in all namespaces with label environment=development and tier=frontend:
{
"plugins": {
"Mashroom Portal Remote App Kubernetes Background Job": {
"k8sNamespacesLabelSelector": ["environment=development,tier=frontend"],
"k8sNamespaces": null,
"k8sServiceLabelSelector": ["microfrontend=true,channel!=alpha"]
}
}
}
In case of duplicate Portal Apps the one that appears first in the list of namespaces is taken. For a configuration like this:
{
"k8sNamespacesLabelSelector": ["environment=hotfix", "environment=prod"],
"k8sNamespaces": ["namespace2"]
}
the order is:
In order to allow Mashroom to fetch services for given namespaces you need to attach a Kubernetes Service Account with the correct permissions to the deployment.
Create a role with the required permissions like this:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: list-namespaces-services-cluster-role
rules:
- apiGroups:
- ""
resources:
- services
- namespaces
verbs:
- get
- list
And then create the Service Account and bind the role (we use a ClusterRoleBinding here so the account can read services in all namespaces in the cluster, if you don't want that, you have to create a RoleBinding per allowed namespace):
apiVersion: v1
kind: ServiceAccount
metadata:
name: mashroom-portal
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: mashroom-portal-role-binding
subjects:
- kind: ServiceAccount
name: mashroom-portal
namespace: default
roleRef:
kind: ClusterRole
name: list-namespaces-services-cluster-role
apiGroup: rbac.authorization.k8s.io
And in your deployment resource just state the Service Account name:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mashroom-portal
namespace: default
spec:
# ...
template:
# ...
spec:
containers:
- name: mashroom-portal
# ...
serviceAccountName: mashroom-portal
This Mashroom Portal App can be used to load and test any Portal App with a specific configuration and to interact with the App via Message Bus. It can also be used for end-2-end testing with tools such as Selenium.
The app supports the following query parameters:
btoa(JSON.stringify({
permissionA: true
}))
For an example how to use the sandbox in an end-2-end test see: https://github.com/nonblocking/mashroom-portal-quickstart/tree/master/plugin-packages/example-react-app/test-e2e/example.test.js
This Mashroom Portal App can be used to test remote messaging in the Mashroom Portal. This App requires the mashroom-messaging and mashroom-websocket plugins to be installed.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-remote-messaging as dependency.
After adding the app to a page you can send a message to another user (or another browser tab)
by using the (remote) topic user/<other-username>/
This a simple demo Express webapp which can be developed and run standalone, but also be integrated into Mashroom Server on an arbitrary (configurable) path.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-demo-webapp as dependency.
After that the webapp will be available at /demo/webapp
You can change the path by overriding it in your Mashroom config file like this:
{
"plugins": {
"Mashroom Demo Webapp": {
"path": '/my/path'
}
}
}
This is an alternative demo theme for the Mashroom Portal.
This theme demonstrates how to create a type safe theme with express-react-views and TypeScript.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-alternative-theme as dependency.
This is another simple React based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.
This SPA also supports server side rendering and demonstrates how a custom editor can be used for the Portal App configuration (instead of the default JSON editor).
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-react-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple React based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-react-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple Angular based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-angular-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
Angular was clearly designed for monolithic web applications that use the whole browser window. So, to make it more behave like a Microfrontend you have to change a few things in the template generated by the Angular CLI:
This is a simple Vue based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-vue-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple Svelte based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-svelte-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple SolidJS based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-solidjs-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple SPA that demonstrates how the Mashroom Portal proxy could be used to connect to a REST API that cannot be reached directly by the client.
It fetches data from the SpaceX API, but connects through the Portal. So the actual endpoint will not be visible in the browser.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-rest-proxy-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple SPA that demonstrates how the Mashroom Portal proxy can be used to connect to a WebSocket server that cannot be reached directly by the client.
By default, it connects to an echo server on ws://ws.ifelse.io/, but that server might go down any time. If you can't connect, you can always launch a local WebSocket server (like https://github.com/pmuellr/ws-echo) and change the targetUri in package.json accordingly.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-rest-proxy-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple SPA that demonstrates how an App registered to the Mashroom Portal can load and unload other Portal Apps on a page with a specific config.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-load-dynamically-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.
This is a simple SPA that uses other SPAs (which are registered to the Mashroom Portal) as building blocks. We call this a *Composite App, and it could again be a building block for other Composite Apps.
The SPA itself is written in React but is uses other ones implemented with Vue.js, Angular and Svelte to build a dialog. It is capable of server-side rendering, which includes the embedded Apps.
If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-composite-app as dependency.
Then you can place it on any page via Portal Admin Toolbar.