// ── Admin · Shard visibility ─────────────────────────────────────────────── // // Read/write the per-feature audience config that gates every shard-derived // surface. Admin-only: this decides what anonymous visitors can see, so it is // not part of the moderator tier. // // The policy itself (the ladder, the feature catalog, which fields are locked) // lives in utils/shardVisibility.js. This controller only validates input // against that policy and persists it. const model = require('../../../model/shardVisibility/shardVisibility.model') const visibility = require('../../../utils/shardVisibility') const log = require('../../../utils/logger')('admin-shard-visibility') // GET /admin/shard/visibility — the effective config (defaults merged with any // stored overrides), plus the vocabulary the admin UI needs to render itself: // the ladder, and which fields each feature exposes as configurable. async function getVisibility(req, res) { try { const config = await visibility.getConfig() return res.json({ ladder: visibility.LADDER, lockedFields: Object.keys(visibility.LOCKED_FIELDS), defaults: visibility.compileDefaults(), features: config, }) } catch (err) { log.error('getVisibility', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // PUT /admin/shard/visibility — replace the settings for one or more features. // Body: { features: { : { enabled, audience, stream, fieldRules } } } // // Rejects unknown feature names, unknown rungs, and any attempt to configure a // locked field — a 400 rather than a silent drop, so an admin who tries to make // `acct` public learns that it is not negotiable. async function putVisibility(req, res) { try { const incoming = req.body?.features if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) { return res.status(400).json({ message: 'features object required' }) } const entries = [] for (const [name, patch] of Object.entries(incoming)) { if (!visibility.isFeature(name)) { return res.status(400).json({ message: `Unknown feature: ${name}` }) } if (!patch || typeof patch !== 'object' || Array.isArray(patch)) { return res.status(400).json({ message: `Invalid settings for ${name}` }) } if (patch.audience != null && !visibility.isLevel(patch.audience)) { return res.status(400).json({ message: `Unknown audience for ${name}: ${patch.audience}` }) } const fieldRules = {} for (const [field, level] of Object.entries(patch.fieldRules || {})) { // Matches flattened spellings too (`ownerAcct`, `leaderWebId`), so the // rejection covers every way the field can be named rather than the two // canonical keys. if (visibility.isLockedField(field)) { return res.status(400).json({ message: `Field '${field}' is admin-only and cannot be configured` }) } if (!visibility.isLevel(level)) { return res.status(400).json({ message: `Unknown rung for ${name}.${field}: ${level}` }) } fieldRules[field] = level } const current = (await visibility.getConfig())[name] entries.push({ feature: name, enabled: patch.enabled == null ? current.enabled : !!patch.enabled, audience: patch.audience ?? current.audience, stream: patch.stream == null ? current.stream : !!patch.stream, fieldRules, updatedBy: req.user?.id ?? null, }) } for (const entry of entries) await model.upsert(entry) visibility.invalidate() log.info('shard visibility updated', { by: req.user?.id, features: entries.map((e) => e.feature), }) return res.json({ features: await visibility.getConfig() }) } catch (err) { log.error('putVisibility', err) return res.status(500).json({ message: 'Internal Server Error' }) } } module.exports = { getVisibility, putVisibility }