This commit is contained in:
2026-04-10 13:18:59 -04:00
parent 1af039ab0a
commit b527e24705
10 changed files with 302 additions and 89 deletions

View File

@@ -14,13 +14,13 @@ const fs = require('fs');
const path = require('path');
const bcrypt = require('bcryptjs');
// APP_TYPE validation — host mode requires HOST_DOMAIN and HOST_ADMIN_KEY.
// If either is missing, fall back to selfhost and warn rather than silently
// APP_TYPE validation — host mode requires APP_DOMAIN, HOST_SLUG, and HOST_ADMIN_KEY.
// If any are missing, fall back to selfhost and warn rather than silently
// exposing a broken or insecure host control plane.
let APP_TYPE = (process.env.APP_TYPE || 'selfhost').toLowerCase().trim();
if (APP_TYPE === 'host') {
if (!process.env.HOST_DOMAIN || !process.env.HOST_ADMIN_KEY) {
console.warn('[DB] WARNING: APP_TYPE=host requires HOST_DOMAIN and HOST_ADMIN_KEY to be set.');
if (!process.env.APP_DOMAIN || !process.env.HOST_SLUG || !process.env.HOST_ADMIN_KEY) {
console.warn('[DB] WARNING: APP_TYPE=host requires APP_DOMAIN, HOST_SLUG, and HOST_ADMIN_KEY to be set.');
console.warn('[DB] WARNING: Falling back to APP_TYPE=selfhost for safety.');
APP_TYPE = 'selfhost';
}
@@ -52,12 +52,17 @@ function resolveSchema(req) {
if (APP_TYPE === 'selfhost') return 'public';
const host = (req.headers.host || '').toLowerCase().split(':')[0];
const baseDomain = (process.env.HOST_DOMAIN || 'rosterchirp.com').toLowerCase();
const baseDomain = (process.env.APP_DOMAIN || 'rosterchirp.com').toLowerCase();
const hostSlug = (process.env.HOST_SLUG || 'host').toLowerCase();
const hostControlDomain = `${hostSlug}.${baseDomain}`;
// Internal requests (Docker health checks, localhost) → public schema
if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return 'public';
// Subdomain: team1.rosterchirp.com → tenant_team1
// Host control panel subdomain: chathost.example.com → public schema
if (host === hostControlDomain) return 'public';
// Tenant subdomain: mychat.example.com → tenant_mychat
if (host.endsWith(`.${baseDomain}`)) {
const slug = host.slice(0, -(baseDomain.length + 1));
if (!slug || slug === 'www') throw new Error(`Invalid tenant slug: ${slug}`);
@@ -67,9 +72,6 @@ function resolveSchema(req) {
// Custom domain lookup (populated from host admin DB)
if (tenantDomainCache.has(host)) return tenantDomainCache.get(host);
// Base domain → public schema (host admin panel)
if (host === baseDomain || host === `www.${baseDomain}`) return 'public';
throw new Error(`Unknown tenant for host: ${host}`);
}

View File

@@ -162,7 +162,7 @@ router.post('/tenants', async (req, res) => {
// 7. Reload domain cache
await reloadTenantCache();
const baseDomain = process.env.HOST_DOMAIN || 'rosterchirp.com';
const baseDomain = process.env.APP_DOMAIN || 'rosterchirp.com';
const tenant = tr.rows[0];
tenant.url = `https://${slug}.${baseDomain}`;
@@ -320,7 +320,7 @@ router.get('/status', async (req, res) => {
try {
const tenantCount = await queryOne('public', 'SELECT COUNT(*) AS count FROM tenants');
const active = await queryOne('public', "SELECT COUNT(*) AS count FROM tenants WHERE status='active'");
const baseDomain = process.env.HOST_DOMAIN || 'rosterchirp.com';
const baseDomain = process.env.APP_DOMAIN || 'rosterchirp.com';
res.json({
ok: true,
appType: process.env.APP_TYPE || 'selfhost',

View File

@@ -39,14 +39,16 @@ router.get('/', async (req, res) => {
if (admin) obj.admin_email = admin.email;
obj.app_version = process.env.ROSTERCHIRP_VERSION || 'dev';
obj.user_pass = process.env.USER_PASS || 'user@1234';
// Tell the frontend whether this request came from the HOST_DOMAIN.
// Used to show/hide the Control Panel menu item — only visible on the host's own domain.
// Tell the frontend whether this request came from the host control panel subdomain.
// Used to show/hide the Control Panel menu item — only visible on the host's own subdomain.
const reqHost = (req.headers.host || '').toLowerCase().split(':')[0];
const hostDomain = (process.env.HOST_DOMAIN || '').toLowerCase();
const appDomain = (process.env.APP_DOMAIN || '').toLowerCase();
const hostSlug = (process.env.HOST_SLUG || 'host').toLowerCase();
const hostControlDomain = appDomain ? `${hostSlug}.${appDomain}` : '';
obj.is_host_domain = (
process.env.APP_TYPE === 'host' &&
!!hostDomain &&
(reqHost === hostDomain || reqHost === `www.${hostDomain}` || reqHost === 'localhost')
!!hostControlDomain &&
(reqHost === hostControlDomain || reqHost === 'localhost')
) ? 'true' : 'false';
res.json({ settings: obj });
} catch (e) { res.status(500).json({ error: e.message }); }