A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys, Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun VPN routing, and a web dashboard to manage it all. Free & open forever to self-host; optional paid hosted services fund it. See PROMISE.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
195 lines
7.3 KiB
Bash
Executable File
195 lines
7.3 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
setupWizardTerminal()
|
|
{
|
|
isHeader "LibrePortal Setup Wizard"
|
|
isNotice "Let's get your install configured. This only runs once."
|
|
echo ""
|
|
|
|
local install_name=$(generateInstallName)
|
|
while true; do
|
|
isNotice "Suggested Install Name: $install_name"
|
|
isQuestion "Press Enter to accept, 'r' to roll a new one, or type your own : "
|
|
read -p "" install_name_input
|
|
if [[ -z "$install_name_input" ]]; then
|
|
break
|
|
elif [[ "$install_name_input" =~ ^[rR]$ ]]; then
|
|
install_name=$(generateInstallName)
|
|
continue
|
|
elif [[ "$install_name_input" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
|
install_name="$install_name_input"
|
|
break
|
|
fi
|
|
isNotice "Invalid input. Use letters, numbers, and hyphens only."
|
|
done
|
|
|
|
# Domains — optional, multi (CFG_DOMAIN_1..9). Empty input ends the loop.
|
|
isHeader "Domains (optional)"
|
|
isNotice "Add one or more domains pointed at this server."
|
|
isNotice "Each domain unlocks https://app.<domain> routing via Traefik + a self-signed SSL cert."
|
|
isNotice "Press Enter on a blank line to finish (or skip entirely for a local-only install)."
|
|
echo ""
|
|
|
|
local domains=()
|
|
local domain_idx=1
|
|
while [[ $domain_idx -le 9 ]]; do
|
|
isQuestion "Domain $domain_idx (or blank to finish) : "
|
|
read -p "" d
|
|
[[ -z "$d" ]] && break
|
|
if [[ ! "$d" =~ ^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$ ]]; then
|
|
isNotice "Invalid domain — try again."
|
|
continue
|
|
fi
|
|
local dns_result=$(setupCheckDomainPointsHere "$d")
|
|
local matches=$(echo "$dns_result" | jq -r '.matches')
|
|
local server_ip=$(echo "$dns_result" | jq -r '.server_ip')
|
|
local domain_ip=$(echo "$dns_result" | jq -r '.domain_ip')
|
|
if [[ "$matches" == "true" ]]; then
|
|
isSuccessful "✓ '$d' resolves to this server ($server_ip)."
|
|
domains+=("$d")
|
|
((domain_idx++))
|
|
else
|
|
isNotice "⚠ '$d' resolves to '$domain_ip', this server is '$server_ip'."
|
|
isQuestion "Use it anyway? Traefik may not route this until DNS is fixed. (y/n) : "
|
|
read -p "" use_anyway
|
|
if [[ "$use_anyway" =~ ^[yY]$ ]]; then
|
|
domains+=("$d")
|
|
((domain_idx++))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
local detected_tz=""
|
|
if command -v timedatectl >/dev/null 2>&1; then
|
|
detected_tz=$(timedatectl show -p Timezone --value 2>/dev/null)
|
|
fi
|
|
if [[ -z "$detected_tz" && -r /etc/timezone ]]; then
|
|
detected_tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')
|
|
fi
|
|
|
|
local timezone=""
|
|
local common_timezones=("Europe/London" "Europe/Paris" "Europe/Berlin" "America/New_York" "America/Chicago" "America/Los_Angeles" "America/Denver" "America/Phoenix" "Asia/Tokyo" "Australia/Sydney")
|
|
while true; do
|
|
if [[ -n "$detected_tz" ]]; then
|
|
isNotice "Detected OS timezone: $detected_tz"
|
|
isQuestion "Press Enter to use it, or type a number / 'c' to choose differently : "
|
|
read -p "" tz_choice
|
|
if [[ -z "$tz_choice" ]]; then
|
|
timezone="$detected_tz"
|
|
break
|
|
fi
|
|
else
|
|
isNotice "Select a timezone:"
|
|
for ((i=0; i<${#common_timezones[@]}; i++)); do
|
|
isOption "$((i+1)). ${common_timezones[i]}"
|
|
done
|
|
isOption "c. Custom (enter manually)"
|
|
isQuestion "Choice : "
|
|
read -p "" tz_choice
|
|
fi
|
|
|
|
if [[ "$tz_choice" =~ ^[cC]$ ]]; then
|
|
isQuestion "Custom timezone (e.g. Pacific/Auckland) : "
|
|
read -p "" timezone
|
|
[[ -n "$timezone" ]] && break
|
|
elif [[ "$tz_choice" =~ ^[0-9]+$ && "$tz_choice" -ge 1 && "$tz_choice" -le "${#common_timezones[@]}" ]]; then
|
|
timezone="${common_timezones[tz_choice-1]}"
|
|
break
|
|
else
|
|
isNotice "Invalid selection."
|
|
fi
|
|
done
|
|
isSuccessful "Timezone set to '$timezone'"
|
|
|
|
isHeader "Recommended Apps"
|
|
local apps=()
|
|
local crowdsec_dashboard="false"
|
|
isOption " - traefik (reverse proxy, handles LetsEncrypt SSL)"
|
|
isOption " - crowdsec (host-installed intrusion prevention)"
|
|
echo ""
|
|
if [[ ${#domains[@]} -eq 0 ]]; then
|
|
isNotice "No domains configured — Traefik has nothing to route. Skipping by default."
|
|
isQuestion "Install Traefik anyway? (y/n) : "
|
|
read -p "" want_traefik
|
|
[[ "$want_traefik" =~ ^[yY]$ ]] && apps+=("traefik")
|
|
else
|
|
isQuestion "Install Traefik? (Y/n) : "
|
|
read -p "" want_traefik
|
|
[[ ! "$want_traefik" =~ ^[nN]$ ]] && apps+=("traefik")
|
|
fi
|
|
isQuestion "Install CrowdSec? (Y/n) : "
|
|
read -p "" want_crowdsec
|
|
if [[ ! "$want_crowdsec" =~ ^[nN]$ ]]; then
|
|
apps+=("crowdsec")
|
|
isQuestion " └─ Install CrowdSec Management Console (Metabase web UI)? (Y/n) : "
|
|
read -p "" want_console
|
|
[[ ! "$want_console" =~ ^[nN]$ ]] && crowdsec_dashboard="true"
|
|
fi
|
|
|
|
isHeader "Optional Apps"
|
|
isQuestion "Install Wireguard (VPN — secure remote access)? (y/N) : "
|
|
read -p "" want_wireguard
|
|
[[ "$want_wireguard" =~ ^[yY]$ ]] && apps+=("wireguard")
|
|
|
|
# Traefik is the only app that needs an email — for LetsEncrypt cert
|
|
# registration. We collect it now (before the install task fires) so the
|
|
# Traefik installer doesn't have to prompt mid-task.
|
|
local traefik_email=""
|
|
if [[ " ${apps[*]} " == *" traefik "* ]]; then
|
|
while true; do
|
|
isQuestion "LetsEncrypt email for Traefik (cert-expiry notices) : "
|
|
read -p "" traefik_email
|
|
emailValidation "$traefik_email"
|
|
[[ $? -eq 0 ]] && break
|
|
isNotice "Please provide a valid email address."
|
|
done
|
|
fi
|
|
|
|
isHeader "Confirm"
|
|
isNotice "Install Name : $install_name"
|
|
isNotice "Timezone : $timezone"
|
|
if [[ ${#domains[@]} -gt 0 ]]; then
|
|
isNotice "Domains : ${domains[*]}"
|
|
else
|
|
isNotice "Domains : (none — local install)"
|
|
fi
|
|
isNotice "Apps : ${apps[*]:-none}"
|
|
[[ " ${apps[*]} " == *" crowdsec "* ]] && isNotice "CrowdSec UI : $crowdsec_dashboard"
|
|
[[ -n "$traefik_email" ]] && isNotice "Traefik Email : $traefik_email"
|
|
echo ""
|
|
isQuestion "Apply these settings? (Y/n) : "
|
|
read -p "" confirm
|
|
if [[ "$confirm" =~ ^[nN]$ ]]; then
|
|
isNotice "Setup cancelled. You can re-run from the menu."
|
|
return 1
|
|
fi
|
|
|
|
local app_options="{}"
|
|
if [[ " ${apps[*]} " == *" crowdsec "* ]]; then
|
|
app_options=$(jq -n --argjson d "$crowdsec_dashboard" '{crowdsec:{dashboard:$d}}')
|
|
fi
|
|
|
|
local payload=$(jq -n \
|
|
--arg n "$install_name" \
|
|
--arg t "$timezone" \
|
|
--arg te "$traefik_email" \
|
|
--argjson a "$(printf '%s\n' "${apps[@]}" | jq -R . | jq -s .)" \
|
|
--argjson dom "$(printf '%s\n' "${domains[@]}" | jq -R . | jq -s 'map(select(length > 0))')" \
|
|
--argjson opts "$app_options" \
|
|
'{install_name:$n, timezone:$t, domains:$dom, apps:$a, appOptions:$opts, traefik_email:$te}')
|
|
|
|
local b64=$(echo -n "$payload" | base64 -w 0)
|
|
setupApply "$b64"
|
|
}
|
|
|
|
checkConfigFirstInstall()
|
|
{
|
|
if isSetupWizardComplete; then
|
|
return 0
|
|
fi
|
|
if [[ "$unattended_setup" == "true" ]]; then
|
|
return 0
|
|
fi
|
|
setupWizardTerminal
|
|
}
|