#!/usr/bin/env bash set -euo pipefail # ============================================================================= # Changemaker Lite V2 — Configuration Wizard # Produces a working .env file and stages the full application stack. # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="$SCRIPT_DIR/.env" ENV_EXAMPLE="$SCRIPT_DIR/.env.example" MKDOCS_YML="$SCRIPT_DIR/mkdocs/mkdocs.yml" SERVICES_YAML="$SCRIPT_DIR/configs/homepage/services.yaml" # --- Detect install mode --- # Release mode: installed from tarball (has VERSION file, no .git directory) # Source mode: cloned from git repository if [[ -f "$SCRIPT_DIR/VERSION" ]] && [[ ! -d "$SCRIPT_DIR/.git" ]]; then INSTALL_MODE="release" RELEASE_VERSION=$(head -1 "$SCRIPT_DIR/VERSION") else INSTALL_MODE="source" RELEASE_VERSION="" fi # --- Colors (respects NO_COLOR convention) --- if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC='' fi # ============================================================================= # Ensure stdin is connected to the terminal (handles curl | bash case) # ============================================================================= if [[ ! -t 0 ]]; then if [[ -e /dev/tty ]]; then exec 0&2 echo " Download and run manually: bash config.sh" >&2 exit 1 fi fi # ============================================================================= # Utility Functions # ============================================================================= info() { echo -e "${CYAN}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERR]${NC} $*" >&2; } header() { echo "" echo -e "${BOLD}${BLUE}── $* ──${NC}" echo "" } generate_secret() { openssl rand -hex 32 } generate_password() { local length=${1:-24} openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length" } # Line-by-line .env replacement — robust with special characters update_env_var() { local key=$1 local value=$2 if grep -q "^${key}=" "$ENV_FILE"; then local tmpfile tmpfile=$(mktemp) while IFS= read -r line; do if [[ "$line" =~ ^${key}= ]]; then echo "${key}=${value}" >> "$tmpfile" else echo "$line" >> "$tmpfile" fi done < "$ENV_FILE" mv "$tmpfile" "$ENV_FILE" else echo "${key}=${value}" >> "$ENV_FILE" fi } backup_env_file() { if [[ -f "$ENV_FILE" ]]; then local ts ts=$(date +"%Y%m%d_%H%M%S") local backup="$ENV_FILE.backup_$ts" cp "$ENV_FILE" "$backup" success "Backed up .env to ${backup##*/}" fi } validate_password() { local pw=$1 [[ ${#pw} -ge 12 ]] || return 1 [[ "$pw" =~ [A-Z] ]] || return 1 [[ "$pw" =~ [a-z] ]] || return 1 [[ "$pw" =~ [0-9] ]] || return 1 return 0 } prompt_yes_no() { local prompt=$1 default=${2:-n} local yn if [[ "$default" == "y" ]]; then read -rp "$prompt [Y/n]: " yn [[ "$yn" =~ ^[Nn]$ ]] && return 1 || return 0 else read -rp "$prompt [y/N]: " yn [[ "$yn" =~ ^[Yy]$ ]] && return 0 || return 1 fi } # ============================================================================= # Prerequisites # ============================================================================= check_prerequisites() { header "Checking Prerequisites" local ok=true if command -v docker &>/dev/null; then success "Docker found: $(docker --version | head -1)" else error "Docker is not installed. See https://docs.docker.com/get-docker/" ok=false fi if docker compose version &>/dev/null; then success "Docker Compose found: $(docker compose version --short)" else error "Docker Compose v2 plugin not found. See https://docs.docker.com/compose/install/" ok=false fi if command -v openssl &>/dev/null; then success "OpenSSL found" else error "OpenSSL is not installed (needed for secret generation)" ok=false fi $ok || { echo ""; error "Missing prerequisites. Install them and re-run."; exit 1; } } # ============================================================================= # Banner # ============================================================================= print_banner() { cat << 'EOF' ██████╗██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ███████╗ ██╔════╝██║ ██║██╔══██╗████╗ ██║██╔════╝ ██╔════╝ ██║ ███████║███████║██╔██╗ ██║██║ ███╗█████╗ ██║ ██╔══██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ╚██████╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝███████╗ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗ ████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗ ██╔████╔██║███████║█████╔╝ █████╗ ██████╔╝ ██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗ ██║ ╚═╝ ██║██║ ██║██║ ██╗███████╗██║ ██║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ V2 Configuration Wizard EOF echo "" info "This wizard will create your .env file, generate secure secrets," info "and prepare your system to run the full Changemaker Lite stack." echo "" } # ============================================================================= # .env Initialization # ============================================================================= initialize_env() { header "Environment File Setup" if [[ ! -f "$ENV_EXAMPLE" ]]; then error ".env.example not found at $ENV_EXAMPLE" error "Make sure you're running this from the project root." exit 1 fi RECONFIGURE_MODE=false if [[ -f "$ENV_FILE" ]]; then warn "Existing .env file found at $ENV_FILE" if prompt_yes_no "Back up existing .env and create a fresh one?"; then backup_env_file cp "$ENV_EXAMPLE" "$ENV_FILE" success "Created fresh .env from .env.example" else info "Keeping existing .env. Existing secrets will be preserved." RECONFIGURE_MODE=true fi else cp "$ENV_EXAMPLE" "$ENV_FILE" success "Created .env from .env.example" fi } # ============================================================================= # Configuration Sections # ============================================================================= configure_domain() { header "Domain Configuration" info "Root domain serves MkDocs documentation." info "All application routes are at app.." echo "" read -rp "Enter your domain (e.g., example.org) [default: cmlite.org]: " domain domain=${domain:-cmlite.org} update_env_var "DOMAIN" "$domain" update_env_var "BASE_DOMAIN" "https://$domain" update_env_var "GITEA_ROOT_URL" "https://git.$domain" update_env_var "GITEA_DOMAIN" "git.$domain" update_env_var "N8N_HOST" "n8n.$domain" update_env_var "SMTP_FROM" "noreply@$domain" update_env_var "SMTP_FROM_NAME" "Changemaker Lite" update_env_var "INITIAL_ADMIN_EMAIL" "admin@$domain" update_env_var "NC_ADMIN_EMAIL" "admin@$domain" update_env_var "TEST_EMAIL_RECIPIENT" "admin@$domain" update_env_var "EXCALIDRAW_WS_URL" "wss://draw.$domain" update_env_var "LISTMONK_SMTP_FROM" "Changemaker Lite " update_env_var "HOMEPAGE_VAR_BASE_URL" "https://$domain" update_env_var "VAULTWARDEN_DOMAIN" "https://vault.$domain" update_env_var "GANCIO_BASE_URL" "https://events.$domain" # Update mkdocs.yml if [[ -f "$MKDOCS_YML" ]]; then sed -i "s|^site_url:.*|site_url: https://$domain|" "$MKDOCS_YML" sed -i "s|^repo_url:.*|repo_url: https://git.$domain/admin/changemaker.lite|" "$MKDOCS_YML" success "Updated mkdocs.yml (site_url, repo_url)" else warn "mkdocs.yml not found — skipping" fi success "Domain set to: $domain" echo "" if prompt_yes_no "Is this a production deployment?"; then update_env_var "NODE_ENV" "production" success "NODE_ENV set to production" IS_PRODUCTION="yes" else info "NODE_ENV stays as development" IS_PRODUCTION="no" fi # Store for later use CONFIGURED_DOMAIN="$domain" } configure_admin() { header "Admin Credentials" local default_email="admin@${CONFIGURED_DOMAIN:-cmlite.org}" read -rp "Admin email [default: $default_email]: " admin_email admin_email=${admin_email:-$default_email} local admin_password="" while true; do echo "" read -rsp "Admin password (min 12 chars, uppercase + lowercase + digit): " admin_password echo "" if validate_password "$admin_password"; then read -rsp "Confirm password: " confirm_password echo "" if [[ "$admin_password" == "$confirm_password" ]]; then break else warn "Passwords do not match. Try again." fi else warn "Password must be 12+ characters with uppercase, lowercase, and a digit." fi done update_env_var "INITIAL_ADMIN_EMAIL" "$admin_email" update_env_var "INITIAL_ADMIN_PASSWORD" "$admin_password" update_env_var "N8N_USER_EMAIL" "$admin_email" success "Admin credentials configured" } # Check if a key already has a real value in .env (non-empty, not a placeholder) env_var_is_set() { local key=$1 local val val=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-) [[ -n "$val" && "$val" != "changeme" && "$val" != *"example"* && "$val" != *"CHANGEME"* ]] } # Update an env var only if not already set (for reconfigure mode) update_env_var_if_empty() { local key=$1 local value=$2 if [[ "$RECONFIGURE_MODE" == "true" ]] && env_var_is_set "$key"; then return 1 # signal: kept existing fi update_env_var "$key" "$value" return 0 } generate_all_secrets() { header "Generating Secrets" if [[ "$RECONFIGURE_MODE" == "true" ]]; then info "Reconfigure mode: existing secrets will be preserved." else info "Auto-generating 22 unique secrets and passwords..." fi echo "" local generated=0 kept=0 # JWT & Encryption (64-char hex) local jwt_access jwt_refresh jwt_invite enc_key jwt_access=$(generate_secret) jwt_refresh=$(generate_secret) jwt_invite=$(generate_secret) enc_key=$(generate_secret) local jwt_changed=false update_env_var_if_empty "JWT_ACCESS_SECRET" "$jwt_access" && jwt_changed=true update_env_var_if_empty "JWT_REFRESH_SECRET" "$jwt_refresh" && jwt_changed=true update_env_var_if_empty "JWT_INVITE_SECRET" "$jwt_invite" && jwt_changed=true update_env_var_if_empty "ENCRYPTION_KEY" "$enc_key" && jwt_changed=true if [[ "$jwt_changed" == "true" ]]; then success "JWT secrets + encryption key" ((generated+=4)) else info "JWT secrets + encryption key (kept existing)" ((kept+=4)) fi # Gitea SSO + service password salt (isolated from JWT secrets) local sso_secret svc_salt sso_secret=$(generate_secret) svc_salt=$(generate_secret) local sso_changed=false update_env_var_if_empty "GITEA_SSO_SECRET" "$sso_secret" && sso_changed=true update_env_var_if_empty "SERVICE_PASSWORD_SALT" "$svc_salt" && sso_changed=true if [[ "$sso_changed" == "true" ]]; then success "Gitea SSO secret + service password salt" ((generated+=2)) else info "Gitea SSO secret + service password salt (kept existing)" ((kept+=2)) fi # Database passwords (24-char alphanum) local pg_pass redis_pass pg_pass=$(generate_password 24) redis_pass=$(generate_password 24) local db_changed=false if update_env_var_if_empty "V2_POSTGRES_PASSWORD" "$pg_pass"; then update_env_var "DATABASE_URL" "postgresql://changemaker:${pg_pass}@localhost:5433/changemaker_v2" db_changed=true fi update_env_var_if_empty "REDIS_PASSWORD" "$redis_pass" && db_changed=true if [[ "$db_changed" == "true" ]]; then # Rebuild REDIS_URL if password changed local current_redis_pass current_redis_pass=$(grep "^REDIS_PASSWORD=" "$ENV_FILE" | cut -d= -f2-) update_env_var "REDIS_URL" "redis://:${current_redis_pass}@redis-changemaker:6379" success "PostgreSQL + Redis passwords" ((generated+=2)) else info "PostgreSQL + Redis passwords (kept existing)" ((kept+=2)) fi # Listmonk local lm_db_pass lm_web_pass lm_api_token lm_db_pass=$(generate_password 24) lm_web_pass=$(generate_password 20) lm_api_token=$(openssl rand -hex 16) local lm_changed=false update_env_var_if_empty "LISTMONK_DB_PASSWORD" "$lm_db_pass" && lm_changed=true update_env_var_if_empty "LISTMONK_WEB_ADMIN_PASSWORD" "$lm_web_pass" && lm_changed=true if update_env_var_if_empty "LISTMONK_API_TOKEN" "$lm_api_token"; then update_env_var "LISTMONK_ADMIN_PASSWORD" "$lm_api_token" lm_changed=true fi if [[ "$lm_changed" == "true" ]]; then success "Listmonk passwords + API token" ((generated+=3)) else info "Listmonk passwords + API token (kept existing)" ((kept+=3)) fi # NocoDB local nc_pass nc_pass=$(generate_password 20) if update_env_var_if_empty "NC_ADMIN_PASSWORD" "$nc_pass"; then success "NocoDB admin password" ((generated++)) else info "NocoDB admin password (kept existing)" ((kept++)) fi # Gitea local gitea_db gitea_root gitea_db=$(generate_password 20) gitea_root=$(generate_password 20) local gitea_changed=false update_env_var_if_empty "GITEA_DB_PASSWD" "$gitea_db" && gitea_changed=true update_env_var_if_empty "GITEA_DB_ROOT_PASSWORD" "$gitea_root" && gitea_changed=true if [[ "$gitea_changed" == "true" ]]; then success "Gitea database passwords" ((generated+=2)) else info "Gitea database passwords (kept existing)" ((kept+=2)) fi # n8n local n8n_enc n8n_pass n8n_enc=$(generate_password 32) n8n_pass=$(generate_password 20) local n8n_changed=false update_env_var_if_empty "N8N_ENCRYPTION_KEY" "$n8n_enc" && n8n_changed=true update_env_var_if_empty "N8N_USER_PASSWORD" "$n8n_pass" && n8n_changed=true if [[ "$n8n_changed" == "true" ]]; then success "n8n encryption key + admin password" ((generated+=2)) else info "n8n encryption key + admin password (kept existing)" ((kept+=2)) fi # Monitoring local grafana_pass gotify_pass grafana_pass=$(generate_password 20) gotify_pass=$(generate_password 20) local mon_changed=false update_env_var_if_empty "GRAFANA_ADMIN_PASSWORD" "$grafana_pass" && mon_changed=true update_env_var_if_empty "GOTIFY_ADMIN_PASSWORD" "$gotify_pass" && mon_changed=true if [[ "$mon_changed" == "true" ]]; then success "Grafana + Gotify admin passwords" ((generated+=2)) else info "Grafana + Gotify admin passwords (kept existing)" ((kept+=2)) fi # Vaultwarden local vw_admin_token vw_admin_token=$(generate_secret) if update_env_var_if_empty "VAULTWARDEN_ADMIN_TOKEN" "$vw_admin_token"; then success "Vaultwarden admin token" ((generated++)) else info "Vaultwarden admin token (kept existing)" ((kept++)) fi # Rocket.Chat local rc_pass rc_pass=$(generate_password 20) if update_env_var_if_empty "ROCKETCHAT_ADMIN_PASSWORD" "$rc_pass"; then success "Rocket.Chat admin password" ((generated++)) else info "Rocket.Chat admin password (kept existing)" ((kept++)) fi # MongoDB (required for Rocket.Chat — runs with --auth) local mongo_pass mongo_pass=$(generate_password 24) if update_env_var_if_empty "MONGO_ROOT_PASSWORD" "$mongo_pass"; then success "MongoDB root password" ((generated++)) else info "MongoDB root password (kept existing)" ((kept++)) fi # Gancio local gancio_pass gancio_pass=$(generate_password 20) if update_env_var_if_empty "GANCIO_ADMIN_PASSWORD" "$gancio_pass"; then success "Gancio admin password" ((generated++)) else info "Gancio admin password (kept existing)" ((kept++)) fi # Jitsi Meet local jitsi_secret jitsi_jicofo jitsi_jvb jitsi_secret=$(generate_secret) jitsi_jicofo=$(openssl rand -hex 16) jitsi_jvb=$(openssl rand -hex 16) local jitsi_changed=false update_env_var_if_empty "JITSI_APP_SECRET" "$jitsi_secret" && jitsi_changed=true update_env_var_if_empty "JITSI_JICOFO_AUTH_PASSWORD" "$jitsi_jicofo" && jitsi_changed=true update_env_var_if_empty "JITSI_JVB_AUTH_PASSWORD" "$jitsi_jvb" && jitsi_changed=true if [[ "$jitsi_changed" == "true" ]]; then success "Jitsi Meet secrets (JWT + XMPP)" ((generated+=3)) else info "Jitsi Meet secrets (kept existing)" ((kept+=3)) fi echo "" if [[ $kept -gt 0 ]]; then success "Secrets: ${generated} generated, ${kept} preserved from existing .env" else success "All 22 secrets generated. No placeholder passwords remain." fi } configure_smtp() { header "Email Configuration" info "By default, emails are captured by MailHog (test mode)." info "You can configure a production SMTP server now or later." echo "" if prompt_yes_no "Configure production SMTP now?"; then read -rp " SMTP host (e.g., smtp.protonmail.ch): " smtp_host read -rp " SMTP port (e.g., 587): " smtp_port read -rp " SMTP user: " smtp_user read -rsp " SMTP password: " smtp_pass echo "" update_env_var "SMTP_HOST" "$smtp_host" update_env_var "SMTP_PORT" "$smtp_port" update_env_var "SMTP_USER" "$smtp_user" update_env_var "SMTP_PASS" "$smtp_pass" update_env_var "EMAIL_TEST_MODE" "false" if prompt_yes_no " Also use this SMTP for Listmonk newsletters?"; then update_env_var "LISTMONK_SMTP_HOST" "$smtp_host" update_env_var "LISTMONK_SMTP_PORT" "$smtp_port" update_env_var "LISTMONK_SMTP_USER" "$smtp_user" update_env_var "LISTMONK_SMTP_PASSWORD" "$smtp_pass" update_env_var "LISTMONK_SMTP_TLS_TYPE" "STARTTLS" success "Listmonk SMTP configured" fi # Vaultwarden needs matching SMTP security update_env_var "VAULTWARDEN_SMTP_SECURITY" "starttls" success "Production SMTP configured" SMTP_MODE="production" else info "Using MailHog for email testing (port 8025)" update_env_var "VAULTWARDEN_SMTP_SECURITY" "off" SMTP_MODE="mailhog" fi } configure_features() { header "Feature Flags" if prompt_yes_no "Enable Media Manager (video library)?"; then update_env_var "ENABLE_MEDIA_FEATURES" "true" success "Media Manager enabled" MEDIA_ENABLED="yes" else MEDIA_ENABLED="no" fi if prompt_yes_no "Enable Listmonk newsletter sync?"; then update_env_var "LISTMONK_SYNC_ENABLED" "true" success "Listmonk sync enabled" LISTMONK_SYNC="yes" else LISTMONK_SYNC="no" fi if prompt_yes_no "Enable Payments (Stripe)?"; then update_env_var "ENABLE_PAYMENTS" "true" success "Payments enabled" PAYMENTS_ENABLED="yes" else PAYMENTS_ENABLED="no" fi if prompt_yes_no "Enable Rocket.Chat (team chat)?"; then update_env_var "ENABLE_CHAT" "true" success "Rocket.Chat enabled" CHAT_ENABLED="yes" else CHAT_ENABLED="no" fi if prompt_yes_no "Enable Gancio event sync (shift → event)?"; then update_env_var "GANCIO_SYNC_ENABLED" "true" success "Gancio sync enabled" GANCIO_SYNC="yes" else GANCIO_SYNC="no" fi if prompt_yes_no "Enable Jitsi Meet (video conferencing)?"; then update_env_var "ENABLE_MEET" "true" success "Jitsi Meet enabled" MEET_ENABLED="yes" echo "" info "Jitsi requires your server's public IP for media traffic (NAT traversal)." info "Firewall must allow UDP port 10000 for video/audio." read -rp " Server public IP [leave blank to set later]: " jvb_ip if [[ -n "$jvb_ip" ]]; then update_env_var "JVB_ADVERTISE_IP" "$jvb_ip" success "JVB advertise IP set to $jvb_ip" else warn "Set JVB_ADVERTISE_IP in .env before starting Jitsi containers." fi else MEET_ENABLED="no" fi if prompt_yes_no "Enable SMS Campaigns (Termux Android bridge)?"; then update_env_var "ENABLE_SMS" "true" success "SMS Campaigns enabled" SMS_ENABLED="yes" echo "" info "SMS uses a Termux-based Android phone as the sending device." read -rp " Termux API URL [default: http://10.0.0.193:5001]: " termux_url termux_url=${termux_url:-http://10.0.0.193:5001} update_env_var "TERMUX_API_URL" "$termux_url" read -rp " Termux API Key [leave blank to set later]: " termux_key if [[ -n "$termux_key" ]]; then update_env_var "TERMUX_API_KEY" "$termux_key" fi else SMS_ENABLED="no" fi if prompt_yes_no "Enable Docs Comments & Version History (Gitea-backed)?"; then update_env_var "GITEA_COMMENTS_ENABLED" "true" success "Docs Comments & Version History enabled" DOCS_COMMENTS_ENABLED="yes" echo "" info "Gitea auto-setup will create the API token, repos, and OAuth app automatically." info "You need to provide the Gitea admin password (set during Gitea's first-run install)." echo "" read -srp " Gitea admin password [leave blank to set up later via admin GUI]: " gitea_admin_pw echo "" if [[ -n "$gitea_admin_pw" ]]; then update_env_var "GITEA_ADMIN_PASSWORD" "$gitea_admin_pw" update_env_var "GITEA_COMMENTS_REPO_OWNER" "admin" success "Gitea admin password saved — auto-setup will run on next start" else info "No password provided. Run Gitea Setup from the admin GUI after first start." fi else DOCS_COMMENTS_ENABLED="no" fi if prompt_yes_no "Enable Monitoring stack (Prometheus, Grafana, Alertmanager, cAdvisor)?" "y"; then update_env_var "COMPOSE_PROFILES" "monitoring" success "Monitoring enabled (COMPOSE_PROFILES=monitoring)" MONITORING_ENABLED="yes" else MONITORING_ENABLED="no" fi if prompt_yes_no "Enable Bunker Ops (fleet metrics push to central server)?"; then update_env_var "BUNKER_OPS_ENABLED" "true" success "Bunker Ops enabled" BUNKER_OPS_ENABLED="yes" echo "" read -rp " Instance label [default: domain name]: " instance_label if [[ -n "$instance_label" ]]; then update_env_var "INSTANCE_LABEL" "$instance_label" fi read -rp " Remote write URL (VictoriaMetrics endpoint): " remote_write_url if [[ -n "$remote_write_url" ]]; then update_env_var "BUNKER_OPS_REMOTE_WRITE_URL" "$remote_write_url" fi else BUNKER_OPS_ENABLED="no" fi echo "" if prompt_yes_no "Enable Analytics & GeoIP tracking (visitor geography)?"; then update_env_var "ENABLE_ANALYTICS" "true" success "Analytics enabled" echo "" info "GeoIP tracking requires a free MaxMind account." info "Sign up at: https://www.maxmind.com/en/geolite2/signup" read -rp " MaxMind Account ID [leave blank to set later]: " maxmind_id if [[ -n "$maxmind_id" ]]; then update_env_var "MAXMIND_ACCOUNT_ID" "$maxmind_id" read -rp " MaxMind License Key: " maxmind_key if [[ -n "$maxmind_key" ]]; then update_env_var "MAXMIND_LICENSE_KEY" "$maxmind_key" success "MaxMind GeoIP credentials configured" fi else info "Set MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY in .env to enable geo tracking." fi else info "Analytics disabled (can enable later in admin Settings)" fi } configure_pangolin() { header "Tunnel Configuration (Pangolin)" info "Pangolin provides secure public access to your services." info "Skip this if you'll configure tunneling later." echo "" if prompt_yes_no "Configure Pangolin tunnel now?"; then read -rp " Pangolin API URL [default: https://api.bnkserve.org/v1]: " pang_url pang_url=${pang_url:-https://api.bnkserve.org/v1} read -rp " Pangolin API key: " pang_key read -rp " Pangolin Organization ID: " pang_org update_env_var "PANGOLIN_API_URL" "$pang_url" update_env_var "PANGOLIN_API_KEY" "$pang_key" update_env_var "PANGOLIN_ORG_ID" "$pang_org" success "Pangolin configured" info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." PANGOLIN_CONFIGURED="yes" else PANGOLIN_CONFIGURED="no" fi } configure_cors() { local domain="${CONFIGURED_DOMAIN:-cmlite.org}" # Include app subdomain + root domain (for MkDocs payment widgets) + localhost fallbacks local origins="http://app.$domain,https://app.$domain,http://$domain,https://$domain,http://localhost:3000,http://localhost,http://localhost:4003" update_env_var "CORS_ORIGINS" "$origins" success "CORS origins set for $domain" } generate_nginx_configs() { header "Nginx Configuration" local domain="${CONFIGURED_DOMAIN:-cmlite.org}" local template_dir="$SCRIPT_DIR/nginx/conf.d" local templates_found=0 for template in "$template_dir"/*.conf.template; do [[ -f "$template" ]] || continue templates_found=$((templates_found + 1)) local conf_file="${template%.template}" local conf_name conf_name=$(basename "$conf_file") # Substitute ${DOMAIN} while preserving nginx variables ($host, $scheme, etc.) sed "s/\${DOMAIN}/$domain/g" "$template" > "$conf_file" success "Generated $conf_name" done if [[ $templates_found -eq 0 ]]; then warn "No nginx .conf.template files found — skipping" else success "Generated $templates_found nginx configs for domain: $domain" info "Restart nginx to apply: docker compose restart nginx" fi } # ============================================================================= # Homepage services.yaml # ============================================================================= # Read a variable from .env with fallback default read_env_var() { local key=$1 default=$2 local val val=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-) echo "${val:-$default}" } generate_services_yaml() { local domain="${CONFIGURED_DOMAIN:-cmlite.org}" # Read embed proxy ports from .env (supports multi-instance port customization) local rc_port; rc_port=$(read_env_var ROCKETCHAT_EMBED_PORT 8891) local jitsi_port; jitsi_port=$(read_env_var JITSI_EMBED_PORT 8893) mkdir -p "$(dirname "$SERVICES_YAML")" cat > "$SERVICES_YAML" << YAML --- # Homepage Services Configuration — Generated by config.sh # Production tab: public URLs via $domain # Local tab: localhost URLs with ports - Production - Core: - Admin GUI: icon: mdi-view-dashboard href: "https://app.$domain" description: Application dashboard and public pages container: changemaker-v2-admin - API: icon: mdi-api href: "https://api.$domain" description: V2 REST API container: changemaker-v2-api - Media API: icon: mdi-video href: "https://media.$domain" description: Video library and streaming container: changemaker-media-api - Main Site: icon: mdi-web href: "https://$domain" description: Documentation and marketing site container: mkdocs-site-server-changemaker - Production - Tools: - Code Server: icon: mdi-code-braces href: "https://code.$domain" description: VS Code in the browser container: code-server-changemaker - NocoDB: icon: mdi-database href: "https://db.$domain" description: Database browser (read-only) container: changemaker-v2-nocodb - MkDocs (Live): icon: mdi-book-open-page-variant href: "https://docs.$domain" description: Live documentation with hot reload container: mkdocs-changemaker - Mini QR: icon: mdi-qrcode href: "https://qr.$domain" description: QR code generator container: mini-qr - Excalidraw: icon: mdi-draw href: "https://draw.$domain" description: Collaborative whiteboard container: excalidraw-changemaker - Vaultwarden: icon: mdi-lock href: "https://vault.$domain" description: Password manager (Bitwarden-compatible) container: vaultwarden-changemaker - Gancio: icon: mdi-calendar-multiple href: "https://events.$domain" description: Event management platform container: gancio-changemaker - Rocket.Chat: icon: mdi-chat href: "https://chat.$domain" description: Team communication platform container: rocketchat-changemaker - Jitsi Meet: icon: mdi-video href: "https://meet.$domain" description: Video conferencing container: jitsi-web-changemaker - Production - Integrations: - Listmonk: icon: mdi-email-newsletter href: "https://listmonk.$domain" description: Newsletter and mailing list manager container: listmonk-app - MailHog: icon: mdi-email-check href: "https://mail.$domain" description: Email capture for testing container: mailhog-changemaker - n8n: icon: mdi-robot-industrial href: "https://n8n.$domain" description: Workflow automation platform container: n8n-changemaker - Gitea: icon: mdi-git href: "https://git.$domain" description: Git repository hosting container: gitea-changemaker - Production - Infrastructure: - Nginx: icon: mdi-web-box href: "#" description: Reverse proxy (subdomain routing) container: changemaker-v2-nginx - PostgreSQL: icon: mdi-database-outline href: "#" description: Primary database (V2) container: changemaker-v2-postgres - Redis: icon: mdi-database-sync href: "#" description: Cache, rate limiting, job queues container: redis-changemaker - Production - Monitoring: - Grafana: icon: mdi-chart-box href: "https://grafana.$domain" description: Monitoring dashboards container: grafana-changemaker - Prometheus: icon: mdi-chart-line href: "https://prometheus.$domain" description: Metrics collection container: prometheus-changemaker - Alertmanager: icon: mdi-bell-alert href: "https://alertmanager.$domain" description: Alert routing container: alertmanager-changemaker - Gotify: icon: mdi-cellphone-message href: "https://gotify.$domain" description: Push notifications container: gotify-changemaker - cAdvisor: icon: mdi-docker href: "https://cadvisor.$domain" description: Container metrics container: cadvisor-changemaker - Node Exporter: icon: mdi-server href: "#" description: Host system metrics container: node-exporter-changemaker - Redis Exporter: icon: mdi-database-export href: "#" description: Redis metrics exporter container: redis-exporter-changemaker # ───────────────────────────────────────────────── # LOCAL DEVELOPMENT # ───────────────────────────────────────────────── - Local - Core: - Admin GUI: icon: mdi-view-dashboard href: "http://localhost:3000" description: Application dashboard (port 3000) container: changemaker-v2-admin - API: icon: mdi-api href: "http://localhost:4000" description: V2 REST API (port 4000) container: changemaker-v2-api - Media API: icon: mdi-video href: "http://localhost:4100" description: Video library API (port 4100) container: changemaker-media-api - Main Site: icon: mdi-web href: "http://localhost:4004" description: Documentation site (port 4004) container: mkdocs-site-server-changemaker - Homepage: icon: mdi-home href: "http://localhost:3010" description: This dashboard (port 3010) container: homepage-changemaker - Local - Tools: - Code Server: icon: mdi-code-braces href: "http://localhost:8888" description: VS Code in the browser (port 8888) container: code-server-changemaker - NocoDB: icon: mdi-database href: "http://localhost:8091" description: Database browser (port 8091) container: changemaker-v2-nocodb - MkDocs (Live): icon: mdi-book-open-page-variant href: "http://localhost:4003" description: Live documentation (port 4003) container: mkdocs-changemaker - Mini QR: icon: mdi-qrcode href: "http://localhost:8089" description: QR code generator (port 8089) container: mini-qr - Excalidraw: icon: mdi-draw href: "http://localhost:8090" description: Collaborative whiteboard (port 8090) container: excalidraw-changemaker - Vaultwarden: icon: mdi-lock href: "http://localhost:8445" description: Password manager (port 8445) container: vaultwarden-changemaker - Gancio: icon: mdi-calendar-multiple href: "http://localhost:8092" description: Event management (port 8092) container: gancio-changemaker - Rocket.Chat: icon: mdi-chat href: "http://localhost:$rc_port" description: Team chat (port $rc_port) container: rocketchat-changemaker - Jitsi Meet: icon: mdi-video href: "http://localhost:$jitsi_port" description: Video conferencing (port $jitsi_port) container: jitsi-web-changemaker - Local - Integrations: - Listmonk: icon: mdi-email-newsletter href: "http://localhost:9001" description: Newsletter manager (port 9001) container: listmonk-app - MailHog: icon: mdi-email-check href: "http://localhost:8025" description: Email capture UI (port 8025) container: mailhog-changemaker - n8n: icon: mdi-robot-industrial href: "http://localhost:5678" description: Workflow automation (port 5678) container: n8n-changemaker - Gitea: icon: mdi-git href: "http://localhost:3030" description: Git repository hosting (port 3030) container: gitea-changemaker - Local - Infrastructure: - Nginx: icon: mdi-web-box href: "#" description: Reverse proxy (port 80) container: changemaker-v2-nginx - PostgreSQL: icon: mdi-database-outline href: "#" description: Primary database (port 5433) container: changemaker-v2-postgres - Redis: icon: mdi-database-sync href: "#" description: Cache and job queues (port 6379) container: redis-changemaker - Local - Monitoring: - Grafana: icon: mdi-chart-box href: "http://localhost:3005" description: Monitoring dashboards (port 3005) container: grafana-changemaker - Prometheus: icon: mdi-chart-line href: "http://localhost:9090" description: Metrics collection (port 9090) container: prometheus-changemaker - Alertmanager: icon: mdi-bell-alert href: "http://localhost:9093" description: Alert routing (port 9093) container: alertmanager-changemaker - Gotify: icon: mdi-cellphone-message href: "http://localhost:8889" description: Push notifications (port 8889) container: gotify-changemaker - cAdvisor: icon: mdi-docker href: "http://localhost:8086" description: Container metrics (port 8086) container: cadvisor-changemaker - Node Exporter: icon: mdi-server href: "http://localhost:9100/metrics" description: Host system metrics (port 9100) container: node-exporter-changemaker - Redis Exporter: icon: mdi-database-export href: "http://localhost:9121/metrics" description: Redis metrics (port 9121) container: redis-exporter-changemaker YAML success "Generated services.yaml for Homepage dashboard" } # ============================================================================= # Documentation Site Reset # ============================================================================= configure_docs_reset() { header "Documentation Site" if prompt_yes_no "Reset documentation site to baseline? (keeps header & tracking)"; then touch "$SCRIPT_DIR/mkdocs/.reset-docs-on-startup" success "Docs reset scheduled for first startup" info "Custom code (header, analytics, hooks, assets) will be preserved" else info "Keeping existing documentation content" fi } # ============================================================================= # Directory Permissions # ============================================================================= fix_container_permissions() { header "Container Directory Permissions" local -a dirs=( "configs/code-server/.config:Code Server config" "configs/code-server/.local:Code Server local data" "mkdocs:MkDocs root" "mkdocs/docs:MkDocs source docs" "mkdocs/overrides:MkDocs template overrides" "mkdocs/.cache:MkDocs cache" "mkdocs/site:MkDocs built site" "assets/uploads:Listmonk uploads" "assets/images:Shared images" "assets/icons:Homepage icons" "media/local/inbox:Media upload inbox" "media/local/photos:Media photos" "media/local/thumbnails:Video thumbnails" "media/public:Public media files" "local-files:n8n local files" "data:NAR import data" "data/upgrade:Upgrade trigger directory" ) local errors=0 for entry in "${dirs[@]}"; do local dir_path="$SCRIPT_DIR/${entry%%:*}" local dir_desc="${entry#*:}" mkdir -p "$dir_path" if chmod 775 "$dir_path" 2>/dev/null; then success "$dir_desc" else warn "$dir_desc — could not set permissions (may need sudo)" ((errors++)) fi done echo "" if [[ $errors -eq 0 ]]; then success "All directories ready" else warn "Some directories may need: sudo chown -R $(id -u):$(id -g) $SCRIPT_DIR" fi } # ============================================================================= # Upgrade Watcher (systemd) # ============================================================================= install_upgrade_watcher() { header "System Upgrade Watcher" info "The upgrade watcher lets you trigger upgrades from the admin Settings page." info "It installs a systemd path watcher that monitors for trigger files." echo "" # Ensure upgrade IPC directory exists mkdir -p "$SCRIPT_DIR/data/upgrade" local unit_src="$SCRIPT_DIR/scripts/systemd" if [[ ! -f "$unit_src/changemaker-upgrade.path" ]] || [[ ! -f "$unit_src/changemaker-upgrade.service" ]]; then warn "Systemd unit templates not found in scripts/systemd/ — skipping" UPGRADE_WATCHER="skipped" return fi if ! command -v systemctl &>/dev/null; then warn "systemctl not found — skipping (not a systemd host?)" UPGRADE_WATCHER="skipped" return fi if prompt_yes_no "Install the upgrade watcher (requires sudo)?"; then # Generate units with correct paths substituted local tmp_path tmp_service tmp_path=$(mktemp) tmp_service=$(mktemp) sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" "$unit_src/changemaker-upgrade.path" > "$tmp_path" sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" -e "s|__USER__|$(whoami)|g" "$unit_src/changemaker-upgrade.service" > "$tmp_service" if sudo cp "$tmp_path" /etc/systemd/system/changemaker-upgrade.path \ && sudo cp "$tmp_service" /etc/systemd/system/changemaker-upgrade.service \ && sudo systemctl daemon-reload \ && sudo systemctl enable --now changemaker-upgrade.path; then success "Upgrade watcher installed and enabled" UPGRADE_WATCHER="yes" else warn "Failed to install systemd units (sudo may have failed)" warn "Install manually later:" echo -e " ${CYAN}cd ~/changemaker.lite && sudo ./scripts/systemd/install.sh${NC}" UPGRADE_WATCHER="manual" fi rm -f "$tmp_path" "$tmp_service" else info "Skipped. You can install it later from Settings > System." UPGRADE_WATCHER="skipped" fi } # ============================================================================= # Automated Backups (systemd) # ============================================================================= install_backup_timer() { header "Automated Backups" info "Daily automated backups protect against data loss." info "Backs up PostgreSQL databases + uploads to ./backups/ with 30-day retention." echo "" local unit_src="$SCRIPT_DIR/scripts/systemd" if [[ ! -f "$unit_src/changemaker-backup.timer" ]] || [[ ! -f "$unit_src/changemaker-backup.service" ]]; then warn "Systemd backup unit templates not found in scripts/systemd/ — skipping" BACKUP_TIMER="skipped" return fi if ! command -v systemctl &>/dev/null; then warn "systemctl not found — skipping (not a systemd host?)" BACKUP_TIMER="skipped" return fi if prompt_yes_no "Install daily automated backups (requires sudo)?" "y"; then local tmp_timer tmp_service tmp_timer=$(mktemp) tmp_service=$(mktemp) sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" "$unit_src/changemaker-backup.timer" > "$tmp_timer" sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" -e "s|__USER__|$(whoami)|g" "$unit_src/changemaker-backup.service" > "$tmp_service" if sudo cp "$tmp_timer" /etc/systemd/system/changemaker-backup.timer \ && sudo cp "$tmp_service" /etc/systemd/system/changemaker-backup.service \ && sudo systemctl daemon-reload \ && sudo systemctl enable --now changemaker-backup.timer; then success "Daily backup timer installed and enabled (runs at 02:00)" BACKUP_TIMER="yes" else warn "Failed to install systemd units (sudo may have failed)" warn "Install manually later:" echo -e " ${CYAN}sudo cp scripts/systemd/changemaker-backup.* /etc/systemd/system/${NC}" echo -e " ${CYAN}sudo systemctl daemon-reload && sudo systemctl enable --now changemaker-backup.timer${NC}" BACKUP_TIMER="manual" fi rm -f "$tmp_timer" "$tmp_service" else info "Skipped. Run backups manually: ./scripts/backup.sh" BACKUP_TIMER="skipped" fi } # ============================================================================= # Summary & Next Steps # ============================================================================= print_summary() { header "Configuration Complete" echo -e " ${BOLD}Domain:${NC} ${CONFIGURED_DOMAIN:-cmlite.org}" echo -e " ${BOLD}Admin email:${NC} (see .env: INITIAL_ADMIN_EMAIL)" echo -e " ${BOLD}Admin password:${NC} [set]" echo -e " ${BOLD}SMTP:${NC} ${SMTP_MODE:-mailhog}" echo -e " ${BOLD}Media Manager:${NC} ${MEDIA_ENABLED:-no}" echo -e " ${BOLD}Payments:${NC} ${PAYMENTS_ENABLED:-no}" echo -e " ${BOLD}Listmonk sync:${NC} ${LISTMONK_SYNC:-no}" echo -e " ${BOLD}Rocket.Chat:${NC} ${CHAT_ENABLED:-no}" echo -e " ${BOLD}Gancio sync:${NC} ${GANCIO_SYNC:-no}" echo -e " ${BOLD}Jitsi Meet:${NC} ${MEET_ENABLED:-no}" echo -e " ${BOLD}SMS Campaigns:${NC} ${SMS_ENABLED:-no}" echo -e " ${BOLD}Monitoring:${NC} ${MONITORING_ENABLED:-no}" echo -e " ${BOLD}Docs Comments:${NC} ${DOCS_COMMENTS_ENABLED:-no}" echo -e " ${BOLD}Bunker Ops:${NC} ${BUNKER_OPS_ENABLED:-no}" echo -e " ${BOLD}Pangolin:${NC} ${PANGOLIN_CONFIGURED:-no}" echo -e " ${BOLD}Upgrade watcher:${NC} ${UPGRADE_WATCHER:-skipped}" echo -e " ${BOLD}Backup timer:${NC} ${BACKUP_TIMER:-skipped}" echo -e " ${BOLD}Secrets:${NC} 22 auto-generated" echo "" echo -e " ${DIM}Config file: $ENV_FILE${NC}" } print_next_steps() { echo "" echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}" echo -e "${BOLD}${BLUE} Next Steps${NC}" echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}" echo "" if [[ "$INSTALL_MODE" == "release" ]]; then # Release mode: simpler instructions (production images, auto-migration) echo -e " ${BOLD}1.${NC} Start all services:" echo -e " ${CYAN}docker compose up -d${NC}" echo "" echo -e " Pre-built images will be pulled from the registry (~2 min first time)." echo -e " Database migrations and seeding run automatically on startup." echo "" echo -e " ${BOLD}2.${NC} Access the application:" echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}" echo -e " API: ${CYAN}http://localhost:4000${NC}" echo "" echo -e " ${BOLD}3.${NC} Check status:" echo -e " ${CYAN}docker compose ps${NC}" echo -e " ${CYAN}docker compose logs -f api --tail 20${NC}" echo "" else # Source mode: existing instructions echo -e " ${BOLD}1.${NC} Start core services:" echo -e " ${CYAN}docker compose up -d v2-postgres redis api admin${NC}" echo "" echo -e " ${BOLD}2.${NC} Run database setup:" echo -e " ${CYAN}docker compose exec api npx prisma migrate deploy${NC}" echo -e " ${CYAN}docker compose exec api npx prisma db seed${NC}" echo "" echo -e " ${BOLD}3.${NC} Access the application:" echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}" echo -e " API: ${CYAN}http://localhost:4000${NC}" echo "" echo -e " ${BOLD}4.${NC} Optional — start additional services:" echo -e " ${CYAN}docker compose up -d nginx${NC} # Reverse proxy" echo -e " ${CYAN}docker compose up -d media-api${NC} # Video library" echo -e " ${CYAN}docker compose up -d listmonk-app${NC} # Newsletters" echo -e " ${CYAN}docker compose up -d rocketchat${NC} # Team chat" echo -e " ${CYAN}docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb${NC} # Video calls" echo -e " ${CYAN}docker compose up -d homepage${NC} # Service dashboard" echo "" echo -e " ${BOLD}5.${NC} Or start everything at once (monitoring included if enabled above):" echo -e " ${CYAN}docker compose up -d${NC}" echo "" fi echo -e " ${YELLOW}IMPORTANT: Change your admin password after first login!${NC}" echo -e " ${YELLOW}JITSI: Ensure UDP port 10000 is open in your firewall for video/audio.${NC}" echo "" } # ============================================================================= # Main # ============================================================================= main() { print_banner check_prerequisites initialize_env configure_domain configure_admin generate_all_secrets configure_smtp configure_features configure_pangolin configure_cors generate_nginx_configs generate_services_yaml configure_docs_reset fix_container_permissions install_upgrade_watcher install_backup_timer # Release mode: auto-set production defaults if [[ "$INSTALL_MODE" == "release" ]]; then header "Release Mode Settings" update_env_var "IMAGE_TAG" "latest" update_env_var "NODE_ENV" "production" # Ensure monitoring is included if user opted in if [[ "${MONITORING_ENABLED:-no}" == "yes" ]]; then update_env_var "COMPOSE_PROFILES" "monitoring" fi success "Set IMAGE_TAG=latest, NODE_ENV=production (pre-built images)" fi print_summary print_next_steps } main "$@"