Initial v2 commit: complete rebuild with unified API + React admin
Phase 1-14 complete: - Unified Express.js API (TypeScript, Prisma ORM, PostgreSQL 16) - React Admin GUI (Vite + Ant Design + Zustand) - JWT auth with refresh tokens - Influence: Campaigns, Representatives, Responses, Email Queue - Map: Locations, Cuts, Shifts, Canvassing System - NAR data import infrastructure (2025 format) - Listmonk newsletter integration - Landing page builder (GrapesJS) - MkDocs + Code Server integration - Volunteer portal with GPS tracking - Monitoring stack (Prometheus, Grafana, Alertmanager) - Pangolin tunnel integration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
172
scripts/backup.sh
Executable file
172
scripts/backup.sh
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Changemaker Lite V2 — Backup Script
|
||||
# Backs up PostgreSQL databases, uploads, and generates a manifest.
|
||||
# Usage: ./scripts/backup.sh [--s3] [--retention DAYS]
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# --- Configuration ---
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
BACKUP_DIR="${BACKUP_DIR:-$PROJECT_DIR/backups}"
|
||||
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
BACKUP_NAME="changemaker-v2-backup-${TIMESTAMP}"
|
||||
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}"
|
||||
S3_UPLOAD=false
|
||||
|
||||
# --- Parse args ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--s3) S3_UPLOAD=true; shift ;;
|
||||
--retention) RETENTION_DAYS="$2"; shift 2 ;;
|
||||
--help)
|
||||
echo "Usage: $0 [--s3] [--retention DAYS]"
|
||||
echo " --s3 Upload backup to S3 (requires AWS CLI + S3_BUCKET env var)"
|
||||
echo " --retention N Delete local backups older than N days (default: 30)"
|
||||
exit 0 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Load .env if present (safe parsing to handle special characters) ---
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
while IFS='=' read -r key value; do
|
||||
# Skip comments and empty lines
|
||||
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
# Trim leading/trailing whitespace from key
|
||||
key="$(echo "$key" | xargs)"
|
||||
# Strip surrounding quotes from value if present
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
# Only export valid variable names
|
||||
if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
|
||||
export "$key=$value"
|
||||
fi
|
||||
done < "$PROJECT_DIR/.env"
|
||||
fi
|
||||
|
||||
# --- Derived vars ---
|
||||
PG_CONTAINER="${PG_CONTAINER:-changemaker-v2-postgres}"
|
||||
PG_USER="${V2_POSTGRES_USER:-changemaker}"
|
||||
PG_DB="${V2_POSTGRES_DB:-changemaker_v2}"
|
||||
LISTMONK_PG_CONTAINER="${LISTMONK_PG_CONTAINER:-listmonk-db}"
|
||||
LISTMONK_PG_USER="${LISTMONK_DB_USER:-listmonk}"
|
||||
LISTMONK_PG_DB="${LISTMONK_DB_NAME:-listmonk}"
|
||||
UPLOADS_DIR="${PROJECT_DIR}/assets/uploads"
|
||||
|
||||
echo "=========================================="
|
||||
echo " Changemaker Lite V2 — Backup"
|
||||
echo " ${TIMESTAMP}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# --- Create backup directory ---
|
||||
mkdir -p "$BACKUP_PATH"
|
||||
|
||||
# --- 1. V2 PostgreSQL Dump ---
|
||||
echo "[1/4] Dumping V2 PostgreSQL (${PG_DB})..."
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${PG_CONTAINER}$"; then
|
||||
docker exec "$PG_CONTAINER" pg_dump -U "$PG_USER" -d "$PG_DB" --no-owner --no-acl \
|
||||
| gzip > "${BACKUP_PATH}/v2-postgres.sql.gz"
|
||||
echo " -> v2-postgres.sql.gz ($(du -h "${BACKUP_PATH}/v2-postgres.sql.gz" | cut -f1))"
|
||||
else
|
||||
echo " [WARN] Container ${PG_CONTAINER} not running, skipping V2 DB dump."
|
||||
fi
|
||||
|
||||
# --- 2. Listmonk PostgreSQL Dump (optional) ---
|
||||
echo "[2/4] Dumping Listmonk PostgreSQL (${LISTMONK_PG_DB})..."
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${LISTMONK_PG_CONTAINER}$"; then
|
||||
docker exec "$LISTMONK_PG_CONTAINER" pg_dump -U "$LISTMONK_PG_USER" -d "$LISTMONK_PG_DB" --no-owner --no-acl \
|
||||
| gzip > "${BACKUP_PATH}/listmonk-postgres.sql.gz"
|
||||
echo " -> listmonk-postgres.sql.gz ($(du -h "${BACKUP_PATH}/listmonk-postgres.sql.gz" | cut -f1))"
|
||||
else
|
||||
echo " [WARN] Container ${LISTMONK_PG_CONTAINER} not running, skipping Listmonk dump."
|
||||
fi
|
||||
|
||||
# --- 3. Uploads Archive ---
|
||||
echo "[3/4] Archiving uploads..."
|
||||
if [ -d "$UPLOADS_DIR" ] && [ "$(ls -A "$UPLOADS_DIR" 2>/dev/null)" ]; then
|
||||
tar -czf "${BACKUP_PATH}/uploads.tar.gz" -C "$(dirname "$UPLOADS_DIR")" "$(basename "$UPLOADS_DIR")"
|
||||
echo " -> uploads.tar.gz ($(du -h "${BACKUP_PATH}/uploads.tar.gz" | cut -f1))"
|
||||
else
|
||||
echo " [INFO] No uploads directory or empty, skipping."
|
||||
fi
|
||||
|
||||
# --- 4. Manifest ---
|
||||
echo "[4/4] Generating manifest..."
|
||||
MANIFEST_FILE="${BACKUP_PATH}/manifest.json"
|
||||
{
|
||||
echo "{"
|
||||
echo " \"timestamp\": \"${TIMESTAMP}\","
|
||||
echo " \"backup_name\": \"${BACKUP_NAME}\","
|
||||
echo " \"files\": ["
|
||||
|
||||
FIRST=true
|
||||
for f in "${BACKUP_PATH}"/*.{sql.gz,tar.gz}; do
|
||||
[ -f "$f" ] || continue
|
||||
SIZE="$(stat --printf='%s' "$f" 2>/dev/null || stat -f '%z' "$f" 2>/dev/null || echo 0)"
|
||||
SHA256="$(sha256sum "$f" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$f" | cut -d' ' -f1)"
|
||||
BASENAME="$(basename "$f")"
|
||||
if [ "$FIRST" = true ]; then
|
||||
FIRST=false
|
||||
else
|
||||
echo ","
|
||||
fi
|
||||
printf ' {"file": "%s", "size_bytes": %s, "sha256": "%s"}' "$BASENAME" "$SIZE" "$SHA256"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo " ],"
|
||||
echo " \"v2_database\": \"${PG_DB}\","
|
||||
echo " \"listmonk_database\": \"${LISTMONK_PG_DB}\","
|
||||
echo " \"retention_days\": ${RETENTION_DAYS}"
|
||||
echo "}"
|
||||
} > "$MANIFEST_FILE"
|
||||
|
||||
echo " -> manifest.json"
|
||||
echo ""
|
||||
|
||||
# --- Create single archive ---
|
||||
ARCHIVE_FILE="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
|
||||
tar -czf "$ARCHIVE_FILE" -C "$BACKUP_DIR" "$BACKUP_NAME"
|
||||
rm -rf "$BACKUP_PATH"
|
||||
echo "Archive: ${ARCHIVE_FILE} ($(du -h "$ARCHIVE_FILE" | cut -f1))"
|
||||
echo ""
|
||||
|
||||
# --- Optional S3 Upload ---
|
||||
if [ "$S3_UPLOAD" = true ]; then
|
||||
S3_BUCKET="${S3_BUCKET:-}"
|
||||
S3_PREFIX="${S3_PREFIX:-changemaker-backups}"
|
||||
if [ -z "$S3_BUCKET" ]; then
|
||||
echo "[WARN] S3_BUCKET not set, skipping S3 upload."
|
||||
elif ! command -v aws &>/dev/null; then
|
||||
echo "[WARN] AWS CLI not found, skipping S3 upload."
|
||||
else
|
||||
echo "Uploading to s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}.tar.gz..."
|
||||
aws s3 cp "$ARCHIVE_FILE" "s3://${S3_BUCKET}/${S3_PREFIX}/${BACKUP_NAME}.tar.gz"
|
||||
echo "S3 upload complete."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# --- Retention Cleanup ---
|
||||
echo "Cleaning up backups older than ${RETENTION_DAYS} days..."
|
||||
DELETED=0
|
||||
for old in "${BACKUP_DIR}"/changemaker-v2-backup-*.tar.gz; do
|
||||
[ -f "$old" ] || continue
|
||||
if [ "$(find "$old" -mtime +"$RETENTION_DAYS" -print 2>/dev/null)" ]; then
|
||||
rm -f "$old"
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
done
|
||||
echo " Deleted ${DELETED} old backup(s)."
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo " Backup complete!"
|
||||
echo " Archive: ${ARCHIVE_FILE}"
|
||||
echo "=========================================="
|
||||
1546
scripts/legacy/config-cloudflare.sh
Executable file
1546
scripts/legacy/config-cloudflare.sh
Executable file
File diff suppressed because it is too large
Load Diff
666
scripts/legacy/start-production-cloudflare.sh
Executable file
666
scripts/legacy/start-production-cloudflare.sh
Executable file
@@ -0,0 +1,666 @@
|
||||
#!/bin/bash
|
||||
|
||||
cat << "EOF"
|
||||
██████╗██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ███████╗
|
||||
██╔════╝██║ ██║██╔══██╗████╗ ██║██╔════╝ ██╔════╝
|
||||
██║ ███████║███████║██╔██╗ ██║██║ ███╗█████╗
|
||||
██║ ██╔══██║██╔══██║██║╚██╗██║██║ ██║██╔══╝
|
||||
╚██████╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝███████╗
|
||||
╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝
|
||||
|
||||
███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗
|
||||
████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗
|
||||
██╔████╔██║███████║█████╔╝ █████╗ ██████╔╝
|
||||
██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗
|
||||
██║ ╚═╝ ██║██║ ██║██║ ██╗███████╗██║ ██║
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
|
||||
Start Production Wizard
|
||||
EOF
|
||||
|
||||
echo "#############################################################"
|
||||
echo "# "
|
||||
echo "# Changemaker.lite Production Deployment "
|
||||
echo "# "
|
||||
echo "# This script will: "
|
||||
echo "# 1. Install and configure cloudflared "
|
||||
echo "# 2. Create a systemd service for the tunnel "
|
||||
echo "# 3. Configure DNS records "
|
||||
echo "# 4. Set up access policies "
|
||||
echo "# "
|
||||
echo "#############################################################"
|
||||
echo ""
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
ENV_FILE="$SCRIPT_DIR/.env"
|
||||
TUNNEL_CONFIG_FILE="$SCRIPT_DIR/configs/cloudflare/tunnel-config.yml"
|
||||
TUNNEL_CREDS_DIR="$SCRIPT_DIR/configs/cloudflare"
|
||||
SERVICE_NAME="cloudflared-changemaker"
|
||||
|
||||
# Source environment variables
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
export $(grep -v '^#' "$ENV_FILE" | xargs)
|
||||
else
|
||||
echo "Error: .env file not found. Please run ./config.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prompt user for tunnel name suffix
|
||||
echo "Choose a name for your tunnel:"
|
||||
echo "This will create a tunnel named 'changemaker-lite-[your-input]'"
|
||||
echo "Examples: 'prod', 'staging', 'myproject', 'server1'"
|
||||
echo ""
|
||||
read -p "Enter tunnel name suffix (or press Enter for default 'main'): " TUNNEL_SUFFIX
|
||||
|
||||
# Set default if empty
|
||||
if [ -z "$TUNNEL_SUFFIX" ]; then
|
||||
TUNNEL_SUFFIX="main"
|
||||
fi
|
||||
|
||||
# Validate tunnel suffix (alphanumeric and hyphens only)
|
||||
if [[ ! "$TUNNEL_SUFFIX" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
||||
echo "Error: Tunnel suffix can only contain letters, numbers, and hyphens."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TUNNEL_NAME="changemaker-lite-$TUNNEL_SUFFIX"
|
||||
SERVICE_NAME="cloudflared-changemaker-lite-$TUNNEL_SUFFIX"
|
||||
echo "Using tunnel name: $TUNNEL_NAME"
|
||||
echo "Using service name: $SERVICE_NAME"
|
||||
echo ""
|
||||
|
||||
# Check if Cloudflare credentials are properly configured
|
||||
if [ -z "$CF_API_TOKEN" ] || [ "$CF_API_TOKEN" == "your_cloudflare_api_token" ] || \
|
||||
[ -z "$CF_ZONE_ID" ] || [ "$CF_ZONE_ID" == "your_cloudflare_zone_id" ] || \
|
||||
[ -z "$CF_DOMAIN" ] || [ -z "$CF_ACCOUNT_ID" ] || [ "$CF_ACCOUNT_ID" == "your_cloudflare_account_id" ]; then
|
||||
echo "Error: Cloudflare configuration incomplete."
|
||||
echo ""
|
||||
echo "Current values:"
|
||||
echo " CF_API_TOKEN: ${CF_API_TOKEN:-not set}"
|
||||
echo " CF_ZONE_ID: ${CF_ZONE_ID:-not set}"
|
||||
echo " CF_DOMAIN: ${CF_DOMAIN:-not set}"
|
||||
echo " CF_ACCOUNT_ID: ${CF_ACCOUNT_ID:-not set}"
|
||||
echo ""
|
||||
echo "Please run ./config.sh and complete the Cloudflare configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if cloudflared is installed
|
||||
check_cloudflared() {
|
||||
if ! command -v cloudflared &> /dev/null; then
|
||||
echo "cloudflared not found. Installing..."
|
||||
install_cloudflared
|
||||
else
|
||||
echo "✅ cloudflared is already installed: $(cloudflared --version | head -n1)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install cloudflared
|
||||
install_cloudflared() {
|
||||
echo "Installing cloudflared..."
|
||||
|
||||
# Check if we need to add the Cloudflare repository
|
||||
if [ ! -f /usr/share/keyrings/cloudflare-main.gpg ]; then
|
||||
# Add the cloudflare gpg key
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
|
||||
|
||||
# Add the repository
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||
fi
|
||||
|
||||
# Update and install
|
||||
sudo apt-get update && sudo apt-get install -y cloudflared
|
||||
|
||||
if ! command -v cloudflared &> /dev/null; then
|
||||
echo "Failed to install cloudflared. Please install it manually following the instructions at:"
|
||||
echo "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ cloudflared installed successfully: $(cloudflared --version | head -n1)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Authenticate with Cloudflare
|
||||
authenticate_cloudflare() {
|
||||
echo "Checking Cloudflare authentication..."
|
||||
|
||||
# Remove --account-tag parameter as it's not supported in this version
|
||||
if ! cloudflared tunnel list &> /dev/null; then
|
||||
echo "Not authenticated with Cloudflare. Starting authentication process..."
|
||||
echo "Log in to Cloudflare in the browser window that opens..."
|
||||
|
||||
# Run login
|
||||
if ! cloudflared tunnel login; then
|
||||
echo "Authentication failed. Please try again or authenticate manually using:"
|
||||
echo "cloudflared tunnel login"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✅ Already authenticated with Cloudflare"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create or get existing tunnel
|
||||
setup_tunnel() {
|
||||
echo "Setting up Cloudflare tunnel..."
|
||||
|
||||
# Check if we have a tunnel ID in the environment
|
||||
local tunnel_id=""
|
||||
|
||||
# Check if tunnel exists by name
|
||||
echo "Checking for existing tunnel named '$TUNNEL_NAME'..."
|
||||
local tunnel_list_output
|
||||
# Remove --account-tag parameter as it's not supported in this version
|
||||
tunnel_list_output=$(cloudflared tunnel list --output json 2>&1)
|
||||
|
||||
# Check if the command was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error listing tunnels: $tunnel_list_output"
|
||||
echo "Please ensure you're properly authenticated."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fix for invalid JSON output - try to clean the output if needed
|
||||
if ! echo "$tunnel_list_output" | jq '.' &>/dev/null; then
|
||||
echo "Warning: Invalid JSON returned from cloudflared. Attempting to clean output..."
|
||||
# Try to extract valid JSON portion
|
||||
tunnel_list_output=$(echo "$tunnel_list_output" | grep -o '\[.*\]' || echo "[]")
|
||||
fi
|
||||
|
||||
# Now try to extract the tunnel info
|
||||
local tunnel_info
|
||||
if echo "$tunnel_list_output" | jq '.' &>/dev/null; then
|
||||
tunnel_info=$(echo "$tunnel_list_output" | jq -r '.[] | select(.name=="'$TUNNEL_NAME'")' 2>/dev/null || echo "")
|
||||
else
|
||||
echo "Failed to parse tunnel list. Cannot check for existing tunnels."
|
||||
tunnel_info=""
|
||||
fi
|
||||
|
||||
if [ -n "$tunnel_info" ]; then
|
||||
# Tunnel exists
|
||||
tunnel_id=$(echo "$tunnel_info" | jq -r '.id' 2>/dev/null)
|
||||
if [ -n "$tunnel_id" ] && [ "$tunnel_id" != "null" ]; then
|
||||
echo "✅ Found existing tunnel: $TUNNEL_NAME (ID: $tunnel_id)"
|
||||
|
||||
# Update the environment variable if needed
|
||||
if [ "$CF_TUNNEL_ID" != "$tunnel_id" ]; then
|
||||
echo "Updating CF_TUNNEL_ID in .env file..."
|
||||
sed -i "s/CF_TUNNEL_ID=.*/CF_TUNNEL_ID=$tunnel_id/" "$ENV_FILE"
|
||||
# Reload the variable
|
||||
export CF_TUNNEL_ID="$tunnel_id"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Found tunnel but couldn't extract ID"
|
||||
fi
|
||||
else
|
||||
# Create new tunnel
|
||||
echo "Creating new tunnel: $TUNNEL_NAME"
|
||||
local tunnel_create_output
|
||||
# Remove --account-tag parameter as it's not supported in this version
|
||||
tunnel_create_output=$(cloudflared tunnel create "$TUNNEL_NAME" 2>&1)
|
||||
echo "Tunnel creation output:"
|
||||
echo "$tunnel_create_output"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to create tunnel. Please check your Cloudflare credentials."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Try multiple methods to extract the tunnel ID from output
|
||||
# Method 1: Try the original regex pattern with new tunnel name
|
||||
tunnel_id=$(echo "$tunnel_create_output" | grep -oP "Created tunnel $TUNNEL_NAME with id \K[a-f0-9-]+" || echo "")
|
||||
|
||||
# Method 2: Look for UUID pattern
|
||||
if [ -z "$tunnel_id" ]; then
|
||||
tunnel_id=$(echo "$tunnel_create_output" | grep -oP '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' || echo "")
|
||||
fi
|
||||
|
||||
# Method 3: Look for hex string that might be the ID
|
||||
if [ -z "$tunnel_id" ]; then
|
||||
tunnel_id=$(echo "$tunnel_create_output" | grep -oP '\b[a-f0-9-]{20,}\b' || echo "")
|
||||
fi
|
||||
|
||||
# Interactive method as fallback
|
||||
if [ -z "$tunnel_id" ]; then
|
||||
echo "Could not automatically extract tunnel ID from output."
|
||||
echo "Please check the output above and enter the tunnel ID manually:"
|
||||
read -p "Tunnel ID: " tunnel_id
|
||||
|
||||
if [ -z "$tunnel_id" ]; then
|
||||
echo "No tunnel ID provided. Please create the tunnel manually and set CF_TUNNEL_ID in the .env file."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Created new tunnel: $TUNNEL_NAME (ID: $tunnel_id)"
|
||||
|
||||
# Update the environment variable
|
||||
sed -i "s/CF_TUNNEL_ID=.*/CF_TUNNEL_ID=$tunnel_id/" "$ENV_FILE"
|
||||
export CF_TUNNEL_ID="$tunnel_id"
|
||||
fi
|
||||
|
||||
# Make sure credentials directory exists
|
||||
mkdir -p "$TUNNEL_CREDS_DIR"
|
||||
|
||||
# Check if we need to copy the credentials file
|
||||
local creds_file="$HOME/.cloudflared/$tunnel_id.json"
|
||||
local target_creds_file="$TUNNEL_CREDS_DIR/$tunnel_id.json"
|
||||
|
||||
if [ -f "$creds_file" ] && [ ! -f "$target_creds_file" ]; then
|
||||
echo "Copying tunnel credentials to project directory..."
|
||||
cp "$creds_file" "$target_creds_file"
|
||||
echo "✅ Credentials copied to $target_creds_file"
|
||||
elif [ ! -f "$creds_file" ]; then
|
||||
echo "Warning: Tunnel credentials not found at $creds_file"
|
||||
echo "Checking for credentials in alternative locations..."
|
||||
|
||||
# Try to find credentials in ~/.cloudflared directory
|
||||
local found_creds=false
|
||||
for cred_file in "$HOME/.cloudflared/"*.json; do
|
||||
if [ -f "$cred_file" ]; then
|
||||
echo "Found credentials file: $cred_file"
|
||||
echo "Checking if this file contains credentials for our tunnel..."
|
||||
|
||||
# Check if the file contains the tunnel ID
|
||||
if grep -q "$tunnel_id" "$cred_file"; then
|
||||
echo "✅ This appears to be the correct credentials file!"
|
||||
cp "$cred_file" "$target_creds_file"
|
||||
echo "✅ Credentials copied to $target_creds_file"
|
||||
found_creds=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$found_creds" = false ]; then
|
||||
echo "Error: Could not find tunnel credentials."
|
||||
echo "Please locate the credentials file for tunnel $tunnel_id and copy it to:"
|
||||
echo "$target_creds_file"
|
||||
echo ""
|
||||
echo "You can also run this command manually to create a new tunnel with credentials:"
|
||||
echo "cloudflared tunnel create --account-tag \"$CF_ACCOUNT_ID\" \"$tunnel_name\""
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update tunnel configuration file
|
||||
update_tunnel_config "$target_creds_file"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Update tunnel configuration
|
||||
update_tunnel_config() {
|
||||
local creds_file="$1"
|
||||
|
||||
echo "Updating tunnel configuration..."
|
||||
|
||||
# Create or update the config file
|
||||
cat > "$TUNNEL_CONFIG_FILE" << EOL
|
||||
# Cloudflare Tunnel Configuration for $CF_DOMAIN
|
||||
# Generated by Changemaker.lite start-production.sh on $(date)
|
||||
|
||||
tunnel: ${CF_TUNNEL_ID}
|
||||
credentials-file: ${creds_file}
|
||||
ingress:
|
||||
- hostname: homepage.${CF_DOMAIN}
|
||||
service: http://localhost:${HOMEPAGE_PORT:-3010}
|
||||
- hostname: code.${CF_DOMAIN}
|
||||
service: http://localhost:${CODE_SERVER_PORT:-8888}
|
||||
- hostname: listmonk.${CF_DOMAIN}
|
||||
service: http://localhost:${LISTMONK_PORT:-9000}
|
||||
- hostname: docs.${CF_DOMAIN}
|
||||
service: http://localhost:${MKDOCS_PORT:-4000}
|
||||
- hostname: ${CF_DOMAIN}
|
||||
service: http://localhost:${MKDOCS_SITE_SERVER_PORT:-4001}
|
||||
- hostname: n8n.${CF_DOMAIN}
|
||||
service: http://localhost:${N8N_PORT:-5678}
|
||||
- hostname: db.${CF_DOMAIN}
|
||||
service: http://localhost:${NOCODB_PORT:-8090}
|
||||
- hostname: git.${CF_DOMAIN}
|
||||
service: http://localhost:${GITEA_WEB_PORT:-3030}
|
||||
- hostname: map.${CF_DOMAIN}
|
||||
service: http://localhost:${MAP_PORT:-3000}
|
||||
- hostname: influence.${CF_DOMAIN}
|
||||
service: http://localhost:${INFLUENCE_PORT:-3333}
|
||||
- hostname: qr.${CF_DOMAIN}
|
||||
service: http://localhost:${MINI_QR_PORT:-8089}
|
||||
- service: http_status:404
|
||||
EOL
|
||||
|
||||
echo "✅ Tunnel configuration updated at $TUNNEL_CONFIG_FILE"
|
||||
}
|
||||
|
||||
# Create systemd service for cloudflared
|
||||
create_systemd_service() {
|
||||
echo "Creating systemd service for cloudflared tunnel: $SERVICE_NAME"
|
||||
|
||||
local cloudflared_path=$(which cloudflared)
|
||||
local username=$(whoami)
|
||||
|
||||
# Create service file content
|
||||
local service_content="[Unit]
|
||||
Description=Cloudflare Tunnel for Changemaker.lite
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=$username
|
||||
ExecStart=$cloudflared_path tunnel --config $TUNNEL_CONFIG_FILE run
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StartLimitInterval=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target"
|
||||
|
||||
# Write service file
|
||||
echo "$service_content" | sudo tee /etc/systemd/system/$SERVICE_NAME.service > /dev/null
|
||||
|
||||
# Reload systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Enable and start the service
|
||||
sudo systemctl enable $SERVICE_NAME.service
|
||||
sudo systemctl restart $SERVICE_NAME.service
|
||||
|
||||
# Check service status
|
||||
sleep 3
|
||||
if sudo systemctl is-active --quiet $SERVICE_NAME.service; then
|
||||
echo "✅ Cloudflared service is running"
|
||||
else
|
||||
echo "⚠️ Cloudflared service failed to start. Checking logs..."
|
||||
sudo systemctl status $SERVICE_NAME.service
|
||||
echo "Viewing recent logs:"
|
||||
sudo journalctl -u $SERVICE_NAME.service --no-pager -n 20
|
||||
|
||||
echo "Please check the configuration and try again."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create/update DNS record
|
||||
create_dns_record() {
|
||||
local name=$1
|
||||
local full_name
|
||||
local content="$CF_TUNNEL_ID.cfargotunnel.com"
|
||||
|
||||
# Determine the full DNS name
|
||||
if [ "$name" == "@" ]; then
|
||||
full_name="$CF_DOMAIN"
|
||||
else
|
||||
full_name="$name.$CF_DOMAIN"
|
||||
fi
|
||||
|
||||
echo "Configuring DNS record for $full_name..."
|
||||
|
||||
# Check if record exists
|
||||
EXISTING=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=$full_name" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
RECORD_ID=$(echo "$EXISTING" | jq -r '.result[] | select(.name == "'$full_name'") | .id' | head -1)
|
||||
|
||||
if [ ! -z "$RECORD_ID" ] && [ "$RECORD_ID" != "null" ]; then
|
||||
# Update existing record
|
||||
RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{
|
||||
\"type\": \"CNAME\",
|
||||
\"name\": \"$name\",
|
||||
\"content\": \"$content\",
|
||||
\"ttl\": 1,
|
||||
\"proxied\": true
|
||||
}")
|
||||
else
|
||||
# Create new record
|
||||
RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{
|
||||
\"type\": \"CNAME\",
|
||||
\"name\": \"$name\",
|
||||
\"content\": \"$content\",
|
||||
\"ttl\": 1,
|
||||
\"proxied\": true
|
||||
}")
|
||||
fi
|
||||
|
||||
if echo "$RESPONSE" | jq -e '.success' > /dev/null; then
|
||||
echo " ✅ DNS record configured"
|
||||
else
|
||||
echo " ❌ Failed to configure DNS record"
|
||||
echo "$RESPONSE" | jq '.errors'
|
||||
fi
|
||||
}
|
||||
|
||||
# Set up access policies
|
||||
setup_access_policies() {
|
||||
echo ""
|
||||
echo "Setting up Cloudflare Access policies..."
|
||||
read -p "Enter email address for admin access to protected services: " ADMIN_EMAIL
|
||||
|
||||
if [[ "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||||
# Protected services
|
||||
PROTECTED_SERVICES=("homepage" "code")
|
||||
|
||||
for service in "${PROTECTED_SERVICES[@]}"; do
|
||||
echo "Creating access policy for $service.$CF_DOMAIN..."
|
||||
|
||||
# Check if application already exists
|
||||
EXISTING_APPS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
APP_ID=$(echo "$EXISTING_APPS" | jq -r ".result[] | select(.domain == \"$service.$CF_DOMAIN\") | .id" | head -1)
|
||||
|
||||
if [ -z "$APP_ID" ] || [ "$APP_ID" == "null" ]; then
|
||||
# Create new access application
|
||||
APP_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{
|
||||
\"name\": \"$service.$CF_DOMAIN\",
|
||||
\"domain\": \"$service.$CF_DOMAIN\",
|
||||
\"type\": \"self_hosted\",
|
||||
\"session_duration\": \"24h\",
|
||||
\"auto_redirect_to_identity\": false
|
||||
}")
|
||||
|
||||
APP_ID=$(echo "$APP_RESPONSE" | jq -r '.result.id')
|
||||
fi
|
||||
|
||||
if [ ! -z "$APP_ID" ] && [ "$APP_ID" != "null" ]; then
|
||||
# Create/update policy
|
||||
POLICY_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps/$APP_ID/policies" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{
|
||||
\"name\": \"Email Policy\",
|
||||
\"decision\": \"allow\",
|
||||
\"include\": [{
|
||||
\"email\": {\"email\": \"$ADMIN_EMAIL\"}
|
||||
}]
|
||||
}")
|
||||
|
||||
if echo "$POLICY_RESPONSE" | jq -e '.success' > /dev/null; then
|
||||
echo " ✅ Access policy configured"
|
||||
else
|
||||
echo " ⚠️ Policy may already exist or update failed"
|
||||
fi
|
||||
else
|
||||
echo " ❌ Failed to create access application"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "Invalid email format. Skipping access policy setup."
|
||||
echo "You can configure access policies manually in the Cloudflare dashboard."
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if services are running
|
||||
check_services() {
|
||||
echo "Checking if services are running..."
|
||||
|
||||
local all_running=true
|
||||
|
||||
# Check common ports to see if services are running
|
||||
if ! nc -z localhost ${HOMEPAGE_PORT:-3010} >/dev/null 2>&1; then
|
||||
echo "❌ Homepage service not detected on port ${HOMEPAGE_PORT:-3010}"
|
||||
all_running=false
|
||||
fi
|
||||
|
||||
if ! nc -z localhost ${CODE_SERVER_PORT:-8888} >/dev/null 2>&1; then
|
||||
echo "❌ Code Server service not detected on port ${CODE_SERVER_PORT:-8888}"
|
||||
all_running=false
|
||||
fi
|
||||
|
||||
if ! nc -z localhost ${LISTMONK_PORT:-9000} >/dev/null 2>&1; then
|
||||
echo "❌ Listmonk service not detected on port ${LISTMONK_PORT:-9000}"
|
||||
all_running=false
|
||||
fi
|
||||
|
||||
# At least check that some services are running
|
||||
if [ "$all_running" = false ]; then
|
||||
echo "Warning: Some services may not be running."
|
||||
read -p "Continue anyway? (y/n): " continue_answer
|
||||
if [[ ! "$continue_answer" =~ ^[Yy]$ ]]; then
|
||||
echo "Please start your services with 'docker compose up -d' first."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✅ Services appear to be running"
|
||||
fi
|
||||
}
|
||||
|
||||
# Add this function to verify Cloudflare API credentials
|
||||
verify_cloudflare_credentials() {
|
||||
echo "Verifying Cloudflare API credentials..."
|
||||
|
||||
# Test the API token and Zone ID with a simple request
|
||||
local TEST_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
if echo "$TEST_RESPONSE" | jq -e '.success' > /dev/null 2>&1; then
|
||||
echo "✅ Cloudflare API credentials verified successfully"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Cloudflare API credentials verification failed"
|
||||
echo "Error details:"
|
||||
echo "$TEST_RESPONSE" | jq '.errors'
|
||||
echo ""
|
||||
echo "Common issues:"
|
||||
echo "1. Zone ID may be incorrect - make sure you're using the correct Zone ID from the Cloudflare dashboard"
|
||||
echo "2. API Token may not have the required permissions - make sure it has Zone:DNS and Zone:Settings permissions"
|
||||
echo ""
|
||||
echo "Your current Zone ID: $CF_ZONE_ID"
|
||||
echo "To continue without configuring DNS records, enter 'continue'. To quit and fix the credentials, enter 'quit'."
|
||||
read -p "Continue or quit? [continue/quit]: " cf_choice
|
||||
|
||||
if [[ "$cf_choice" == "quit" ]]; then
|
||||
echo "Exiting. Please update your credentials in .env and try again."
|
||||
exit 1
|
||||
else
|
||||
echo "Continuing without configuring DNS records..."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution flow
|
||||
echo "Starting Changemaker.lite production deployment..."
|
||||
|
||||
# Check if services are running
|
||||
check_services
|
||||
|
||||
# Check if cloudflared is installed, install if needed
|
||||
check_cloudflared
|
||||
|
||||
# Authenticate with Cloudflare
|
||||
authenticate_cloudflare
|
||||
|
||||
# Set up tunnel
|
||||
setup_tunnel
|
||||
|
||||
# Create systemd service
|
||||
create_systemd_service
|
||||
|
||||
# Verify Cloudflare credentials before proceeding with DNS configuration
|
||||
CF_CREDS_VALID=true
|
||||
if ! verify_cloudflare_credentials; then
|
||||
CF_CREDS_VALID=false
|
||||
echo "Skipping DNS record creation and access policy setup due to invalid credentials"
|
||||
fi
|
||||
|
||||
# Only proceed with DNS configuration if credentials are valid
|
||||
if [ "$CF_CREDS_VALID" = true ]; then
|
||||
# Create/update DNS records
|
||||
echo ""
|
||||
echo "Creating/updating DNS records..."
|
||||
|
||||
# Define subdomains
|
||||
declare -A SUBDOMAINS=(
|
||||
["homepage"]="Homepage"
|
||||
["code"]="Code Server"
|
||||
["listmonk"]="Listmonk"
|
||||
["docs"]="Documentation"
|
||||
["n8n"]="n8n"
|
||||
["db"]="NocoDB"
|
||||
["git"]="Gitea"
|
||||
["map"]="Map"
|
||||
["influence"]="Influence"
|
||||
["qr"]="Mini QR"
|
||||
)
|
||||
|
||||
for subdomain in "${!SUBDOMAINS[@]}"; do
|
||||
create_dns_record "$subdomain"
|
||||
done
|
||||
|
||||
# Create root domain record
|
||||
create_dns_record "@"
|
||||
|
||||
echo ""
|
||||
echo "✅ All DNS records configured"
|
||||
|
||||
# Set up access policies
|
||||
setup_access_policies
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ DNS records and access policies were not configured."
|
||||
echo "To manually configure DNS records, you can use the Cloudflare dashboard."
|
||||
echo "Add CNAME records for each subdomain pointing to: $CF_TUNNEL_ID.cfargotunnel.com"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "#############################################################"
|
||||
echo "# "
|
||||
echo "# Production Deployment Complete! "
|
||||
echo "# "
|
||||
echo "#############################################################"
|
||||
echo ""
|
||||
echo "Tunnel Name: $TUNNEL_NAME"
|
||||
echo "Tunnel ID: $CF_TUNNEL_ID"
|
||||
echo "Service Name: $SERVICE_NAME"
|
||||
echo ""
|
||||
echo "Your services are now accessible at:"
|
||||
echo ""
|
||||
echo "Public services:"
|
||||
echo " - Main Site: https://$CF_DOMAIN"
|
||||
echo " - Listmonk: https://listmonk.$CF_DOMAIN"
|
||||
echo " - Documentation: https://docs.$CF_DOMAIN"
|
||||
echo " - n8n: https://n8n.$CF_DOMAIN"
|
||||
echo " - NocoDB: https://db.$CF_DOMAIN"
|
||||
echo " - Gitea: https://git.$CF_DOMAIN"
|
||||
echo " - Map: https://map.$CF_DOMAIN"
|
||||
echo " - Influence: https://influence.$CF_DOMAIN"
|
||||
echo " - Mini QR: https://qr.$CF_DOMAIN"
|
||||
echo ""
|
||||
echo "Protected services (requires login with $ADMIN_EMAIL):"
|
||||
echo " - Dashboard: https://homepage.$CF_DOMAIN"
|
||||
echo " - Code Server: https://code.$CF_DOMAIN"
|
||||
echo ""
|
||||
echo "Cloudflared service status: sudo systemctl status $SERVICE_NAME"
|
||||
echo "View tunnel logs: sudo journalctl -u $SERVICE_NAME -f"
|
||||
echo ""
|
||||
29
scripts/legacy/tunnel-config.yml
Normal file
29
scripts/legacy/tunnel-config.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# Cloudflare Tunnel Configuration for cmlite.org
|
||||
# Generated by Changemaker.lite start-production.sh on Sat Jul 5 09:07:25 PM MDT 2025
|
||||
|
||||
tunnel: 0447884a-8052-41fa-9ff1-f6d16abdc5e1
|
||||
credentials-file: /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/cloudflare/0447884a-8052-41fa-9ff1-f6d16abdc5e1.json
|
||||
ingress:
|
||||
- hostname: homepage.cmlite.org
|
||||
service: http://localhost:3010
|
||||
- hostname: code.cmlite.org
|
||||
service: http://localhost:8888
|
||||
- hostname: listmonk.cmlite.org
|
||||
service: http://localhost:9001
|
||||
- hostname: docs.cmlite.org
|
||||
service: http://localhost:4000
|
||||
- hostname: cmlite.org
|
||||
service: http://localhost:4002
|
||||
- hostname: n8n.cmlite.org
|
||||
service: http://localhost:5678
|
||||
- hostname: db.cmlite.org
|
||||
service: http://localhost:8090
|
||||
- hostname: git.cmlite.org
|
||||
service: http://localhost:3030
|
||||
- hostname: map.cmlite.org
|
||||
service: http://localhost:3000
|
||||
- hostname: qr.cmlite.org
|
||||
service: http://localhost:8089
|
||||
- hostname: influence.cmlite.org
|
||||
service: http://localhost:3333
|
||||
- service: http_status:404
|
||||
Reference in New Issue
Block a user