From 90911d69a41ff2fda8cc4faca867cf359b9ef7cf Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Wed, 11 Feb 2026 00:18:22 +0100 Subject: [PATCH] Auto-configure HTTPS proxy and access management --- .env.example | 41 +++-- README.md | 42 ++++- scripts/auto_config.sh | 380 ++++++++++++++++++++++++++++------------- suite_services.py | 239 +++++++++++++++++++++++--- 4 files changed, 546 insertions(+), 156 deletions(-) diff --git a/.env.example b/.env.example index 4845edd..bd64c64 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,24 @@ LANGFUSE_SALT=your-super-secret-key-1 NEXTAUTH_SECRET=your-super-secret-key-2 ENCRYPTION_KEY=your-super-secret-key-3 +############ +# [required for production] +# Access Auto-configuration settings +############ + +AC_SUDO_USER='change_this_user' +AC_USERNAME='ai_suite_user' +AC_PASSWORD='*******' +AC_LOG_PATH=scripts +AC_LOCAL=True +AC_DOMAIN='local.com' +AC_CONFIRM=False +AC_PROXY=caddy +AC_WITH_AUTHELIA=False +AC_EMAIL='ai.suite.user@local.com' +AC_DISPLAY_NAME='AI-Suite User' +AC_WITH_REDIS=False + ############ # [required for production] # Caddy Config @@ -97,18 +115,17 @@ ENCRYPTION_KEY=your-super-secret-key-3 # Replace internal by your email (require to create a Let's Encrypt certificate) ############ -DOMAIN_NAME=yourdomain.com -# N8N_HOSTNAME=n8n.${DOMAIN_NAME} -# WEBHOOK_URL=https:n8n.${DOMAIN_NAME} -# WEBUI_HOSTNAME=openwebui.${DOMAIN_NAME} -# FLOWISE_HOSTNAME=flowise.${DOMAIN_NAME} -# SUPABASE_HOSTNAME=supabase.${DOMAIN_NAME} -# LANGFUSE_HOSTNAME=langfuse.${DOMAIN_NAME} -# OLLAMA_HOSTNAME=ollama.${DOMAIN_NAME} -# LLAMACPP_HOSTNAME=llamacpp.${DOMAIN_NAME} -# SEARXNG_HOSTNAME=searxng.${DOMAIN_NAME} -# NEO4J_HOSTNAME=neo4j.${DOMAIN_NAME} -# LETSENCRYPT_EMAIL=internal +# N8N_HOSTNAME=n8n.${AC_DOMAIN} +# WEBHOOK_URL=https:n8n.${AC_DOMAIN} +# WEBUI_HOSTNAME=openwebui.${AC_DOMAIN} +# FLOWISE_HOSTNAME=flowise.${AC_DOMAIN} +# SUPABASE_HOSTNAME=supabase.${AC_DOMAIN} +# LANGFUSE_HOSTNAME=langfuse.${AC_DOMAIN} +# OLLAMA_HOSTNAME=ollama.${AC_DOMAIN} +# LLAMACPP_HOSTNAME=llamacpp.${AC_DOMAIN} +# SEARXNG_HOSTNAME=searxng.${AC_DOMAIN} +# NEO4J_HOSTNAME=neo4j.${AC_DOMAIN} +# LETSENCRYPT_EMAIL=${AC_EMAIL} # Everything below this point is optional. # Default values will suffice unless you need more features/customization. diff --git a/README.md b/README.md index e25da2a..c477a68 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ system that can generate analytical data reports in real-time. ✅ [**Caddy**](https://caddyserver.com/) - Managed HTTPS/TLS for custom domains. +✅ [**Nginx**](https://nginx.org) - HTTPS/TLS server, reverse proxy, TCP/UDP +proxy server + +✅ [**Authelia**](https://www.authelia.com) - Authentication and authorization +server, identity and access management (IAM). + ## Prerequisites System specifications: @@ -711,6 +717,25 @@ Example command: python suite_services.py --profile n8n opencode cpp-cpu --operation update --log DEBUG ``` +#### HTTPS Reverse Proxy and Access Management + +By default, _self-hosted_ **AI-Suite** will configure **Caddy** (Default) or **Nginx** +HTTPS reverse proxy and **Authelia** 2FA (Two Factor Authentication) IAM (Identity +and Access Management) on install or update. You can disable this behaviour using +the `no-auto-config` profile argument when running these operations. + +`suite_services.py` `--profile` No auto-configure argument: + +| Argument | Behaviour | +| -----------------------: | ------: | +| `no-auto-config` | Auto-configure - setup Caddy/Nginx and Authelia 2FA IAM | + +Example command: + +```powershell +python suite_services.py --profile ai-all no-auto-config --operation update +``` + --- ## Deploying to the Cloud @@ -1437,10 +1462,16 @@ caddy: ```bash . +├── LICENSE +├── README.md ├── assets/ # README.md gif image +├── authelia/ +│   ├── configuration.yml # Authelia configuration +│   └── db/ # Authelia schema ├── caddy/ # Caddy proxy server -│   └── addons/ │   └── Caddyfile # Caddy configuration +│   └── addons/ +│   └── cors.conf # Caddy cors configuration ├── flowise/ # Ready-to-import Flowise workflows │   └── uploads/ ├── n8n/ @@ -1448,6 +1479,13 @@ caddy: │   │   ├── credentials/ # Ready-to-import n8n credentials │   │   └── workflows/ # Ready-to-import n8n workflows │   └── local-files/ +├── nginx/ +│   └── addons/ # Nginx configuration +│   ├── authelia-authrequest.conf +│   ├── authelia-location.conf +│   ├── common_proxy_headers.conf +│   ├── cors.conf # Nginx cors configuration +│   └── proxy.conf # Nginx proxy configuration ├── neo4j/ │   ├── logs/ │   └── plugins/ @@ -1468,6 +1506,8 @@ caddy: ├── opencode/ │   ├── opencode.jsonc # OpenCode configuration │   └── run_opencode_docker.py # OpenCode launch script +├── scripts +│   └── auto_config.sh # Proxy setup and access management script ├── searxng/ │ └── settings-base.yml # SearXNG configuration ├── supabase/ diff --git a/scripts/auto_config.sh b/scripts/auto_config.sh index 0ad8775..9138331 100644 --- a/scripts/auto_config.sh +++ b/scripts/auto_config.sh @@ -1,6 +1,6 @@ #!/bin/bash # Trevor SANDY -# Last Update February 09, 2026 +# Last Update February 12, 2026 # Copyright (C) 2026 by Trevor SANDY # # This script is adapted from Inder Singh's setup.sh shell script. @@ -16,63 +16,65 @@ set -euo pipefail # https://stackoverflow.com/a/28085062/18954618 : "${CI:=false}" -: "${AIS:=false}" -: "${AIS_LOCAL:=false}" +: "${AC:=false}" +: "${AC_LOCAL:=false}" : "${WITH_REDIS:=false}" -: "${SUDO_USER:=""}" +: "${SUDO_USER:="$(whoami)"}" ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" # Colors -sgr() { echo -e "\033[$*m"; } - +END='' +COLOR='' BOLD='' DIM='' ITALIC='' UNDERLINE='' -END='' RED='' -RED_BG='' +#RED_BG='' GREEN='' -YELLOW='' +#YELLOW='' BLUE='' -MAGENTA='' +#MAGENTA='' CYAN='' WHITE='' DIM_CYAN='' ITALIC_RED_BG='' UNDERLINE_YELLOW='' ME_HDR="${ME}" -CRITICAL='CRITICAL:' -ERROR='ERROR:' -WARNING='WARNING:' -INFO='INFO:' +CRITICAL="${ME_HDR} CRITICAL:" +ERROR="${ME_HDR} ERROR:" +WARNING="${ME_HDR} WARNING:" +DEBUG="${ME_HDR} DEBUG:" +INFO="${ME_HDR} INFO:" # Check if terminal supports colors https://unix.stackexchange.com/a/10065/642181 +sgr() { [ -n "${END}" ] && echo -e "\033[$*m" || : ; } if [ -t 1 ]; then total_colors=$(tput colors) if [[ -n "$total_colors" && $total_colors -ge 8 ]]; then + END='\033[0m' BOLD='1;' DIM='2;' ITALIC='3;' UNDERLINE='4;' # https://stackoverflow.com/a/28938235/18954618 - END="$(sgr '0')" - RED="$(sgr '31')" - RED_BG="$(sgr '41')" # Red background (White foreground) - GREEN="$(sgr '32')" - YELLOW="$(sgr '33')" - BLUE="$(sgr '34')" - MAGENTA="$(sgr '35')" - CYAN="$(sgr '36')" - WHITE="$(sgr '37')" - DIM_CYAN="$(sgr "${DIM}36")" - ITALIC_RED_BG="$(sgr "${ITALIC}41")" - UNDERLINE_YELLOW="$(sgr "${UNDERLINE}93")" + RED=$(sgr '31') + #RED_BG=$(sgr '41') # Red background (White foreground) + GREEN=$(sgr '32') + #YELLOW=$(sgr '33') + BLUE=$(sgr '34') + #MAGENTA=$(sgr '35') + CYAN=$(sgr '36') + WHITE=$(sgr '37') + DIM_CYAN=$(sgr "${DIM}36") + ITALIC_RED_BG=$(sgr "${ITALIC}41") + UNDERLINE_YELLOW=$(sgr "${UNDERLINE}93") ME_HDR="$(sgr "${ITALIC}34")${ME}${END}" CRITICAL="${ME_HDR} $(sgr "${BOLD}41")CRITICAL:${END}" ERROR="${ME_HDR} $(sgr "${BOLD}91")ERROR:${END}" WARNING="${ME_HDR} $(sgr "${UNDERLINE}93")WARNING:${END}" + DEBUG="${ME_HDR} $(sgr "${BOLD}67")DEBUG:${END}" INFO="${ME_HDR} $(sgr '36')INFO:${END}" fi fi @@ -87,6 +89,9 @@ log_error() { log_warning() { echo -e "${WARNING} ${UNDERLINE_YELLOW}$1${END}" } +log_debug() { + echo -e "${DEBUG} ${WHITE}$1${END}" +} log_info() { echo -e "${INFO} ${DIM_CYAN}$1${END}" } @@ -94,34 +99,64 @@ critical_exit() { log_critical "$*" exit 1 } -FNAME="supabase_setup.sh" +FNAME="auto_config.sh" if [ "${ME}" = "${FNAME}" ]; then - [ -z "${LOG_PATH}" ] && LOG_PATH==`pwd` || : - LOG="$LOG_PATH/$ME.log" - if [ -f ${LOG} -a -r ${LOG} ]; then - rm ${LOG} + [ -z "${AC_LOG_PATH}" ] && AC_LOG_PATH="$(pwd)" || : + LOG="$AC_LOG_PATH/$ME.log" + if [ -f "${LOG}" ] && [ -r "${LOG}" ]; then + rm "${LOG}" fi - exec > >(tee -a ${LOG} ) - exec 2> >(tee -a ${LOG} >&2) + exec > >(tee -a "${LOG}" ) + exec 2> >(tee -a "${LOG}" >&2) fi # Process arguments usage() { - echo "Usage: $0 [OPTIONS]" + echo "Usage: [ENVIRONMENT] $0 [OPTIONS]" echo "" - echo "Setup self-hosted AI-Suite with nginx/caddy and authelia 2FA." + echo "Setup self-hosted AI-Suite with Caddy/Nginx proxy and Authelia 2FA" + echo "identity and access management." + echo "" + echo "Environment:" + echo " CI:false Non-interactive mode - e.g running a GitHub build test" + echo " SUDO_USER:$(whoami) Non-root SUDO user ID - brew does not allow installation as root user" + echo " WITH_REDIS:bool Setup Authelia to use Redis - optional if using --with-authelia option" + echo "" + echo " Auto-configure environment variables:" + echo " AC:false Auto-Configure mode - expects required inputs using env env=input" + echo " AC_LOCAL:false Local (private) installation - * requires additional configuration" + echo " AC_LOG_PATH:str Directory path where install runtime log is deposited" + echo " AC_DOMAIN:str Domain (optional) - Required for global (public) configuration" + echo " AC_USERNAME:str User name for PROXY configuration" + echo " AC_PASSWORD:str User password for PROXY configuration" + echo " AC_CONFIRM:false Send confirmation email on user registration - ** SMTP server required" + echo " AC_EMAIL:str Email address for Authelia - required if using --with-authelia option" + echo " AC_DISPLAY_NAME:str Display name for Authelia - required if using --with-authelia option" + echo " AC_SUDO_PASSWORD:str SUDO user password - directed to sudo using a Here string" + echo " AC_SUDO_USER:str Non-root SUDO user ID - brew does not allow installation as root user" + echo " AC_WITH_REDIS:bool Setup Authelia to use Redis - optional if using --with-authelia option" + echo " AC_WITH_AUTHELIA:false Enable Authelia 2FA support" + echo " AC_PROXY:caddy Set the reverse proxy to use (Caddy or Nginx)" + echo "" + echo " * Add your local domain to the hosts file to loop back to your machine like localhost." + echo " For example: 127.0.0.1 https://supabase.local.com" + echo "" + echo " ** If not using an SMTP server, enter any well formatted email address." + echo " You can view codes sent by Authelia in ./authelia/notifications.txt." echo "" echo "Options:" echo " -h, --help Show this help message and exit" - echo " --proxy PROXY Set the reverse proxy to use (nginx or caddy). Default: caddy" - echo " --with-authelia Enable or disable Authelia 2FA support" + echo " --proxy PROXY Set the reverse proxy to use (Caddy or Nginx) - Default: Caddy" + echo " --with-authelia Enable or disable Authelia 2FA support - Default: false (disable)" echo "" echo "Examples:" - echo " $0 --proxy nginx --with-authelia # Set up Supabase with nginx and Authelia 2FA" - echo " $0 --proxy caddy # Set up Supabase with caddy and no 2FA" + echo " chmod +x $0 # Make $0 executable" + echo " $0 # Basic username and password authentication" + echo " $0 --proxy nginx --with-authelia # Configuration with Nginx and Authelia 2FA" + echo " $0 --proxy caddy # Configuration with Caddy and no 2FA" echo "" - echo "For more information, visit the project repository:" - echo "https://github.com/trevorsandy/ai-suite" + echo "For more information, see README.md:" + echo "https://github.com/trevorsandy/ai-suite/blob/Dev/README.md" } has_argument() { @@ -161,13 +196,22 @@ while [ $# -gt 0 ]; do shift done +if [ "$AC" == true ]; then + SUDO_USER="${AC_SUDO_USER:=${SUDO_USER}}" + with_authelia="${AC_WITH_AUTHELIA:=${with_authelia}}" + WITH_REDIS="${AC_WITH_REDIS:=${WITH_REDIS}}" + proxy="${AC_PROXY:=${proxy}}" +fi + if [[ "$proxy" != "caddy" && "$proxy" != "nginx" ]]; then critical_exit "proxy can only be caddy or nginx" fi -log_info "${END}${GREEN}Configuration Summary" -log_info "${END} ${GREEN}Proxy:${END} ${WHITE}${proxy}" -log_info "${END} ${GREEN}Authelia 2FA:${END} ${WHITE}${with_authelia}" +COLOR=$(sgr "${BOLD}${UNDERLINE}92") +log_info "${END}${COLOR}Configuration Summary" +log_info " -${END} ${GREEN}Proxy:${END} ${WHITE}${proxy}" +log_info " -${END} ${GREEN}Authelia 2FA:${END} ${WHITE}${with_authelia}" +echo -e "${INFO}\n" detect_arch() { case $(uname -m) in @@ -197,67 +241,122 @@ if [[ "$arch" == "err" ]]; then critical_exit "Unsupported cpu architecture"; fi packages=(curl wget jq openssl git) package_is_installed() { - local i=1 - type $1 >/dev/null 2>&1 || { local i=0; } # set i to 0 if not found - if [ "$i" == 1 ]; then - log_info "${END}${GREEN}✔${END} ${WHITE}${1}" - else - log_info "${END}${RED}✘${END} ${WHITE}${1}" - fi - echo "$i" + local i=1 + type "$1" >/dev/null 2>&1 || { local i=0; } # set i to 0 if not found + if [ "$i" == 1 ]; then + log_info "${END} ${GREEN}✔${END} ${WHITE}${1}" + else + log_info "${END} ${RED}✘${END} ${WHITE}${1}" + fi + echo "$i" } +COLOR=$(sgr "${BOLD}${UNDERLINE}95") +echo -e "${INFO}\n" +echo -e "${INFO} ${COLOR}Required Packages:" missing_packages=() for i in "${packages[@]}"; do - if [ "$(package_is_installed $i)" == 0 ]; then missing_packages+=("$i"); fi + if [ "$(package_is_installed "$i")" == 0 ]; then missing_packages+=("$i"); fi done packages=("${missing_packages[@]}") unset missing_packages +is_root() { return "$(id -u "$1")" ; } + +privilage() { + local prompt + if prompt=$(sudo -nv 2>&1); then + echo "has_sudo__pass_set" + elif echo "$prompt" | grep -q '^sudo:'; then + echo "has_sudo__needs_pass" + else + echo "no_sudo" + fi +} + +run_cmd() { + local cmd=$* + local BOLD_CYAN + BOLD_CYAN=$(sgr "${BOLD}36") + user_privilage=$(privilage) + case "$user_privilage" in + has_sudo__pass_set) + sudo "$cmd" + ;; + has_sudo__needs_pass) + echo -e "${CYAN}Supply${END} ${BOLD_CYAN}sudo${END} ${CYAN}password for command:${END} ${BLUE}sudo${END} ${WHITE}$cmd${END}" + sudo "$cmd" + ;; + *) + echo -e "${CYAN}Supply${END} ${BOLD_CYAN}root${END} ${CYAN}password for command:${END} ${BLUE}su -c${END} ${WHITE}\"$cmd\"${END}" + su -c "$cmd" + ;; + esac +} + if (( ${#packages[@]} != 0 )); then - # https://stackoverflow.com/a/18216122/18954618 - if [ "$EUID" -ne 0 ]; then critical_exit "You must run $0 as root user to install packages." ; fi + install_failed=false + + # https://superuser.com/questions/553932 + if ! is_root; then + [ -n "${AC_SUDO_PASSWD}" ] && sudo -S -v <<<"${AC_SUDO_PASSWD}" || : + fi + unset AC_SUDO_PASSWORD + + if [ -x "$(command -v apt-get)" ]; then + run_cmd apt-get update && apt-get install -y "${packages[@]}" apache2-utils + + elif [ -x "$(command -v apk)" ]; then + run_cmd apk update && apk add --no-cache "${packages[@]}" apache2-utils + + elif [ -x "$(command -v dnf)" ]; then + run_cmd dnf makecache && dnf install -y "${packages[@]}" httpd-tools + + elif [ -x "$(command -v zypper)" ]; then + run_cmd zypper refresh && zypper install "${packages[@]}" apache2-utils + + elif [ -x "$(command -v pacman)" ]; then + run_cmd pacman -Syu --noconfirm "${packages[@]}" apache + + elif [ -x "$(command -v pkg)" ]; then + run_cmd pkg update && pkg install -y "${packages[@]}" apache24 + + elif [[ -x "$(command -v brew)" && -n "$SUDO_USER" ]]; then + # brew does not allow installation as root so run install as target SUDO user with SUDO privileges + if is_root; then + if is_root "$SUDO_USER"; then + critical_exit "Cannot run brew install! Current user ($(whoami)) and SUDO_USER ($SUDO_USER) are root!" + fi + fi + run_cmd -u "$SUDO_USER" brew install "${packages[@]}" httpd + else + # diff between array expansion with "@" and "*" https://linuxsimply.com/bash-scripting-tutorial/expansion/array-expansion/ + critical_exit "Failed to install required packages. Package manager apt, apk, dnf, zypper, pacman, pkg, or brew not found." + fi # set -e doesn't work if any command is part of an if statement. package installation errors have to be checked https://stackoverflow.com/a/821419/18954618 # https://unix.stackexchange.com/a/571192/642181 - if [ -x "$(command -v apt-get)" ]; then - apt-get update && apt-get install -y "${packages[@]}" apache2-utils + # shellcheck disable=SC2181 + if [ $? -ne 0 ]; then install_failed=true ; fi - elif [ -x "$(command -v apk)" ]; then - apk update && apk add --no-cache "${packages[@]}" apache2-utils + echo -e "${INFO} ${COLOR}Installed Packages:" + for i in "${packages[@]}"; do + package_is_installed "$i" + done + echo -e "${INFO} ${COLOR}------------------\n" - elif [ -x "$(command -v dnf)" ]; then - dnf makecache && dnf install -y "${packages[@]}" httpd-tools - - elif [ -x "$(command -v zypper)" ]; then - zypper refresh && zypper install "${packages[@]}" apache2-utils - - elif [ -x "$(command -v pacman)" ]; then - pacman -Syu --noconfirm "${packages[@]}" apache - - elif [ -x "$(command -v pkg)" ]; then - pkg update && pkg install -y "${packages[@]}" apache24 - - elif [[ -x "$(command -v brew)" && -n "$SUDO_USER" ]]; then - # brew doesn't allow installation with sudo privileges, thats why have to run script as user who initiated this script with sudo privileges - sudo -u "$SUDO_USER" brew install "${packages[@]}" httpd - else - # diff between array expansion with "@" and "*" https://linuxsimply.com/bash-scripting-tutorial/expansion/array-expansion/ - critical_exit "Failed to install packages. Package manager not found.\nSupported package managers: apt, apk, dnf, zypper, pacman, pkg, brew" - fi - - if [ $? -ne 0 ]; then critical_exit "Failed to install packages."; fi + [ "$install_failed" == true ] && critical_exit "Failed to install required packages." || : fi -github_ac="https://github.com/trevorsandy" -repo_url="$github_ac/ai-suite" -if [ "$AIS" == true ]; then +repo_base="https://github.com/trevorsandy" +repo_url="$repo_base/ai-suite" +if [ "$AC" == true ]; then directory="$(pwd)" else directory="$(basename "$repo_url")" fi -if [[ "$AIS" == true && -d "$directory" ]]; then +if [[ "$AC" == true && -d "$directory" ]]; then log_info "Working directory: $directory" elif [ -d "$directory" ]; then log_info "$directory directory present, skipping git clone" @@ -265,7 +364,7 @@ else git clone --depth=1 "$repo_url" "$directory" fi -if [ "$AIS" == true ]; then +if [ "$AC" == true ]; then if ! cd "$directory"; then critical_exit "Unable to access working directory."; fi else if ! cd "$directory"/docker; then critical_exit "Unable to access $directory/docker directory."; fi @@ -273,13 +372,13 @@ fi if [ ! -f ".env.example" ]; then critical_exit ".env.example file not found. Exiting!"; fi download_binary() { wget "$1" -O "$2" &>/dev/null && chmod +x "$2" &>/dev/null; } -github_ac="https://github.com/singh-inder" +repo_base="https://github.com/singh-inder" url_parser_bin="./url-parser" yq_bin="./yq" if [ ! -x "$url_parser_bin" ]; then - log_info "Downloading url-parser from $github_ac/url-parser" - download_binary "$github_ac"/url-parser/releases/download/v1.1.0/url-parser-"$os"-"$arch" "$url_parser_bin" + log_info "Downloading url-parser from $repo_base/url-parser" + download_binary "$repo_base"/url-parser/releases/download/v1.1.0/url-parser-"$os"-"$arch" "$url_parser_bin" fi if [ ! -x "$yq_bin" ]; then @@ -316,12 +415,14 @@ confirmation_prompt() { # --------------------------------------------------------------------------- -# Get Domain +# Get Supabase Domain domain="" while [ -z "$domain" ]; do if [ "$CI" == true ]; then domain="https://supabase.example.com" - elif [ "$AIS" == true ]; then + elif [ "$AC" == true ]; then + [ -n "$AC_DOMAIN" ] && \ + domain="https://supabase.$AC_DOMAIN" || \ domain="https://$SUPABASE_HOSTNAME" else read -rp "$(format_prompt "Enter your domain:") " domain @@ -363,12 +464,12 @@ done # Get Username username="" if [[ "$CI" == true ]]; then username="inder"; \ -elif [[ "$AIS" == true ]]; then username="$AIS_USERNAME"; fi +elif [[ "$AC" == true ]]; then username="$AC_USERNAME"; fi while [ -z "$username" ]; do read -rp "$(format_prompt "Enter username:") " username - # https://stackoverflow.com/questions/18041761/bash-need-to-test-for-alphanumeric-string + # https://stackoverflow.com/questions/18041761 if [[ ! "$username" =~ ^[a-zA-Z0-9]+$ ]]; then log_error "Only alphabets and numbers are allowed" username="" @@ -382,9 +483,9 @@ confirm_password="" if [[ "$CI" == true ]]; then password="password" confirm_password="password" -elif [[ "$AIS" == true ]]; then - password="$AIS_PASSWORD" - confirm_password="$AIS_PASSWORD" +elif [[ "$AC" == true ]]; then + password="$AC_PASSWORD" + confirm_password="$AC_PASSWORD" fi while [[ -z "$password" || "$password" != "$confirm_password" ]]; do @@ -401,10 +502,10 @@ done # Get Auto-confirm Registered User auto_confirm="" if [[ "$CI" == true ]]; then auto_confirm="false"; \ -elif [[ "$AIS" == true ]]; then auto_confirm="$AIS_AUTO_CONFIRM"; fi +elif [[ "$AC" == true ]]; then auto_confirm="$AC_CONFIRM"; fi while [ -z "$auto_confirm" ]; do - confirmation_prompt auto_confirm "Do you want to send confirmation emails to register users? If yes, you'll have to setup your own SMTP server [y/n]: " + confirmation_prompt auto_confirm "Do you want to send confirmation email when registering a user? If yes, you'll have to setup your own SMTP server [y/n]: " if [[ "$auto_confirm" == true ]]; then auto_confirm="false" elif [[ "$auto_confirm" == false ]]; then @@ -422,38 +523,38 @@ if [[ "$with_authelia" == true ]]; then email="johndoe@gmail.com" display_name="Inder Singh" if [[ "$WITH_REDIS" == true ]]; then setup_redis=true; fi - elif [[ "$AIS" == true ]]; then - email="$AIS_EMAIL" - display_name="$AIS_DISPLAY_NAME" + elif [[ "$AC" == true ]]; then + email="$AC_EMAIL" + display_name="$AC_DISPLAY_NAME" setup_redis="$WITH_REDIS" fi - # Get Admin Email + # Get Authelia Email Address while [ -z "$email" ]; do - read -rp "$(format_prompt "Enter your email for Authelia:") " email + read -rp "$(format_prompt "Enter your email address for Authelia:") " email # split email string on @ symbol IFS="@" read -r before_at after_at <<<"$email" if [[ -z "$before_at" || -z "$after_at" ]]; then - log_error "Invalid email" + log_error "Invalid email address: $email" email="" fi done - # Get Display Name + # Get Authelia Display Name while [ -z "$display_name" ]; do - read -rp "$(format_prompt "Enter Display Name:") " display_name + read -rp "$(format_prompt "Enter your display name for Authelia:") " display_name if [[ ! "$display_name" =~ ^[a-zA-Z0-9[:space:]]+$ ]]; then - log_error "Only alphabets, numbers and spaces are allowed" + log_error "Only alphabets, numbers and spaces are allowed. Received $display_name" display_name="" fi done - # Get Setup Redis - while [[ "$CI" == false && "$AIS" == false && -z "$setup_redis" ]]; do - confirmation_prompt setup_redis "Do you want to setup redis with authelia? [y/n]: " + # Get Setup Authelia to use Redis + while [[ "$CI" == false && "$AC" == false && -z "$setup_redis" ]]; do + confirmation_prompt setup_redis "Do you want to setup Authelia to use Redis? [y/n]: " done fi @@ -497,6 +598,25 @@ gen_token() { anon_token=$(gen_token "anon") service_role_token=$(gen_token "service_role") +#log_debug ": $" +log_debug "————————————" +log_debug "username: $username" +log_debug "AC: $AC" +log_debug "AC_LOCAL: $AC_LOCAL" +log_debug "with_authelia: $with_authelia" +log_debug "email: $email" +log_debug "display_name: $display_name" +log_debug "setup_redis: $setup_redis" +log_debug "jwt_secret: $jwt_secret" +log_debug "anon_token: $anon_token" +log_debug "service_role_token: $service_role_token" +log_debug "domain: $domain" +log_debug "auto_confirm: $auto_confirm" +log_debug "proxy: $proxy" +exit 0 + +# --------------------------------------------------------------------------- + sed -e "3d" \ -e "s|POSTGRES_PASSWORD.*|POSTGRES_PASSWORD=$(gen_hex 16)|" \ -e "s|JWT_SECRET.*|JWT_SECRET=$jwt_secret|" \ @@ -581,7 +701,7 @@ else .services.nginx.command=[\"/bin/bash\",\"-c\",strenv(nginx_cmd)] " - if [[ "$CI" == true || "$AIS_LOCAL" == true ]]; then + if [[ "$CI" == true || "$AC_LOCAL" == true ]]; then # https://github.com/JonasAlfredsson/docker-nginx-certbot/blob/master/docs/advanced_usage.md#local-ca proxy_service_yaml="${proxy_service_yaml} | .services.nginx.environment.USE_LOCAL_CA=1" fi @@ -668,7 +788,6 @@ if [[ "$with_authelia" == true ]]; then "AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET": "${AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET:?error}" }' - # TODO: add db/schema-authelia.sh to ./authelia authelia_docker_supabase_service_yaml='.services.db.environment.AUTHELIA_SCHEMA = strenv(authelia_schema) | .services.db.volumes += "./authelia/db/schema-authelia.sh:/docker-entrypoint-initdb.d/schema-authelia.sh"' @@ -699,7 +818,7 @@ if [[ "$proxy" == "caddy" ]]; then import $caddy_addons_path/cors.conf {\$DOMAIN} { - $([[ "$CI" == true || "$AIS_LOCAL" == true ]] && echo "tls internal") + $([[ "$CI" == true || "$AC_LOCAL" == true ]] && echo "tls internal") @supa_api path /rest/v1/* /auth/v1/* /realtime/v1/* /functions/v1/* /mcp /api/mcp $([[ "$with_authelia" == true ]] && echo "@authelia path /authenticate /authenticate/* @@ -839,25 +958,44 @@ server { " >"$nginx_local_template_file" fi -unset password confirm_password -if [ -n "$SUDO_USER" ]; then chown -R "$SUDO_USER": .; fi -log_info "Cleaning up!" +unset AC_PASSWORD password confirm_password +if [ -n "$SUDO_USER" ]; then + log_info "Setting $(basename "$(pwd)")/* ownership to $SUDO_USER..." + chown -R "$SUDO_USER": .; +fi + +log_info "Cleaning up..." for bin in "$yq_bin" "$url_parser_bin"; do rm "$bin"; done success="${GREEN}Success!${END}" access_message="${CYAN}To access the dashboard over the internet, ensure your firewall allows traffic on ports 80 and 443${END}" -if [[ "$AIS" == true ]]; then +edit_host_file() { + echo -e "\n${INFO} 👉 ${BLUE}Next steps:${END}" + echo "${INFO}${BLUE}1.${END} ${GREEN}Edit your hosts file so your domain will loop back to your${END}" + echo "${INFO} ${GREEN}machine - just like localhost.${END}" + echo "${INFO} ${BLUE}1a.${END} ${GREEN}Create a backup of the original hosts file.${END}" + echo "${INFO} ${GREEN}sudo cp /etc/hosts /etc/hosts.bak${END}" + echo "${INFO} ${BLUE}1b.${END} ${GREEN}Open /etc/hosts file in your editor. Here, I am using vim.${END}" + echo "${INFO} ${GREEN}sudo vim /etc/hosts${END}" + echo "${INFO} ${BLUE}1c.${END} ${GREEN}Add a new entry with format: 'ip-address' 'hostname-or-domain-name'.${END}" + echo "${INFO} ${GREEN}127.0.0.1 https://supabase.local.com${END}" + echo "${INFO} ${GREEN}Save the file and quit your editor.${END}" + echo "" +} + +if [[ "$AC" == true ]]; then echo -e "\n${INFO} 🎉 ${success}" + if [ "${AC_LOCAL}" == true ]; then edit_host_file ; fi echo -e "\n${INFO} 🌐 ${access_message}\n" exit 0 fi echo -e "\n${INFO} 🎉 ${success}" echo "${INFO} 👉 ${CYAN}Next steps:${END}" -echo "${INFO} ${WHITE}1.${END} ${CYAN}Change into the docker directory:${END}" -echo "${INFO} ${WHITE}cd $directory/docker${END}" -echo "${INFO} ${WHITE}2.${END} ${CYAN}Start the services with Docker Compose:${END}" -echo "${INFO} ${WHITE}docker compose up -d${END}" -echo "${INFO} 🚀 ${GREEN}Everything should now be running!${END}" +echo "${INFO} ${WHITE}1.${END} ${CYAN}Change into $directory:${END}" +echo "${INFO} ${WHITE}cd $directory${END}" +echo "${INFO} ${WHITE}2.${END} ${CYAN}Run suite_services.py:${END}" +echo "${INFO} ${WHITE}python suite_services.py --profile ai-all --operation start${END}" +echo "${INFO} 🚀 ${GREEN}Confirm everything is running from console output${END}" echo -e "\n${INFO} 🌐 ${access_message}\n" diff --git a/suite_services.py b/suite_services.py index d38c287..9e92c68 100644 --- a/suite_services.py +++ b/suite_services.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Trevor SANDY -Last Update February 9, 2026 +Last Update February 12, 2026 Copyright (c) 2025-Present by Trevor SANDY AI-Suite uses this script for the installation command that handles the AI-Suite @@ -57,6 +57,7 @@ import sys import argparse import datetime import dotenv +import getpass import gzip import logging import pathlib @@ -716,11 +717,13 @@ def operate_ai_suite(operation, profile, environment, env_vars): cmd.extend(["--profile", argument]) cmd.extend(["-f", "docker-compose.yml", operation]) run_command(cmd) + insert = operation if operation == 'pull': + insert.join(' and prune') cmd = ["docker", "image", "prune", "--force"] run_command(cmd) log.info("="*60, extra=LSHF.style(logging.INFO, LSHF.BLUE)) - log.info(f"{name} services '{operation}' completed.", extra=log_bright) + log.info(f"{name} services image '{operation}' completed.", extra=log_bright) def start_built_container(compose_file=None, environment=None, build=False): """Start the locally built container services (using its compose file).""" @@ -1523,6 +1526,194 @@ def display_service_endpoints(profile, supabase, env_vars): log.info("❌ {}".format(container), extra=fail_style) log.info("="*60, extra=line_style) +def configure_ai_suite_access(env_vars:dict): + """Setup env_vars for self-hosted AI-Suite with Caddy/Nginx proxy and Authelia + 2FA identity and access management. + """ + ac_env_list = [] + public = False + # AC env + log.info(f"Auto-configuring {name} access...") + env_vars.update({'AC': 'true'}) + # AC_SUDO_USER env + default = env_vars.get('AC_SUDO_USER', getpass.getuser()) + non_root = "WSL non-root" if system == 'Windows' else "non-root" + response = input(f"Enter a {non_root} sudo user (current user: {default}): ") + default = response if response else default + env_vars.update({'AC_SUDO_USER': default}) + # AC_SUDO_PASSWORD env + password = getpass.getpass(f"Enter sudo password for package install: ") + if not password: + log.warning(f"A sudo password prompt will trigger on package install.") + ac_env_list.append("AC_SUDO_PASSWORD='{}'".format(password)) + # AC_USERNAME env # + default = env_vars.get('AC_USERNAME', 'ai_suite_user') + response = input(f"Enter proxy user name (required: {default}): ") + default = response.strip() if response else default + env_vars.update({'AC_USERNAME': default}) + # AC_PASSWORD env + password = getpass.getpass(f"Enter proxy user password (required): ") + if not password: + log.error(f"A proxy user password is required. Exiting auto-configure...") + return [] + ac_env_list.append("AC_PASSWORD='{}'".format(password)) + env_vars.update({'AC_PASSWORD': '*******'}) + # AC_LOG_PATH + default = env_vars.get('AC_LOG_PATH', os.path.normpath('scripts')) + env_vars.update({'AC_LOG_PATH': default}) + # AC_LOCAL env + default = env_vars.get('AC_LOCAL', 'false') + default = True if str(default).lower() == 'true' else False + response = input("Is this a local (private) installation? y/n ({}): " + .format('y' if default else 'n')) + default = True if response.lower() == 'y' else default + env_vars.update({'AC_LOCAL': str(default)}) + public = not default + # AC_DOMAIN env + default = env_vars.get('AC_DOMAIN', 'ai-suite.fr' if public else 'local.com') + response = input(f"Enter a domain ({default}): ") + default = response.strip() if response else default + env_vars.update({'AC_DOMAIN': default}) + # AC_CONFIRM env + default = env_vars.get('AC_CONFIRM', 'false') + default = True if str(default).lower() == 'true' else False + response = input("Send confirmation email on user registration? y/n ({}): " + .format('y' if default else 'n')) + default = True if response.lower() == 'y' else default + env_vars.update({'AC_CONFIRM': str(default)}) + if default: + # AC_EXIM_SMTP env + default = env_vars.get('AC_EXIM_SMTP', 'false') + default = True if str(default).lower() == 'true' else False + #response = input("Add Exim SMTP server? y/n ({}): " + # .format('y' if default else 'n')) + default = True if response.lower() == 'y' else default + #env_vars.update({'AC_EXIM_SMTP': str(default)}) + # AC_PROXY env + default = env_vars.get('AC_PROXY', 'Caddy') + response = input(f"Enter proxy (Caddy or Nginx: {default}): ") + if response.strip().lower() not in ['', 'caddy', 'nginx']: + log.warning(f"Invalid proxy specified: {response}. Using {default}.") + response = None + default = response.strip() if response else default + env_vars.update({'AC_PROXY': default.lower()}) + # AC_WITH_AUTHELIA env + default = env_vars.get('AC_WITH_AUTHELIA', 'false') + default = True if str(default).lower() == 'true' else False + response = input("Include Authelia 2FA (Two Factor Authentication)? y/n ({}): " + .format('y' if default else 'n')) + with_authelia = True if response.lower() == 'y' else default + env_vars.update({'AC_WITH_AUTHELIA': str(with_authelia)}) + if with_authelia: + # AC_EMAIL env + default = env_vars.get('AC_EMAIL', 'ai.suite.user@local.com') + response = input(f"Enter Authelia user email (required: {default}): ") + default = response.strip() if response else default + env_vars.update({'AC_EMAIL': default}) + # AC_DISPLAY_NAME env + default = env_vars.get('AC_DISPLAY_NAME', f'{name} User') + response = input(f"Enter Authelia user display name (required: {default}): ") + default = response.strip() if response else default + env_vars.update({'AC_DISPLAY_NAME': default}) + # AC_WITH_REDIS env + default = env_vars.get('AC_WITH_REDIS', ('true' if public else 'false')) + default = True if str(default).lower() == 'true' else False + response = input("Use Redis with Authelia? y/n ({}: {}): " + .format('recommended' if public else 'optional', + 'y' if default else 'n')) + default = True if response.lower() == 'y' else default + env_vars.update({'AC_WITH_REDIS': str(default)}) + # Generate Auto-configure Env list + ac_env_prefix = 'AC_' + ac_env_bool = ['AC', 'AC_LOCAL', 'AC_CONFIRM', 'AC_WITH_AUTHELIA', 'AC_WITH_REDIS'] + for env, var in env_vars.items(): + if not var or not env.startswith(ac_env_prefix): + continue + if env == 'AC_PASSWORD': + continue + if env in ac_env_bool: + var = str(var).lower() + ac_env_var = "{}='{}'".format(env, var) + ac_env_list.append(ac_env_var) + # Display Env settings + header = ("{} Auto-configure Access Env Settings").format(name) + emoji = '🚀' + color = LSHF.GREEN + header_style = { + 'prefix': ("{0} {1}{2}").format( + emoji, LSHF.prefix(color, bold=True, underline=True), header), + 'suffix': LSHF.suffix()} + header_style.update({'purge_msg': 'True'}) + line_style = LSHF.style(logging.INFO, LSHF.BLUE) + env_prefix = LSHF.prefix(color) + var_prefix = LSHF.prefix(LSHF.BLUE) + + log.info("") + log.info("="*60, extra=line_style) + log.info(header, extra=header_style) + log.info("="*60, extra=line_style) + + for env, var in env_vars.items(): + if not env.startswith(ac_env_prefix): + continue + emoji = '🌐' + env_var_prefix = ("{}• {} {:18s}{}={}'{}'").format( + env_prefix, emoji, env, LSHF.suffix(), + var_prefix, var) + env_var_style = {'prefix': env_var_prefix, 'suffix': LSHF.suffix()} + env_var_style.update({'purge_msg':'True'}) + raw_msg = ("• {} {:18s}='{}'").format(emoji, env, var) + log.info(raw_msg, extra=env_var_style) + log.info("="*60, extra=line_style) + + accept = True + response = input(f"Are these auto-configure settings ok to continue? y/n: (y)") + accept = True if response.lower() == 'y' else accept if not response else False + if accept: + return ac_env_list + else: + log.info(f"Auto-configure settings rejected.") + return [] + +def run_configure_ai_suite_access_command(ac_env_vars:list): + """Configure self-hosted AI-Suite with Caddy/Nginx proxy and Authelia 2FA + identity and access management. + """ + if not ac_env_vars: + log.error(f"Auto-configure env_var list is empty!") + return + ac_script = os.path.normpath(os.path.join(".", "scripts", "auto_config.sh")) + if not os.path.exists(ac_script): + log.error(f"Auto-configure script not found at {ac_script}") + return + ac_log_path = os.path.normpath(os.path.dirname(ac_script)) + if system == 'Windows': + ac_log_path = ac_log_path.replace("\\", "/") + ac_script = ac_script.replace("\\", "/") + ac_log = "".join(['>', os.path.join(ac_log_path, 'auto_config.log'), ' 2>&1']) + cmd = [] + if system == "Windows": + cmd = ["wsl", "-e", "bash", "-c", "env"] + ac_env_vars + ["./" + ac_script] + else: + cmd = ["bash", "-c", "env"] + ac_env_vars + [ac_script] + cmd_msg = [] + for element in cmd: + if element.split('=')[0].endswith('_PASSWORD'): + continue + cmd_msg.append(element) + raw_msg = " ".join([log_run_cmd, " ".join(cmd_msg)]) + log.info(raw_msg, extra=LSHF.style(header=log_run_cmd, msg=" ".join(cmd_msg))) + try: + completed = subprocess.run(cmd, capture_output=True, text=True, check=True) + if completed.returncode != 0: + log.error(f"Command: auto-configure: {completed.stderr}") + else: + info_style = LSHF.style(logging.INFO, LSHF.GREEN) + ac_log_file = os.path.join(ac_log_path, 'auto_config.sh.log') + log.info(f"See details in run log: {ac_log_file}", extra=info_style) + except Exception as e: + log.error(f"Exception: auto-configure: {e}.") + def main(): # Name and file globals global name, file @@ -1559,7 +1750,7 @@ def main(): agent_all_profiles = open_webui_all_profiles + ['opencode'] server_profiles = ['supabase', 'flowise', 'searxng', 'langfuse', 'neo4j', 'caddy'] profiles = agent_all_profiles + open_webui_utils_profiles + server_profiles + \ - llama_docker_profiles + llama_host_profiles + llama_docker_profiles + llama_host_profiles + ['no-auto-config'] managemant_operations = ['stop', 'stop-llama', 'start', 'pause', 'unpause'] installation_operations = ['update', 'install'] data_operations = ['backup-data', 'restore-data'] @@ -1602,6 +1793,7 @@ def main(): open-webui-all Open WebUI complete bundle n8n-all n8n, Open WebUI and selected utilities bundle ai-all full {name} bundle + no-auto-config do not auto-configure access on install or update - LLM modules: cpu gpu-nvidia gpu-amd Ollama CPU/GPU options running in Docker @@ -1657,6 +1849,9 @@ def main(): ...to update all modules and restart using Ollama CPU running in Docker: python {file} --profile ai-all cpu --operation update + ...to update all modules but do not auto-configure access management: + python {file} --profile ai-all no-auto-config --operation update + ...to install all modules and start using Ollama running on the Host: python {file} --operation install @@ -1756,7 +1951,8 @@ def main(): attempted_launch = False llama_found = False llama_path = normalize_path(env_vars.get('LLAMA_PATH')) - llama_cpp = any(p for p in args.profile if p == 'llama.cpp') + if any(p for p in args.profile if p == 'llama.cpp'): + llama_cpp = True llama = "LLaMA.cpp" if llama_cpp else "Ollama" if llama_path: llama_exe = os.path.normpath(llama_path) @@ -1859,6 +2055,15 @@ def main(): for env in ['LLAMACPP_DEFAULT_MODEL', 'LLAMACPP_MODEL_PATH', 'LLAMA_ARG_HF_REPO']: log.debug(f" - {env}: {env_vars[env]}", extra=debug_style) + # Auto-configuration + ac_env_vars = [] + if any(p for p in args.profile if p == 'no-auto-config'): + ac_env_vars = configure_ai_suite_access(env_vars) + for env, var in env_vars.items(): + if not env.startswith('AC_'): + continue + set_dotenv_var(env_file, env, var, None) + # Process operation argument install = False build = False @@ -1883,7 +2088,7 @@ def main(): build = args.operation in ['update', 'install'] if build: install = args.operation == 'install' - if args.operation == 'update': + if args.operation == 'update++': user_confirm = input(textwrap.dedent(f"""\ Performing an {name} update can impact its integrity. Consider backing up your data to enable rollback. @@ -1904,10 +2109,13 @@ def main(): docker_compose_include(True, True, False) destroy_ai_suite(args.profile, install) operate_ai_suite(args.operation, args.profile, args.environment, env_vars) - if not build: + if build: + insert = "Installing" if install else "Updating" + log.info(f"{insert} '{name}' with profile arguments: {args.profile}...") + else: sys.exit(0) else: - raw_msg = f"{name} will perform operations similar to an update..." + raw_msg = f"Updating '{name}' with profile arguments: {args.profile}..." log.info(" ".join([log_notice, raw_msg]), extra=LSHF.style(header=log_notice, msg=raw_msg)) os.remove('.operation') if os.path.exists('.operation') else None @@ -1932,21 +2140,8 @@ def main(): clone_supabase_repo() convert_supabase_pooler_line_endings() - # Auto-configuration - if build: - insert = "Installing" if install else "Updating" - log.info(f"{insert} '{name}' with profile arguments: {args.profile}...") - if not any(p for p in args.profile if p == 'no-auto-config'): - config_script = os.path.join('scripts', 'auto_config.sh') - if os.path.exists(config_script): - log.info(f"Auto-configuring {name}...") - if system == "Windows": - cmd = ['bash', str(config_script)] - else: - cmd = ['bash', str(config_script)] - run_command(cmd) - else: - log.error(f"Auto-config script not found at {config_script}") + if ac_env_vars: + run_configure_ai_suite_access_command(ac_env_vars) if any(p for p in args.profile if p in n8n_all_profiles): env_vars['POSTGRES_HOST'] = "db" if supabase else "postgres"