Files
stack/examples/calibr/setup.sh
Jason Woltje a5416e4a66 fix(#180): Update pnpm to 10.27.0 in Dockerfiles
Updated pnpm version from 10.19.0 to 10.27.0 to fix HIGH severity
vulnerabilities (CVE-2025-69262, CVE-2025-69263, CVE-2025-6926).

Changes:
- apps/api/Dockerfile: line 8
- apps/web/Dockerfile: lines 8 and 81

Fixes #180
2026-02-01 20:52:43 -06:00

1337 lines
44 KiB
Bash
Executable File

#!/bin/bash
# Calibr Setup Wizard
# Interactive installer for Calibr sports betting prediction system
# Supports both native Python and Docker deployments
set -e
# ============================================================================
# Configuration
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Set up logging
LOG_DIR="$PROJECT_ROOT/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/setup-$(date +%Y%m%d_%H%M%S).log"
# Redirect stdout/stderr to both console and log file
exec > >(tee -a "$LOG_FILE") 2>&1
# Send trace output (set -x) ONLY to log file on fd 3
exec 3>>"$LOG_FILE"
export BASH_XTRACEFD=3
# Enable verbose command tracing (only goes to log file now)
set -x
echo "==================================================================="
echo "Calibr Setup Wizard"
echo "Started: $(date)"
echo "Full log: $LOG_FILE"
echo "==================================================================="
echo ""
# Source common functions
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR/lib/common.sh"
# ============================================================================
# Global Variables
# ============================================================================
NON_INTERACTIVE=false
DRY_RUN=false
MODE=""
ODDS_API_KEY=""
ENABLE_SSO=false
ENABLE_TELEGRAM=false
ENABLE_ML=false
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""
USE_BUNDLED_AUTHENTIK=false
EXTERNAL_AUTHENTIK_URL=""
CALIBR_BASE_URL=""
CALIBR_ALLOWED_HOSTS=""
AUTHENTIK_BASE_URL=""
DETECTED_OS=""
DETECTED_PKG_MANAGER=""
PORT_OVERRIDES=()
# ============================================================================
# Help and Usage
# ============================================================================
show_help() {
cat << EOF
Calibr Setup Wizard
USAGE:
$0 [OPTIONS]
OPTIONS:
-h, --help Show this help message
--non-interactive Run in non-interactive mode (requires all options)
--dry-run Show what would happen without executing
--mode MODE Deployment mode: native or docker
--odds-api-key KEY The Odds API key (required)
--enable-sso Enable Authentik SSO (Docker mode only)
--bundled-authentik Use bundled Authentik server (with --enable-sso)
--external-authentik URL Use external Authentik server URL (with --enable-sso)
--enable-telegram Enable Telegram bot notifications
--enable-ml Enable ML prediction models
--telegram-token TOKEN Telegram bot token (if --enable-telegram)
--telegram-chat-id ID Telegram chat ID (if --enable-telegram)
EXAMPLES:
# Interactive mode (recommended for first-time setup)
$0
# Non-interactive Docker deployment with bundled SSO
$0 --non-interactive --mode docker --odds-api-key "abc123" --enable-sso --bundled-authentik
# Non-interactive Docker with external Authentik
$0 --non-interactive --mode docker --odds-api-key "abc123" --enable-sso --external-authentik "https://auth.example.com"
# Non-interactive native deployment with ML models
$0 --non-interactive --mode native --odds-api-key "abc123" --enable-ml
# Dry run to see what would happen
$0 --dry-run --mode docker --odds-api-key "abc123" --enable-sso --bundled-authentik
EOF
}
# ============================================================================
# Argument Parsing
# ============================================================================
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
--non-interactive)
NON_INTERACTIVE=true
;;
--dry-run)
DRY_RUN=true
;;
--mode)
MODE="$2"
shift
;;
--odds-api-key)
ODDS_API_KEY="$2"
shift
;;
--enable-sso)
ENABLE_SSO=true
;;
--bundled-authentik)
USE_BUNDLED_AUTHENTIK=true
;;
--external-authentik)
EXTERNAL_AUTHENTIK_URL="$2"
shift
;;
--enable-telegram)
ENABLE_TELEGRAM=true
;;
--enable-ml)
ENABLE_ML=true
;;
--telegram-token)
TELEGRAM_BOT_TOKEN="$2"
shift
;;
--telegram-chat-id)
TELEGRAM_CHAT_ID="$2"
shift
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
shift
done
# Validate non-interactive mode
if [[ "$NON_INTERACTIVE" == true ]]; then
if [[ -z "$MODE" || -z "$ODDS_API_KEY" ]]; then
print_error "Non-interactive mode requires --mode and --odds-api-key"
exit 1
fi
if [[ "$MODE" != "native" && "$MODE" != "docker" ]]; then
print_error "Invalid mode: $MODE (must be 'native' or 'docker')"
exit 1
fi
if [[ "$ENABLE_TELEGRAM" == true ]]; then
if [[ -z "$TELEGRAM_BOT_TOKEN" || -z "$TELEGRAM_CHAT_ID" ]]; then
print_error "Telegram enabled but --telegram-token or --telegram-chat-id missing"
exit 1
fi
fi
fi
}
# ============================================================================
# Welcome Banner
# ============================================================================
show_banner() {
if [[ "$NON_INTERACTIVE" == true ]]; then
return
fi
cat << "EOF"
____ _ _ _
/ ___|__ _| (_) |__ _ __
| | / _` | | | '_ \| '__|
| |__| (_| | | | |_) | |
\____\__,_|_|_|_.__/|_|
Sports Betting Prediction System
EOF
echo "Welcome to the Calibr Setup Wizard!"
echo ""
echo "This wizard will guide you through setting up Calibr on your system."
echo "You can choose between native Python or Docker deployment, and configure"
echo "optional features like Authentik SSO and Telegram notifications."
echo ""
}
# ============================================================================
# Platform Detection
# ============================================================================
detect_platform() {
print_header "Detecting Platform"
DETECTED_OS=$(detect_os)
DETECTED_PKG_MANAGER=$(detect_package_manager "$DETECTED_OS")
local os_name
os_name=$(get_os_name "$DETECTED_OS")
if [[ "$DETECTED_OS" == "unknown" ]]; then
print_warning "Could not detect operating system"
print_info "Detected OS type: $OSTYPE"
echo ""
if [[ "$NON_INTERACTIVE" == true ]]; then
print_error "Cannot proceed in non-interactive mode on unknown OS"
exit 1
fi
if ! confirm "Continue anyway?"; then
exit 1
fi
else
print_success "Detected: $os_name ($DETECTED_PKG_MANAGER)"
fi
}
# ============================================================================
# Mode Selection
# ============================================================================
select_deployment_mode() {
if [[ -n "$MODE" ]]; then
print_header "Deployment Mode"
print_info "Using mode: $MODE"
return
fi
print_header "Deployment Mode"
echo ""
echo "How would you like to run Calibr?"
echo ""
echo " 1) Native Python"
echo " - Best for development and testing"
echo " - Runs directly on your system Python"
echo " - Uses SQLite database by default"
echo " - Easier to debug and modify"
echo ""
echo " 2) Docker"
echo " - Best for production deployment"
echo " - Isolated environment with all dependencies"
echo " - Includes PostgreSQL, Redis, Celery workers"
echo " - Optional Authentik SSO integration"
echo ""
local selection
selection=$(select_option "Select deployment mode:" \
"Native Python (development)" \
"Docker (production)")
if [[ "$selection" == *"Docker"* ]]; then
MODE="docker"
else
MODE="native"
fi
print_success "Selected: $MODE mode"
}
# ============================================================================
# Dependency Checking
# ============================================================================
check_and_install_dependencies() {
if ! check_dependencies "$MODE" "$DETECTED_PKG_MANAGER"; then
if [[ "$NON_INTERACTIVE" == true ]]; then
print_error "Dependency check failed in non-interactive mode"
exit 1
fi
echo ""
if confirm "Would you like to install missing dependencies?"; then
install_missing_dependencies
else
print_error "Cannot proceed without required dependencies"
exit 1
fi
fi
# Check port conflicts for Docker mode
if [[ "$MODE" == "docker" ]]; then
check_port_conflicts || {
print_warning "Port conflict check failed, continuing anyway"
}
fi
}
check_port_conflicts() {
local result
result=$(check_docker_ports "$MODE" "$ENABLE_SSO" 2>&1)
local port_check_result=$?
# If no conflicts, return early
if [[ $port_check_result -eq 0 ]]; then
return 0
fi
# Check if we got valid output
if [[ ! "$result" =~ CONFLICTS:.*SUGGESTIONS: ]]; then
print_warning "Port check returned unexpected output, skipping conflict resolution"
return 0
fi
# Parse conflicts and suggestions
local conflicts_part="${result#*CONFLICTS:}"
conflicts_part="${conflicts_part%%|*}"
local suggestions_part="${result#*SUGGESTIONS:}"
suggestions_part="${suggestions_part%%$'\n'*}"
# Convert to arrays, handling empty cases
local conflicts=()
local suggestions=()
if [[ -n "$conflicts_part" ]]; then
IFS='~' read -ra conflicts <<< "$conflicts_part"
fi
if [[ -n "$suggestions_part" ]]; then
IFS='~' read -ra suggestions <<< "$suggestions_part"
fi
# Only show if we have actual conflicts
if [[ ${#conflicts[@]} -eq 0 ]]; then
return 0
fi
echo ""
print_warning "Port conflicts detected!"
echo ""
echo "The following ports are already in use:"
for conflict in "${conflicts[@]}"; do
echo " - $conflict"
done
echo ""
if [[ "$NON_INTERACTIVE" == true ]]; then
print_error "Port conflicts in non-interactive mode. Please free these ports or use --dry-run to see alternatives."
exit 1
fi
echo "Suggested alternative port configuration:"
for suggestion in "${suggestions[@]}"; do
echo " $suggestion"
done
echo ""
if confirm "Use alternative ports automatically?"; then
# Store suggestions for use in generate_env_file
PORT_OVERRIDES=("${suggestions[@]}")
print_success "Will use alternative ports"
else
print_error "Cannot proceed with port conflicts"
echo ""
echo "Please either:"
echo " 1. Stop services using these ports"
echo " 2. Run with --dry-run to see alternatives and configure manually"
exit 1
fi
}
install_missing_dependencies() {
print_header "Installing Dependencies"
check_sudo
if [[ "$MODE" == "docker" ]]; then
# Install Docker dependencies
if ! check_command docker; then
local docker_pkg
docker_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker")
if [[ -n "$docker_pkg" ]]; then
install_package "$DETECTED_PKG_MANAGER" "$docker_pkg"
# Start Docker service
case "$DETECTED_PKG_MANAGER" in
pacman|dnf)
print_step "Starting Docker service..."
sudo systemctl enable --now docker
;;
esac
# Add user to docker group
print_step "Adding user to docker group..."
sudo usermod -aG docker "$USER"
print_warning "You may need to log out and back in for docker group membership to take effect"
print_info "Or run: newgrp docker"
fi
fi
if ! check_docker_compose; then
local compose_pkg
compose_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker-compose")
if [[ -n "$compose_pkg" ]]; then
install_package "$DETECTED_PKG_MANAGER" "$compose_pkg"
fi
fi
if ! check_docker_buildx; then
print_warning "Docker Buildx not found. Attempting to install..."
docker buildx install 2>/dev/null || true
fi
else
# Install Python dependencies
if ! check_python "3.10"; then
local python_pkg
python_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "python3")
if [[ -n "$python_pkg" ]]; then
install_package "$DETECTED_PKG_MANAGER" "$python_pkg"
fi
fi
if ! check_python_venv; then
local venv_pkg
venv_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "python3-venv")
if [[ -n "$venv_pkg" ]]; then
install_package "$DETECTED_PKG_MANAGER" "$venv_pkg"
fi
fi
if ! check_pip; then
local pip_pkg
pip_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "python3-pip")
if [[ -n "$pip_pkg" ]]; then
install_package "$DETECTED_PKG_MANAGER" "$pip_pkg"
fi
fi
fi
# Re-check dependencies
echo ""
if ! check_dependencies "$MODE" "$DETECTED_PKG_MANAGER"; then
print_error "Dependency installation failed"
exit 1
fi
}
# ============================================================================
# SSO Configuration
# ============================================================================
configure_sso_from_scratch() {
echo ""
echo "Authentik SSO Setup Options:"
echo ""
echo " 1) Bundled Authentik (included with Docker)"
echo " - Runs on http://localhost:9000"
echo " - Automatically configured"
echo " - Requires ~500MB additional disk space"
echo " - Recommended for new deployments"
echo ""
echo " 2) External Authentik (use existing instance)"
echo " - Connect to your own Authentik server"
echo " - Requires manual OAuth2 provider setup"
echo ""
local sso_choice
sso_choice=$(select_option "Select SSO configuration:" \
"Bundled Authentik (recommended)" \
"External Authentik")
if [[ "$sso_choice" == *"Bundled"* ]]; then
ENABLE_SSO=true
USE_BUNDLED_AUTHENTIK=true
print_success "Will use bundled Authentik server"
# Ask about Authentik URL configuration
echo ""
local authentik_url_choice
authentik_url_choice=$(select_option "How will users access Authentik?" \
"localhost (http://localhost:PORT)" \
"Custom domain (https://auth.example.com)")
if [[ "$authentik_url_choice" == *"localhost"* ]]; then
AUTHENTIK_BASE_URL="http://localhost:\${AUTHENTIK_PORT:-9000}"
print_info "Authentik will be available at http://localhost:9000"
print_info "Initial admin setup: http://localhost:9000/if/flow/initial-setup/"
else
echo ""
local authentik_domain
while [[ -z "$authentik_domain" ]]; do
read -r -p "Enter Authentik domain (e.g., auth.example.com): " authentik_domain
if [[ -z "$authentik_domain" ]]; then
print_error "Domain cannot be empty"
fi
done
local authentik_protocol
if confirm "Use HTTPS? (recommended for production)" "y"; then
authentik_protocol="https"
else
authentik_protocol="http"
fi
AUTHENTIK_BASE_URL="${authentik_protocol}://${authentik_domain}"
print_success "Authentik will be available at: $AUTHENTIK_BASE_URL"
print_info "Make sure your reverse proxy forwards to localhost:\${AUTHENTIK_PORT:-9000}"
fi
else
ENABLE_SSO=true
USE_BUNDLED_AUTHENTIK=false
echo ""
echo "External Authentik Configuration"
echo ""
while [[ -z "$EXTERNAL_AUTHENTIK_URL" ]]; do
read -r -p "Enter your Authentik server URL (e.g., https://auth.example.com): " EXTERNAL_AUTHENTIK_URL
if ! validate_url "$EXTERNAL_AUTHENTIK_URL"; then
print_error "Invalid URL format. Please try again."
EXTERNAL_AUTHENTIK_URL=""
fi
done
# Remove trailing slash
EXTERNAL_AUTHENTIK_URL="${EXTERNAL_AUTHENTIK_URL%/}"
print_success "Will connect to external Authentik at $EXTERNAL_AUTHENTIK_URL"
print_warning "You'll need to configure OAuth2 provider in Authentik admin"
fi
}
# ============================================================================
# Configuration Collection
# ============================================================================
load_existing_env() {
if [[ -f "$PROJECT_ROOT/.env" ]]; then
print_header "Detecting Existing Configuration"
parse_env_file "$PROJECT_ROOT/.env"
print_success "Found existing .env file"
return 0
fi
return 1
}
collect_configuration() {
print_header "Configuration"
# Try to load existing .env
local has_existing_env=false
if load_existing_env; then
has_existing_env=true
echo ""
print_info "Found existing configuration. I'll ask about each setting."
echo ""
fi
# Odds API Key (required)
if [[ -z "$ODDS_API_KEY" ]]; then
local existing_key
existing_key=$(get_env_value "ODDS_API_KEY")
if [[ -n "$existing_key" ]] && ! is_placeholder "$existing_key"; then
# Show masked existing value
local masked
masked=$(mask_value "$existing_key")
echo "The Odds API Key: $masked (existing)"
if confirm "Keep this API key?" "y"; then
ODDS_API_KEY="$existing_key"
print_success "Using existing Odds API key"
else
while true; do
read -r -p "Enter new The Odds API key: " ODDS_API_KEY
if validate_api_key "$ODDS_API_KEY"; then
break
fi
print_error "Invalid API key format. Please try again."
done
fi
else
echo ""
echo "The Odds API is required for fetching sports odds data."
echo "Get your free API key at: https://the-odds-api.com/"
echo ""
while true; do
read -r -p "Enter your The Odds API key: " ODDS_API_KEY
if validate_api_key "$ODDS_API_KEY"; then
break
fi
print_error "Invalid API key format. Please try again."
done
fi
fi
if [[ -z "$ODDS_API_KEY" ]] || is_placeholder "$ODDS_API_KEY"; then
print_error "Valid Odds API key is required"
exit 1
fi
print_success "Odds API key configured"
# URL Configuration (Docker only)
if [[ "$MODE" == "docker" ]]; then
echo ""
echo "URL Configuration"
echo ""
echo "You can configure Calibr to be accessed via:"
echo " - localhost (development/testing)"
echo " - Custom domain with reverse proxy (production)"
echo ""
local url_choice
url_choice=$(select_option "How will users access Calibr?" \
"localhost (http://localhost:PORT)" \
"Custom domain (https://calibr.example.com)")
if [[ "$url_choice" == *"localhost"* ]]; then
# Use localhost with port
CALIBR_BASE_URL="http://localhost:\${WEB_PORT_PROD:-8001}"
CALIBR_ALLOWED_HOSTS="localhost,127.0.0.1"
print_success "Using localhost configuration"
else
# Custom domain
echo ""
local calibr_domain
while [[ -z "$calibr_domain" ]]; do
read -r -p "Enter Calibr domain (e.g., calibr.example.com): " calibr_domain
if [[ -z "$calibr_domain" ]]; then
print_error "Domain cannot be empty"
fi
done
local calibr_protocol
if confirm "Use HTTPS? (recommended for production)" "y"; then
calibr_protocol="https"
else
calibr_protocol="http"
fi
CALIBR_BASE_URL="${calibr_protocol}://${calibr_domain}"
CALIBR_ALLOWED_HOSTS="${calibr_domain},localhost,127.0.0.1"
print_success "Using custom domain: $CALIBR_BASE_URL"
print_info "Make sure your reverse proxy forwards to localhost:\${WEB_PORT_PROD:-8001}"
fi
fi
# SSO (Docker only)
if [[ "$MODE" == "docker" ]] && [[ "$ENABLE_SSO" == false ]]; then
# Check for existing Authentik configuration
local existing_oidc_endpoint
existing_oidc_endpoint=$(get_env_value "OIDC_OP_AUTHORIZATION_ENDPOINT")
if [[ -n "$existing_oidc_endpoint" ]] && ! is_placeholder "$existing_oidc_endpoint"; then
echo ""
echo "Existing Authentik SSO configuration detected:"
echo " Endpoint: $existing_oidc_endpoint"
if confirm "Keep existing SSO configuration?" "y"; then
ENABLE_SSO=true
print_success "Using existing SSO configuration"
else
configure_sso_from_scratch
fi
else
echo ""
echo "Authentik SSO provides centralized authentication and user management."
echo "This is optional and can be configured later if needed."
echo ""
if confirm "Enable Authentik SSO integration?"; then
ENABLE_SSO=true
configure_sso_from_scratch
else
print_info "SSO will be disabled (can be enabled later)"
fi
fi
fi
# Telegram Bot
if [[ "$ENABLE_TELEGRAM" == false ]] && [[ "$NON_INTERACTIVE" == false ]]; then
# Check for existing Telegram config
local existing_token existing_chat_id
existing_token=$(get_env_value "TELEGRAM_BOT_TOKEN")
existing_chat_id=$(get_env_value "TELEGRAM_CHAT_ID")
# If we have valid existing values, ask if they want to keep
if [[ -n "$existing_token" ]] && ! is_placeholder "$existing_token" && \
[[ -n "$existing_chat_id" ]] && ! is_placeholder "$existing_chat_id"; then
echo ""
local masked_token masked_chat
masked_token=$(mask_value "$existing_token")
masked_chat=$(mask_value "$existing_chat_id")
echo "Telegram Bot Token: $masked_token (existing)"
echo "Telegram Chat ID: $masked_chat (existing)"
if confirm "Keep existing Telegram configuration?" "y"; then
ENABLE_TELEGRAM=true
TELEGRAM_BOT_TOKEN="$existing_token"
TELEGRAM_CHAT_ID="$existing_chat_id"
print_success "Using existing Telegram configuration"
else
if confirm "Configure new Telegram bot?"; then
ENABLE_TELEGRAM=true
while [[ -z "$TELEGRAM_BOT_TOKEN" ]]; do
echo ""
echo "Get your bot token from @BotFather on Telegram"
read -r -p "Enter Telegram bot token: " TELEGRAM_BOT_TOKEN
done
while [[ -z "$TELEGRAM_CHAT_ID" ]]; do
echo ""
echo "Get your chat ID from @userinfobot on Telegram"
read -r -p "Enter Telegram chat ID: " TELEGRAM_CHAT_ID
done
print_success "Telegram bot configured"
else
print_info "Telegram bot will be disabled"
fi
fi
else
echo ""
echo "Telegram bot enables real-time notifications for picks and updates."
echo ""
if confirm "Enable Telegram bot notifications?"; then
ENABLE_TELEGRAM=true
while [[ -z "$TELEGRAM_BOT_TOKEN" ]]; do
echo ""
echo "Get your bot token from @BotFather on Telegram"
read -r -p "Enter Telegram bot token: " TELEGRAM_BOT_TOKEN
done
while [[ -z "$TELEGRAM_CHAT_ID" ]]; do
echo ""
echo "Get your chat ID from @userinfobot on Telegram"
read -r -p "Enter Telegram chat ID: " TELEGRAM_CHAT_ID
done
print_success "Telegram bot configured"
else
print_info "Telegram bot will be disabled (can be enabled later)"
fi
fi
fi
# ML Models
if [[ "$ENABLE_ML" == false ]] && [[ "$NON_INTERACTIVE" == false ]]; then
echo ""
echo "ML prediction models (XGBoost, LightGBM) enhance prediction accuracy."
echo "This increases installation time and requires ~500MB additional disk space."
echo ""
if confirm "Enable ML prediction models?"; then
ENABLE_ML=true
print_success "ML models will be installed"
else
print_info "ML models will be disabled (can be enabled later)"
fi
fi
}
# ============================================================================
# Environment File Generation
# ============================================================================
generate_env_file() {
print_header "Generating Configuration"
cd "$PROJECT_ROOT" || exit 1
# Backup existing .env
if [[ -f .env ]]; then
backup_file .env
fi
# Preserve or generate secrets
local django_secret existing_django_secret
existing_django_secret=$(get_env_value "DJANGO_SECRET_KEY")
if [[ -n "$existing_django_secret" ]] && ! is_placeholder "$existing_django_secret"; then
django_secret="$existing_django_secret"
print_info "Preserving existing Django secret key"
else
django_secret=$(generate_secret 50)
print_info "Generated new Django secret key"
fi
# Preserve or generate DB password
local db_password existing_db_password
existing_db_password=$(get_env_value "DB_PASSWORD")
if [[ -n "$existing_db_password" ]] && ! is_placeholder "$existing_db_password" && [[ "$MODE" == "docker" ]]; then
db_password="$existing_db_password"
print_info "Preserving existing database password"
else
db_password=$(generate_secret 32)
fi
# Generate admin password (always new for security)
local admin_password
admin_password=$(generate_secret 16)
# Preserve Ollama configuration if exists
local ollama_url ollama_model ollama_timeout
ollama_url=$(get_env_value "OLLAMA_BASE_URL")
ollama_model=$(get_env_value "OLLAMA_MODEL")
ollama_timeout=$(get_env_value "OLLAMA_TIMEOUT")
# Preserve Authentik secrets if exist
local authentik_secret authentik_db_password
authentik_secret=$(get_env_value "AUTHENTIK_SECRET_KEY")
authentik_db_password=$(get_env_value "AUTHENTIK_POSTGRES_PASSWORD")
# Create .env file
cat > .env << EOF
# Calibr Configuration
# Generated by setup.sh on $(date)
# ============================================================================
# API Keys
# ============================================================================
# The Odds API (required)
ODDS_API_KEY=$ODDS_API_KEY
EOF
# Add Telegram config if enabled
if [[ "$ENABLE_TELEGRAM" == true ]]; then
cat >> .env << EOF
# Telegram Bot (optional)
TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN
TELEGRAM_CHAT_ID=$TELEGRAM_CHAT_ID
EOF
fi
# Add Django config
cat >> .env << EOF
# ============================================================================
# Django Configuration
# ============================================================================
DJANGO_SECRET_KEY=$django_secret
DJANGO_DEBUG=false
DJANGO_ALLOWED_HOSTS=${CALIBR_ALLOWED_HOSTS:-localhost,127.0.0.1}
EOF
# Add Ollama config if exists
if [[ -n "$ollama_url" ]] && ! is_placeholder "$ollama_url"; then
cat >> .env << EOF
# ============================================================================
# LLM Configuration (Ollama)
# ============================================================================
OLLAMA_BASE_URL=$ollama_url
EOF
if [[ -n "$ollama_model" ]] && ! is_placeholder "$ollama_model"; then
echo "OLLAMA_MODEL=$ollama_model" >> .env
fi
if [[ -n "$ollama_timeout" ]] && ! is_placeholder "$ollama_timeout"; then
echo "OLLAMA_TIMEOUT=$ollama_timeout" >> .env
fi
echo "" >> .env
print_info "Preserved Ollama LLM configuration"
fi
# Add Docker-specific config
if [[ "$MODE" == "docker" ]]; then
# Extract port overrides if they exist
local web_port=8001
local web_port_dev=8000
local postgres_port=5433
local valkey_port=6380
local authentik_port=9000
local authentik_https_port=9443
for override in "${PORT_OVERRIDES[@]}"; do
local key="${override%%=*}"
local value="${override#*=}"
case "$key" in
WEB_PORT_PROD) web_port="$value" ;;
WEB_PORT) web_port_dev="$value" ;;
POSTGRES_PORT) postgres_port="$value" ;;
VALKEY_PORT) valkey_port="$value" ;;
AUTHENTIK_PORT) authentik_port="$value" ;;
AUTHENTIK_PORT_HTTPS) authentik_https_port="$value" ;;
esac
done
cat >> .env << EOF
# ============================================================================
# Port Configuration
# ============================================================================
# Web application ports (external)
WEB_PORT=$web_port_dev
WEB_PORT_PROD=$web_port
# Database port (external)
POSTGRES_PORT=$postgres_port
# Cache port (external)
VALKEY_PORT=$valkey_port
# ============================================================================
# Application URLs
# ============================================================================
# Base URL for Calibr web application
CALIBR_BASE_URL=${CALIBR_BASE_URL:-http://localhost:$web_port}
# Base URL for Authentik server (if using SSO)
AUTHENTIK_BASE_URL=${AUTHENTIK_BASE_URL:-http://localhost:\${AUTHENTIK_PORT:-9000}}
EOF
if [[ "$ENABLE_SSO" == true ]]; then
cat >> .env << EOF
# Authentik ports (external)
AUTHENTIK_PORT=$authentik_port
AUTHENTIK_PORT_HTTPS=$authentik_https_port
EOF
fi
cat >> .env << EOF
# ============================================================================
# Database (PostgreSQL for Docker)
# ============================================================================
DB_ENGINE=postgres
DB_NAME=calibr
DB_USER=calibr
DB_PASSWORD=$db_password
DB_HOST=db
DB_PORT=5432
# ============================================================================
# Cache (Redis/Valkey)
# ============================================================================
REDIS_URL=redis://valkey:6379/0
CELERY_BROKER_URL=redis://valkey:6379/1
EOF
# Add SSO config if enabled
if [[ "$ENABLE_SSO" == true ]]; then
# Always generate new secrets or preserve existing valid ones
if [[ -z "$authentik_secret" ]] || is_placeholder "$authentik_secret" || [[ ${#authentik_secret} -lt 20 ]]; then
authentik_secret=$(generate_secret 60)
print_info "Generated new Authentik secret key (60 chars)"
else
print_info "Preserved existing Authentik secret key"
fi
if [[ -z "$authentik_db_password" ]] || is_placeholder "$authentik_db_password" || [[ ${#authentik_db_password} -lt 16 ]]; then
authentik_db_password=$(generate_secret 32)
print_info "Generated new Authentik database password (32 chars)"
else
print_info "Preserved existing Authentik database password"
fi
# Ensure secrets are not empty (failsafe)
if [[ -z "$authentik_secret" ]]; then
authentik_secret=$(generate_secret 60)
print_warning "Failsafe: Generated Authentik secret key"
fi
if [[ -z "$authentik_db_password" ]]; then
authentik_db_password=$(generate_secret 32)
print_warning "Failsafe: Generated Authentik database password"
fi
# Set OIDC endpoints based on configuration
local auth_base_url
local auth_type="bundled"
if [[ -n "$AUTHENTIK_BASE_URL" ]]; then
# Use the URL configured during setup
auth_base_url="$AUTHENTIK_BASE_URL"
if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then
print_info "Configured bundled Authentik endpoint: $auth_base_url"
else
auth_type="external"
print_info "Configured external Authentik endpoint: $auth_base_url"
fi
elif [[ -n "$EXTERNAL_AUTHENTIK_URL" ]]; then
# External Authentik
auth_type="external"
auth_base_url="$EXTERNAL_AUTHENTIK_URL"
print_info "Configured for external Authentik: $auth_base_url"
else
# Preserve existing or use default
local existing_endpoint
existing_endpoint=$(get_env_value "OIDC_OP_AUTHORIZATION_ENDPOINT")
if [[ -n "$existing_endpoint" ]] && ! is_placeholder "$existing_endpoint"; then
# Extract base URL from existing endpoint
auth_base_url=$(echo "$existing_endpoint" | sed -E 's|(https?://[^/]+).*|\1|')
print_info "Preserved existing Authentik endpoint ($auth_base_url)"
else
auth_base_url="http://localhost:9000"
print_info "Using default bundled Authentik (localhost:9000)"
fi
fi
# Write Authentik configuration
print_info "Writing Authentik secrets to .env (secret: ${#authentik_secret} chars, password: ${#authentik_db_password} chars)"
cat >> .env << EOF
# ============================================================================
# Authentik SSO
# ============================================================================
# Authentik server secrets (required for SSO)
AUTHENTIK_SECRET_KEY=$authentik_secret
AUTHENTIK_POSTGRES_PASSWORD=$authentik_db_password
# OAuth2 / OIDC Configuration
# OIDC will be disabled until you complete Authentik setup
OIDC_ENABLED=false
OIDC_RP_CLIENT_ID=calibr
OIDC_RP_CLIENT_SECRET=change-after-authentik-setup
# Authentik endpoints (automatically configured)
OIDC_OP_AUTHORIZATION_ENDPOINT=$auth_base_url/application/o/authorize/
OIDC_OP_TOKEN_ENDPOINT=$auth_base_url/application/o/token/
OIDC_OP_USER_ENDPOINT=$auth_base_url/application/o/userinfo/
OIDC_OP_JWKS_ENDPOINT=$auth_base_url/application/o/calibr/jwks/
OIDC_OP_LOGOUT_ENDPOINT=$auth_base_url/application/o/calibr/end-session/
EOF
fi
else
cat >> .env << EOF
# ============================================================================
# Database (SQLite for Native)
# ============================================================================
DB_ENGINE=sqlite
DATABASE_PATH=data/db.sqlite3
EOF
fi
# Add ML config if enabled
if [[ "$ENABLE_ML" == true ]]; then
cat >> .env << EOF
# ============================================================================
# Machine Learning Models
# ============================================================================
ENABLE_ML_MODELS=true
EOF
fi
chmod 600 .env
print_success "Created .env file"
# Save credentials
if [[ "$MODE" == "docker" ]]; then
cat > .admin-credentials << EOF
Calibr Admin Credentials
Generated: $(date)
Web App: http://localhost:8000
Admin Username: admin
Admin Password: $admin_password
Database Password: $db_password
EOF
chmod 600 .admin-credentials
print_success "Saved credentials to .admin-credentials"
fi
}
# ============================================================================
# Deployment
# ============================================================================
setup_authentik_blueprint() {
if [[ "$ENABLE_SSO" != true ]] || [[ "$USE_BUNDLED_AUTHENTIK" != true ]]; then
return
fi
print_header "Configuring Authentik Blueprint"
# Create blueprints directory
mkdir -p "$PROJECT_ROOT/docker/authentik-blueprints"
# Copy blueprint file
if [[ -f "$PROJECT_ROOT/docker/authentik-blueprint-calibr.yaml" ]]; then
cp "$PROJECT_ROOT/docker/authentik-blueprint-calibr.yaml" \
"$PROJECT_ROOT/docker/authentik-blueprints/calibr.yaml"
print_success "Blueprint configured for auto-import"
print_info "OAuth2 provider will be created automatically on Authentik first start"
else
print_warning "Blueprint template not found, skipping auto-configuration"
fi
# Generate docker-compose override for blueprint mounting
if [[ ! -f "$PROJECT_ROOT/docker-compose.authentik.yml" ]]; then
cat > "$PROJECT_ROOT/docker-compose.authentik.yml" << 'EOF'
# Auto-generated by setup.sh - Authentik blueprint auto-import
# Include with: docker compose -f docker-compose.yml -f docker-compose.authentik.yml up -d
version: '3.8'
services:
authentik_server:
volumes:
- ./docker/authentik-blueprints:/blueprints/custom:ro
authentik_worker:
volumes:
- ./docker/authentik-blueprints:/blueprints/custom:ro
EOF
print_success "Created docker-compose.authentik.yml for blueprint mounting"
print_info "Blueprints will be auto-imported on Authentik startup"
fi
}
run_deployment() {
if [[ "$DRY_RUN" == true ]]; then
print_header "Dry Run - Deployment"
print_info "Would execute: $MODE deployment"
return
fi
print_header "Deploying Calibr"
cd "$PROJECT_ROOT" || exit 1
# Setup Authentik blueprint if SSO enabled
if [[ "$MODE" == "docker" ]]; then
setup_authentik_blueprint
fi
if [[ "$MODE" == "native" ]]; then
print_step "Running native Python installer..."
"$SCRIPT_DIR/install.sh"
else
print_step "Running Docker quick-start..."
# Pass --with-sso flag if SSO enabled
if [[ "$ENABLE_SSO" == true ]]; then
"$PROJECT_ROOT/docker/quick-start.sh" --with-sso
else
"$PROJECT_ROOT/docker/quick-start.sh"
fi
fi
}
# ============================================================================
# Post-Installation
# ============================================================================
show_post_install_info() {
print_header "Installation Complete!"
echo ""
echo "Calibr has been successfully installed!"
echo ""
if [[ "$MODE" == "docker" ]]; then
# Get ports from .env or use defaults
local web_port
local authentik_port
web_port=$(get_env_value "WEB_PORT")
web_port="${web_port:-8000}"
authentik_port=$(get_env_value "AUTHENTIK_PORT")
authentik_port="${authentik_port:-9000}"
cat << EOF
🌐 Web Application: http://localhost:$web_port
👤 Admin Login: Check .admin-credentials file
📋 Next Steps:
1. Access the web app at http://localhost:8000
2. Log in with credentials from .admin-credentials
3. Configure league thresholds in Django admin
4. View predictions and place bets!
🔧 Managing Services:
Start: docker compose up -d
Stop: docker compose down
Logs: docker compose logs -f
Restart: docker compose restart
EOF
if [[ "$ENABLE_SSO" == true ]]; then
if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then
cat << EOF
🔐 Authentik SSO Setup (Bundled Server - Auto-Configured!)
Authentik OAuth2 provider has been pre-configured via blueprint!
You only need to complete initial setup and get the client secret:
Step 1: Create Authentik Admin Account
URL: http://localhost:$authentik_port/if/flow/initial-setup/
Create your Authentik admin account (recommended username: akadmin)
Note: This is separate from Calibr admin - it manages SSO
Step 2: Verify Auto-Configuration
1. Log into Authentik admin: http://localhost:$authentik_port
2. Go to Applications → Applications
3. You should see "Calibr Sports Betting" application (auto-created!)
4. Go to Applications → Providers
5. Click on "calibr" provider
6. Copy the Client Secret (shown once)
Step 3: Enable OIDC in Calibr
1. Edit .env file:
- Set OIDC_ENABLED=true
- Set OIDC_RP_CLIENT_SECRET=<paste-secret-from-step-2>
2. Restart: docker compose restart web
Step 4: Test SSO Login
1. Visit http://localhost:$web_port
2. Click "Login with SSO"
3. Authenticate with your Authentik credentials
4. Grant access to Calibr
✨ The OAuth2 provider and application were created automatically!
You only needed to create the admin account and copy the secret.
📚 Full Setup Guide: docs/06-operations/07-authentik-setup.md
🔧 Blueprint Location: docker/authentik-blueprints/calibr.yaml
EOF
else
cat << EOF
🔐 Authentik SSO Setup (External Server)
You configured an external Authentik server.
Complete the setup:
1. Create OAuth2 provider in your Authentik admin
2. Use Client ID: calibr
3. Add redirect URI: http://localhost:8000/oidc/callback/
4. Copy the Client Secret
5. Update .env:
- Set OIDC_ENABLED=true
- Set OIDC_RP_CLIENT_SECRET=<your-client-secret>
6. Restart: docker compose restart web
📚 Detailed Guide: docs/06-operations/07-authentik-setup.md
EOF
fi
fi
else
cat << EOF
🌐 Web Application:
Start: cd packages/webapp && python manage.py runserver
Access: http://localhost:8000
📋 Next Steps:
1. Activate the virtual environment: source venv/bin/activate
2. Start the web app (see command above)
3. Configure league thresholds in Django admin
4. View predictions and place bets!
🔧 Database:
Location: data/db.sqlite3
Backup: cp data/db.sqlite3 data/db.sqlite3.backup
EOF
fi
cat << EOF
📚 Documentation:
Quick Start: README.md
Full Docs: docs/README.md
Web App: docs/03-user-guide/02-web-app.md
🐛 Troubleshooting:
Issues: docs/03-user-guide/04-troubleshooting.md
Support: https://github.com/noahwoltje/sports_betting_bot/issues
EOF
if [[ "$NON_INTERACTIVE" == false ]]; then
echo ""
if [[ "$MODE" == "docker" ]]; then
if command -v xdg-open >/dev/null 2>&1; then
if confirm "Open web app in browser?"; then
xdg-open http://localhost:8000 2>/dev/null || true
fi
fi
fi
fi
}
# ============================================================================
# Main Execution
# ============================================================================
main() {
parse_arguments "$@"
show_banner
detect_platform
select_deployment_mode
check_and_install_dependencies
collect_configuration
generate_env_file
run_deployment
show_post_install_info
echo ""
print_success "Setup complete!"
echo ""
echo "==================================================================="
echo "Setup completed: $(date)"
echo "Full log saved to: $LOG_FILE"
echo "==================================================================="
}
# Run main function
main "$@"