From 9f8ff41bf4fa79b498a563b17917104b7dc2a2ac Mon Sep 17 00:00:00 2001 From: aki Date: Sat, 26 Apr 2025 17:54:36 +0800 Subject: [PATCH] feat(update-setup)!: Replace update-config.sh with a new script to manage setup --- docker-compose.yml | 2 +- update-config.sh | 49 -- update-setup.sh | 1370 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1371 insertions(+), 50 deletions(-) delete mode 100755 update-config.sh create mode 100755 update-setup.sh diff --git a/docker-compose.yml b/docker-compose.yml index 1aa9001..d34d1c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -519,7 +519,7 @@ services: [sh, -c, "cp -n /app/config/tpl/*.yaml /app/config && node server.js"] labels: - traefik.enable=true - - traefik.http.routers.homepage.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/`) # Changed rule to root + - traefik.http.routers.homepage.rule=Host(`${APP_HOSTNAME}`) && PathPrefix(`/home`) # Changed rule to root - traefik.http.routers.homepage.entrypoints=web # - traefik.http.routers.homepage.priority=10 # Removed priority # Global middleware for setting HTTPS header diff --git a/update-config.sh b/update-config.sh deleted file mode 100755 index b7ce725..0000000 --- a/update-config.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# See https://stackoverflow.com/a/44864004 for the sed GNU/BSD compatible hack - -function update_arr_config { - echo "Updating ${container} configuration..." - until [ -f "${CONFIG_ROOT:-.}"/"$container"/config.xml ]; do sleep 1; done - sed -i.bak "s/<\/UrlBase>/\/$1<\/UrlBase>/" "${CONFIG_ROOT:-.}"/"$container"/config.xml && rm "${CONFIG_ROOT:-.}"/"$container"/config.xml.bak - CONTAINER_NAME_UPPER=$(echo "$container" | tr '[:lower:]' '[:upper:]') - sed -i.bak 's/^'"${CONTAINER_NAME_UPPER}"'_API_KEY=.*/'"${CONTAINER_NAME_UPPER}"'_API_KEY='"$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config.xml)"'/' .env && rm .env.bak - echo "Update of ${container} configuration complete, restarting..." - docker compose restart "$container" -} - -function update_qbittorrent_config { - echo "Updating ${container} configuration..." - docker compose stop "$container" - until [ -f "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf ]; do sleep 1; done - sed -i.bak '/WebUI\\ServerDomains=*/a WebUI\\Password_PBKDF2="@ByteArray(ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==)"' "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf && rm "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf.bak - echo "Update of ${container} configuration complete, restarting..." - docker compose start "$container" -} - -function update_bazarr_config { - echo "Updating ${container} configuration..." - until [ -f "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml ]; do sleep 1; done - sed -i.bak "s/base_url: ''/base_url: '\/$container'/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - sed -i.bak "s/use_radarr: false/use_radarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - until [ -f "${CONFIG_ROOT:-.}"/sonarr/config.xml ]; do sleep 1; done - SONARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/sonarr/config.xml) - sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - until [ -f "${CONFIG_ROOT:-.}"/radarr/config.xml ]; do sleep 1; done - RADARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/radarr/config.xml) - sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak - sed -i.bak 's/^BAZARR_API_KEY=.*/BAZARR_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak - echo "Update of ${container} configuration complete, restarting..." - docker compose restart "$container" -} - -for container in $(docker ps --format '{{.Names}}'); do - if [[ "$container" =~ ^(radarr|sonarr|lidarr|prowlarr)$ ]]; then - update_arr_config "$container" - elif [[ "$container" =~ ^(bazarr)$ ]]; then - update_bazarr_config "$container" - elif [[ "$container" =~ ^(qbittorrent)$ ]]; then - update_qbittorrent_config "$container" - fi -done diff --git a/update-setup.sh b/update-setup.sh new file mode 100755 index 0000000..77d17cf --- /dev/null +++ b/update-setup.sh @@ -0,0 +1,1370 @@ +#!/bin/bash + +# Combined update script for docker-compose-nas +# - Updates .env file from .env.example while preserving values +# - Updates Authelia configuration from example file +# - Configures services with correct paths and API keys +# - Manages Authelia accounts +# - Controls service authentication requirements +# Created: April 26, 2025 + +set -e + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Files +ENV_FILE=".env" +ENV_EXAMPLE=".env.example" +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +ENV_BACKUP=".env.${TIMESTAMP}.bak" +AUTHELIA_CONFIG="authelia/configuration.yml" +AUTHELIA_CONFIG_EXAMPLE="authelia/configuration.example.yml" +AUTHELIA_CONFIG_BACKUP="authelia/configuration.${TIMESTAMP}.bak" +COMPOSE_FILE="docker-compose.yml" +COMPOSE_BACKUP="docker-compose.${TIMESTAMP}.bak" + +# Check if yq is installed +check_yq() { + if ! command -v yq &> /dev/null; then + echo -e "${YELLOW}Warning: 'yq' is not installed. While not required, it provides better YAML handling.${NC}" + echo -e "${YELLOW}Installation instructions: https://github.com/mikefarah/yq#install${NC}" + return 1 + fi + return 0 +} + +# Print section header +print_header() { + echo -e "\n${CYAN}${BOLD}$1${NC}" + echo -e "${CYAN}$(printf '=%.0s' $(seq 1 ${#1}))${NC}" +} + +# Check if a file exists +check_file() { + if [ ! -f "$1" ]; then + echo -e "${RED}Error: $1 doesn't exist${NC}" + return 1 + fi + return 0 +} + +# Function to create a backup +create_backup() { + echo -e "${BLUE}Creating backup of $1 as $2...${NC}" + cp "$1" "$2" +} + +################################################## +# PART 1: Update .env file from .env.example +################################################## + +update_env_file() { + print_header "Environment File Update Tool" + + # Check if files exist + if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}Error: $ENV_FILE doesn't exist${NC}" + echo -e "Creating a new $ENV_FILE from $ENV_EXAMPLE" + cp "$ENV_EXAMPLE" "$ENV_FILE" + echo -e "${GREEN}Done! Please review and fill in required values in $ENV_FILE${NC}" + return 0 + fi + + if ! check_file "$ENV_EXAMPLE"; then + return 1 + fi + + echo -e "${BLUE}This will update your $ENV_FILE based on the structure in $ENV_EXAMPLE${NC}" + echo -e "${BLUE}Your existing values will be preserved where possible${NC}" + echo -e "${BLUE}Backup will be created as: $ENV_BACKUP${NC}" + echo -e "${YELLOW}Continue? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${RED}Environment update cancelled.${NC}" + return 0 + fi + + # Create backup of current .env + create_backup "$ENV_FILE" "$ENV_BACKUP" + + # Store current env values + echo -e "${BLUE}Reading current environment values...${NC}" + declare -A current_values + declare -A current_keys_present + while IFS='=' read -r key value; do + # Skip comments and empty lines + if [[ ! "$key" =~ ^#.*$ ]] && [[ ! -z "$key" ]]; then + # Clean up any comments after the value + value=$(echo "$value" | sed 's/[[:space:]]*#.*$//') + # Trim leading/trailing whitespace + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + # Store in associative array if key is not empty + if [[ ! -z "$key" ]]; then + current_values["$key"]="$value" + # Track that this key existed in original file, regardless of value + current_keys_present["$key"]=1 + fi + fi + done < "$ENV_FILE" + + # Create new env file from example + echo -e "${BLUE}Creating new $ENV_FILE from $ENV_EXAMPLE...${NC}" + cp "$ENV_EXAMPLE" "$ENV_FILE.new" + + # Track which keys from the current env have been used + declare -A used_keys + # Track new keys that need attention + new_keys=() + # Track keys with special warnings + special_keys=() + + # Process the template and fill in values from current env + while IFS= read -r line; do + if [[ "$line" =~ ^([A-Za-z0-9_]+)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + default_value="${BASH_REMATCH[2]}" + + # Mark the key as used if it exists in the original file + if [[ -n "${current_keys_present[$key]}" ]]; then + used_keys["$key"]=1 + + # Replace the line with the current value if one exists + if [[ -n "${current_values[$key]}" ]]; then + sed -i "s|^$key=.*$|$key=${current_values[$key]}|" "$ENV_FILE.new" + fi + # If key doesn't exist in original file and has empty/placeholder value + elif [[ -z "$default_value" ]] || [[ "$default_value" == '""' ]] || [[ "$default_value" == "''" ]]; then + new_keys+=("$key") + + # Special attention for Authelia keys + if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then + special_keys+=("$key") + fi + fi + fi + done < "$ENV_FILE.new" + + # Create section for unused/deprecated keys at the bottom of the file + echo -e "\n\n# --- DEPRECATED OR UNUSED KEYS (Kept for Reference) ---" >> "$ENV_FILE.new" + echo -e "# Keys below were in your original .env but aren't in the current .env.example" >> "$ENV_FILE.new" + echo -e "# They may be deprecated or renamed. Review and remove if no longer needed\n" >> "$ENV_FILE.new" + + unused_keys_count=0 + for key in "${!current_values[@]}"; do + if [[ -z "${used_keys[$key]}" ]]; then + echo "$key=${current_values[$key]} # DEPRECATED/UNUSED - Review" >> "$ENV_FILE.new" + unused_keys_count=$((unused_keys_count + 1)) + fi + done + + # Replace the old file with the new one + mv "$ENV_FILE.new" "$ENV_FILE" + + # Generate summary + echo -e "\n${GREEN}${BOLD}Environment Update Complete!${NC}" + echo -e "${BLUE}Summary:${NC}" + echo -e " - ${CYAN}Original config backed up to: $ENV_BACKUP${NC}" + echo -e " - ${CYAN}Updated .env structure to match .env.example${NC}" + echo -e " - ${CYAN}Preserved ${#used_keys[@]} existing values${NC}" + + if [[ $unused_keys_count -gt 0 ]]; then + echo -e " - ${YELLOW}Found $unused_keys_count deprecated/unused keys${NC}" + echo -e " ${YELLOW}These have been moved to the bottom of the file with warnings${NC}" + fi + + if [[ ${#new_keys[@]} -gt 0 ]]; then + echo -e "\n${YELLOW}${BOLD}NEW KEYS NEEDING ATTENTION:${NC}" + echo -e "${YELLOW}The following keys are new and may need values set:${NC}" + for key in "${new_keys[@]}"; do + echo -e " - ${MAGENTA}$key${NC}" + done + fi + + if [[ ${#special_keys[@]} -gt 0 ]]; then + echo -e "\n${RED}${BOLD}IMPORTANT SECURITY KEYS:${NC}" + echo -e "${RED}The following keys require secure values:${NC}" + for key in "${special_keys[@]}"; do + echo -e " - ${MAGENTA}$key${NC}" + + # Specific advice for Authelia keys + if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then + echo -e " ${CYAN}Generate with: ${GREEN}openssl rand -hex 32${NC}" + fi + done + fi + + echo -e "\n${BLUE}Review your updated $ENV_FILE file and adjust any values as needed.${NC}" +} + +################################################## +# PART 2: Update Authelia configuration +################################################## + +update_authelia_config() { + print_header "Authelia Configuration Update Tool" + + # Check if files exist + if ! check_file "$AUTHELIA_CONFIG_EXAMPLE"; then + echo -e "${RED}Error: Example configuration file '$AUTHELIA_CONFIG_EXAMPLE' doesn't exist${NC}" + return 1 + fi + if ! check_file "$ENV_FILE"; then + echo -e "${RED}Error: Environment file '$ENV_FILE' doesn't exist. Cannot retrieve domain settings.${NC}" + return 1 + fi + + # Get the tailnet domain and hostname from .env + local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") + local TAILSCALE_HOSTNAME=$(grep -oP "^TAILSCALE_HOSTNAME=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") + local FULL_HOSTNAME="${TAILSCALE_HOSTNAME}.${TAILNET_DOMAIN}" + local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}" + + if [ -z "$TAILNET_DOMAIN" ] || [ -z "$TAILSCALE_HOSTNAME" ]; then + echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN or TAILSCALE_HOSTNAME from $ENV_FILE${NC}" + return 1 + fi + + # If config file doesn't exist, create it from example + if [ ! -f "$AUTHELIA_CONFIG" ]; then + echo -e "${YELLOW}Authelia configuration file '$AUTHELIA_CONFIG' doesn't exist, creating from example...${NC}" + cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG" + echo -e "${GREEN}Created new Authelia configuration file.${NC}" + # Proceed to update the newly created file + else + echo -e "${BLUE}This will update your Authelia configuration '$AUTHELIA_CONFIG' based on '$AUTHELIA_CONFIG_EXAMPLE'${NC}" + echo -e "${BLUE}Your Tailscale domain settings from '$ENV_FILE' will be applied.${NC}" + echo -e "${BLUE}Backup will be created as: $AUTHELIA_CONFIG_BACKUP${NC}" + echo -e "${YELLOW}Continue? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${RED}Authelia config update cancelled.${NC}" + return 0 + fi + # Create backup of current config only if it exists + create_backup "$AUTHELIA_CONFIG" "$AUTHELIA_CONFIG_BACKUP" + fi + + # Check for yq + if check_yq; then + echo -e "${BLUE}Using 'yq' to update Authelia configuration...${NC}" + # Create a temporary file from the example + local TEMP_CONFIG="${AUTHELIA_CONFIG}.tmp" + cp "$AUTHELIA_CONFIG_EXAMPLE" "$TEMP_CONFIG" + + # Preserve specific existing values if the original config exists + if [ -f "$AUTHELIA_CONFIG_BACKUP" ]; then # Use backup as source of truth for existing values + echo -e "${BLUE}Attempting to preserve existing secrets and notifier settings...${NC}" + local existing_jwt_secret=$(yq e '.identity_validation.reset_password.jwt_secret // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_session_secret=$(yq e '.session.secret // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_storage_key=$(yq e '.storage.encryption_key // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_redis_pass=$(yq e '.session.redis.password // ""' "$AUTHELIA_CONFIG_BACKUP") + local existing_notifier=$(yq e '.notifier // ""' "$AUTHELIA_CONFIG_BACKUP") + + # Update secrets in temp file if they existed in the backup + if [[ -n "$existing_jwt_secret" && "$existing_jwt_secret" != '""' && "$existing_jwt_secret" != "null" ]]; then + yq e -i '.identity_validation.reset_password.jwt_secret = strenv(existing_jwt_secret)' --env existing_jwt_secret="$existing_jwt_secret" "$TEMP_CONFIG" + fi + if [[ -n "$existing_session_secret" && "$existing_session_secret" != '""' && "$existing_session_secret" != "null" ]]; then + yq e -i '.session.secret = strenv(existing_session_secret)' --env existing_session_secret="$existing_session_secret" "$TEMP_CONFIG" + fi + if [[ -n "$existing_storage_key" && "$existing_storage_key" != '""' && "$existing_storage_key" != "null" ]]; then + yq e -i '.storage.encryption_key = strenv(existing_storage_key)' --env existing_storage_key="$existing_storage_key" "$TEMP_CONFIG" + fi + if [[ -n "$existing_redis_pass" && "$existing_redis_pass" != '""' && "$existing_redis_pass" != "null" ]]; then + yq e -i '.session.redis.password = strenv(existing_redis_pass)' --env existing_redis_pass="$existing_redis_pass" "$TEMP_CONFIG" + fi + if [[ -n "$existing_notifier" && "$existing_notifier" != '""' && "$existing_notifier" != "null" ]]; then + yq e -i '.notifier = strenv(existing_notifier)' --env existing_notifier="$existing_notifier" "$TEMP_CONFIG" + fi + fi + + # Update domain settings from .env + echo -e "${BLUE}Applying Tailscale domain settings...${NC}" + + # Update domain in session section + yq e -i ".session.cookies[0].domain = \"${TAILNET_DOMAIN}\"" "$TEMP_CONFIG" + + # Update domain in access_control (find wildcard domain rule and update it) + # This assumes there's a rule with a wildcard domain like "*.example.com" + local domain_rule_index=$(yq e ".access_control.rules | map(.domain) | map(select(. == \"*.*\")) | indices" "$TEMP_CONFIG" | head -n 1 | tr -d '[]') + if [[ -n "$domain_rule_index" && "$domain_rule_index" != "null" ]]; then + yq e -i ".access_control.rules[$domain_rule_index].domain = \"${WILDCARD_DOMAIN}\"" "$TEMP_CONFIG" + fi + + # Update authelia_url if it exists (it's a URL that must match cookie scope) + if yq e -e '.identity_validation.reset_password.authelia_url' "$TEMP_CONFIG" &>/dev/null; then + yq e -i ".identity_validation.reset_password.authelia_url = \"https://${FULL_HOSTNAME}\"" "$TEMP_CONFIG" + fi + + # Move the temp file to the final location + mv "$TEMP_CONFIG" "$AUTHELIA_CONFIG" + + echo -e "${GREEN}Authelia configuration updated successfully!${NC}" + else + echo -e "${YELLOW}Warning: 'yq' is not installed. Using sed to update configuration.${NC}" + echo -e "${YELLOW}This is less reliable and may not preserve all settings.${NC}" + + # Create a new file from the example + cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG.new" + + # Update domain settings with sed (more fragile) + sed -i "s/domain: \".*\"/domain: \"${TAILNET_DOMAIN}\"/" "$AUTHELIA_CONFIG.new" + sed -i "s/domain: \"\\*\\..*\"/domain: \"${WILDCARD_DOMAIN}\"/" "$AUTHELIA_CONFIG.new" + sed -i "s|authelia_url: \"https://.*\"|authelia_url: \"https://${FULL_HOSTNAME}\"|" "$AUTHELIA_CONFIG.new" + + # Move the new file to the final location + mv "$AUTHELIA_CONFIG.new" "$AUTHELIA_CONFIG" + + echo -e "${YELLOW}Authelia configuration updated with sed. Secret values might need to be manually transferred.${NC}" + fi + + echo -e "${GREEN}Authelia configuration update completed.${NC}" + echo -e "${BLUE}Remember to restart Authelia for changes to take effect:${NC}" + echo -e "${CYAN} docker compose restart authelia${NC}" +} + +################################################## +# PART 3: Update service configurations +################################################## + +update_service_configs() { + print_header "Service Configuration Update Tool" + + # Check if Docker is running + if ! docker ps &> /dev/null; then + echo -e "${RED}Error: Docker is not running or you don't have permission to use it.${NC}" + echo -e "${YELLOW}Please start Docker and ensure your user has permission to run Docker commands.${NC}" + return 1 + fi + + if ! check_file "$ENV_FILE"; then + echo -e "${RED}Error: Environment file '$ENV_FILE' doesn't exist. Cannot update services.${NC}" + return 1 + fi + + # Get CONFIG_ROOT from .env if defined + CONFIG_ROOT=$(grep -oP "^CONFIG_ROOT=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'" || echo ".") + + echo -e "${BLUE}This will update configurations for running containers and extract API keys.${NC}" + echo -e "${BLUE}Services may be restarted during this process.${NC}" + echo -e "${YELLOW}Continue? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${RED}Service configuration update cancelled.${NC}" + return 0 + fi + + # Create a temporary .env file for updates + local ENV_TEMP="${ENV_FILE}.${TIMESTAMP}.tmp" + cp "$ENV_FILE" "$ENV_TEMP" + + # Service update tracker + declare -A updated_services + declare -A api_keys_updated + + # Function to restart a container if needed + restart_container() { + local service=$1 + if [ "${updated_services[$service]}" == "1" ]; then + echo -e "${YELLOW}Restarting $service to apply changes...${NC}" + if docker compose restart "$service"; then + echo -e "${GREEN}$service restarted successfully!${NC}" + else + echo -e "${RED}Failed to restart $service. Please restart manually.${NC}" + fi + fi + } + + # Function to extract API key from a container + extract_api_key() { + local service=$1 + local config_path=$2 + local api_key_pattern=$3 + local env_var_name=$4 + + # Skip if service is not running + if ! docker compose ps "$service" 2>/dev/null | grep -q "Up"; then + echo -e "${YELLOW}Service $service is not running, skipping API key extraction.${NC}" + return 0 + fi + + echo -e "${BLUE}Attempting to extract API key for $service...${NC}" + + # Try to get API key directly from the config file in the container + local api_key="" + if [ -n "$config_path" ] && [ -n "$api_key_pattern" ]; then + api_key=$(docker compose exec -T "$service" grep -oP "$api_key_pattern" "$config_path" 2>/dev/null | head -1 | sed 's/.*[":=]\s*\([^"]*\).*/\1/g') + fi + + # If API key was found and is not empty + if [ -n "$api_key" ]; then + echo -e "${GREEN}API key for $service extracted successfully!${NC}" + + # Check if the API key is already in the .env file + if grep -q "^$env_var_name=" "$ENV_TEMP"; then + local current_key=$(grep -oP "^$env_var_name=\K.*" "$ENV_TEMP" | tr -d '"' | tr -d "'") + + # Only update if key is different and not empty + if [ "$current_key" != "$api_key" ] && [ -n "$current_key" ]; then + echo -e "${YELLOW}API key for $service in .env differs from extracted key.${NC}" + echo -e "${YELLOW}Would you like to update it? [y/N]:${NC}" + read -r update_key + if [[ ! "$update_key" =~ ^[Yy]$ ]]; then + echo -e "${BLUE}Keeping existing API key in .env file.${NC}" + return 0 + fi + fi + + # Update the existing entry + sed -i "s|^$env_var_name=.*|$env_var_name=$api_key|" "$ENV_TEMP" + else + # Add the new API key to the .env file + echo -e "\n# API key for $service (extracted $(date +"%Y-%m-%d"))" >> "$ENV_TEMP" + echo "$env_var_name=$api_key" >> "$ENV_TEMP" + fi + + api_keys_updated["$service"]="1" + else + echo -e "${YELLOW}Could not extract API key for $service.${NC}" + fi + } + + # Update *arr services with the correct URL base + for service in "sonarr" "radarr" "lidarr" "prowlarr" "bazarr"; do + # Skip if service is not running + if ! docker compose ps "$service" 2>/dev/null | grep -q "Up"; then + echo -e "${YELLOW}Service $service is not running, skipping.${NC}" + continue + fi + + echo -e "${BLUE}Updating $service configuration...${NC}" + + local config_file="/config/config.xml" + + # Check if the service's config file exists + if ! docker compose exec "$service" test -f "$config_file"; then + echo -e "${YELLOW}Configuration file for $service not found, skipping.${NC}" + continue + fi + + # Check current URLBase setting + local current_urlbase=$(docker compose exec -T "$service" grep -oP '\K[^<]+' "$config_file" 2>/dev/null || echo "") + local desired_urlbase="/$service" + + # Update URLBase if needed + if [ "$current_urlbase" != "$desired_urlbase" ]; then + echo -e "${YELLOW}$service URL base needs to be updated from '$current_urlbase' to '$desired_urlbase'.${NC}" + + if [ -z "$current_urlbase" ]; then + # If URLBase tag doesn't exist, add it + docker compose exec "$service" sed -i "//i\\ $desired_urlbase" "$config_file" || \ + docker compose exec "$service" sed -i "//i\\ $desired_urlbase" "$config_file" + else + # If URLBase exists, update it + docker compose exec "$service" sed -i "s|[^<]*|$desired_urlbase|g" "$config_file" + fi + + if [ $? -eq 0 ]; then + echo -e "${GREEN}$service URL base updated to $desired_urlbase${NC}" + updated_services["$service"]="1" + else + echo -e "${RED}Failed to update $service URL base.${NC}" + fi + else + echo -e "${GREEN}$service URL base already set to $desired_urlbase${NC}" + fi + + # Extract API key based on service + case "$service" in + "sonarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "SONARR_API_KEY" + ;; + "radarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "RADARR_API_KEY" + ;; + "lidarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "LIDARR_API_KEY" + ;; + "prowlarr") + extract_api_key "$service" "$config_file" '\K[^<]+' "PROWLARR_API_KEY" + ;; + "bazarr") + # Bazarr uses a different config format (YAML/settings.ini) + extract_api_key "$service" "/config/config/config.ini" 'auth_apikey\s*=\s*\K[^#\n]+' "BAZARR_API_KEY" + ;; + esac + done + + # Update qBittorrent WebUI URL + if docker compose ps "qbittorrent" 2>/dev/null | grep -q "Up"; then + echo -e "${BLUE}Updating qBittorrent configuration...${NC}" + + local qbit_config_dir="/config/qBittorrent" + local qbit_config_file="$qbit_config_dir/qBittorrent.conf" + + # Check if the config file exists + if docker compose exec "qbittorrent" test -f "$qbit_config_file"; then + # Check current WebUI\RootFolder setting + local current_root=$(docker compose exec -T "qbittorrent" grep -oP '^WebUI\\RootFolder\s*=\s*\K.*' "$qbit_config_file" 2>/dev/null || echo "") + local desired_root="/qbittorrent" + + # Update WebUI\RootFolder if needed + if [ "$current_root" != "$desired_root" ]; then + echo -e "${YELLOW}qBittorrent WebUI root needs to be updated from '$current_root' to '$desired_root'.${NC}" + + if docker compose exec "qbittorrent" grep -q "^WebUI\\\\RootFolder=" "$qbit_config_file"; then + # If setting exists, update it + docker compose exec "qbittorrent" sed -i "s|^WebUI\\\\RootFolder=.*|WebUI\\\\RootFolder=$desired_root|g" "$qbit_config_file" + else + # If setting doesn't exist, add it to the [Preferences] section + if docker compose exec "qbittorrent" grep -q "^\[Preferences\]" "$qbit_config_file"; then + docker compose exec "qbittorrent" sed -i "/^\[Preferences\]/a WebUI\\\\RootFolder=$desired_root" "$qbit_config_file" + else + # Add [Preferences] section if it doesn't exist + docker compose exec "qbittorrent" bash -c "echo -e '\n[Preferences]\nWebUI\\\\RootFolder=$desired_root' >> $qbit_config_file" + fi + fi + + if [ $? -eq 0 ]; then + echo -e "${GREEN}qBittorrent WebUI root updated to $desired_root${NC}" + updated_services["qbittorrent"]="1" + else + echo -e "${RED}Failed to update qBittorrent WebUI root.${NC}" + fi + else + echo -e "${GREEN}qBittorrent WebUI root already set to $desired_root${NC}" + fi + else + echo -e "${YELLOW}qBittorrent configuration file not found, skipping.${NC}" + fi + fi + + # Extract Jellyfin API key + if docker compose ps "jellyfin" 2>/dev/null | grep -q "Up"; then + # Jellyfin API key is in a different location + extract_api_key "jellyfin" "/config/config/system.xml" '\K[^<]+' "JELLYFIN_API_KEY" + fi + + # Extract Jellyseerr API key + if docker compose ps "jellyseerr" 2>/dev/null | grep -q "Up"; then + # Jellyseerr stores API key in settings.json + extract_api_key "jellyseerr" "/app/config/settings.json" '"apiKey":\s*"\K[^"]+' "JELLYSEERR_API_KEY" + fi + + # Check for optional services based on COMPOSE_PROFILES + local profiles=$(grep -oP "^COMPOSE_PROFILES=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'" || echo "") + + # Process profiles as a comma-separated list + IFS=',' read -ra profile_array <<< "$profiles" + for profile in "${profile_array[@]}"; do + profile=$(echo "$profile" | xargs) # Trim whitespace + + # Handle specific profiles + case "$profile" in + "sabnzbd") + if docker compose ps "sabnzbd" 2>/dev/null | grep -q "Up"; then + # Extract SABnzbd API key from its config + extract_api_key "sabnzbd" "/config/sabnzbd.ini" 'api_key\s*=\s*\K[0-9a-f]+' "SABNZBD_API_KEY" + fi + ;; + "immich") + if docker compose ps "immich-server" 2>/dev/null | grep -q "Up"; then + # Extract Immich API key from user database + echo -e "${BLUE}Attempting to extract Immich API key...${NC}" + local immich_api_key=$(docker compose exec -T immich-server /usr/local/bin/cli api-key --show 2>/dev/null | grep -oP '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}') + + if [ -n "$immich_api_key" ]; then + echo -e "${GREEN}Immich API key extracted successfully!${NC}" + + # Update or add the API key to the .env file + if grep -q "^IMMICH_API_KEY=" "$ENV_TEMP"; then + sed -i "s|^IMMICH_API_KEY=.*|IMMICH_API_KEY=$immich_api_key|" "$ENV_TEMP" + else + echo -e "\n# API key for Immich (extracted $(date +"%Y-%m-%d"))" >> "$ENV_TEMP" + echo "IMMICH_API_KEY=$immich_api_key" >> "$ENV_TEMP" + fi + + api_keys_updated["immich"]="1" + else + echo -e "${YELLOW}Could not extract Immich API key.${NC}" + fi + fi + ;; + "homeassistant") + if docker compose ps "homeassistant" 2>/dev/null | grep -q "Up"; then + echo -e "${BLUE}Home Assistant detected.${NC}" + echo -e "${YELLOW}Home Assistant requires a Long-Lived Access Token for API access.${NC}" + echo -e "${YELLOW}This must be generated manually in the Home Assistant UI:${NC}" + echo -e "${CYAN} Profile > Long-Lived Access Tokens > Create Token${NC}" + + # Check if token already exists + if ! grep -q "^HOMEASSISTANT_ACCESS_TOKEN=" "$ENV_TEMP" || [ -z "$(grep -oP "^HOMEASSISTANT_ACCESS_TOKEN=\K.*" "$ENV_TEMP" | tr -d '"' | tr -d "'")" ]; then + echo -e "${YELLOW}Would you like to add a Home Assistant token now? [y/N]:${NC}" + read -r add_token + if [[ "$add_token" =~ ^[Yy]$ ]]; then + echo -e "${BLUE}Enter your Home Assistant Long-Lived Access Token:${NC}" + read -rs token + if [ -n "$token" ]; then + if grep -q "^HOMEASSISTANT_ACCESS_TOKEN=" "$ENV_TEMP"; then + sed -i "s|^HOMEASSISTANT_ACCESS_TOKEN=.*|HOMEASSISTANT_ACCESS_TOKEN=$token|" "$ENV_TEMP" + else + echo -e "\n# Home Assistant Access Token (added $(date +"%Y-%m-%d"))" >> "$ENV_TEMP" + echo "HOMEASSISTANT_ACCESS_TOKEN=$token" >> "$ENV_TEMP" + fi + api_keys_updated["homeassistant"]="1" + echo -e "${GREEN}Home Assistant token added!${NC}" + else + echo -e "${RED}No token provided. Skipping.${NC}" + fi + fi + else + echo -e "${GREEN}Home Assistant token already exists in .env${NC}" + fi + fi + ;; + esac + done + + # Apply changes to the .env file if any API keys were updated + if [ ${#api_keys_updated[@]} -gt 0 ]; then + echo -e "${YELLOW}API keys were extracted for these services:${NC}" + for service in "${!api_keys_updated[@]}"; do + echo -e " - ${CYAN}$service${NC}" + done + + echo -e "${YELLOW}Update .env file with these API keys? [Y/n]:${NC}" + read -r update_env + if [[ ! "$update_env" =~ ^[Nn]$ ]]; then + # Backup the current .env before replacing + if [ ! -f "$ENV_BACKUP" ]; then + create_backup "$ENV_FILE" "$ENV_BACKUP" + fi + + # Replace the .env file with the updated temp file + mv "$ENV_TEMP" "$ENV_FILE" + echo -e "${GREEN}API keys updated in $ENV_FILE${NC}" + else + echo -e "${YELLOW}API key updates were not saved to .env file.${NC}" + rm "$ENV_TEMP" + fi + else + echo -e "${BLUE}No API key updates were found.${NC}" + rm "$ENV_TEMP" + fi + + # Restart services that were updated + if [ ${#updated_services[@]} -gt 0 ]; then + echo -e "${YELLOW}The following services had their configurations updated:${NC}" + for service in "${!updated_services[@]}"; do + echo -e " - ${CYAN}$service${NC}" + done + + echo -e "${YELLOW}Would you like to restart these services now? [Y/n]:${NC}" + read -r restart_now + if [[ ! "$restart_now" =~ ^[Nn]$ ]]; then + for service in "${!updated_services[@]}"; do + restart_container "$service" + done + else + echo -e "${YELLOW}Remember to restart these services manually for changes to take effect:${NC}" + for service in "${!updated_services[@]}"; do + echo -e " ${CYAN}docker compose restart $service${NC}" + done + fi + fi + + echo -e "\n${GREEN}${BOLD}Service Configuration Update Complete!${NC}" + if [ ${#updated_services[@]} -gt 0 ] || [ ${#api_keys_updated[@]} -gt 0 ]; then + echo -e "${GREEN}Services were successfully updated.${NC}" + else + echo -e "${BLUE}No changes were needed.${NC}" + fi +} + +################################################## +# PART 5: Authelia Account Management +################################################## +# PART 4: Authelia Policy Management +################################################## + +# Get the current policy for a service from Authelia config +# Usage: get_authelia_policy +# Output: policy (e.g., one_factor, bypass), not_found, or error +get_authelia_policy() { + local service=$1 + local config_file=$2 + + if ! check_file "$config_file"; then + echo "error: config file not found" + return 1 + fi + + # Use yq if available for more reliable YAML parsing + if check_yq; then + # Find the rule matching the path_regex for the service + # Note: This assumes path_regex is unique enough for the service (e.g., ^/service.*) + local policy=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\") | .policy" "$config_file" 2>/dev/null) + if [ -n "$policy" ] && [ "$policy" != "null" ]; then + echo "$policy" + else + # Check if a rule exists for the service path at all + local rule_exists=$(yq e ".access_control.rules[] | select(.path_regex == \"^/${service}.*\")" "$config_file" 2>/dev/null) + if [ -n "$rule_exists" ] && [ "$rule_exists" != "null" ]; then + echo "error: policy not found in rule" # Rule exists but policy couldn't be read + else + echo "not_found" # No rule found for this service path + fi + fi + else + # Fall back to grep if yq isn't available (less reliable) + # This is fragile and depends heavily on formatting + local policy_line=$(grep -A 1 "path_regex: '^/${service}.*'" "$config_file" | grep "policy:") + if [ -n "$policy_line" ]; then + echo "$policy_line" | awk '{print $2}' + else + echo "not_found" + fi + fi + return 0 +} + +# Set the policy for a service in Authelia config +# Usage: set_authelia_policy +set_authelia_policy() { + local service=$1 + local policy=$2 + local config_file=$3 + local backup_file="${config_file}.${TIMESTAMP}.bak" + + if ! check_file "$config_file"; then + echo -e "${RED}Error: Authelia configuration file '$config_file' not found.${NC}" + return 1 + fi + + if [[ "$policy" != "one_factor" && "$policy" != "two_factor" && "$policy" != "deny" && "$policy" != "bypass" ]]; then + echo -e "${RED}Error: Invalid policy '$policy'. Must be one of: one_factor, two_factor, deny, bypass.${NC}" + return 1 + fi + + echo -e "${BLUE}Setting policy for service '$service' to '$policy' in '$config_file'...${NC}" + + # Create backup if it doesn't exist for this run + if [ ! -f "$backup_file" ]; then + create_backup "$config_file" "$backup_file" + fi + + # Use yq if available + if check_yq; then + # Check if the rule exists first using the path_regex + local rule_index=$(yq e ".access_control.rules | map(.path_regex == \"^/${service}.*\") | indexOf(true)" "$config_file" 2>/dev/null) + + if [ "$rule_index" == "-1" ] || [ -z "$rule_index" ]; then + echo -e "${YELLOW}Warning: No rule found for service path '^/${service}.*' in '$config_file'.${NC}" + echo -e "${YELLOW}Attempting to add a new rule...${NC}" + + # Get the tailnet domain from .env for the new rule + local TAILNET_DOMAIN=$(grep -oP "^TAILSCALE_TAILNET_DOMAIN=\K.*" "$ENV_FILE" | tr -d '"' | tr -d "'") + if [ -z "$TAILNET_DOMAIN" ]; then + echo -e "${RED}Error: Could not read TAILSCALE_TAILNET_DOMAIN from $ENV_FILE. Cannot add rule.${NC}" + return 1 + fi + local WILDCARD_DOMAIN="*.${TAILNET_DOMAIN}" + + # Add the new rule to the access_control.rules array + # Places it before the generic domain rule if it exists, otherwise at the end + # This assumes a generic rule like "- domain: '*.domain.tld'" exists near the end + yq e -i ".access_control.rules |= select(.domain != \"${WILDCARD_DOMAIN}\" or .path_regex != null) + [{\"domain\": \"${WILDCARD_DOMAIN}\", \"path_regex\": \"^/${service}.*\", \"policy\": \"${policy}\"}] + select(.domain == \"${WILDCARD_DOMAIN}\" and .path_regex == null)" "$config_file" + + if [ $? -eq 0 ]; then + echo -e "${GREEN}Added new rule for '$service' with policy '$policy'.${NC}" + return 0 + else + echo -e "${RED}Error: Failed to add new rule for '$service' using yq.${NC}" + return 1 + fi + else + # Rule exists, update the policy at the found index + yq e -i "(.access_control.rules[$rule_index].policy) = \"$policy\"" "$config_file" + if [ $? -eq 0 ]; then + echo -e "${GREEN}Policy for '$service' updated to '$policy'.${NC}" + return 0 + else + echo -e "${RED}Error: Failed to update policy for '$service' using yq.${NC}" + return 1 + fi + fi + else + # Fallback to sed (much less reliable, especially for adding rules) + echo -e "${YELLOW}Warning: 'yq' not found. Using 'sed' which is less reliable for YAML manipulation.${NC}" + # Check if rule exists (simple grep) + if grep -q "path_regex: '^/${service}.*'" "$config_file"; then + # Attempt to find the line number of path_regex and update the policy line below it + local line_num=$(grep -n "path_regex: '^/${service}.*'" "$config_file" | head -n 1 | cut -d: -f1) # Use head -n 1 just in case + if [ -n "$line_num" ]; then + # Assuming policy is the next line (fragile!) + local policy_line_num=$((line_num + 1)) + # Check if the next line actually contains 'policy:' + if sed -n "${policy_line_num}p" "$config_file" | grep -q "policy:"; then + sed -i "${policy_line_num}s/policy:.*/policy: $policy/" "$config_file" + if [ $? -eq 0 ]; then + echo -e "${GREEN}Policy for '$service' updated to '$policy' (using sed).${NC}" + return 0 + else + echo -e "${RED}Error: Failed to update policy line using sed.${NC}" + return 1 + fi + else + echo -e "${RED}Error: Could not reliably find policy line for '$service' using sed (expected on line $policy_line_num).${NC}" + return 1 + fi + else + echo -e "${RED}Error: Could not find line number for service '$service' using sed.${NC}" + return 1 + fi + else + echo -e "${RED}Error: Rule for service '$service' not found. Cannot add rule using sed.${NC}" + return 1 + fi + fi +} + +# List services and their Authelia policy status +list_authelia_services() { + print_header "Authelia Service Policy Status" + + if ! check_file "$AUTHELIA_CONFIG"; then + echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}" + return 1 + fi + + echo -e "${BLUE}Checking service policies in $AUTHELIA_CONFIG...${NC}" + echo -e "${CYAN}SERVICE\t\tPOLICY${NC}" + echo -e "${CYAN}-------\t\t------${NC}" + + local service_count=0 + local processed_services="" # Track processed services + + # Use yq to get all rules with path_regex if available + if check_yq; then + # Extract path_regex and policy for rules that have path_regex + local rules_data=$(yq e '.access_control.rules[] | select(has("path_regex")) | {"path": .path_regex, "policy": .policy}' "$AUTHELIA_CONFIG") + + # Process each rule found + while IFS= read -r line; do + # Extract service name from path_regex (e.g., "^/sonarr.*" -> "sonarr") + local path_regex=$(echo "$line" | grep -oP 'path: \K.*' | tr -d '"' | tr -d "'") + local policy=$(echo "$line" | grep -oP 'policy: \K.*' | tr -d '"' | tr -d "'") + + if [[ "$path_regex" =~ ^\^/([a-zA-Z0-9_-]+)\.\* ]]; then + local service="${BASH_REMATCH[1]}" + + # Skip duplicates if multiple rules somehow match the same pattern start + if [[ "$processed_services" == *"$service"* ]]; then + continue + fi + processed_services="$processed_services $service" + + printf "${BOLD}%-20s${NC}" "$service" + case "$policy" in + "one_factor"|"two_factor") + echo -e "${GREEN}${policy}${NC}" + ;; + "bypass") + echo -e "${YELLOW}${policy}${NC}" + ;; + "deny") + echo -e "${RED}${policy}${NC}" + ;; + *) + echo -e "${MAGENTA}Unknown ($policy)${NC}" + ;; + esac + service_count=$((service_count + 1)) + fi + # Use yq -N to prevent splitting lines with spaces + done <<< "$(echo "$rules_data" | yq -N e '.' -)" + + else + # Fallback to grep (less reliable) + echo -e "${YELLOW}Warning: yq not found, using grep (may miss services or show duplicates).${NC}" + # Find lines with path_regex, then try to get the policy line after + grep -n "path_regex: '^/.*" "$AUTHELIA_CONFIG" | while IFS=: read -r line_num line_content; do + if [[ "$line_content" =~ path_regex:\ \'^\/([a-zA-Z0-9_-]+)\.\*\' ]]; then + local service="${BASH_REMATCH[1]}" + # Skip duplicates + if [[ "$processed_services" == *"$service"* ]]; then + continue + fi + processed_services="$processed_services $service" + + # Try to get policy from the next line (very fragile) + local policy_line_num=$((line_num + 1)) + local policy=$(sed -n "${policy_line_num}p" "$AUTHELIA_CONFIG" | grep "policy:" | awk '{print $2}') + + printf "${BOLD}%-20s${NC}" "$service" + if [ -n "$policy" ]; then + case "$policy" in + "one_factor"|"two_factor") echo -e "${GREEN}${policy}${NC}" ;; + "bypass") echo -e "${YELLOW}${policy}${NC}" ;; + "deny") echo -e "${RED}${policy}${NC}" ;; + *) echo -e "${MAGENTA}Unknown ($policy)${NC}" ;; + esac + else + echo -e "${RED}Unknown (could not read policy)${NC}" + fi + service_count=$((service_count + 1)) + fi + done + fi + + if [ $service_count -eq 0 ]; then + echo -e "${YELLOW}No services with path_regex rules found in $AUTHELIA_CONFIG.${NC}" + echo -e "${YELLOW}Only services explicitly defined with path_regex rules are listed.${NC}" + fi + return 0 +} + + +cleanup_backups() { + print_header "Backup Files Cleanup" + + echo -e "${BLUE}Searching for backup files...${NC}" + + local env_backups=$(find . -maxdepth 1 -name ".env.*.bak" | sort) + local compose_backups=$(find . -maxdepth 1 -name "docker-compose.*.bak" | sort) + local authelia_backups=$(find ./authelia -maxdepth 1 -name "configuration.*.bak" 2>/dev/null | sort) + + local env_count=$(echo "$env_backups" | grep -c "^") + local compose_count=$(echo "$compose_backups" | grep -c "^") + local authelia_count=$(echo "$authelia_backups" | grep -c "^") + + echo -e "${CYAN}Found:${NC}" + echo -e " - ${CYAN}$env_count .env backup files${NC}" + echo -e " - ${CYAN}$compose_count docker-compose.yml backup files${NC}" + echo -e " - ${CYAN}$authelia_count Authelia configuration backup files${NC}" + + local total_count=$((env_count + compose_count + authelia_count)) + + if [ $total_count -eq 0 ]; then + echo -e "${GREEN}No backup files found.${NC}" + return 0 + fi + + echo -e "${YELLOW}Do you want to delete all backup files? [y/N]:${NC}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Would you like to keep the most recent backup of each type? [Y/n]:${NC}" + read -r keep_recent + + if [[ "$keep_recent" =~ ^[Nn]$ ]]; then + echo -e "${RED}Backup cleanup cancelled.${NC}" + return 0 + fi + + if [ $env_count -gt 1 ]; then + local keep_env=$(echo "$env_backups" | tail -1) + local del_env=$(echo "$env_backups" | grep -v "$keep_env") + echo -e "${BLUE}Keeping most recent .env backup: ${CYAN}$keep_env${NC}" + echo -e "${BLUE}Deleting ${CYAN}$((env_count - 1))${BLUE} older .env backups${NC}" + for file in $del_env; do + rm "$file" + done + fi + + if [ $compose_count -gt 1 ]; then + local keep_compose=$(echo "$compose_backups" | tail -1) + local del_compose=$(echo "$compose_backups" | grep -v "$keep_compose") + echo -e "${BLUE}Keeping most recent docker-compose.yml backup: ${CYAN}$keep_compose${NC}" + echo -e "${BLUE}Deleting ${CYAN}$((compose_count - 1))${BLUE} older docker-compose.yml backups${NC}" + for file in $del_compose; do + rm "$file" + done + fi + + if [ $authelia_count -gt 1 ]; then + local keep_authelia=$(echo "$authelia_backups" | tail -1) + local del_authelia=$(echo "$authelia_backups" | grep -v "$keep_authelia") + echo -e "${BLUE}Keeping most recent Authelia config backup: ${CYAN}$keep_authelia${NC}" + echo -e "${BLUE}Deleting ${CYAN}$((authelia_count - 1))${BLUE} older Authelia config backups${NC}" + for file in $del_authelia; do + rm "$file" + done + fi + + echo -e "${GREEN}Cleanup completed with most recent backups retained.${NC}" + else + for file in $env_backups $compose_backups $authelia_backups; do + rm "$file" + done + echo -e "${GREEN}All $total_count backup files have been deleted.${NC}" + fi +} + +# Interactive menu for managing Authelia policies +manage_authelia_policies() { + print_header "Authelia Policy Management" + + if ! check_file "$AUTHELIA_CONFIG"; then + echo -e "${RED}Error: Authelia configuration file '$AUTHELIA_CONFIG' not found.${NC}" + return 1 + fi + + while true; do + echo -e "\n${BLUE}Choose an option:${NC}" + echo -e " ${CYAN}1. ${NC}List services and their current Authelia policy" + echo -e " ${CYAN}2. ${NC}Set Authelia policy for a service" + echo -e " ${CYAN}3. ${NC}Return to main menu" + echo + + local choice + echo -e "${YELLOW}Enter your choice [1-3]: ${NC}" + read -r choice + + case "$choice" in + 1) + list_authelia_services + ;; + 2) + echo -e "${BLUE}Enter the service name to set the policy for (e.g., sonarr, radarr):${NC}" + read -r service + if [ -z "$service" ]; then + echo -e "${RED}No service name provided.${NC}" + continue + fi + + echo -e "${BLUE}Select the desired policy for '$service':${NC}" + echo -e " ${CYAN}1. ${GREEN}one_factor${NC} (Requires login)" + echo -e " ${CYAN}2. ${GREEN}two_factor${NC} (Requires login + 2FA)" + echo -e " ${CYAN}3. ${YELLOW}bypass${NC} (No login required)" + echo -e " ${CYAN}4. ${RED}deny${NC} (Access denied)" + echo -e " ${CYAN}5. ${NC}Cancel" + + local policy_choice + local policy="" + while true; do + echo -e "${YELLOW}Enter policy choice [1-5]: ${NC}" + read -r policy_choice + case "$policy_choice" in + 1) policy="one_factor"; break ;; + 2) policy="two_factor"; break ;; + 3) policy="bypass"; break ;; + 4) policy="deny"; break ;; + 5) policy=""; break ;; # Cancel + *) echo -e "${RED}Invalid choice.${NC}" ;; + esac + done + + if [ -z "$policy" ]; then + echo -e "${YELLOW}Policy change cancelled.${NC}" + continue + fi + + # Call the function to set the policy + set_authelia_policy "$service" "$policy" "$AUTHELIA_CONFIG" + + # Check the return status of set_authelia_policy + if [ $? -eq 0 ]; then + echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}" + echo -e " ${CYAN}docker compose restart authelia${NC}" + else + echo -e "${RED}Failed to set policy. Please check errors above.${NC}" + # Optionally offer to restore backup here + fi + ;; + 3) + return 0 + ;; + *) + echo -e "${RED}Invalid choice. Please try again.${NC}" + ;; + esac + echo # Add a newline for better readability between menu iterations + done +} + + +################################################## +# PART 5: Authelia Account Management +################################################## + +# Function to generate a secure, random passphrase +generate_passphrase() { + # Generate a random passphrase using common words and numbers + local words=("apple" "banana" "orange" "grape" "melon" "cherry" "lemon" "peach" "plum" "kiwi" + "red" "blue" "green" "yellow" "purple" "cyan" "magenta" "white" "black" "grey" + "dog" "cat" "bird" "fish" "horse" "tiger" "lion" "bear" "wolf" "fox" + "river" "ocean" "mountain" "forest" "desert" "island" "valley" "canyon" "lake" "hill") + + local w1=${words[$((RANDOM % ${#words[@]}))]} + local w2=${words[$((RANDOM % ${#words[@]}))]} + local w3=${words[$((RANDOM % ${#words[@]}))]} + local num=$((1000 + RANDOM % 9000)) # 4-digit number + + echo "${w1^}${w2^}${w3^}${num}" # Capitalize first letter of each word +} + +manage_authelia_accounts() { + print_header "Authelia Account Management" + + local users_file="${CONFIG_ROOT:-.}/authelia/users_database.yml" + + if [ ! -f "$users_file" ]; then + echo -e "${RED}Error: users_database.yml not found at $users_file${NC}" + echo -e "${YELLOW}Would you like to create a new users database file? [y/N]:${NC}" + read -r answer + if [[ "$answer" =~ ^[Yy]$ ]]; then + cat > "$users_file" </dev/null) + + if [ -z "$password_hash" ]; then + echo -e "${RED}Error: Failed to generate password hash. Is the authelia container available?${NC}" + echo -e "${YELLOW}Trying direct docker run method...${NC}" + password_hash=$(docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "$password" 2>/dev/null) + + if [ -z "$password_hash" ]; then + echo -e "${RED}Error: Both methods failed to generate a password hash. Skipping user.${NC}" + continue + fi + fi + + password_hash=$(echo "$password_hash" | sed 's/^Digest: //') + + if [[ ! "$password_hash" =~ ^\$argon2id.*$ ]]; then + echo -e "${RED}Error: Generated hash does not have the expected format. Actual value:${NC}" + echo -e "${YELLOW}$password_hash${NC}" + echo -e "${RED}Skipping user creation.${NC}" + continue + fi + + echo -e "${GREEN}Password hash generated successfully.${NC}" + + if grep -q "^[[:space:]]*${username}:" "$users_file"; then + sed -i "/^[[:space:]]*${username}:/,/^[[:space:]]*[a-zA-Z0-9_-]\+:/ s/^/# UPDATED: /" "$users_file" + sed -i "0,/^# UPDATED: [[:space:]]*[a-zA-Z0-9_-]\+:/ s/^# UPDATED: //" "$users_file" + fi + + cat >> "$users_file" < ${NC} Set Authelia policy for a service (e.g., 'one_factor', 'bypass')." + echo -e " ${CYAN}cleanup${NC} Interactively clean up old backup files (.bak)." + echo -e " ${CYAN}all${NC} Run 'update-env', 'update-authelia', and 'update-services'." + echo -e " ${CYAN}help${NC} Show this help message." + echo -e "" + echo -e "${BLUE}Examples:${NC}" + echo -e " $0 update-authelia" + echo -e " $0 set-policy sonarr one_factor" + echo -e " $0 set-policy radarr bypass" + echo -e " $0 manage-policies" + echo -e " $0 all" + echo -e "" + echo -e "${YELLOW}Note:${NC} Some commands require Docker to be running and may restart containers." + echo -e "${YELLOW}Policy changes require an Authelia restart ('docker compose restart authelia').${NC}" +} + +# Check if any arguments were provided +if [ $# -eq 0 ]; then + show_help + exit 0 +fi + +# Process command line arguments +case "$1" in + update-env) + update_env_file + ;; + update-authelia) + update_authelia_config + ;; + update-services) + update_service_configs + ;; + manage-accounts) + manage_authelia_accounts # Interactive + ;; + manage-policies) + manage_authelia_policies # Interactive + ;; + list-policies) + list_authelia_services + ;; + set-policy) + if [ -z "$2" ] || [ -z "$3" ]; then + echo -e "${RED}Error: Service name and policy are required.${NC}" >&2 + echo -e "Usage: $0 set-policy ${NC}" >&2 + echo -e "Valid policies: one_factor, two_factor, bypass, deny" >&2 + exit 1 + fi + if ! check_file "$AUTHELIA_CONFIG"; then exit 1; fi + # Backup is handled within set_authelia_policy if needed + set_authelia_policy "$2" "$3" "$AUTHELIA_CONFIG" + if [ $? -eq 0 ]; then + echo -e "\n${YELLOW}Remember to restart Authelia for the policy change to take effect:${NC}" + echo -e " ${CYAN}docker compose restart authelia${NC}" + fi + ;; + cleanup) + cleanup_backups + ;; + all) + print_header "Running All Updates" + update_env_file + update_authelia_config + update_service_configs + echo -e "\n${GREEN}${BOLD}All core updates completed!${NC}" + echo -e "${BLUE}Review output for any required actions (e.g., setting new .env variables).${NC}" + echo -e "${BLUE}Consider running 'manage-accounts' if needed.${NC}" + echo -e "${YELLOW}Remember to restart relevant services or the full stack if necessary.${NC}" + ;; + help|-h|--help) + show_help + ;; + *) + echo -e "${RED}Unknown command: $1${NC}" >&2 + echo -e "${BLUE}Run '$0 help' for usage information.${NC}" >&2 + exit 1 + ;; +esac + +exit 0 \ No newline at end of file