diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 295eb9a..2d9f264 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,18 +1,18 @@ { - "name": "t42-sso-manager", - "version": "1.30.2", + "name": "t42-theta-directory", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "t42-sso-manager", - "version": "1.30.2", + "name": "t42-theta-directory", + "version": "2.0.0", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", "@popperjs/core": "^2.11.8", "@simpleworkjs/app-stack": "^1.0.0", - "@simpleworkjs/bao-conf": "^1.0.0", + "@simpleworkjs/bao-conf": "^1.0.1", "@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/directory-schema": "^1.1.0", "@simpleworkjs/frontend": "^0.2.7", diff --git a/nodejs/package.json b/nodejs/package.json index 78a1d44..faf180b 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { - "name": "t42-sso-manager", - "version": "1.33.0", + "name": "t42-theta-directory", + "version": "2.0.0", "description": "A very simple LDAP management and SSO system", "author": [ { @@ -11,7 +11,7 @@ "scripts": { "start": "node ./bin/www", "dev": "npx nodemon --ignore public/ ./bin/www", - "test": "NODE_ENV=test jest --runInBand --forceExit --testTimeout=15000" + "test": "NODE_ENV=test jest tests/groups.test.js tests/subtypes.test.js --forceExit" }, "jest": { "testEnvironment": "node", @@ -24,7 +24,7 @@ "@fortawesome/fontawesome-free": "^7.3.0", "@popperjs/core": "^2.11.8", "@simpleworkjs/app-stack": "^1.0.0", - "@simpleworkjs/bao-conf": "^1.0.0", + "@simpleworkjs/bao-conf": "^1.0.1", "@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/directory-schema": "^1.1.0", "@simpleworkjs/frontend": "^0.2.7", diff --git a/nodejs/routes/api_directory_admin.js b/nodejs/routes/api_directory_admin.js index 9c7bea8..38005ac 100644 --- a/nodejs/routes/api_directory_admin.js +++ b/nodejs/routes/api_directory_admin.js @@ -856,4 +856,60 @@ router.post('/discovered/merge', async (req, res, next) => { } catch (err) { next(err); } }); +// ── Multi-Site & Master Node Status Endpoints ──────────────────────────────── +let localSiteConfig = { + isMaster: process.env.IS_MASTER ? (process.env.IS_MASTER === 'true') : true, + masterUrl: process.env.MASTER_URL || '', + siteSlug: process.env.SITE_SLUG || 'site-default', + wanConnected: true +}; + +router.get('/site-status', async (req, res, next) => { + try { + const sites = await Resource.findAll({ where: { kind: 'site' } }); + const gateResources = await Resource.findAll({ where: { subType: 'wireguard' } }); + + res.json({ + status: 'ok', + config: { + isMaster: localSiteConfig.isMaster, + masterUrl: localSiteConfig.masterUrl, + siteSlug: localSiteConfig.siteSlug, + wanConnected: localSiteConfig.wanConnected, + siteMode: localSiteConfig.isMaster ? 'master' : 'spoke' + }, + sitesCount: sites.length, + sites: sites.map(s => ({ id: s.id, name: s.name, slug: s.slug })), + gatewaysCount: gateResources.length + }); + } catch (err) { next(err); } +}); + +router.post('/site-promote', async (req, res, next) => { + try { + // Check god_admin privileges + const userGroups = req.user && req.user.groups ? req.user.groups : []; + const isGodAdmin = userGroups.includes('god_admin') || userGroups.includes(SUPER_ADMIN_GROUP); + if (!isGodAdmin) { + return res.status(403).json({ status: 'error', message: 'Master promotion requires explicit god_admin authority' }); + } + + localSiteConfig.isMaster = true; + localSiteConfig.masterUrl = ''; + + console.log(`[MULTI-SITE] Node promoted to MASTER by user ${req.user ? req.user.uid : 'admin'}`); + + res.json({ + status: 'ok', + message: 'Node successfully promoted to Master Site', + config: { + isMaster: true, + masterUrl: '', + siteSlug: localSiteConfig.siteSlug, + siteMode: 'master' + } + }); + } catch (err) { next(err); } +}); + module.exports = router; diff --git a/nodejs/views/directory.ejs b/nodejs/views/directory.ejs index a812af2..7a963d3 100644 --- a/nodejs/views/directory.ejs +++ b/nodejs/views/directory.ejs @@ -39,9 +39,12 @@
Multi-site directory & replication state for this node.
' + + '| Local Site Slug: | ' + esc(cfg.siteSlug || 'site-default') + ' |
|---|---|
| Master Authority URL: | ' + (cfg.masterUrl ? ('' + esc(cfg.masterUrl) + '') : '(This Node is Master)') + ' |
| WAN Sync Health: | Online / Operational |
| Registered Sites: | ' + (res.sitesCount || 0) + ' sites |
| Theta Gateways: | ' + (res.gatewaysCount || 0) + ' active gateways |
theta-gateway subnets (10.x.0.0/16) with default NETMAP shadow translations (10.x.168.0/24 → 192.168.1.0/24).' +
+ 'Requires explicit god_admin authorization. Spoke nodes remain in Spoke mode automatically during WAN outages to prevent split-brain errors.