Users could not submit scheduling poll votes when an invalid or partial email was entered — Zod rejected empty strings and non-email text with a generic validation error. Added client-side email validation in both SchedulingPollPage and SchedulingPollWidget, plus z.preprocess() on the backend to coerce empty strings to undefined. Also added pridecorner.ca to all nginx server blocks and added generate_nginx_configs() to config.sh so template-based configs are generated during setup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1092 lines
34 KiB
Bash
Executable File
1092 lines
34 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"
|
|
|
|
# --- 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
|
|
|
|
# =============================================================================
|
|
# 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
|
|
|
|
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. Will update values in place."
|
|
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"
|
|
}
|
|
|
|
generate_all_secrets() {
|
|
header "Generating Secrets"
|
|
|
|
info "Auto-generating 21 unique secrets and passwords..."
|
|
echo ""
|
|
|
|
# JWT & Encryption (64-char hex)
|
|
local jwt_access jwt_refresh enc_key
|
|
jwt_access=$(generate_secret)
|
|
jwt_refresh=$(generate_secret)
|
|
enc_key=$(generate_secret)
|
|
|
|
update_env_var "JWT_ACCESS_SECRET" "$jwt_access"
|
|
update_env_var "JWT_REFRESH_SECRET" "$jwt_refresh"
|
|
update_env_var "ENCRYPTION_KEY" "$enc_key"
|
|
success "JWT secrets + encryption key"
|
|
|
|
# Database passwords (24-char alphanum)
|
|
local pg_pass redis_pass
|
|
pg_pass=$(generate_password 24)
|
|
redis_pass=$(generate_password 24)
|
|
|
|
update_env_var "V2_POSTGRES_PASSWORD" "$pg_pass"
|
|
update_env_var "DATABASE_URL" "postgresql://changemaker:${pg_pass}@localhost:5433/changemaker_v2"
|
|
update_env_var "REDIS_PASSWORD" "$redis_pass"
|
|
update_env_var "REDIS_URL" "redis://:${redis_pass}@redis-changemaker:6379"
|
|
success "PostgreSQL + Redis passwords"
|
|
|
|
# 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)
|
|
|
|
update_env_var "LISTMONK_DB_PASSWORD" "$lm_db_pass"
|
|
update_env_var "LISTMONK_WEB_ADMIN_PASSWORD" "$lm_web_pass"
|
|
update_env_var "LISTMONK_API_TOKEN" "$lm_api_token"
|
|
update_env_var "LISTMONK_ADMIN_PASSWORD" "$lm_api_token"
|
|
success "Listmonk passwords + API token"
|
|
|
|
# NocoDB
|
|
local nc_pass
|
|
nc_pass=$(generate_password 20)
|
|
update_env_var "NC_ADMIN_PASSWORD" "$nc_pass"
|
|
success "NocoDB admin password"
|
|
|
|
# Gitea
|
|
local gitea_db gitea_root
|
|
gitea_db=$(generate_password 20)
|
|
gitea_root=$(generate_password 20)
|
|
update_env_var "GITEA_DB_PASSWD" "$gitea_db"
|
|
update_env_var "GITEA_DB_ROOT_PASSWORD" "$gitea_root"
|
|
success "Gitea database passwords"
|
|
|
|
# n8n
|
|
local n8n_enc n8n_pass
|
|
n8n_enc=$(generate_password 32)
|
|
n8n_pass=$(generate_password 20)
|
|
update_env_var "N8N_ENCRYPTION_KEY" "$n8n_enc"
|
|
update_env_var "N8N_USER_PASSWORD" "$n8n_pass"
|
|
success "n8n encryption key + admin password"
|
|
|
|
# Monitoring
|
|
local grafana_pass gotify_pass
|
|
grafana_pass=$(generate_password 20)
|
|
gotify_pass=$(generate_password 20)
|
|
update_env_var "GRAFANA_ADMIN_PASSWORD" "$grafana_pass"
|
|
update_env_var "GOTIFY_ADMIN_PASSWORD" "$gotify_pass"
|
|
success "Grafana + Gotify admin passwords"
|
|
|
|
# Vaultwarden
|
|
local vw_admin_token
|
|
vw_admin_token=$(generate_secret)
|
|
update_env_var "VAULTWARDEN_ADMIN_TOKEN" "$vw_admin_token"
|
|
success "Vaultwarden admin token"
|
|
|
|
# Rocket.Chat
|
|
local rc_pass
|
|
rc_pass=$(generate_password 20)
|
|
update_env_var "ROCKETCHAT_ADMIN_PASSWORD" "$rc_pass"
|
|
success "Rocket.Chat admin password"
|
|
|
|
# Gancio
|
|
local gancio_pass
|
|
gancio_pass=$(generate_password 20)
|
|
update_env_var "GANCIO_ADMIN_PASSWORD" "$gancio_pass"
|
|
success "Gancio admin password"
|
|
|
|
# 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)
|
|
update_env_var "JITSI_APP_SECRET" "$jitsi_secret"
|
|
update_env_var "JITSI_JICOFO_AUTH_PASSWORD" "$jitsi_jicofo"
|
|
update_env_var "JITSI_JVB_AUTH_PASSWORD" "$jitsi_jvb"
|
|
success "Jitsi Meet secrets (JWT + XMPP)"
|
|
|
|
echo ""
|
|
success "All 21 secrets generated. No placeholder passwords remain."
|
|
}
|
|
|
|
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 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
|
|
# =============================================================================
|
|
|
|
generate_services_yaml() {
|
|
local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
|
|
|
|
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:8891"
|
|
description: Team chat (port 8891)
|
|
container: rocketchat-changemaker
|
|
|
|
- Jitsi Meet:
|
|
icon: mdi-video
|
|
href: "http://localhost:8893"
|
|
description: Video conferencing (port 8893)
|
|
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/.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/thumbnails:Video thumbnails"
|
|
"media/public:Public media files"
|
|
"local-files:n8n local files"
|
|
"data:NAR import data"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
# =============================================================================
|
|
# 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}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}Secrets:${NC} 21 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 ""
|
|
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 -e " ${CYAN}docker compose --profile monitoring up -d${NC} # Monitoring"
|
|
echo ""
|
|
echo -e " ${BOLD}5.${NC} Or start everything at once:"
|
|
echo -e " ${CYAN}docker compose up -d${NC}"
|
|
echo ""
|
|
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
|
|
|
|
print_summary
|
|
print_next_steps
|
|
}
|
|
|
|
main "$@"
|