#!/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" # --- Non-interactive mode flags --- NON_INTERACTIVE=false NI_DOMAIN="" NI_ADMIN_EMAIL="" NI_ADMIN_PASSWORD="" NI_PRODUCTION=true NI_ENABLE_ALL=false # --- Arg parser --- while [[ $# -gt 0 ]]; do case "$1" in --non-interactive|-y) NON_INTERACTIVE=true; shift ;; --domain) NI_DOMAIN="$2"; shift 2 ;; --admin-email) NI_ADMIN_EMAIL="$2"; shift 2 ;; --admin-password) NI_ADMIN_PASSWORD="$2"; shift 2 ;; --development) NI_PRODUCTION=false; shift ;; --enable-all) NI_ENABLE_ALL=true; shift ;; --help|-h) echo "Usage: bash config.sh [OPTIONS]" echo "" echo "Options:" echo " --non-interactive, -y Skip all prompts (generate secrets, use defaults)" echo " --domain DOMAIN Set domain (default: cmlite.org)" echo " --admin-email EMAIL Set admin email (default: admin@DOMAIN)" echo " --admin-password PASS Set admin password (must meet policy: 12+ chars, upper+lower+digit)" echo " --development Set NODE_ENV=development (default: production)" echo " --enable-all Enable all optional features" echo " --help, -h Show this help" echo "" echo "Example:" echo " bash config.sh --non-interactive --domain example.org --admin-password MyStr0ngPass123" exit 0 ;; *) shift ;; esac done # --- 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 [[ "$NON_INTERACTIVE" == "false" ]]; then if [[ ! -t 0 ]]; then if [[ -e /dev/tty ]]; then exec 0&2 echo " Use --non-interactive for headless mode, or run manually: bash config.sh" >&2 exit 1 fi 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} # In non-interactive mode, use default (or yes-for-all if --enable-all) if [[ "$NON_INTERACTIVE" == "true" ]]; then if [[ "$NI_ENABLE_ALL" == "true" ]]; then return 0 # yes to everything fi [[ "$default" == "y" ]] && return 0 || return 1 fi 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 if [[ "$NON_INTERACTIVE" == "true" ]]; then # Non-interactive: back up and create fresh backup_env_file cp "$ENV_EXAMPLE" "$ENV_FILE" success "Created fresh .env from .env.example (backed up existing)" else 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 fi else cp "$ENV_EXAMPLE" "$ENV_FILE" success "Created .env from .env.example" fi } # ============================================================================= # Configuration Sections # ============================================================================= configure_domain() { header "Domain Configuration" if [[ "$NON_INTERACTIVE" == "true" ]]; then domain="${NI_DOMAIN:-cmlite.org}" else 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} fi 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" if [[ "$NON_INTERACTIVE" == "true" ]]; then if [[ "$NI_PRODUCTION" == "true" ]]; then update_env_var "NODE_ENV" "production" IS_PRODUCTION="yes" else IS_PRODUCTION="no" fi else 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 fi # Store for later use CONFIGURED_DOMAIN="$domain" } configure_admin() { header "Admin Credentials" if [[ "$NON_INTERACTIVE" == "true" ]]; then local admin_email="${NI_ADMIN_EMAIL:-admin@${CONFIGURED_DOMAIN:-cmlite.org}}" local admin_password="${NI_ADMIN_PASSWORD:-}" # Generate a password if not provided if [[ -z "$admin_password" ]]; then admin_password="CmLite$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 12)1" info "Generated admin password: $admin_password" info "SAVE THIS — it will not be shown again." fi if ! validate_password "$admin_password"; then error "Admin password does not meet policy (12+ chars, uppercase + lowercase + digit)" exit 1 fi 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 ($admin_email)" else 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" fi } # 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" if [[ "$NON_INTERACTIVE" == "true" ]]; then # Non-interactive: use MailHog defaults (production SMTP can be configured later) update_env_var "VAULTWARDEN_SMTP_SECURITY" "off" info "Using MailHog for email (configure SMTP later via .env)" SMTP_MODE="mailhog" return fi 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" if [[ "$NON_INTERACTIVE" == "false" ]]; then 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 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" if [[ "$NON_INTERACTIVE" == "false" ]]; then 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 fi else SMS_ENABLED="no" fi if prompt_yes_no "Enable Social Connections (friendships, challenges, spotlights)?"; then update_env_var "ENABLE_SOCIAL" "true" success "Social Connections enabled" else update_env_var "ENABLE_SOCIAL" "false" fi if prompt_yes_no "Enable People CRM (unified contact management)?"; then update_env_var "ENABLE_PEOPLE" "true" success "People CRM enabled" else update_env_var "ENABLE_PEOPLE" "false" fi if prompt_yes_no "Enable Analytics & GeoIP (visitor tracking, geo dashboard)?"; then update_env_var "ENABLE_ANALYTICS" "true" success "Analytics enabled" else update_env_var "ENABLE_ANALYTICS" "false" 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" if [[ "$NON_INTERACTIVE" == "false" ]]; then 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 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" if [[ "$NON_INTERACTIVE" == "false" ]]; then 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 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" if [[ "$NON_INTERACTIVE" == "false" ]]; then 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 fi else info "Analytics disabled (can enable later in admin Settings)" fi } configure_pangolin() { header "Tunnel Configuration (Pangolin)" if [[ "$NON_INTERACTIVE" == "true" ]]; then info "Skipping Pangolin setup (configure later via admin GUI or .env)" PANGOLIN_CONFIGURED="no" return fi 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 PANGOLIN_CONFIGURED="no" return fi 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 API credentials saved" # Check if curl and jq are available for site setup if ! command -v curl &>/dev/null || ! command -v jq &>/dev/null; then warn "curl or jq not found — skipping site setup." info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." PANGOLIN_CONFIGURED="yes" return fi # Verify API connectivity info "Verifying Pangolin API connectivity..." local health_status health_status=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \ -H "Authorization: Bearer $pang_key" \ "$pang_url/" 2>/dev/null) || true if [[ "$health_status" != "200" ]]; then warn "Could not reach Pangolin API (HTTP $health_status)." info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." PANGOLIN_CONFIGURED="yes" return fi success "Pangolin API reachable" echo "" # Offer site setup options info "Set up a tunnel site now?" echo -e " ${BOLD}1)${NC} Create a new site" echo -e " ${BOLD}2)${NC} Connect to an existing site" echo -e " ${BOLD}3)${NC} Skip (configure later in admin GUI)" echo "" local site_choice read -rp " Choose [1-3, default: 3]: " site_choice site_choice=${site_choice:-3} case "$site_choice" in 1) pangolin_create_site "$pang_url" "$pang_key" "$pang_org" ;; 2) pangolin_connect_site "$pang_url" "$pang_key" "$pang_org" ;; *) info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." ;; esac PANGOLIN_CONFIGURED="yes" } # Helper: Call Pangolin API and extract data from response envelope pangolin_api() { local method=$1 url=$2 api_key=$3 shift 3 local body_args=() if [[ $# -gt 0 ]]; then body_args=(-d "$1") fi curl -s -m 15 -X "$method" "$url" \ -H "Authorization: Bearer $api_key" \ -H "Content-Type: application/json" \ "${body_args[@]}" 2>/dev/null } pangolin_create_site() { local api_url=$1 api_key=$2 org_id=$3 local domain="${CONFIGURED_DOMAIN:-cmlite.org}" local site_name read -rp " Site name [default: changemaker-$domain]: " site_name site_name=${site_name:-changemaker-$domain} info "Fetching Newt credentials..." local defaults_resp defaults_resp=$(pangolin_api GET "$api_url/org/$org_id/pick-site-defaults" "$api_key") local newt_id newt_secret client_address newt_id=$(echo "$defaults_resp" | jq -r '.data.newtId // .newtId // empty' 2>/dev/null) newt_secret=$(echo "$defaults_resp" | jq -r '.data.newtSecret // .newtSecret // .data.secret // .secret // empty' 2>/dev/null) client_address=$(echo "$defaults_resp" | jq -r '.data.clientAddress // .clientAddress // .data.address // .address // empty' 2>/dev/null) if [[ -z "$newt_id" || -z "$newt_secret" ]]; then warn "Could not fetch Newt credentials from pickSiteDefaults." info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." return fi success "Got Newt credentials (newtId: $newt_id)" info "Creating site \"$site_name\"..." local create_payload create_payload=$(jq -n \ --arg name "$site_name" \ --arg newtId "$newt_id" \ --arg secret "$newt_secret" \ --arg address "$client_address" \ '{name: $name, type: "newt", newtId: $newtId, secret: $secret, address: $address}') local site_resp site_resp=$(pangolin_api PUT "$api_url/org/$org_id/site" "$api_key" "$create_payload") local site_id site_id=$(echo "$site_resp" | jq -r '.data.siteId // .siteId // empty' 2>/dev/null) if [[ -z "$site_id" ]]; then local err_msg err_msg=$(echo "$site_resp" | jq -r '.message // .error // "Unknown error"' 2>/dev/null) warn "Site creation failed: $err_msg" info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." return fi success "Site created: $site_id ($site_name)" # Derive endpoint from API URL (strip /v1 path) local endpoint endpoint=$(echo "$api_url" | sed 's|/v1/*$||') # Write credentials to .env update_env_var "PANGOLIN_SITE_ID" "$site_id" update_env_var "PANGOLIN_NEWT_ID" "$newt_id" update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret" update_env_var "PANGOLIN_ENDPOINT" "$endpoint" success "Tunnel credentials written to .env" info "Resources will be created automatically via the admin GUI or sync endpoint." } pangolin_connect_site() { local api_url=$1 api_key=$2 org_id=$3 info "Fetching sites from organization..." local sites_resp sites_resp=$(pangolin_api GET "$api_url/org/$org_id/sites" "$api_key") # Extract sites array (handle Pangolin response envelope) local sites_json sites_json=$(echo "$sites_resp" | jq '.data.sites // .data // .sites // []' 2>/dev/null) local site_count site_count=$(echo "$sites_json" | jq 'length' 2>/dev/null) if [[ -z "$site_count" || "$site_count" -eq 0 ]]; then warn "No sites found in this organization." info "Use option 1 to create a new site, or set up via the admin GUI." return fi echo "" echo -e " ${BOLD}Available sites:${NC}" echo "" local i=1 while IFS= read -r line; do local name site_id online last_seen name=$(echo "$line" | jq -r '.name') site_id=$(echo "$line" | jq -r '.siteId') online=$(echo "$line" | jq -r '.online // false') last_seen=$(echo "$line" | jq -r '.lastSeen // "never"') local status_tag if [[ "$online" == "true" ]]; then status_tag="${GREEN}online${NC}" else status_tag="${DIM}offline${NC}" fi echo -e " ${BOLD}$i)${NC} $name ${DIM}(ID: $site_id)${NC} — $status_tag" i=$((i + 1)) done < <(echo "$sites_json" | jq -c '.[]') echo "" local choice read -rp " Select site [1-$site_count]: " choice if [[ -z "$choice" || "$choice" -lt 1 || "$choice" -gt "$site_count" ]] 2>/dev/null; then warn "Invalid selection." return fi local selected selected=$(echo "$sites_json" | jq -c ".[$((choice - 1))]") local sel_id sel_name sel_id=$(echo "$selected" | jq -r '.siteId') sel_name=$(echo "$selected" | jq -r '.name') # Derive endpoint from API URL local endpoint endpoint=$(echo "$api_url" | sed 's|/v1/*$||') # Write site ID to .env update_env_var "PANGOLIN_SITE_ID" "$sel_id" update_env_var "PANGOLIN_ENDPOINT" "$endpoint" success "Connected to site: $sel_name (ID: $sel_id)" # Check if we also need Newt credentials local existing_newt_id existing_newt_id=$(grep "^PANGOLIN_NEWT_ID=" "$ENV_FILE" 2>/dev/null | cut -d= -f2-) if [[ -z "$existing_newt_id" ]]; then info "Newt credentials not yet set." info "If you have the Newt ID and Secret for this site, enter them now." info "Otherwise, set them later via the admin GUI." echo "" read -rp " Newt ID (leave blank to skip): " newt_id_input if [[ -n "$newt_id_input" ]]; then read -rp " Newt Secret: " newt_secret_input if [[ -n "$newt_secret_input" ]]; then update_env_var "PANGOLIN_NEWT_ID" "$newt_id_input" update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret_input" success "Newt credentials written to .env" fi fi else info "Existing Newt credentials found in .env (newtId: $existing_newt_id)" fi } configure_control_panel() { header "Control Panel Registration" if prompt_yes_no "Register this instance with a Changemaker Control Panel?"; then echo "" read -rp " Enter Control Panel URL (e.g., https://ccp.example.com): " ccp_url read -rp " Enter invite code: " invite_code read -rp " Agent URL (how the CCP reaches this host, e.g., https://this-host:7443): " agent_url update_env_var "ENABLE_CCP_AGENT" "true" update_env_var "CCP_URL" "$ccp_url" update_env_var "CCP_INVITE_CODE" "$invite_code" update_env_var "CCP_AGENT_URL" "$agent_url" # Add ccp-agent to compose profiles local existing_profiles existing_profiles=$(grep -oP 'COMPOSE_PROFILES=\K.*' "$ENV_FILE" 2>/dev/null || echo "") if [[ -n "$existing_profiles" ]]; then update_env_var "COMPOSE_PROFILES" "${existing_profiles},ccp-agent" else update_env_var "COMPOSE_PROFILES" "ccp-agent" fi success "Control Panel registration configured — agent will phone home on startup" else update_env_var "ENABLE_CCP_AGENT" "false" 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 [[ "$NON_INTERACTIVE" == "true" ]]; then info "Keeping existing documentation content" return fi 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" if [[ "$NON_INTERACTIVE" == "true" ]]; then mkdir -p "$SCRIPT_DIR/data/upgrade" info "Skipping systemd watcher install (run manually later)" UPGRADE_WATCHER="skipped" return fi 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" if [[ "$NON_INTERACTIVE" == "true" ]]; then info "Skipping backup timer install (run manually later)" BACKUP_TIMER="skipped" return fi 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_control_panel 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 "$@"