Add Theme Pack Support for Custom UI Themes (Modern UI ONLY) (#7506)
* Add theme pack support for domain customization Introduces a 'themePack' option in the domain configuration schema, updates meshctrl.js to support setting this value, and enhances webserver.js to serve theme pack assets and inject theme-specific CSS/JS when enabled. This allows domains to override UI appearance using dedicated theme packs.
TheDevRyan committed
Feb 1, 2026 at 17:11 UTC
0512dec89ffdafa5f915686c213e71d499d48800
3 files changed
+54
-4
meshcentral-config-schema.json
+4
@@ -2005,6 +2005,10 @@
2005
"customUI": {
2006
"type": "object"
2007
},
2008
+ "themePack": {
2009
+ "type": "string",
2010
+ "description": "The name of the theme pack to use for this domain. Example: \"Stylish-UI\""
2011
+ },
2012
"customFiles": {
2013
"type": "object",
2014
"description": "Advanced customization system allowing multiple CSS and JavaScript file configurations per domain. Each configuration is identified by a unique name and can target specific templates using the 'scope' property, enabling granular control over where custom files are loaded. All custom files are loaded after the default custom.css and custom.js files.",
meshctrl.js
+3
-2
@@ -1190,14 +1190,15 @@ function displayConfigHelp() {
1190
console.log(" --removefromdomain [domain] - Remove values from the domain.");
1191
console.log("\r\nWith adddomain, removedomain, settodomain and removefromdomain you can add the key and value pair. For example:\r\n");
1192
console.log(" --adddomain \"MyDomain\" --title \"My Server Name\" --newAccounts false");
1193
+ console.log(" --settodomain \"MyDomain\" --themePack \"Stylish-UI\"");
1194
console.log(" --settodomain \"MyDomain\" --title \"My Server Name\"");
1195
console.log(" --removefromdomain \"MyDomain\" --title");
1196
}
1197
1198
function performConfigOperations(args) {
1198
- var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide', 'customFiles'];
1199
+ var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide', 'customFiles', 'themePack'];
1200
var domainObjectValues = ['ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'customFiles'];
1200
- var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig'];
1201
+ var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig', 'themePack'];
1202
var configChange = false;
1203
var fs = require('fs');
1204
var path = require('path');
webserver.js
+47
-2
@@ -7675,6 +7675,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7675
// Handle all incoming requests as web relays
7676
obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7677
}
7678
+
7679
+ // Theme Pack Override Middleware
7680
+ obj.app.use(url, function (req, res, next) {
7681
+ if (req.method !== 'GET') return next();
7682
+ var domain = getDomain(req);
7683
+ // Serve theme pack files if domain has a theme pack configured
7684
+ if (domain && domain.themepack) {
7685
+ var themeFilePath = obj.path.join(obj.parent.datapath, 'theme-pack', domain.themepack, 'public', req.path);
7686
+ // Prevent directory traversal
7687
+ if (themeFilePath.indexOf('..') >= 0) return next();
7688
+
7689
+ obj.fs.stat(themeFilePath, function (err, stats) {
7690
+ if (err || !stats.isFile()) return next();
7691
+ res.sendFile(themeFilePath);
7692
+ });
7693
+ } else {
7694
+ next();
7695
+ }
7696
+ });
7697
7698
// Indicates to ExpressJS that the override public folder should be used to serve static files.
7699
obj.app.use(url, function(req, res, next){
@@ -9604,7 +9623,32 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9623
9624
return jsTags.trim();
9625
}
9607
-
9626
+
9627
+ function generateThemePackCSSTags(domain, currentTemplate) {
9628
+ var cssTags = '';
9629
+ // Load theme pack if domain has one configured AND (domain forces sitestyle 3 OR user is viewing sitestyle 3)
9630
+ var isModernUI = (currentTemplate === 'default3') || (domain.sitestyle === 3);
9631
+ if (domain && domain.themepack && isModernUI) {
9632
+ var themePath = obj.path.join(obj.parent.datapath, 'theme-pack', domain.themepack, 'public');
9633
+ if (obj.fs.existsSync(obj.path.join(themePath, 'styles', 'theme.css'))) {
9634
+ cssTags += '<link keeplink=1 type="text/css" href="styles/theme.css" media="screen" rel="stylesheet" title="CSS" />\n ';
9635
+ }
9636
+ }
9637
+ return cssTags;
9638
+ }
9639
+ function generateThemePackJSTags(domain, currentTemplate) {
9640
+ var jsTags = '';
9641
+ // Load theme pack if domain has one configured AND (domain forces sitestyle 3 OR user is viewing sitestyle 3)
9642
+ var isModernUI = (currentTemplate === 'default3') || (domain.sitestyle === 3);
9643
+ if (domain && domain.themepack && isModernUI) {
9644
+ var themePath = obj.path.join(obj.parent.datapath, 'theme-pack', domain.themepack, 'public');
9645
+ if (obj.fs.existsSync(obj.path.join(themePath, 'scripts', 'theme.js'))) {
9646
+ jsTags += '<script keeplink=1 type="text/javascript" src="scripts/theme.js"></script>\n ';
9647
+ }
9648
+ }
9649
+ return jsTags;
9650
+ }
9651
+
9652
// Return the correct render page arguments.
9653
function getRenderArgs(xargs, req, domain, page) {
9654
var minify = (domain.minify == true);
@@ -9658,7 +9702,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9702
xargs.customCSSTags = generateCustomCSSTags(null, page);
9703
xargs.customJSTags = generateCustomJSTags(null, page);
9704
}
9661
-
9705
+ xargs.customCSSTags += generateThemePackCSSTags(domain, page);
9706
+ xargs.customJSTags += generateThemePackJSTags(domain, page);
9707
return xargs;
9708
}
9709