#!/bin/bash cat << "EOF" ██████╗██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ███████╗ ██╔════╝██║ ██║██╔══██╗████╗ ██║██╔════╝ ██╔════╝ ██║ ███████║███████║██╔██╗ ██║██║ ███╗█████╗ ██║ ██╔══██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ╚██████╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝███████╗ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗ ████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗ ██╔████╔██║███████║█████╔╝ █████╗ ██████╔╝ ██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗ ██║ ╚═╝ ██║██║ ██║██║ ██╗███████╗██║ ██║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ Configuration Wizard EOF # Get the absolute path of the script directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ENV_FILE="$SCRIPT_DIR/.env" MKDOCS_YML="$SCRIPT_DIR/mkdocs/mkdocs.yml" # Fixed path - removed extra 'docs' directory TUNNEL_CONFIG_DIR="$SCRIPT_DIR/configs/cloudflare" TUNNEL_CONFIG_FILE="$TUNNEL_CONFIG_DIR/tunnel-config.yml" SERVICES_YAML="$SCRIPT_DIR/configs/homepage/services.yaml" MAIN_HTML="$SCRIPT_DIR/mkdocs/docs/overrides/main.html" MAP_ENV_FILE="$SCRIPT_DIR/map/.env" # Add the map's .env file path DOCKER_COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" echo "Looking for .env file at: $ENV_FILE" # Function to generate a random secure password generate_password() { local length=${1:-16} openssl rand -base64 48 | tr -dc 'a-zA-Z0-9!@#$%^&*()-_=+' | head -c "$length" } # Function to safely update environment variables in .env file update_env_var() { local key=$1 local value=$2 # More robust method to handle special characters in passwords if grep -q "^$key=" "$ENV_FILE"; then # Create a temporary file local tmpfile=$(mktemp) # Process the .env file line by line while IFS= read -r line; do if [[ "$line" =~ ^$key= ]]; then echo "$key=$value" >> "$tmpfile" else echo "$line" >> "$tmpfile" fi done < "$ENV_FILE" # Replace the original file with the temporary file mv "$tmpfile" "$ENV_FILE" echo "Updated $key in .env file" else # Add new key-value pair if it doesn't exist echo "$key=$value" >> "$ENV_FILE" echo "Added $key to .env file" fi } # Function to create a timestamped backup of the .env file backup_env_file() { if [ -f "$ENV_FILE" ]; then local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="$ENV_FILE.backup_$timestamp" echo "Creating backup of current .env file to: $backup_file" if cp "$ENV_FILE" "$backup_file"; then echo "Backup created successfully!" return 0 else echo "Failed to create backup file. Proceeding with caution..." return 1 fi fi } # Function to get all used ports on the system get_used_ports() { local used_ports=() # Try different methods to get used ports, in order of preference if command -v ss >/dev/null 2>&1; then # Use ss (preferred, modern replacement for netstat) while IFS= read -r line; do if [[ "$line" =~ :([0-9]+)[[:space:]] ]]; then used_ports+=("${BASH_REMATCH[1]}") fi done < <(ss -tuln 2>/dev/null | grep -E ':[0-9]+[[:space:]]') elif command -v netstat >/dev/null 2>&1; then # Use netstat (fallback) while IFS= read -r line; do if [[ "$line" =~ :([0-9]+)[[:space:]] ]]; then used_ports+=("${BASH_REMATCH[1]}") fi done < <(netstat -tuln 2>/dev/null | grep -E ':[0-9]+[[:space:]]') elif command -v lsof >/dev/null 2>&1; then # Use lsof (another fallback) while IFS= read -r line; do if [[ "$line" =~ :([0-9]+)[[:space:]] ]]; then used_ports+=("${BASH_REMATCH[1]}") fi done < <(lsof -i -P -n 2>/dev/null | grep LISTEN | grep -E ':[0-9]+[[:space:]]') else echo "Warning: No port scanning tools available (ss, netstat, or lsof)" return 1 fi # Remove duplicates and sort printf '%s\n' "${used_ports[@]}" | sort -nu } # Function to check if a specific port is available is_port_available() { local port=$1 local used_ports_list=$2 # Check if port is in the used ports list if echo "$used_ports_list" | grep -q "^$port$"; then return 1 # Port is in use else return 0 # Port is available fi } # Function to find next available port starting from a given port find_next_available_port() { local start_port=$1 local used_ports_list=$2 local max_attempts=${3:-100} # Maximum ports to check for ((i=0; i&2 return 1 } # Function to initialize available ports for services initialize_available_ports() { echo "Scanning system for used ports to avoid conflicts..." # Get list of all used ports local used_ports_list used_ports_list=$(get_used_ports) if [ $? -ne 0 ] || [ -z "$used_ports_list" ]; then echo "Warning: Could not scan system ports. Using default ports." echo "You may need to manually resolve conflicts later." return 1 fi echo "Found $(echo "$used_ports_list" | wc -l) ports currently in use" # Define desired starting ports for each service local -A service_ports=( ["CODE_SERVER_PORT"]=8888 ["LISTMONK_PORT"]=9000 ["LISTMONK_DB_PORT"]=5432 ["MKDOCS_PORT"]=4000 ["MKDOCS_SITE_SERVER_PORT"]=4001 ["N8N_PORT"]=5678 ["NOCODB_PORT"]=8090 ["HOMEPAGE_PORT"]=3010 ["GITEA_WEB_PORT"]=3030 ["GITEA_SSH_PORT"]=2222 ["MAP_PORT"]=3000 ["INFLUENCE_PORT"]=3333 ["MINI_QR_PORT"]=8089 ["REDIS_PORT"]=6379 ["PROMETHEUS_PORT"]=9090 ["GRAFANA_PORT"]=3001 ) # Find available ports for each service local -A available_ports for service in "${!service_ports[@]}"; do local preferred_port=${service_ports[$service]} local available_port if is_port_available "$preferred_port" "$used_ports_list"; then available_port=$preferred_port echo "✅ $service: Using preferred port $preferred_port" else available_port=$(find_next_available_port "$preferred_port" "$used_ports_list") if [ $? -eq 0 ]; then echo "⚠️ $service: Port $preferred_port in use, using $available_port instead" else echo "❌ $service: Could not find available port, using $preferred_port (may conflict)" available_port=$preferred_port fi fi available_ports[$service]=$available_port # Add the assigned port to used_ports_list to avoid double-assignment used_ports_list=$(printf '%s\n%s' "$used_ports_list" "$available_port" | sort -nu) done # Export the available ports as global variables for service in "${!available_ports[@]}"; do export "$service=${available_ports[$service]}" done echo "Port assignment completed successfully!" return 0 } # Function to initialize the .env file with default values initialize_env_file() { echo "Initializing new .env file with available ports..." # Initialize available ports before creating the .env file initialize_available_ports cat > "$ENV_FILE" << EOL # Never share this file publicly. It contains sensitive information. # This file is used to configure various applications and services. # Generated by Changemaker Config Wizard on $(date) # User and Group Configuration USER_NAME=coder USER_ID=1000 GROUP_ID=1000 # Port Configuration (automatically assigned to avoid conflicts) CODE_SERVER_PORT=${CODE_SERVER_PORT:-8888} LISTMONK_PORT=${LISTMONK_PORT:-9000} LISTMONK_DB_PORT=${LISTMONK_DB_PORT:-5432} MKDOCS_PORT=${MKDOCS_PORT:-4000} MKDOCS_SITE_SERVER_PORT=${MKDOCS_SITE_SERVER_PORT:-4001} N8N_PORT=${N8N_PORT:-5678} NOCODB_PORT=${NOCODB_PORT:-8090} HOMEPAGE_PORT=${HOMEPAGE_PORT:-3010} GITEA_WEB_PORT=${GITEA_WEB_PORT:-3030} GITEA_SSH_PORT=${GITEA_SSH_PORT:-2222} MAP_PORT=${MAP_PORT:-3000} INFLUENCE_PORT=${INFLUENCE_PORT:-3333} MINI_QR_PORT=${MINI_QR_PORT:-8089} # Centralized Services Ports REDIS_PORT=${REDIS_PORT:-6379} PROMETHEUS_PORT=${PROMETHEUS_PORT:-9090} GRAFANA_PORT=${GRAFANA_PORT:-3001} # Domain Configuration BASE_DOMAIN=https://changeme.org DOMAIN=changeme.org LISTMONK_HOSTNAME=listmonk.changeme.org N8N_HOST=n8n.changeme.org GITEA_DOMAIN=git.changeme.org GITEA_ROOT_URL=https://git.changeme.org HOMEPAGE_VAR_BASE_URL=https://changeme.org # Cookie and CORS Configuration COOKIE_DOMAIN=.changeme.org ALLOWED_ORIGINS=https://map.changeme.org,http://localhost:3000 # Cloudflare Configuration CF_API_TOKEN=your_cloudflare_api_token CF_ZONE_ID=your_cloudflare_zone_id CF_TUNNEL_ID=your_cloudflared_tunnel_id CF_DOMAIN=changeme.org CF_ACCOUNT_ID=your_cloudflare_account_id # Database Configuration (PostgreSQL for Listmonk) POSTGRES_USER=listmonk POSTGRES_PASSWORD=changeMe POSTGRES_DB=listmonk # Listmonk Database Performance Settings LISTMONK_DB_MAX_OPEN=25 LISTMONK_DB_MAX_IDLE=25 LISTMONK_DB_MAX_LIFETIME=300s # Listmonk Admin Configuration LISTMONK_ADMIN_USER=admin LISTMONK_ADMIN_PASSWORD=changeMe # N8N Configuration N8N_USER_EMAIL=admin@example.com N8N_USER_PASSWORD=changeMe N8N_ENCRYPTION_KEY=changeMe GENERIC_TIMEZONE=UTC # Nocodb Configuration NOCODB_JWT_SECRET=changeMe NOCODB_DB_NAME=nocodb NOCODB_DB_USER=noco NOCODB_DB_PASSWORD=changeMe # Gitea Database Configuration GITEA_DB_PASSWD=changeMe GITEA_DB_ROOT_PASSWORD=changeMe # Centralized Services Configuration # Redis (used by all applications for caching, sessions, queues) REDIS_HOST=redis-changemaker REDIS_PORT=6379 REDIS_PASSWORD= # Prometheus (metrics collection) PROMETHEUS_PORT=9090 PROMETHEUS_RETENTION_TIME=30d # Grafana (monitoring dashboards) GRAFANA_PORT=3001 GRAFANA_ADMIN_USER=admin GRAFANA_ADMIN_PASSWORD=changeMe EOL echo "New .env file created with conflict-free port assignments." # Show port assignments summary echo "" echo "=== Port Assignments Summary ===" echo "Code Server: ${CODE_SERVER_PORT:-8888}" echo "Listmonk: ${LISTMONK_PORT:-9000}" echo "Listmonk DB: ${LISTMONK_DB_PORT:-5432}" echo "MkDocs: ${MKDOCS_PORT:-4000}" echo "MkDocs Site: ${MKDOCS_SITE_SERVER_PORT:-4001}" echo "N8N: ${N8N_PORT:-5678}" echo "NocoDB: ${NOCODB_PORT:-8090}" echo "Homepage: ${HOMEPAGE_PORT:-3010}" echo "Gitea Web: ${GITEA_WEB_PORT:-3030}" echo "Gitea SSH: ${GITEA_SSH_PORT:-2222}" echo "Map: ${MAP_PORT:-3000}" echo "Influence: ${INFLUENCE_PORT:-3333}" echo "Mini QR: ${MINI_QR_PORT:-8089}" echo "" echo "=== Centralized Services ===" echo "Redis: ${REDIS_PORT:-6379}" echo "Prometheus: ${PROMETHEUS_PORT:-9090}" echo "Grafana: ${GRAFANA_PORT:-3001}" echo "================================" } # Function to update the site_url in mkdocs.yml update_mkdocs_yml() { local new_domain=$1 if [ ! -f "$MKDOCS_YML" ]; then echo "Warning: mkdocs.yml not found at $MKDOCS_YML" return 1 fi echo "Updating site_url in mkdocs.yml..." # Create a backup of the mkdocs.yml file local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="${MKDOCS_YML}.backup_${timestamp}" cp "$MKDOCS_YML" "$backup_file" echo "Created backup of mkdocs.yml at $backup_file" # Update the site_url value - handle both http and https sed -i "s|^site_url:.*|site_url: https://$new_domain|" "$MKDOCS_YML" if grep -q "site_url: https://$new_domain" "$MKDOCS_YML"; then echo "✅ Updated site_url in mkdocs.yml to: https://$new_domain" return 0 else echo "Warning: Failed to update site_url in mkdocs.yml" return 1 fi } # Function to update service URLs in services.yaml update_services_yaml() { local new_domain=$1 if [ ! -f "$SERVICES_YAML" ]; then echo "Warning: services.yaml not found at $SERVICES_YAML" return 1 fi echo "Updating service URLs in services.yaml..." # Create a backup of the services.yaml file local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="${SERVICES_YAML}.backup_${timestamp}" cp "$SERVICES_YAML" "$backup_file" echo "Created backup of services.yaml at $backup_file" # Define service name to subdomain mapping # This approach is URL-agnostic - it doesn't matter what the current URLs are declare -A service_mappings=( ["Code Server"]="code.$new_domain" ["Listmonk"]="listmonk.$new_domain" ["NocoDB"]="db.$new_domain" ["Map Server"]="map.$new_domain" ["Influence"]="influence.$new_domain" ["Main Site"]="$new_domain" ["MkDocs (Live)"]="docs.$new_domain" ["Mini QR"]="qr.$new_domain" ["n8n"]="n8n.$new_domain" ["Gitea"]="git.$new_domain" ["Prometheus"]="prometheus.$new_domain" ["Grafana"]="grafana.$new_domain" ) # Process each service mapping for service_name in "${!service_mappings[@]}"; do local target_url="https://${service_mappings[$service_name]}" # Use awk to find and update the href for each specific service # This finds the service by name and updates its href regardless of current URL awk -v service="$service_name" -v new_url="$target_url" ' BEGIN { in_service = 0 } # Check if we found the service name /- [^:]+:/ { if ($0 ~ ("- " service ":")) { in_service = 1 } else { in_service = 0 } } # If we are in the target service and find href line, update it in_service && /href:/ { gsub(/href: "[^"]*"/, "href: \"" new_url "\"") } # Print the line (modified or not) { print } ' "$SERVICES_YAML" > "${SERVICES_YAML}.tmp" # Replace the original file with the updated version mv "${SERVICES_YAML}.tmp" "$SERVICES_YAML" echo " ✓ Updated $service_name -> $target_url" done echo "✅ All service URLs updated to use domain: $new_domain" return 0 } # Function to update the login URL in main.html update_main_html() { local new_domain=$1 if [ ! -f "$MAIN_HTML" ]; then echo "Warning: main.html not found at $MAIN_HTML" return 1 fi echo "Updating login URL in main.html..." # Create a backup of the main.html file local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="${MAIN_HTML}.backup_${timestamp}" cp "$MAIN_HTML" "$backup_file" echo "Created backup of main.html at $backup_file" # Update the login button href - handle current domain sed -i "s|href=\"https://homepage\.[^\"]*\"|href=\"https://homepage.$new_domain\"|g" "$MAIN_HTML" # Also update any albertademocracytaskforce.org references sed -i "s|albertademocracytaskforce\.org|$new_domain|g" "$MAIN_HTML" # Update any test.com or changeme.org references sed -i "s|test\.com|$new_domain|g" "$MAIN_HTML" sed -i "s|changeme\.org|$new_domain|g" "$MAIN_HTML" echo "✅ Updated login URL in main.html to: https://homepage.$new_domain" return 0 } # Function to get Cloudflare API Token and Zone ID get_cloudflare_credentials() { echo "" echo "To use Cloudflare tunnels, you'll need to configure your credentials." echo "" echo "=== Cloudflare Configuration ===" echo "" echo "You'll need:" echo "1. Your Cloudflare API Token (with Zone.DNS and Account.Cloudflare Tunnel permissions)" echo "2. Your Cloudflare Zone ID (found in the Cloudflare dashboard overview)" echo "3. Your Cloudflare Account ID (found in the Cloudflare dashboard)" echo "" echo "These credentials will be used for:" echo "- Creating and configuring tunnels" echo "- Managing DNS records" echo "- Setting up access policies" echo "" } # Function to update the tunnel configuration file with new domain update_tunnel_config() { local new_domain=$1 local tunnel_id=${2:-"(insert-tunnel-id)"} local creds_path=${3:-"/path/to/credentials/file"} # Create directory if it doesn't exist mkdir -p "$TUNNEL_CONFIG_DIR" echo "Updating tunnel configuration with new domain..." # Create a backup if file exists if [ -f "$TUNNEL_CONFIG_FILE" ]; then local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="${TUNNEL_CONFIG_FILE}.backup_${timestamp}" cp "$TUNNEL_CONFIG_FILE" "$backup_file" echo "Created backup of tunnel-config.yml at $backup_file" fi # Load current environment variables to get port numbers load_env_vars # Create/update the tunnel configuration cat > "$TUNNEL_CONFIG_FILE" << EOL # filepath: /home/bunker-admin/changemaker.lite/configs/cloudflare/tunnel-config.yml # Cloudflare Tunnel Configuration # Auto-generated by Changemaker Configuration Wizard tunnel: $tunnel_id # e.g. 1234567890abcdef credentials-file: $creds_path # e.g. /home/bunker-admin/.cloudflared/[insert tunnel number].json ingress: - hostname: homepage.$new_domain service: http://localhost:${HOMEPAGE_PORT:-3010} - hostname: code.$new_domain service: http://localhost:${CODE_SERVER_PORT:-8888} - hostname: listmonk.$new_domain service: http://localhost:${LISTMONK_PORT:-9001} - hostname: docs.$new_domain service: http://localhost:${MKDOCS_PORT:-4000} - hostname: $new_domain service: http://localhost:${MKDOCS_SITE_SERVER_PORT:-4002} - hostname: n8n.$new_domain service: http://localhost:${N8N_PORT:-5678} - hostname: db.$new_domain service: http://localhost:${NOCODB_PORT:-8090} - hostname: git.$new_domain service: http://localhost:${GITEA_WEB_PORT:-3030} - hostname: map.$new_domain service: http://localhost:${MAP_PORT:-3000} - hostname: influence.$new_domain service: http://localhost:${INFLUENCE_PORT:-3333} - hostname: qr.$new_domain service: http://localhost:${MINI_QR_PORT:-8089} - hostname: prometheus.$new_domain service: http://localhost:${PROMETHEUS_PORT:-9090} - hostname: grafana.$new_domain service: http://localhost:${GRAFANA_PORT:-3001} # Catch-all rule (required) - service: http_status:404 EOL echo "✅ Updated tunnel configuration for domain: $new_domain" return 0 } # Function to load environment variables from .env file load_env_vars() { if [ -f "$ENV_FILE" ]; then # Load variables from .env file, ignoring comments and empty lines while IFS= read -r line; do if [[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then export "$line" fi done < <(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_FILE") fi } # Function to sync ports from root .env to map .env sync_map_ports() { echo "Syncing ports from root .env to map configuration..." # Load the current port values from root .env local mkdocs_port=$(grep "^MKDOCS_PORT=" "$ENV_FILE" | cut -d'=' -f2) local mkdocs_site_port=$(grep "^MKDOCS_SITE_SERVER_PORT=" "$ENV_FILE" | cut -d'=' -f2) local map_port=$(grep "^MAP_PORT=" "$ENV_FILE" | cut -d'=' -f2) # Set defaults if not found mkdocs_port=${mkdocs_port:-4000} mkdocs_site_port=${mkdocs_site_port:-4002} map_port=${map_port:-3000} # Update the map's .env with the correct ports if grep -q "^PORT=" "$MAP_ENV_FILE"; then sed -i "s|^PORT=.*|PORT=$map_port|" "$MAP_ENV_FILE" else echo "PORT=$map_port" >> "$MAP_ENV_FILE" fi # Add/Update MkDocs configuration if grep -q "^MKDOCS_URL=" "$MAP_ENV_FILE"; then sed -i "s|^MKDOCS_URL=.*|MKDOCS_URL=http://localhost:$mkdocs_site_port|" "$MAP_ENV_FILE" else echo "" >> "$MAP_ENV_FILE" echo "# MkDocs Integration" >> "$MAP_ENV_FILE" echo "MKDOCS_URL=http://localhost:$mkdocs_site_port" >> "$MAP_ENV_FILE" fi if grep -q "^MKDOCS_SEARCH_URL=" "$MAP_ENV_FILE"; then sed -i "s|^MKDOCS_SEARCH_URL=.*|MKDOCS_SEARCH_URL=http://localhost:$mkdocs_site_port|" "$MAP_ENV_FILE" else echo "MKDOCS_SEARCH_URL=http://localhost:$mkdocs_site_port" >> "$MAP_ENV_FILE" fi if grep -q "^MKDOCS_SITE_SERVER_PORT=" "$MAP_ENV_FILE"; then sed -i "s|^MKDOCS_SITE_SERVER_PORT=.*|MKDOCS_SITE_SERVER_PORT=$mkdocs_site_port|" "$MAP_ENV_FILE" else echo "MKDOCS_SITE_SERVER_PORT=$mkdocs_site_port" >> "$MAP_ENV_FILE" fi echo "✅ Synced ports to map configuration:" echo " - Map Port: $map_port" echo " - MkDocs Dev Port: $mkdocs_port" echo " - MkDocs Site Port: $mkdocs_site_port" echo " - MkDocs URL: http://localhost:$mkdocs_site_port" } # Function to update or create the map's .env file with domain settings update_map_env() { local new_domain=$1 # If the map .env file does not exist, create it with defaults if [ ! -f "$MAP_ENV_FILE" ]; then echo "Map .env file not found at $MAP_ENV_FILE, creating a new one with defaults." cat > "$MAP_ENV_FILE" </dev/null || echo "changeme") # Map Defaults (Edmonton, Alberta, Canada) DEFAULT_LAT=53.5461 DEFAULT_LNG=-113.4938 DEFAULT_ZOOM=11 # Optional: Map Boundaries (prevents users from adding points outside area) # BOUND_NORTH=53.7 # BOUND_SOUTH=53.4 # BOUND_EAST=-113.3 # BOUND_WEST=-113.7 # Cloudflare Settings TRUST_PROXY=true COOKIE_DOMAIN=.$new_domain # Update NODE_ENV to production for HTTPS NODE_ENV=production # Add allowed origin ALLOWED_ORIGINS=https://map.$new_domain,http://localhost:3000 # Add allowed origin ALLOWED_ORIGINS=https://map.$new_domain,http://localhost:3000 # SMTP Configuration SMTP_HOST=smtp.insert.here SMTP_PORT=insert_port SMTP_SECURE=false SMTP_USER=changeme@$new_domain SMTP_PASS=changeme EMAIL_FROM_NAME="$new_domain Map" EMAIL_FROM_ADDRESS=changeme@$new_domain # App Configuration APP_NAME="$new_domain Map" # Listmonk Configuration LISTMONK_API_URL=http://listmonk_app:9000/api LISTMONK_PASSWORD=changeme LISTMONK_SYNC_ENABLED=true LISTMONK_INITIAL_SYNC=false # Set to true to sync existing data EOL echo "✅ Created new map .env file at $MAP_ENV_FILE" return 0 fi echo "Updating map .env file with new domain settings..." # Create a backup of the map's .env file local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="${MAP_ENV_FILE}.backup_${timestamp}" cp "$MAP_ENV_FILE" "$backup_file" echo "Created backup of map .env at $backup_file" # Update DOMAIN if grep -q "^DOMAIN=" "$MAP_ENV_FILE"; then sed -i "s|^DOMAIN=.*|DOMAIN=$new_domain|" "$MAP_ENV_FILE" else echo "DOMAIN=$new_domain" >> "$MAP_ENV_FILE" fi # Update NOCODB_API_URL to use new domain if grep -q "^NOCODB_API_URL=" "$MAP_ENV_FILE"; then sed -i "s|^NOCODB_API_URL=.*|NOCODB_API_URL=https://db.$new_domain/api/v1|" "$MAP_ENV_FILE" else echo "NOCODB_API_URL=https://db.$new_domain/api/v1" >> "$MAP_ENV_FILE" fi # Add NOCODB_CUTS_SHEET if missing if ! grep -q "^NOCODB_CUTS_SHEET=" "$MAP_ENV_FILE"; then echo "NOCODB_CUTS_SHEET=" >> "$MAP_ENV_FILE" fi # Update MKDOCS_URL to use new domain if grep -q "^MKDOCS_URL=" "$MAP_ENV_FILE"; then sed -i "s|^MKDOCS_URL=.*|MKDOCS_URL=https://$new_domain|" "$MAP_ENV_FILE" else echo "MKDOCS_URL=https://$new_domain" >> "$MAP_ENV_FILE" fi # Update MKDOCS_SEARCH_URL to use new domain if grep -q "^MKDOCS_SEARCH_URL=" "$MAP_ENV_FILE"; then sed -i "s|^MKDOCS_SEARCH_URL=.*|MKDOCS_SEARCH_URL=https://$new_domain|" "$MAP_ENV_FILE" else echo "MKDOCS_SEARCH_URL=https://$new_domain" >> "$MAP_ENV_FILE" fi # Update COOKIE_DOMAIN if grep -q "^COOKIE_DOMAIN=" "$MAP_ENV_FILE"; then sed -i "s|^COOKIE_DOMAIN=.*|COOKIE_DOMAIN=.$new_domain|" "$MAP_ENV_FILE" else echo "COOKIE_DOMAIN=.$new_domain" >> "$MAP_ENV_FILE" fi # Update ALLOWED_ORIGINS local allowed_origins="https://map.$new_domain,http://localhost:3000" if grep -q "^ALLOWED_ORIGINS=" "$MAP_ENV_FILE"; then sed -i "s|^ALLOWED_ORIGINS=.*|ALLOWED_ORIGINS=$allowed_origins|" "$MAP_ENV_FILE" else echo "ALLOWED_ORIGINS=$allowed_origins" >> "$MAP_ENV_FILE" fi # Update EMAIL_FROM_NAME if grep -q "^EMAIL_FROM_NAME=" "$MAP_ENV_FILE"; then sed -i "s|^EMAIL_FROM_NAME=.*|EMAIL_FROM_NAME=\"$new_domain Map\"|" "$MAP_ENV_FILE" else echo "EMAIL_FROM_NAME=\"$new_domain Map\"" >> "$MAP_ENV_FILE" fi # Update EMAIL_FROM_ADDRESS if it contains a domain reference if grep -q "^EMAIL_FROM_ADDRESS=" "$MAP_ENV_FILE"; then # Only update if it looks like it contains a domain placeholder if grep -q "changeme@\|insert_from_address" "$MAP_ENV_FILE"; then sed -i "s|^EMAIL_FROM_ADDRESS=.*|EMAIL_FROM_ADDRESS=changeme@$new_domain|" "$MAP_ENV_FILE" fi else echo "EMAIL_FROM_ADDRESS=changeme@$new_domain" >> "$MAP_ENV_FILE" fi # Update SMTP_USER if it contains a domain reference if grep -q "^SMTP_USER=" "$MAP_ENV_FILE"; then # Only update if it looks like it contains a domain placeholder if grep -q "changeme@\|@bnkops.ca" "$MAP_ENV_FILE"; then sed -i "s|^SMTP_USER=.*|SMTP_USER=changeme@$new_domain|" "$MAP_ENV_FILE" fi else echo "SMTP_USER=changeme@$new_domain" >> "$MAP_ENV_FILE" fi # Update APP_NAME if grep -q "^APP_NAME=" "$MAP_ENV_FILE"; then sed -i "s|^APP_NAME=.*|APP_NAME=\"$new_domain Map\"|" "$MAP_ENV_FILE" else echo "APP_NAME=\"$new_domain Map\"" >> "$MAP_ENV_FILE" fi # Update domain references in NocoDB URLs while preserving the sheet IDs and paths # This will update domains like cmlite.org, changeme.org, etc. to the new domain sed -i "s|://db\.cmlite\.org/|://db.$new_domain/|g" "$MAP_ENV_FILE" sed -i "s|://map\.cmlite\.org|://map.$new_domain|g" "$MAP_ENV_FILE" echo "✅ Updated map .env file with:" echo " - NOCODB_API_URL=https://db.$new_domain/api/v1" echo " - COOKIE_DOMAIN=.$new_domain" echo " - ALLOWED_ORIGINS=$allowed_origins" echo " - Updated all NocoDB URLs to use $new_domain domain" # Sync ports to map configuration sync_map_ports return 0 } # Function to check for port conflicts in existing .env file check_port_conflicts() { echo "Checking for port conflicts in existing configuration..." # Get list of all used ports on system local used_ports_list used_ports_list=$(get_used_ports) if [ $? -ne 0 ] || [ -z "$used_ports_list" ]; then echo "Warning: Could not scan system ports. Skipping conflict check." return 1 fi # Check each configured port local -a conflicts=() local -a port_vars=( "CODE_SERVER_PORT" "LISTMONK_PORT" "LISTMONK_DB_PORT" "MKDOCS_PORT" "MKDOCS_SITE_SERVER_PORT" "N8N_PORT" "NOCODB_PORT" "HOMEPAGE_PORT" "GITEA_WEB_PORT" "GITEA_SSH_PORT" "MAP_PORT" "INFLUENCE_PORT" "MINI_QR_PORT" "REDIS_PORT" "PROMETHEUS_PORT" "GRAFANA_PORT" ) for var in "${port_vars[@]}"; do local port_value="${!var}" if [ -n "$port_value" ] && ! is_port_available "$port_value" "$used_ports_list"; then conflicts+=("$var=$port_value") fi done if [ ${#conflicts[@]} -gt 0 ]; then echo "" echo "⚠️ WARNING: Port conflicts detected!" echo "The following ports are already in use on your system:" for conflict in "${conflicts[@]}"; do echo " - $conflict" done echo "" echo "You may need to:" echo "1. Stop services using these ports, or" echo "2. Edit .env file to use different ports" echo "" else echo "✅ No port conflicts detected!" fi return 0 } # Function to get instance identifier get_instance_identifier() { # Try to get from directory name first local dir_name=$(basename "$SCRIPT_DIR") local default_instance="" # Extract potential instance name from directory if [[ "$dir_name" =~ changemaker\.lite\.(.+)$ ]]; then default_instance="${BASH_REMATCH[1]}" elif [[ "$dir_name" != "changemaker.lite" ]]; then default_instance="$dir_name" fi # Send informational messages to stderr so they don't get captured echo "" >&2 echo "=== Instance Configuration ===" >&2 echo "To run multiple Changemaker instances on the same machine," >&2 echo "each instance needs a unique identifier for containers and networks." >&2 echo "" >&2 if [ -n "$default_instance" ]; then echo "Detected potential instance name from directory: $default_instance" >&2 read -p "Use this instance identifier? [Y/n]: " use_detected if [[ ! "$use_detected" =~ ^[Nn]$ ]]; then echo "$default_instance" return 0 fi fi read -p "Enter instance identifier (letters, numbers, hyphens only) [default: main]: " instance_id # Validate and sanitize instance identifier if [ -z "$instance_id" ]; then instance_id="main" fi # Remove invalid characters and convert to lowercase instance_id=$(echo "$instance_id" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]//g') if [ -z "$instance_id" ]; then instance_id="main" fi # Only output the final instance_id to stdout (this gets captured) echo "$instance_id" } # Function to update docker-compose.yml with unique names update_docker_compose_names() { local instance_id=$1 if [ -z "$instance_id" ] || [ "$instance_id" = "main" ]; then echo "Using default container names (no instance suffix)" return 0 fi # Check if docker-compose.yml exists and is not empty if [ ! -f "$DOCKER_COMPOSE_FILE" ] || [ ! -s "$DOCKER_COMPOSE_FILE" ]; then echo "Error: docker-compose.yml does not exist or is empty at: $DOCKER_COMPOSE_FILE" echo "Please ensure docker-compose.yml exists before running this script." return 1 fi echo "Updating docker-compose.yml with instance identifier: $instance_id" # Create a backup of the docker-compose.yml file local timestamp=$(date +"%Y%m%d_%H%M%S") local backup_file="${DOCKER_COMPOSE_FILE}.backup_${timestamp}" cp "$DOCKER_COMPOSE_FILE" "$backup_file" echo "Created backup of docker-compose.yml at $backup_file" # Create temporary file for modifications local temp_file=$(mktemp) # Verify temp file was created if [ ! -f "$temp_file" ]; then echo "Error: Could not create temporary file" return 1 fi # Update container names, network names, and volume names # Process the file and save to temp file sed \ -e "s/container_name: \([^-]*\)-changemaker$/container_name: \1-changemaker-${instance_id}/g" \ -e "s/container_name: \([^-]*\)_\([^-]*\)_changemaker$/container_name: \1_\2_changemaker_${instance_id}/g" \ -e "s/container_name: \([^-]*\)_\([^-]*\)$/container_name: \1_\2_${instance_id}/g" \ -e "s/container_name: \([^-]*\)$/container_name: \1-${instance_id}/g" \ -e "s/changemaker-lite:/changemaker-lite-${instance_id}:/g" \ -e "s/- changemaker-lite$/- changemaker-lite-${instance_id}/g" \ -e "s/listmonk-data:/listmonk-data-${instance_id}:/g" \ -e "s/source: listmonk-data$/source: listmonk-data-${instance_id}/g" \ -e "s/n8n_data:/n8n_data_${instance_id}:/g" \ -e "s/source: n8n_data$/source: n8n_data_${instance_id}/g" \ -e "s/nc_data:/nc_data_${instance_id}:/g" \ -e "s/source: nc_data$/source: nc_data_${instance_id}/g" \ -e "s/db_data:/db_data_${instance_id}:/g" \ -e "s/source: db_data$/source: db_data_${instance_id}/g" \ -e "s/gitea_data:/gitea_data_${instance_id}:/g" \ -e "s/source: gitea_data$/source: gitea_data_${instance_id}/g" \ -e "s/mysql_data:/mysql_data_${instance_id}:/g" \ -e "s/source: mysql_data$/source: mysql_data_${instance_id}/g" \ -e "s/redis-data:/redis-data-${instance_id}:/g" \ -e "s/source: redis-data$/source: redis-data-${instance_id}/g" \ -e "s/prometheus-data:/prometheus-data-${instance_id}:/g" \ -e "s/source: prometheus-data$/source: prometheus-data-${instance_id}/g" \ -e "s/grafana-data:/grafana-data-${instance_id}:/g" \ -e "s/source: grafana-data$/source: grafana-data-${instance_id}/g" \ "$DOCKER_COMPOSE_FILE" > "$temp_file" # Check if temp file has content if [ ! -s "$temp_file" ]; then echo "Error: sed operation produced empty file" echo "Restoring from backup..." cp "$backup_file" "$DOCKER_COMPOSE_FILE" rm -f "$temp_file" return 1 fi # Replace the original file only if temp file has content mv "$temp_file" "$DOCKER_COMPOSE_FILE" echo "✅ Updated docker-compose.yml with instance-specific names:" echo " - Container names: *-${instance_id}" echo " - Network name: changemaker-lite-${instance_id}" echo " - Volume names: *-${instance_id}" return 0 } # Function to update .env file with instance identifier update_env_instance_config() { local instance_id=$1 if [ -n "$instance_id" ] && [ "$instance_id" != "main" ]; then update_env_var "INSTANCE_ID" "$instance_id" update_env_var "COMPOSE_PROJECT_NAME" "changemaker-lite-${instance_id}" echo "Updated .env with instance configuration" fi } # Initialize a new .env file if it doesn't exist if [ ! -f "$ENV_FILE" ]; then echo "No .env file found. Creating a new one from scratch." touch "$ENV_FILE" initialize_env_file else echo "Found existing .env file. Will update values." backup_env_file # For existing .env files, also scan ports and suggest alternatives if conflicts exist echo "" echo "Checking existing port assignments for conflicts..." load_env_vars check_port_conflicts fi # Load existing environment variables load_env_vars echo -e "\n\nWelcome to Changemaker Config!\n" echo "This script will help you configure your .env file for Changemaker." echo "Please provide the following information:" # Get instance identifier and update docker-compose.yml echo -e "\n---- Instance Configuration ----" instance_identifier=$(get_instance_identifier) # Strip any whitespace/newlines from the captured value instance_identifier=$(echo "$instance_identifier" | tr -d '\n\r' | xargs) # Only update docker-compose.yml if we have a non-default instance ID if [ -n "$instance_identifier" ] && [ "$instance_identifier" != "main" ]; then if update_docker_compose_names "$instance_identifier"; then echo "✅ Docker Compose configuration updated successfully" else echo "⚠️ Warning: Failed to update docker-compose.yml" echo " Continuing with default configuration..." fi else echo "Using default instance configuration (no modifications to docker-compose.yml)" fi update_env_instance_config "$instance_identifier" # Domain configuration read -p "Enter your domain name (without protocol, e.g., example.com): " domain_name if [ -z "$domain_name" ]; then echo "Domain name cannot be empty. Using default: changeme.org" domain_name="changeme.org" fi echo -e "\nUpdating domain settings in .env file..." # Update main domain settings update_env_var "DOMAIN" "$domain_name" update_env_var "BASE_DOMAIN" "https://$domain_name" update_env_var "HOMEPAGE_VAR_BASE_URL" "https://$domain_name" update_env_var "LISTMONK_HOSTNAME" "listmonk.$domain_name" update_env_var "N8N_HOST" "n8n.$domain_name" update_env_var "CF_DOMAIN" "$domain_name" update_env_var "GITEA_DOMAIN" "git.$domain_name" update_env_var "GITEA_ROOT_URL" "https://git.$domain_name" # Update cookie and CORS settings update_env_var "COOKIE_DOMAIN" ".$domain_name" update_env_var "ALLOWED_ORIGINS" "https://map.$domain_name,http://localhost:3000" echo "Domain settings updated successfully!" # Update the map's .env file echo -e "\nUpdating map configuration..." update_map_env "$domain_name" # Cloudflare Configuration echo -e "\n---- Cloudflare Configuration ----" get_cloudflare_credentials read -p "Do you want to configure Cloudflare settings now? [Y/n]: " configure_cf if [[ ! "$configure_cf" =~ ^[Nn]$ ]]; then # Get Cloudflare API token read -p "Enter your Cloudflare API Token: " cf_api_token if [ -z "$cf_api_token" ] || [ "$cf_api_token" = "your_cloudflare_api_token" ]; then echo "Warning: API token is required for Cloudflare integration" cf_api_token="your_cloudflare_api_token" fi update_env_var "CF_API_TOKEN" "$cf_api_token" # Get Cloudflare Zone ID read -p "Enter your Cloudflare Zone ID: " cf_zone_id if [ -z "$cf_zone_id" ] || [ "$cf_zone_id" = "your_cloudflare_zone_id" ]; then echo "Warning: Zone ID is required for DNS configuration" cf_zone_id="your_cloudflare_zone_id" fi update_env_var "CF_ZONE_ID" "$cf_zone_id" # Get Cloudflare Account ID read -p "Enter your Cloudflare Account ID: " cf_account_id if [ -z "$cf_account_id" ] || [ "$cf_account_id" = "your_cloudflare_account_id" ]; then echo "Warning: Account ID is required for tunnel operations" cf_account_id="your_cloudflare_account_id" fi update_env_var "CF_ACCOUNT_ID" "$cf_account_id" # Note: CF_TUNNEL_ID will be set by start-production.sh when the tunnel is created update_env_var "CF_TUNNEL_ID" "will_be_set_by_start_production" echo "" echo "Cloudflare configuration saved to .env file." echo "" echo "To complete the setup and deploy your services:" echo "1. Start your services with: docker compose up -d" echo "2. Run ./start-production.sh to configure the Cloudflare tunnel" echo "" else echo "Skipping Cloudflare configuration." echo "You'll need to configure Cloudflare settings manually before running start-production.sh." fi # Update the site_url in mkdocs.yml echo -e "\nUpdating site_url in mkdocs.yml..." update_mkdocs_yml "$domain_name" # Update service URLs in services.yaml echo -e "\nUpdating service URLs in services.yaml..." update_services_yaml "$domain_name" # Update the login URL in main.html echo -e "\nUpdating login URL in main.html..." update_main_html "$domain_name" # Listmonk Admin Credentials configuration echo -e "\n---- Listmonk Admin Credentials ----" read -p "Enter Listmonk admin email/username [default: admin@example.com]: " listmonk_user read -sp "Enter Listmonk admin password [default: changeMe]: " listmonk_password echo # Add new line after password input if [ -z "$listmonk_user" ]; then listmonk_user="admin@example.com" fi if [ -z "$listmonk_password" ]; then listmonk_password="changeMe" fi update_env_var "LISTMONK_ADMIN_USER" "$listmonk_user" update_env_var "LISTMONK_ADMIN_PASSWORD" "$listmonk_password" echo "Listmonk admin credentials updated." # N8N User Credentials configuration echo -e "\n---- N8N Admin Credentials ----" read -p "Enter N8N admin email [default: admin@example.com]: " n8n_email read -sp "Enter N8N admin password [default: changeMe]: " n8n_password echo # Add new line after password input if [ -z "$n8n_email" ]; then n8n_email="admin@example.com" fi if [ -z "$n8n_password" ]; then n8n_password="changeMe" fi update_env_var "N8N_USER_EMAIL" "$n8n_email" update_env_var "N8N_USER_PASSWORD" "$n8n_password" echo "N8N admin credentials updated." # Generate secure passwords for database and encryption echo -e "\n---- Generating Secure Passwords ----" echo "Generating secure passwords for database and encryption keys..." # Generate and update database password postgres_password=$(generate_password 20) update_env_var "POSTGRES_PASSWORD" "$postgres_password" # Generate and update N8N encryption key n8n_encryption_key=$(generate_password 32) update_env_var "N8N_ENCRYPTION_KEY" "$n8n_encryption_key" # Generate and update NocoDB passwords nocodb_jwt_secret=$(generate_password 32) update_env_var "NOCODB_JWT_SECRET" "$nocodb_jwt_secret" nocodb_db_password=$(generate_password 20) update_env_var "NOCODB_DB_PASSWORD" "$nocodb_db_password" # Generate and update Gitea passwords (these should already exist in the file) gitea_db_password=$(generate_password 20) update_env_var "GITEA_DB_PASSWD" "$gitea_db_password" gitea_db_root_password=$(generate_password 20) update_env_var "GITEA_DB_ROOT_PASSWORD" "$gitea_db_root_password" # Generate and update Grafana admin password grafana_admin_password=$(generate_password 20) update_env_var "GRAFANA_ADMIN_PASSWORD" "$grafana_admin_password" echo "Secure passwords generated and updated." echo -e "\n✅ Configuration completed successfully!" echo "Your .env file has been configured with:" echo "- Instance ID: $instance_identifier" echo "- Domain: $domain_name" echo "- Cookie Domain: .$domain_name" echo "- Allowed Origins: https://map.$domain_name,http://localhost:3000" echo "- Map .env updated with domain settings" echo "- Listmonk Admin: $listmonk_user" echo "- N8N Admin Email: $n8n_email" echo "- Secure random passwords for database, encryption, and NocoDB" echo "- Grafana Admin Password: Generated (see .env file)" echo "- Centralized services: Redis, Prometheus, Grafana" echo "- Tunnel configuration updated at: $TUNNEL_CONFIG_FILE" echo -e "\nYour .env file is located at: $ENV_FILE" echo "A backup of your original .env file was created before modifications." echo "" echo "======================================" echo "Next Steps:" echo "======================================" echo "" if [ -n "$instance_identifier" ] && [ "$instance_identifier" != "main" ]; then echo "Instance: $instance_identifier" echo "" fi echo "1. Start services locally:" echo " docker compose up -d" echo "" echo "2. Test all services at:" echo " - Homepage: http://localhost:${HOMEPAGE_PORT:-3010}" echo " - Code Server: http://localhost:${CODE_SERVER_PORT:-8888}" echo " - Listmonk: http://localhost:${LISTMONK_PORT:-9000}" echo " - Documentation: http://localhost:${MKDOCS_PORT:-4000}" echo " - n8n: http://localhost:${N8N_PORT:-5678}" echo " - NocoDB: http://localhost:${NOCODB_PORT:-8090}" echo " - Gitea: http://localhost:${GITEA_WEB_PORT:-3030}" echo " - Map: http://localhost:${MAP_PORT:-3000}" echo " - Influence: http://localhost:${INFLUENCE_PORT:-3333}" echo " - Mini QR: http://localhost:${MINI_QR_PORT:-8089}" echo "" echo " Centralized Services (optional monitoring profile):" echo " - Prometheus: http://localhost:${PROMETHEUS_PORT:-9090}" echo " - Grafana: http://localhost:${GRAFANA_PORT:-3001} (admin/${GRAFANA_ADMIN_PASSWORD})" echo "" echo " To start with monitoring:" echo " docker compose --profile monitoring up -d" echo "" echo "3. When ready for production:" echo " ./start-production.sh" echo "" echo "======================================"