feat(site): multi-site join server endpoints + persisted site role + emoji fix
Server endpoints for joining a spoke to a master directory (MULTI_SITE_SPEC.md).
This pass is server-only; setup.sh wiring and the UI are the next layer.
- Site join keys (SiteJoinKey model, stj_ prefix): mint/revoke/delete/list,
hashed at rest, shown once — the same model as agent join keys.
- POST /api/site/export (master, Bearer stj_ key, no admin session): returns the
local LDAP tree (slapcat LDIF) + resource catalog + siteSlug + baseDn.
- POST /api/site/join (spoke, admin): { masterUrl, joinKey } pulls the master
export, imports resources (upsert by slug) + LDAP (ldapadd -c), and persists
the spoke role. Refused if already a spoke.
- Persisted site role: utils/site_config.js keeps isMaster/masterUrl/siteSlug in
/config/site.json (env seeds defaults); site-status/site-promote now use it.
- Unit tests (site_join, site_config) with in-memory stubs, wired into npm test.
- docs/site-join.md + docs router entry.
- Repairs the corrupted multi-site emojis (crown/bolt) in directory.ejs.
- .gitguardian.yml ignores the generic-password false positive on reading the
LDAP bind credential from runtime config (never a hardcoded secret).
This commit is contained in:
@@ -882,12 +882,11 @@ router.post('/discovered/merge', async (req, res, next) => {
|
||||
});
|
||||
|
||||
// ── 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
|
||||
};
|
||||
// The site role (master/spoke, site slug, master URL) is persisted by
|
||||
// utils/site_config.js so it survives restarts; the env vars IS_MASTER /
|
||||
// MASTER_URL / SITE_SLUG only seed the defaults. site-promote and the
|
||||
// /api/site/join flow both write to it.
|
||||
const siteConfig = require('../utils/site_config');
|
||||
|
||||
router.get('/site-status', async (req, res, next) => {
|
||||
try {
|
||||
@@ -895,14 +894,15 @@ router.get('/site-status', async (req, res, next) => {
|
||||
const allResources = await Resource.list();
|
||||
const gateResources = allResources.filter(r => r.metadata && r.metadata.subType === 'wireguard');
|
||||
|
||||
const cfg = siteConfig.get();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
config: {
|
||||
isMaster: localSiteConfig.isMaster,
|
||||
masterUrl: localSiteConfig.masterUrl,
|
||||
siteSlug: localSiteConfig.siteSlug,
|
||||
wanConnected: localSiteConfig.wanConnected,
|
||||
siteMode: localSiteConfig.isMaster ? 'master' : 'spoke'
|
||||
isMaster: cfg.isMaster,
|
||||
masterUrl: cfg.masterUrl,
|
||||
siteSlug: cfg.siteSlug,
|
||||
wanConnected: cfg.wanConnected,
|
||||
siteMode: cfg.isMaster ? 'master' : 'spoke'
|
||||
},
|
||||
sitesCount: sites.length,
|
||||
sites: sites.map(s => ({ id: s.id, name: s.name, slug: s.slug })),
|
||||
@@ -920,18 +920,18 @@ router.post('/site-promote', async (req, res, next) => {
|
||||
return res.status(403).json({ status: 'error', message: 'Master promotion requires explicit god_admin authority' });
|
||||
}
|
||||
|
||||
localSiteConfig.isMaster = true;
|
||||
localSiteConfig.masterUrl = '';
|
||||
|
||||
siteConfig.save({ isMaster: true, masterUrl: '' });
|
||||
|
||||
console.log(`[MULTI-SITE] Node promoted to MASTER by user ${req.user ? req.user.uid : 'admin'}`);
|
||||
|
||||
const cfg = siteConfig.get();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'Node successfully promoted to Master Site',
|
||||
config: {
|
||||
isMaster: true,
|
||||
masterUrl: '',
|
||||
siteSlug: localSiteConfig.siteSlug,
|
||||
siteSlug: cfg.siteSlug,
|
||||
siteMode: 'master'
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user