c96a4b6652
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).
68 lines
1.9 KiB
JavaScript
68 lines
1.9 KiB
JavaScript
'use strict';
|
|
|
|
// Persisted multi-site role (MULTI_SITE_SPEC.md). Whether this node is the
|
|
// master authority, which site it belongs to, and the master it replicates
|
|
// from live in /config/site.json so they survive restarts (the old code kept
|
|
// them in Node memory, so a container recreate silently reverted a spoke back
|
|
// to "master").
|
|
//
|
|
// Boot-time defaults come from the environment (IS_MASTER / MASTER_URL /
|
|
// SITE_SLUG, which docker-compose passes); a written site.json overrides for
|
|
// the life of the deployment. site-promote and the site-join flow both write
|
|
// here.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Overridable so tests can point at a temp file instead of /config/site.json.
|
|
function configFile() {
|
|
return process.env.SITE_CONFIG_FILE || '/config/site.json';
|
|
}
|
|
|
|
function envDefaults() {
|
|
return {
|
|
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
|
|
};
|
|
}
|
|
|
|
let current = null;
|
|
|
|
function load() {
|
|
const env = envDefaults();
|
|
const file = configFile();
|
|
try {
|
|
if (fs.existsSync(file)) {
|
|
const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
return { ...env, ...saved };
|
|
}
|
|
} catch (e) {
|
|
console.error('[site] could not read ' + file + ': ' + e.message);
|
|
}
|
|
return env;
|
|
}
|
|
|
|
// get returns the current site config.
|
|
function get() {
|
|
if (!current) current = load();
|
|
return { ...current };
|
|
}
|
|
|
|
// save merges a patch and persists it to the site config file.
|
|
function save(patch) {
|
|
current = { ...get(), ...patch };
|
|
const file = configFile();
|
|
try {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, JSON.stringify(current, null, 2) + '\n');
|
|
} catch (e) {
|
|
console.error('[site] could not write ' + file + ': ' + e.message);
|
|
throw e;
|
|
}
|
|
return get();
|
|
}
|
|
|
|
module.exports = { get, save, configFile };
|