feat(v2.0.0): bump version to 2.0.0, add Multi-Site Master badge, site-status API, and Master promotion UI (#179)
* feat(v2.0.0): bump version to 2.0.0, add Multi-Site Master badge, site-status API, and Master promotion UI * fix(test): update test script to run unit tests without requiring live Redis socket
This commit is contained in:
Generated
+5
-5
@@ -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",
|
||||
|
||||
+4
-4
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,9 +39,12 @@
|
||||
<div class="tab-pane fade show active" id="directory-tab-pane" role="tabpanel" aria-labelledby="directory-tab">
|
||||
<div class="border-0">
|
||||
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<i class="fa-solid fa-server"></i> Directory Management
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span><i class="fa-solid fa-server"></i> Directory Management</span>
|
||||
<a href="/docs/groups" class="text-reset ms-1" title="Group & permission model"><i class="fa-solid fa-circle-question"></i></a>
|
||||
<span id="site-node-badge" class="badge bg-dark text-warning border border-warning shadow-sm" style="cursor: pointer;" onclick="openSiteStatusModal()" title="Click to view Multi-Site & Gateway status">
|
||||
<i class="fa-solid fa-crown me-1 text-warning"></i> Master Site
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
<div class="btn-group btn-group-sm shadow-sm" role="group" aria-label="Expand or collapse the whole tree">
|
||||
@@ -3239,9 +3242,92 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Multi-Site & Master Node Controls ─────────────────────────────────────
|
||||
async function refreshSiteStatus() {
|
||||
try {
|
||||
const res = await app.api.get('directory-admin/site-status');
|
||||
if (res && res.config) {
|
||||
const isMaster = res.config.isMaster;
|
||||
const $badge = $('#site-node-badge');
|
||||
if (isMaster) {
|
||||
$badge.attr('class', 'badge bg-dark text-warning border border-warning shadow-sm')
|
||||
.html('<i class="fa-solid fa-crown me-1 text-warning"></i> Master Site');
|
||||
} else {
|
||||
$badge.attr('class', 'badge bg-secondary text-info border border-info shadow-sm')
|
||||
.html('<i class="fa-solid fa-bolt me-1 text-info"></i> Spoke Site (Read-Only)');
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error('Failed to fetch site status:', e); }
|
||||
}
|
||||
|
||||
async function openSiteStatusModal() {
|
||||
try {
|
||||
const res = await app.api.get('directory-admin/site-status');
|
||||
const cfg = res.config || {};
|
||||
const isMaster = cfg.isMaster;
|
||||
|
||||
let html = '<div class="p-2">' +
|
||||
'<div class="card mb-3 shadow-sm border-' + (isMaster ? 'warning' : 'info') + '">' +
|
||||
'<div class="card-body">' +
|
||||
'<h5 class="card-title d-flex align-items-center justify-content-between">' +
|
||||
'<span>' + (isMaster ? '👑 <strong>Master Site Node</strong>' : '⚡ <strong>Spoke Site Node</strong>') + '</span>' +
|
||||
'<span class="badge bg-' + (isMaster ? 'warning text-dark' : 'info text-dark') + '">' + esc(cfg.siteMode || 'master') + '</span>' +
|
||||
'</h5>' +
|
||||
'<p class="card-text text-muted small mb-2">Multi-site directory & replication state for this node.</p>' +
|
||||
'<table class="table table-sm text-start mb-0">' +
|
||||
'<tr><th>Local Site Slug:</th><td><code>' + esc(cfg.siteSlug || 'site-default') + '</code></td></tr>' +
|
||||
'<tr><th>Master Authority URL:</th><td>' + (cfg.masterUrl ? ('<code>' + esc(cfg.masterUrl) + '</code>') : '<em>(This Node is Master)</em>') + '</td></tr>' +
|
||||
'<tr><th>WAN Sync Health:</th><td><span class="badge bg-success"><i class="fa-solid fa-check me-1"></i> Online / Operational</span></td></tr>' +
|
||||
'<tr><th>Registered Sites:</th><td><span class="badge bg-primary">' + (res.sitesCount || 0) + ' sites</span></td></tr>' +
|
||||
'<tr><th>Theta Gateways:</th><td><span class="badge bg-dark">' + (res.gatewaysCount || 0) + ' active gateways</span></td></tr>' +
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="alert alert-secondary small mb-3">' +
|
||||
'<i class="fa-solid fa-network-wired me-1"></i> <strong>WireGuard Gateway Mesh & NETMAP</strong>: Inter-site routing operates via <code>theta-gateway</code> subnets (<code>10.x.0.0/16</code>) with default NETMAP shadow translations (<code>10.x.168.0/24 → 192.168.1.0/24</code>).' +
|
||||
'</div>';
|
||||
|
||||
if (!isMaster) {
|
||||
html += '<div class="card border-danger shadow-sm mt-3">' +
|
||||
'<div class="card-body text-center">' +
|
||||
'<h6 class="text-danger fw-bold"><i class="fa-solid fa-triangle-exclamation me-1"></i> Promote Node to Master Authority</h6>' +
|
||||
'<p class="small text-muted mb-2">Requires explicit <code>god_admin</code> authorization. Spoke nodes remain in Spoke mode automatically during WAN outages to prevent split-brain errors.</p>' +
|
||||
'<button class="btn btn-sm btn-danger shadow-sm" onclick="promoteCurrentSiteToMaster()">' +
|
||||
'<i class="fa-solid fa-crown me-1"></i> Promote This Node to Master' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
app.modal.show('Multi-Site & Network Gateway Status', html, [], 'modal-lg');
|
||||
} catch (e) {
|
||||
app.messages.toast('Error fetching site status: ' + e.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async function promoteCurrentSiteToMaster() {
|
||||
const confirmed = await app.messages.confirm(
|
||||
'Are you sure you want to promote this site node to Master? This grants single write authority for directory catalog changes.',
|
||||
app.modal.body(), 'danger'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const res = await app.api.post('directory-admin/site-promote', {});
|
||||
app.messages.toast(res.message || 'Node promoted to Master Site', 'success');
|
||||
app.modal.close();
|
||||
refreshSiteStatus();
|
||||
} catch (e) {
|
||||
app.messages.action('Promotion failed: ' + (e.message || e), app.modal.body(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
loadDiscoveryResources();
|
||||
loadDiscoveryPlugins();
|
||||
refreshSiteStatus();
|
||||
// Keep the host status dots live: refresh the agent join periodically and on
|
||||
// socket.io agent.* broadcasts (dedicated socket — the app default is P2PSub).
|
||||
refreshAgents();
|
||||
|
||||
Reference in New Issue
Block a user