Files
sso-manager-node/nodejs/utils/site_config.js
T
wmantly c96a4b6652 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).
2026-08-10 05:58:28 -07:00

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 };