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