LibrePortal/scripts/checks/first_install.sh
librelad 875a60f90f LibrePortal v0.1.0 — initial release
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>
2026-05-21 20:37:54 +01:00

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
}