#!/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= 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= 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 "$@"