godcrm/backend/routes/v3/admin/registration.js
GOD CRM Release 065f2edd23
Some checks are pending
CI / Lint / Typecheck / Test / Build (push) Waiting to run
CI / PostgreSQL Integration Tests (push) Waiting to run
GOD CRM — public scrubbed snapshot (2026-08-30)
Refresh of the open-core distribution from the private tree.

Included since the previous snapshot:
- Mail module (ADR-158/159/160/169): composer, labels, scheduling,
  attachments, reply-tokens, IMAP/SMTP bridge + migrations 079-083
- Crawler-readable SSR for /blog and public spaces (ADR-190):
  blogSeo, publicDocsSeo, per-space SEO prefs, blog index/post pages
- Registration policy + referral/promo settings (ADR-183/188)
- Message translation + language detection (ADR-185)
- Reddit connector for the agent-tool surface

Excised from the public distribution (unchanged policy): infrastructure
topology and host config, internal ops scripts, DB cleanup snapshots,
business documents, throwaway debug scripts, and two private product
lines (SC-SIM simulator, personal one-off tools). Real host addresses
are replaced with placeholders; credential-shaped literals are redacted.

Frontend build verified green on this tree.
2026-08-30 15:13:28 +03:00

111 lines
4.4 KiB
JavaScript

// ADR-188 F1 — owner-only runtime registration policy.
//
// GET /api/v3/admin/registration — read the effective runtime policy
// PUT /api/v3/admin/registration — owner writes policy (pg_notify-evicted cluster-wide)
//
// Storage: `_app_settings` key='registration' (JSONB) — reuses the ADR-0064 table,
// no migration. Authz gates on role==='owner' (see requireOwner) — NOT a hardcoded
// space id — so from-empty community boxes (dev188 / box 205) with no space 11 work.
// The unauthenticated public slice (F5) is a separate route. Route-level enforcement
// of `signup_mode`/`auto_join` (F2/F3) is a follow-up; this endpoint persists +
// resolves the full contract so the owner UI round-trips cleanly and `default_role`
// (already enforced in AuthService) takes effect.
import express from 'express';
import { dbRun } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, error, forbidden } from '../../../utils/response.js';
import {
RUNTIME_SETTINGS_KEY,
sanitizeRuntimeConfig,
getEffectiveRegistrationPolicy,
invalidateRegistrationCache,
notifyRegistrationInvalidate,
} from '../../../services/registrationPolicy.js';
const log = apiLogger.child({ module: 'admin_registration' });
const router = express.Router();
// Owner gate. Gates on the platform-owner ROLE, not a hardcoded space id.
//
// The old code looked up `spaces WHERE id = 11` and 500'd with OWNER_SPACE_MISSING
// when that row was absent. On a from-empty community box (dev188, box 205 — the exact
// target of ADR-188) space 11 never exists (space ids are sequential from 1), so the
// box owner could never save policy. The first registrant on ANY box is minted
// role='owner' by AuthService, which is exactly this gate. `admin` alone is
// intentionally NOT enough — matches the established owner checks in routes/v3/terminal.js.
export async function requireOwner(req, res) {
if (!req.user?.id) {
forbidden(res, 'Authentication required');
return false;
}
if (req.user.role !== 'owner') {
forbidden(res, 'Owner-only endpoint');
return false;
}
return true;
}
// Project the effective policy down to the frozen FE contract slice.
function toContract(policy) {
return {
signup_mode: policy.signup_mode,
default_role: policy.default_role,
auto_join: Array.isArray(policy.auto_join) ? policy.auto_join : [],
};
}
// GET /api/v3/admin/registration — effective runtime policy (env preset if unset).
router.get('/registration', async (req, res) => {
if (!(await requireOwner(req, res))) return;
try {
const policy = await getEffectiveRegistrationPolicy(null);
return success(res, toContract(policy));
} catch (err) {
log.error({ err }, 'admin/registration GET failed');
return error(res, 'GET_REGISTRATION_POLICY_ERROR', err.message, 500);
}
});
// PUT /api/v3/admin/registration — overwrite the runtime policy blob.
router.put('/registration', async (req, res) => {
if (!(await requireOwner(req, res))) return;
try {
const clean = sanitizeRuntimeConfig(req.body);
// Fill unset scalars from the current effective policy so a partial PUT never
// silently downgrades the front door (fail-closed is the client default, not here).
const effective = await getEffectiveRegistrationPolicy(null);
const toStore = {
signup_mode: clean.signup_mode ?? effective.signup_mode,
default_role: clean.default_role ?? effective.default_role,
auto_join: clean.auto_join,
};
await dbRun(
`INSERT INTO _app_settings (key, value, updated_by, updated_at)
VALUES (?, ?::jsonb, ?, NOW())
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value,
updated_by = EXCLUDED.updated_by,
updated_at = NOW()`,
[RUNTIME_SETTINGS_KEY, JSON.stringify(toStore), req.user.id]
);
// Evict locally now (this process) + fire pg_notify for any other listener.
invalidateRegistrationCache();
await notifyRegistrationInvalidate();
log.info({ userId: req.user.id, signup_mode: toStore.signup_mode }, 'admin/registration updated');
// Return the freshly-resolved effective policy so the UI reflects reality.
const resolved = await getEffectiveRegistrationPolicy(null);
return success(res, toContract(resolved));
} catch (err) {
log.error({ err }, 'admin/registration PUT failed');
return error(res, 'PUT_REGISTRATION_POLICY_ERROR', err.message, 500);
}
});
export default router;