diff --git a/changemaker-control-panel/api/scripts/render-for-instance.ts b/changemaker-control-panel/api/scripts/render-for-instance.ts new file mode 100644 index 0000000..6f77b78 --- /dev/null +++ b/changemaker-control-panel/api/scripts/render-for-instance.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env tsx +/** + * render-for-instance.ts — Approach C Phase 0 verification harness. + * + * Loads a CCP-tracked Instance row, builds its template context, and renders + * all templates to a scratch directory under /tmp/render-/. Operator + * then diffs the rendered output against the tenant's actual on-disk files + * to verify the template-vs-prod-compose equivalence contract. + * + * Usage (run inside ccp-api container): + * docker compose exec ccp-api npx tsx scripts/render-for-instance.ts --slug changemakerlite + * docker compose exec ccp-api npx tsx scripts/render-for-instance.ts --id + * + * Output: prints scratch dir path; exits 0 on success, 1 on failure. + * + * This script does NOT touch any tenant. It only reads from the CCP database + * and writes to /tmp on the CCP api container. + */ + +import { prisma } from '../src/lib/prisma'; +import { decryptJson } from '../src/utils/encryption'; +import { + buildTemplateContext, + renderAllTemplates, +} from '../src/services/template-engine'; +import path from 'node:path'; +import fs from 'node:fs/promises'; + +interface Args { + slug?: string; + id?: string; + outDir?: string; +} + +function parseArgs(argv: string[]): Args { + const args: Args = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--slug' && argv[i + 1]) { args.slug = argv[++i]; continue; } + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; continue; } + if (a === '--out' && argv[i + 1]) { args.outDir = argv[++i]; continue; } + if (a === '-h' || a === '--help') { + console.log('usage: render-for-instance.ts (--slug X | --id Y) [--out /tmp/render-X]'); + process.exit(0); + } + } + return args; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (!args.slug && !args.id) { + console.error('error: --slug or --id is required'); + process.exit(1); + } + + const instance = await prisma.instance.findUnique({ + where: args.id ? { id: args.id } : { slug: args.slug! }, + }); + if (!instance) { + console.error(`error: instance not found (slug=${args.slug ?? '?'}, id=${args.id ?? '?'})`); + process.exit(1); + } + + // For isRegistered tenants there are no encrypted secrets. Use empty stubs + // so buildTemplateContext doesn't crash; env.hbs values that read from + // {{secrets.*}} will render as blank, which is fine for diff purposes + // because the tenant's own .env still has the real values via install.sh. + let secrets: Record = {}; + if (instance.encryptedSecrets) { + try { + secrets = decryptJson>(instance.encryptedSecrets); + } catch (err) { + console.warn(`warn: decryptJson failed (${(err as Error).message}); using empty secrets`); + } + } else { + console.log(`(isRegistered=true tenant; using empty secrets for compose/nginx render — env.hbs values will be blank)`); + } + + const outDir = args.outDir ?? path.join('/tmp', `render-${instance.slug}`); + await fs.rm(outDir, { recursive: true, force: true }); + await fs.mkdir(outDir, { recursive: true }); + + const context = buildTemplateContext(instance, secrets); + await renderAllTemplates(context, outDir); + + // Summarize what we rendered + const entries: string[] = []; + async function walk(dir: string, rel = '') { + const items = await fs.readdir(dir, { withFileTypes: true }); + for (const item of items) { + const full = path.join(dir, item.name); + const r = path.join(rel, item.name); + if (item.isDirectory()) await walk(full, r); + else entries.push(r); + } + } + await walk(outDir); + + console.log(`\n=== rendered ${entries.length} files to: ${outDir} ===`); + for (const e of entries.sort()) { + const stat = await fs.stat(path.join(outDir, e)); + console.log(` ${e} (${stat.size} bytes)`); + } + console.log(`\nTo diff against the live tenant:`); + console.log(` ssh 'cat /docker-compose.yml' | diff -u - ${outDir}/docker-compose.yml`); + console.log(``); + + await prisma.$disconnect(); +} + +main().catch((err) => { + console.error('render-for-instance.ts failed:', err); + process.exit(1); +}); diff --git a/changemaker-control-panel/templates/docker-compose.yml.hbs b/changemaker-control-panel/templates/docker-compose.yml.hbs index 6f9fa5b..82aad71 100644 --- a/changemaker-control-panel/templates/docker-compose.yml.hbs +++ b/changemaker-control-panel/templates/docker-compose.yml.hbs @@ -4,16 +4,21 @@ # Instance: {{name}} ({{slug}}) # Compose project: {{composeProject}} # -# This template is a near-mirror of changemaker.lite/docker-compose.prod.yml. -# Approach C (CCP-driven release upgrade) renders this against the tenant's -# context and writes the result to the tenant's filesystem. Variation is -# almost entirely env-var driven (.env file); only image-tag overrides for -# the four core CCP-built images use Handlebars ({{imageTag}}). +# This template is a byte-mirror of changemaker.lite/docker-compose.prod.yml +# (modulo this header comment). Approach C (CCP-driven release upgrade) +# renders this against the tenant's context and writes the result to the +# tenant's filesystem. +# +# All per-instance variation flows through env-var substitution from the +# tenant's .env file (rendered by env.hbs for CCP-provisioned tenants; +# kept as-is for install.sh-registered tenants). The CCP controls image +# tag selection by writing IMAGE_TAG to the tenant's .env, which the +# compose's ${IMAGE_TAG:-latest} substitution then picks up at compose-up. # # To keep this template in sync with canonical docker-compose.prod.yml: # - When a new service is added to changemaker.lite/docker-compose.prod.yml, -# copy the same block here. Use Handlebars only where per-instance -# variation is needed (currently only {{imageTag}} for the 5 CCP images). +# copy the same block here verbatim. Handlebars is NOT used in the +# compose template itself — all variation is env-var driven. ############################################################################### ############################################################################### @@ -30,7 +35,7 @@ services: # Unified Express.js API api: - image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:{{imageTag}} + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:${IMAGE_TAG:-latest} container_name: changemaker-v2-api restart: unless-stopped ports: @@ -179,7 +184,7 @@ services: # Fastify Media API (Microservice for Media Management) media-api: - image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:{{imageTag}} + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:${IMAGE_TAG:-latest} container_name: changemaker-media-api restart: unless-stopped ports: @@ -240,7 +245,7 @@ services: # React Admin GUI (Vite dev server) admin: - image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:{{imageTag}} + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:${IMAGE_TAG:-latest} container_name: changemaker-v2-admin restart: unless-stopped ports: @@ -292,7 +297,7 @@ services: # Nginx reverse proxy nginx: - image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:{{imageTag}} + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:${IMAGE_TAG:-latest} container_name: changemaker-v2-nginx restart: unless-stopped ports: @@ -1428,7 +1433,7 @@ services: # ========================================================================= ccp-agent: - image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-ccp-agent:{{imageTag}} + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-ccp-agent:${IMAGE_TAG:-latest} container_name: ${COMPOSE_PROJECT_NAME:-changemaker-lite}-ccp-agent restart: unless-stopped profiles: ["ccp-agent"] diff --git a/changemaker-control-panel/templates/env.hbs b/changemaker-control-panel/templates/env.hbs index abe425b..dea7e94 100644 --- a/changemaker-control-panel/templates/env.hbs +++ b/changemaker-control-panel/templates/env.hbs @@ -1,65 +1,95 @@ -# ============================================================ -# Changemaker Lite — Instance: {{name}} +# ============================================================================== +# Changemaker Lite v2 — Tenant .env (CCP-rendered) +# Instance: {{name}} ({{slug}}) # Generated by CCP on {{now}} -# ============================================================ +# ============================================================================== +# This file is a near-mirror of changemaker.lite/.env.example with Handlebars +# overlay for tenant-specific values (DOMAIN, secrets, COMPOSE_PROJECT_NAME). +# Static defaults match .env.example so docker-compose.yml.hbs (a mirror of +# docker-compose.prod.yml) has every ${VAR} it references. +# +# Keeping this in sync with .env.example after upstream additions: copy the +# new key + default, replace any tenant-specific value with the matching +# Handlebars expression. Most additions need no Handlebars. +# ============================================================================== -# Core +# --- General --- NODE_ENV=production DOMAIN={{domain}} +COMPOSE_PROJECT_NAME={{composeProject}} +TZ=UTC USER_ID=1000 GROUP_ID=1000 DOCKER_GROUP_ID=984 -# V2 PostgreSQL +# --- V2 PostgreSQL --- V2_POSTGRES_USER=changemaker V2_POSTGRES_PASSWORD={{secrets.postgresPassword}} V2_POSTGRES_DB=changemaker_v2 V2_POSTGRES_PORT={{ports.postgres}} -DATABASE_URL=postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2 -# Redis -REDIS_PASSWORD={{secrets.redisPassword}} -REDIS_URL=redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379 - -# JWT Auth +# --- JWT Auth --- JWT_ACCESS_SECRET={{secrets.jwtAccessSecret}} JWT_REFRESH_SECRET={{secrets.jwtRefreshSecret}} JWT_INVITE_SECRET={{secrets.jwtInviteSecret}} JWT_ACCESS_EXPIRY=15m -# Reduced 2026-04-12 from 7d → 24h (P2-3). Combined with device-fingerprint -# binding in the refresh JWT payload, this tightens the exploitation window -# for stolen refresh tokens. +# Reduced from 7d → 24h on 2026-04-12 (P2-3 hardening). Combined with +# device-fingerprint binding in the JWT payload, this tightens the +# exploitation window for stolen refresh tokens. JWT_REFRESH_EXPIRY=24h -# Gitea SSO cookie signing + service password salt — REQUIRED 2026-04-12 (P2-2). -# Distinct from JWT secrets; empty values will now fail Zod validation on boot. +# Encryption key for DB-stored secrets (SMTP password, etc.) +ENCRYPTION_KEY={{secrets.encryptionKey}} + +# Gitea SSO cookie signing secret + service password salt — REQUIRED 2026-04-12 +# (P2-2). Distinct from JWT secrets; empty values will fail Zod validation on +# boot. Both ≥32 chars, distinct from each other and from JWT_* secrets. GITEA_SSO_SECRET={{secrets.giteaSsoSecret}} SERVICE_PASSWORD_SALT={{secrets.servicePasswordSalt}} -# Encryption -ENCRYPTION_KEY={{secrets.encryptionKey}} - -# Initial Admin +# --- Initial Super Admin User --- INITIAL_ADMIN_EMAIL={{secrets.adminEmail}} INITIAL_ADMIN_PASSWORD={{secrets.initialAdminPassword}} -# API +# --- API --- API_PORT=4000 -PORT=4000 API_URL=https://api.{{domain}} CORS_ORIGINS=https://app.{{domain}},http://localhost:{{ports.admin}},http://localhost + +# --- Admin GUI --- +ADMIN_PORT=3000 ADMIN_URL=https://app.{{domain}} -# Admin GUI -ADMIN_PORT=3000 - -# Nginx +# --- Nginx --- NGINX_HTTP_PORT={{ports.nginx}} NGINX_HTTPS_PORT=443 -# SMTP / Email +# --- Embed Proxy Ports --- +# Dedicated nginx ports for iframe embedding without DNS/subdomain. +# CCP allocates these per-instance via {{ports.embed}} base + offset. +NOCODB_EMBED_PORT={{math ports.embed "+" 0}} +N8N_EMBED_PORT={{math ports.embed "+" 1}} +GITEA_EMBED_PORT={{math ports.embed "+" 2}} +MAILHOG_EMBED_PORT={{math ports.embed "+" 3}} +MINI_QR_EMBED_PORT={{math ports.embed "+" 4}} +EXCALIDRAW_EMBED_PORT={{math ports.embed "+" 5}} +HOMEPAGE_EMBED_PORT={{math ports.embed "+" 6}} +VAULTWARDEN_EMBED_PORT={{math ports.embed "+" 9}} +ROCKETCHAT_EMBED_PORT={{math ports.embed "+" 10}} +GANCIO_EMBED_PORT={{math ports.embed "+" 11}} +JITSI_EMBED_PORT={{math ports.embed "+" 15}} +GRAFANA_EMBED_PORT={{math ports.embed "+" 12}} +ALERTMANAGER_EMBED_PORT={{math ports.embed "+" 16}} + +# --- Docker / Container Management --- +DOCKER_NETWORK_NAME=changemaker-lite +DOCKER_PROXY_URL=http://docker-socket-proxy:2375 +NEWT_CONTAINER_NAME=newt-changemaker +NEWT_COMPOSE_SERVICE=newt + +# --- SMTP / Email --- {{#if emailTestMode}} -SMTP_HOST={{containerPrefix}}-mailhog +SMTP_HOST=mailhog-changemaker SMTP_PORT=1025 SMTP_USER= SMTP_PASS= @@ -75,21 +105,9 @@ SMTP_FROM={{smtpFrom}} SMTP_FROM_NAME={{name}} TEST_EMAIL_RECIPIENT={{secrets.adminEmail}} -# NocoDB -NOCODB_V2_PORT=8080 -NOCODB_URL=http://{{containerPrefix}}-nocodb:8080 -NC_ADMIN_EMAIL={{secrets.adminEmail}} -NC_ADMIN_PASSWORD={{secrets.nocodbAdminPassword}} - -# Listmonk -{{#if enableListmonk}} -LISTMONK_SYNC_ENABLED=true -LISTMONK_URL=http://{{containerPrefix}}-listmonk:9000 -{{else}} -LISTMONK_SYNC_ENABLED=false -LISTMONK_URL= -{{/if}} -LISTMONK_PORT=9000 +# --- Listmonk --- +LISTMONK_PORT=9001 +LISTMONK_DB_PORT=5434 LISTMONK_DB_USER=listmonk LISTMONK_DB_PASSWORD={{secrets.listmonkAdminPassword}} LISTMONK_DB_NAME=listmonk @@ -99,26 +117,41 @@ LISTMONK_API_USER=v2-api LISTMONK_API_TOKEN={{secrets.listmonkApiToken}} LISTMONK_ADMIN_USER=v2-api LISTMONK_ADMIN_PASSWORD={{secrets.listmonkApiToken}} -LISTMONK_PROXY_PORT=9002 +LISTMONK_SYNC_ENABLED={{#if enableListmonk}}true{{else}}false{{/if}} LISTMONK_WEBHOOK_SECRET= -LISTMONK_DB_PORT=5434 -LISTMONK_SMTP_HOST={{containerPrefix}}-mailhog +LISTMONK_PROXY_PORT=9002 +LISTMONK_SMTP_HOST=mailhog-changemaker LISTMONK_SMTP_PORT=1025 LISTMONK_SMTP_USER= LISTMONK_SMTP_PASSWORD= LISTMONK_SMTP_TLS_TYPE=none LISTMONK_SMTP_FROM={{name}} -# Media -{{#if enableMedia}} -ENABLE_MEDIA_FEATURES=true -MEDIA_API_PUBLIC_URL=https://media.{{domain}} -{{else}} -ENABLE_MEDIA_FEATURES=false -MEDIA_API_PUBLIC_URL= -{{/if}} +# --- Represent API (Canadian electoral data) --- +REPRESENT_API_URL=https://represent.opennorth.ca + +# --- NocoDB v2 (read-only data browser) --- +NOCODB_V2_PORT=8091 +NOCODB_URL=http://changemaker-v2-nocodb:8080 +NOCODB_PORT=8091 +NC_ADMIN_EMAIL={{secrets.adminEmail}} +NC_ADMIN_PASSWORD={{secrets.nocodbAdminPassword}} +NC_PUBLIC_URL=https://db.{{domain}} + +# --- Redis --- +REDIS_PASSWORD={{secrets.redisPassword}} +REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379 + +# --- Payments (Stripe) --- +ENABLE_PAYMENTS={{#if enablePayments}}true{{else}}false{{/if}} + +# --- Media Management --- +ENABLE_MEDIA_FEATURES={{#if enableMedia}}true{{else}}false{{/if}} MEDIA_API_PORT=4100 -MEDIA_ROOT=/media/local +MEDIA_API_PUBLIC_URL=https://media.{{domain}} +VITE_MEDIA_API_URL=http://changemaker-media-api:4100 +ENABLE_HLS_TRANSCODE=false +MEDIA_ROOT=/media/library MEDIA_UPLOADS=/media/uploads MAX_UPLOAD_SIZE_GB=10 PUBLIC_MEDIA_PORT=3100 @@ -129,43 +162,111 @@ VIDEO_SCHEDULE_DEFAULT_TIMEZONE=UTC VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24 -# NAR Data -NAR_DATA_DIR=/data +# --- Container Registry --- +GITEA_REGISTRY=gitea.bnkops.com/admin +IMAGE_TAG={{imageTag}} +COMPOSE_PROFILES={{#if enableMonitoring}}monitoring{{/if}}{{#if enableCcpAgent}}{{#if enableMonitoring}},{{/if}}ccp-agent{{/if}} +GITEA_REGISTRY_USER=admin +GITEA_REGISTRY_PASS= +GITEA_REGISTRY_API_TOKEN= -# Platform Service URLs (used for health checks) -MINI_QR_URL=http://{{containerPrefix}}-mini-qr:8080 -EXCALIDRAW_URL=http://{{containerPrefix}}-excalidraw:80 +# --- Gitea (Local Platform Instance) --- +GITEA_URL=http://gitea-changemaker:3000 +GITEA_PORT=3030 +GITEA_WEB_PORT=3030 +GITEA_SSH_PORT=2222 +GITEA_ADMIN_USER=admin +GITEA_ADMIN_PASSWORD={{secrets.giteaAdminPassword}} +GITEA_DB_TYPE=mysql +GITEA_DB_HOST=gitea-db:3306 +GITEA_DB_NAME=gitea +GITEA_DB_USER=gitea +GITEA_DB_PASSWD={{secrets.giteaAdminPassword}} +GITEA_DB_ROOT_PASSWORD={{secrets.giteaAdminPassword}} +GITEA_ROOT_URL=https://git.{{domain}} +GITEA_DOMAIN=git.{{domain}} + +# --- Gitea Docs Comments --- +GITEA_COMMENTS_ENABLED=false +GITEA_API_TOKEN= +GITEA_COMMENTS_REPO_OWNER= +GITEA_COMMENTS_REPO_NAME=docs-comments +GITEA_OAUTH_CLIENT_ID= +GITEA_OAUTH_CLIENT_SECRET= +# Docs source (Gitea repo containing the mkdocs/ tree) +GITEA_DOCS_REPO=admin/changemaker.lite +GITEA_DOCS_PREFIX=mkdocs/docs +GITEA_DOCS_BRANCH=v2 + +# --- n8n --- +N8N_URL=http://n8n-changemaker:5678 +N8N_PORT=5678 +N8N_HOST=n8n.{{domain}} +N8N_ENCRYPTION_KEY={{secrets.n8nEncryptionKey}} +N8N_USER_EMAIL={{secrets.adminEmail}} +N8N_USER_PASSWORD={{secrets.nocodbAdminPassword}} +GENERIC_TIMEZONE=UTC + +# --- MkDocs --- +MKDOCS_PORT=4003 +MKDOCS_SITE_SERVER_PORT=4004 +BASE_DOMAIN=https://{{domain}} +MKDOCS_PREVIEW_URL=http://mkdocs:8000 +MKDOCS_DOCS_PATH=/mkdocs/docs + +# --- Code Server --- +CODE_SERVER_PORT=8888 +CODE_SERVER_URL=http://code-server-changemaker:8443 +USER_NAME=coder + +# --- Homepage --- +HOMEPAGE_PORT=3010 +HOMEPAGE_VAR_BASE_URL=http://localhost + +# --- Mini QR --- +MINI_QR_PORT=8089 +MINI_QR_URL=http://mini-qr:8080 + +# --- Excalidraw (Collaborative Whiteboard) --- +EXCALIDRAW_PORT=8090 +EXCALIDRAW_URL=http://excalidraw-changemaker:80 EXCALIDRAW_WS_URL=wss://draw.{{domain}} -HOMEPAGE_URL=http://{{containerPrefix}}-homepage:3000 -VAULTWARDEN_URL=http://{{containerPrefix}}-vaultwarden:80 + +# --- Vaultwarden (Password Manager) --- +VAULTWARDEN_PORT=8445 +VAULTWARDEN_URL=http://vaultwarden-changemaker:80 VAULTWARDEN_ADMIN_TOKEN={{secrets.vaultwardenAdminToken}} VAULTWARDEN_DOMAIN=https://vault.{{domain}} VAULTWARDEN_SIGNUPS_ALLOWED=false VAULTWARDEN_WEBSOCKET_ENABLED=true VAULTWARDEN_SMTP_SECURITY=off -# Geocoding +# --- MailHog --- +MAILHOG_SMTP_PORT=1025 +MAILHOG_WEB_PORT=8025 + +# --- NAR (National Address Register) --- +NAR_DATA_DIR=/data + +# --- Overpass / Area Import --- +OVERPASS_API_URL=https://overpass-api.de/api/interpreter +OVERPASS_MIN_DELAY_MS=30000 +AREA_IMPORT_MAX_GRID_POINTS=500 + +# --- Geocoding --- MAPBOX_API_KEY= -GOOGLE_MAPS_API_KEY= -GOOGLE_MAPS_ENABLED=false GEOCODING_RATE_LIMIT_MS=1100 GEOCODING_CACHE_ENABLED=true GEOCODING_CACHE_TTL_HOURS=24 +GOOGLE_MAPS_API_KEY= +GOOGLE_MAPS_ENABLED=false GEOCODING_PARALLEL_ENABLED=true GEOCODING_BATCH_SIZE=10 BULK_GEOCODE_ENABLED=true BULK_GEOCODE_MAX_BATCH=5000 -# Represent API -REPRESENT_API_URL=https://represent.opennorth.ca - -# Overpass / Area Import -OVERPASS_API_URL=https://overpass-api.de/api/interpreter -OVERPASS_MIN_DELAY_MS=30000 -AREA_IMPORT_MAX_GRID_POINTS=500 - -# Pangolin Tunnel -PANGOLIN_API_URL= +# --- Pangolin Tunnel --- +PANGOLIN_API_URL=https://api.bnkserve.org/v1 PANGOLIN_API_KEY= PANGOLIN_ORG_ID= PANGOLIN_SITE_ID= @@ -174,178 +275,95 @@ PANGOLIN_ENDPOINT={{pangolin.endpoint}} PANGOLIN_NEWT_ID={{pangolin.newtId}} PANGOLIN_NEWT_SECRET={{pangolin.newtSecret}} {{else}} -PANGOLIN_ENDPOINT= +PANGOLIN_ENDPOINT=https://pangolin.bnkserve.org PANGOLIN_NEWT_ID= PANGOLIN_NEWT_SECRET= {{/if}} -# Gancio -{{#if enableGancio}} -GANCIO_SYNC_ENABLED=true -GANCIO_URL=http://{{containerPrefix}}-gancio:13120 -{{else}} -GANCIO_SYNC_ENABLED=false -GANCIO_URL= -{{/if}} +# --- Prisma CLI (host-side only, NOT used by Docker containers) --- +DATABASE_URL=postgresql://changemaker:{{secrets.postgresPassword}}@localhost:{{ports.postgres}}/changemaker_v2 + +# --- Rocket.Chat (Team Chat) --- +ENABLE_CHAT={{#if enableChat}}true{{else}}false{{/if}} +ROCKETCHAT_ADMIN_USER=rcadmin +ROCKETCHAT_ADMIN_PASSWORD={{secrets.rocketchatAdminPassword}} +ROCKETCHAT_URL=http://rocketchat-changemaker:3000 +MONGO_ROOT_USER=rocketchat +MONGO_ROOT_PASSWORD={{secrets.mongoRootPassword}} + +# --- Gancio (Event Management) --- +GANCIO_PORT=8092 +GANCIO_URL=http://gancio-changemaker:13120 GANCIO_BASE_URL=https://events.{{domain}} GANCIO_ADMIN_USER=admin GANCIO_ADMIN_PASSWORD={{secrets.gancioAdminPassword}} -GANCIO_PORT=8092 +GANCIO_SYNC_ENABLED={{#if enableGancio}}true{{else}}false{{/if}} -# Chat (Rocket.Chat) -{{#if enableChat}} -ENABLE_CHAT=true -ROCKETCHAT_URL=http://{{containerPrefix}}-rocketchat:3000 -ROCKETCHAT_ADMIN_USER=rcadmin -ROCKETCHAT_ADMIN_PASSWORD={{secrets.rocketchatAdminPassword}} -MONGO_ROOT_USER=rocketchat -MONGO_ROOT_PASSWORD={{secrets.mongoRootPassword}} -{{else}} -ENABLE_CHAT=false -ROCKETCHAT_URL= -ROCKETCHAT_ADMIN_USER= -ROCKETCHAT_ADMIN_PASSWORD= -MONGO_ROOT_USER= -MONGO_ROOT_PASSWORD= -{{/if}} - -# Jitsi Meet (Video Conferencing) +# --- Jitsi Meet (Video Conferencing) --- ENABLE_MEET={{#if enableMeet}}true{{else}}false{{/if}} -{{#if enableMeet}} JITSI_APP_ID=changemaker JITSI_APP_SECRET={{secrets.jitsiAppSecret}} JITSI_JICOFO_AUTH_PASSWORD={{secrets.jitsiJicofoAuthPassword}} JITSI_JVB_AUTH_PASSWORD={{secrets.jitsiJvbAuthPassword}} -JITSI_URL=http://{{containerPrefix}}-jitsi-web:80 +JITSI_URL=http://jitsi-web-changemaker:80 JVB_ADVERTISE_IP={{jvbAdvertiseIp}} JVB_PORT=10000 -{{else}} -JITSI_APP_ID= -JITSI_APP_SECRET= -JITSI_JICOFO_AUTH_PASSWORD= -JITSI_JVB_AUTH_PASSWORD= -JITSI_URL= -JVB_ADVERTISE_IP= -JVB_PORT=10000 -{{/if}} -# SMS Campaigns +# --- SMS Campaigns (Termux Android Bridge) --- ENABLE_SMS={{#if enableSms}}true{{else}}false{{/if}} TERMUX_API_URL= TERMUX_API_KEY= SMS_DELAY_BETWEEN_MS=3000 SMS_MAX_RETRIES=3 -SMS_RESPONSE_SYNC_INTERVAL_MS=30000 -SMS_DEVICE_MONITOR_INTERVAL_MS=30000 +SMS_RESPONSE_SYNC_INTERVAL_MS=120000 +SMS_DEVICE_MONITOR_INTERVAL_MS=300000 -# Social Connections +# --- Social, People & Analytics --- ENABLE_SOCIAL={{#if enableSocial}}true{{else}}false{{/if}} - -# People CRM ENABLE_PEOPLE={{#if enablePeople}}true{{else}}false{{/if}} - -# Analytics & GeoIP ENABLE_ANALYTICS={{#if enableAnalytics}}true{{else}}false{{/if}} -MAXMIND_ACCOUNT_ID= -MAXMIND_LICENSE_KEY= -# Monitoring +# --- Control Panel Agent --- +# Tenants registered with CCP have these populated; CCP-provisioned tenants +# get them set by the provisioner. Leaving blank if neither applies. +ENABLE_CCP_AGENT=true +CCP_URL= +CCP_INVITE_CODE= +CCP_AGENT_URL= +CCP_AGENT_PORT=7443 + +# --- Monitoring (only used with --profile monitoring) --- +PROMETHEUS_PORT=9090 +GRAFANA_PORT=3005 GRAFANA_ADMIN_PASSWORD={{secrets.grafanaAdminPassword}} GRAFANA_ROOT_URL=https://grafana.{{domain}} -PROMETHEUS_PORT=9090 -GRAFANA_PORT=3000 CADVISOR_PORT=8086 NODE_EXPORTER_PORT=9100 REDIS_EXPORTER_PORT=9121 ALERTMANAGER_PORT=9093 -ALERTMANAGER_EMBED_PORT={{math ports.embed "+" 16}} GOTIFY_PORT=8889 GOTIFY_ADMIN_USER=admin GOTIFY_ADMIN_PASSWORD=admin -# MkDocs -MKDOCS_PORT={{math ports.embed "+" 8}} -MKDOCS_SITE_SERVER_PORT={{math ports.embed "+" 14}} -MKDOCS_PREVIEW_URL=http://{{containerPrefix}}-mkdocs:8000 -MKDOCS_DOCS_PATH=/mkdocs/docs -CODE_SERVER_PORT={{math ports.embed "+" 7}} -CODE_SERVER_URL=http://{{containerPrefix}}-code-server:8443 -BASE_DOMAIN=https://{{domain}} - -# Gitea -GITEA_URL=http://{{containerPrefix}}-gitea:3000 -GITEA_SSH_PORT=2222 -GITEA_DB_TYPE=postgres -GITEA_DB_HOST={{containerPrefix}}-postgres:5432 -GITEA_DB_NAME=gitea -GITEA_DB_USER=changemaker -GITEA_DB_PASSWD={{secrets.postgresPassword}} -GITEA_ROOT_URL=https://git.{{domain}} -GITEA_DOMAIN=git.{{domain}} -GITEA_COMMENTS_ENABLED=false -GITEA_API_TOKEN= -GITEA_COMMENTS_REPO_OWNER= -GITEA_COMMENTS_REPO_NAME=docs-comments -GITEA_OAUTH_CLIENT_ID= -GITEA_OAUTH_CLIENT_SECRET= - -# n8n -N8N_HOST=n8n.{{domain}} -N8N_URL=http://{{containerPrefix}}-n8n:5678 -N8N_ENCRYPTION_KEY={{secrets.n8nEncryptionKey}} -N8N_USER_EMAIL={{secrets.adminEmail}} -N8N_USER_PASSWORD={{secrets.nocodbAdminPassword}} -GENERIC_TIMEZONE=UTC - -# MailHog -MAILHOG_URL=http://{{containerPrefix}}-mailhog:8025 -MAILHOG_SMTP_PORT=1025 -MAILHOG_WEB_PORT=8025 - -# Homepage -HOMEPAGE_PORT=3010 -HOMEPAGE_VAR_BASE_URL=http://localhost - -# Dev Tools -{{#if enableDevTools}} -ENABLE_DEV_TOOLS=true -{{else}} -ENABLE_DEV_TOOLS=false -{{/if}} - -# Payments -{{#if enablePayments}} -ENABLE_PAYMENTS=true -{{else}} -ENABLE_PAYMENTS=false -{{/if}} - -# Vite (admin build) -VITE_API_URL=http://{{containerPrefix}}-api:4000 -VITE_MKDOCS_URL=http://{{containerPrefix}}-mkdocs:8000 -{{#if enableMedia}} -VITE_MEDIA_API_URL=http://{{containerPrefix}}-media-api:4100 -{{/if}} - -# Bunker Ops (Fleet Management) +# --- Bunker Ops (Fleet Management) --- INSTANCE_LABEL={{slug}} BUNKER_OPS_ENABLED=false BUNKER_OPS_REMOTE_WRITE_URL= -# Embed proxy ports (nginx proxy for iframe embedding in admin GUI) -NOCODB_EMBED_PORT={{math ports.embed "+" 0}} -N8N_EMBED_PORT={{math ports.embed "+" 1}} -GITEA_EMBED_PORT={{math ports.embed "+" 2}} -MAILHOG_EMBED_PORT={{math ports.embed "+" 3}} -MINI_QR_EMBED_PORT={{math ports.embed "+" 4}} -EXCALIDRAW_EMBED_PORT={{math ports.embed "+" 5}} -HOMEPAGE_EMBED_PORT={{math ports.embed "+" 6}} +# --- GeoIP (MaxMind GeoLite2) --- +MAXMIND_ACCOUNT_ID= +MAXMIND_LICENSE_KEY= + +# --- CCP-specific (admin GUI iframe embeds + dev-mode helpers) --- +# These are CCP-only — not in canonical .env.example. Kept here because +# admin/vite uses them at build time and the embed proxies reference them. +PORT=4000 +VITE_API_URL=http://changemaker-v2-api:4000 +HOMEPAGE_URL=http://homepage-changemaker:3000 +MAILHOG_URL=http://mailhog-changemaker:8025 +LISTMONK_URL=http://listmonk-app:9000 CODE_SERVER_EMBED_PORT={{math ports.embed "+" 7}} MKDOCS_EMBED_PORT={{math ports.embed "+" 8}} -VAULTWARDEN_EMBED_PORT={{math ports.embed "+" 9}} -ROCKETCHAT_EMBED_PORT={{math ports.embed "+" 10}} -GANCIO_EMBED_PORT={{math ports.embed "+" 11}} -GRAFANA_EMBED_PORT={{math ports.embed "+" 12}} -LISTMONK_EMBED_PORT={{math ports.embed "+" 13}} MKDOCS_SITE_EMBED_PORT={{math ports.embed "+" 14}} -JITSI_EMBED_PORT={{math ports.embed "+" 15}} +LISTMONK_EMBED_PORT={{math ports.embed "+" 13}} +ENABLE_DEV_TOOLS={{#if enableDevTools}}true{{else}}false{{/if}} diff --git a/changemaker-control-panel/templates/nginx/nginx.conf b/changemaker-control-panel/templates/nginx/nginx.conf index 056abe3..176a77a 100644 --- a/changemaker-control-panel/templates/nginx/nginx.conf +++ b/changemaker-control-panel/templates/nginx/nginx.conf @@ -10,7 +10,14 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + # Redact sensitive query parameters (token, secret) from access logs + map $request_uri $redacted_request { + ~^(?P[^?]*)\?(?P.*token=[^&]*) "$path?"; + ~^(?P[^?]*)\?(?P.*secret=[^&]*) "$path?"; + default $request_uri; + } + + log_format main '$remote_addr - $remote_user [$time_local] "$request_method $redacted_request $server_protocol" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; @@ -25,6 +32,12 @@ http { types_hash_max_size 2048; client_max_body_size 50m; + # Rate limiting zones (defense-in-depth alongside app-level Redis rate limits) + limit_req_zone $binary_remote_addr zone=api_global:10m rate=30r/s; + limit_req_zone $binary_remote_addr zone=api_auth:10m rate=5r/s; + limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s; + limit_req_status 429; + # Gzip compression gzip on; gzip_vary on; @@ -32,11 +45,17 @@ http { gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + # Only send HSTS when the request arrived over HTTPS (via Pangolin tunnel) + map $http_x_forwarded_proto $hsts_header { + https "max-age=31536000; includeSubDomains"; + default ""; + } + # Security headers (applied globally — X-Frame-Options set per server block) add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security $hsts_header always; add_header Permissions-Policy "geolocation=(self), microphone=(), camera=()" always; # Docker internal DNS — enables runtime resolution so nginx starts