diff --git a/deploy.sh b/deploy.sh index 985f17d..87562af 100755 --- a/deploy.sh +++ b/deploy.sh @@ -3,7 +3,7 @@ -# --- MAIN FUNCTIONS LOGIC ---> +# --- UTILITY FUNCTIONS ---> echod() { MESSAGE=${1} @@ -12,14 +12,55 @@ echod() { fi } +ssh_to_host() { + local COMMAND="${1}" + ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" +} + +user_input() { + local VAR_NAME="${1}" + local HEADER="${2}" + local PLACEHOLDER="${3}" + local REGEX="${4}" + local ERROR_MSG="${5}" + local SENSITIVE="${6:-false}" + + echo "" + gum style --foreground 212 --bold "${HEADER}" + + while true; do + [[ "$SENSITIVE" == "false" ]] && INPUT_VALUE=$(gum input --placeholder "${PLACEHOLDER}") + [[ "$SENSITIVE" == "true" ]] && INPUT_VALUE=$(gum input --password --placeholder "${PLACEHOLDER}") + + if [[ -z "${INPUT_VALUE}" ]]; then + echo "āŒ Error: Input cannot be empty. Please provide the necessary information." + continue + fi + + if [[ -n "${REGEX}" ]]; then + if [[ ! "${INPUT_VALUE}" =~ ${REGEX} ]]; then + echo "āŒ Error: ${ERROR_MSG}" + continue + fi + fi + + export "${VAR_NAME}"="${INPUT_VALUE}" + break + done +} +# --- UTILITY FUNCTIONS ---< + + + +# --- GLOBAL FUNCTIONS ---> cleanup() { echo -e "\n āœ… Cleaning up..." - rm -${DIR_RM_FLAGS} /run/numbus/logs - rm -${DIR_RM_FLAGS} /run/numbus/web - rm -${DIR_RM_FLAGS} /run/numbus/config + rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/ - kill ${BRIDGE_PID} + if [[ ${WEB_MODE} -eq 1 && -n "${BRIDGE_PID:-}" ]]; then + kill ${BRIDGE_PID} + fi } compatibility_check() { @@ -50,46 +91,6 @@ compatibility_check() { return 0 } -launch_configurator() { - echo -e "\n šŸš€ Launching Numbus Configurator..." - python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 & - export BRIDGE_PID=$! - - echo -e "\n āž”ļø Open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")" - xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true -} - -preparation() { - SELECTED_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \ - "Numbus Server : Professional-grade hosting, strictly kept under your roof." \ - "Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \ - "Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \ - "Numbus TV : A premium cinematic experience free from trackers and forced subscriptions.") - - SELECTED_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \ - "Interactive : You don't already have a configuration." \ - "Non-interactive : You have a valid configuration hosted on a Git platform.") - - git_url() { - IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :") - } - - git_url - - until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do - echo -e "\n āš ļø This did not work correctly." - - echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}" - read URL - - if [[ "${URL^^}" == "N" ]]; then - git_url - fi - - echo -e "\n You will be prompted for your credentials again. Make sure that they are correct." - done -} - hierarchy_preparation() { echod "\n šŸ”„ Preparing the folder hierarchy for the final configuration..." @@ -101,44 +102,25 @@ hierarchy_preparation() { echo " āœ… Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed." fi + # Script folders + mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config + mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs + mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp + [[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web + # Secrets mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/ mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks - if [[ "${SELECTED_DEVICE_TYPE}" == "" ]]; then + mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system + if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman - mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system - fi - mkdir -${MKDIR_FLAGS} to-keep-preciously/ -} - -setup_ssh() { - echod "\n āœ… Generating new SSH key for numbus-admin..." - - chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ - ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q - - if [[ ${DEBUG} -eq 1 ]]; then - echo -e "\n āž”ļø Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..." fi - if sshpass -p "${LIVE_TARGET_PASSWD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then - if [[ ${DEBUG} -eq 1 ]]; then - echo -e "\n āœ… SSH key copied successfully" - fi - else - echo -e "\n āŒ Failed to copy SSH key. Please check the host IP and password." - exit 1 - fi -} - -ssh_to_host() { - local COMMAND="${1}" - ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" + echod "\n āœ… Folder hierarchy ready" } hardware_detection() { - ### --> Get hardware information local TMPFILE="/tmp/nixos-installation-hw-detection" ssh_to_host 'bash -s' << SSHEND @@ -191,7 +173,7 @@ for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do fi # Disk health - if [[ \$(echo "${LIVE_TARGET_PASSWD}" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') == "PASSED" ]]; then + if [[ \$(echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') == "PASSED" ]]; then DISK_HEALTH+=("PASSED") else DISK_HEALTH+=("N/A") @@ -225,19 +207,15 @@ for var in \ declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}" done SSHEND - ### Get hardware information <-- scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null source "${TMPFILE}" - ### Transform the bash variables into JSON --> - # We prepare the disk data as a flat array to pass to jq local DISK_FLAT_ARRAY=() for i in "${!DISK_NAME[@]}"; do DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}") done - # Generate the JSON file for the configurator jq -n \ --argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \ --argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \ @@ -261,16 +239,154 @@ SSHEND } ] }' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH} - ### Transform the bash variables into JSON <-- - ### --> Generate hardware-configuration.nix if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then echo -e "\nāœ… Hardware configuration generated" else echo -e "\nāŒ Failed to generate hardware configuration" exit 1 fi - ### Generate hardware-configuration.nix <-- +} +# --- GLOBAL FUNCTIONS ---< + + + +# --- MAIN WEB FUNCTIONS ---> +launch_configurator() { + echo -e "\n šŸš€ Launching Numbus Configurator..." + python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 & + export BRIDGE_PID=$! + + echo -e "\n āž”ļø Open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")" + xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true +} +# --- MAIN WEB FUNCTIONS ---< + + + +# --- MAIN TUI FUNCTIONS ---> +preparation() { + echo -e "\n āž”ļø This script will now guide you through the configuration and gather the necessary information." + + echo "" + RAW_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \ + "Numbus Server : Professional-grade hosting, strictly kept under your roof." \ + "Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \ + "Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \ + "Numbus TV : A premium cinematic experience free from trackers and forced subscriptions.") + + case "${RAW_DEVICE_TYPE}" in + "Numbus Server : "* ) DEVICE_TYPE="server" ;; + "Numbus Backup Server : "* ) DEVICE_TYPE="backup" ;; + "Numbus Computer : "* ) DEVICE_TYPE="computer" ;; + "Numbus TV : "* ) DEVICE_TYPE="tv" ;; + esac + + RAW_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \ + "Interactive : You don't already have a configuration." \ + "Non-interactive : You have a valid configuration hosted on a Git platform.") + + case "${RAW_DEPLOYMENT_MODE}" in + "Interactive : "* ) DEPLOYMENT_MODE="interactive" ;; + "Non-interactive : "* ) DEPLOYMENT_MODE="non-interactive" ;; + esac + + if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then + git_url() { + IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :") + } + + git_url + + until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do + echo -e "\n āš ļø This did not work correctly." + + echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}" + read URL + + if [[ "${URL^^}" == "N" ]]; then + git_url + fi + + echo -e "\n You will be prompted for your credentials again. Make sure that they are correct." + done + fi + + echo "" + gum format -- \ + "āž”ļø To continue, you need to start the target device in a NixOS live environment : + 1. Download the NixOS iso from the **[official website](https://nixos.org/download/)**. + 2. Flash it to a USB stick. (use a flashing tool like **[Rufus](https://rufus.ie/en/#download)**, **[BalenaEtcher](https://etcher.balena.io/#download-etcher)**, **[Impression](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression)**, ...) + 3. Make sure your computer allows booting from USB drives and is in UEFI mode. + 4. Boot into the NixOS live environment. + 5. Launch a terminal. Set a password using \`passwd\` and find the IP address using \`ip a\`" + + echo "" + gum confirm "Is the device ready ?" || { echo "āŒ You need to prepare the device. The script cannot continue."; exit 1; } + + # LIVE TARGET SETTINGS + user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" "Invalid IP address format." + user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true" + + # INTERNATIONALIZATION SETTINGS + user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc" "" "" + user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc" "" "" + user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc" "" "" +} + +configuration() { + if [[ "${DEVICE_TYPE}" == "server" ]]; then + + + # Users & Groups + user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" "" "" + user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}" "Invalid email address format." + user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." + + echo -e "\n\n āž”ļø You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains" + # TRAEFIK SETTINGS + user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" "Invalid domain name format." + user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true" + + echo -e "\n\n āž”ļø Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)" + # SMTP SETTINGS + user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format." + user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true" + user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format." + user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number." + + echo -e "\n\n āž”ļø This server will connect to your local network and you will configure its IP address\n" + # NETWORK SETTINGS + user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)." + user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format." + user_input "HOME_SERVER_IP" " Please choose the ip address that your server will use (i.e. any address in the 192.168.1.1/24 range that is not in use.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format." + elif [[ "${DEVICE_TYPE}" == "backup" ]]; then + : + elif [[ "${DEVICE_TYPE}" == "computer" ]]; then + : + elif [[ "${DEVICE_TYPE}" == "tv" ]]; then + : + fi +} + +setup_ssh() { + echod "\n āœ… Generating new SSH key for numbus-admin..." + + chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ + ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q + + if [[ ${DEBUG} -eq 1 ]]; then + echo -e "\n āž”ļø Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..." + fi + + if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then + if [[ ${DEBUG} -eq 1 ]]; then + echo -e "\n āœ… SSH key copied successfully" + fi + else + echo -e "\n āŒ Failed to copy SSH key. Please check the host IP and password." + exit 1 + fi } services_selection() { @@ -304,12 +420,416 @@ services_selection() { gum confirm "Do you want to edit the default subdomain of your services ?" || { echo -e "\n\nāœ… Continuing..."; return 0; } for service in ${SELECTED_WEB_APPLICATIONS[@]} ${SELECTED_DNS_SERVICE[@]}; do - SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" ) + if gum confirm "Change the subdomain of ${service} ?"; then + SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" ) + fi done - export SELECTED_WEB_APPLICATIONS_SUBDOMAIN + return 0 } + + + + + + + + + + + + + + + + + + +############################################################################# +declare -A ACL_GROUPS +declare -A ACL_USERS + +ACL_SERVICES=() + +compute_acl_services() { + local all_services=("${SELECTED_DNS_SERVICE[@]}" "${SELECTED_WEB_APPLICATIONS[@]}" "${SELECTED_SYSTEM_SERVICES[@]}") + ACL_SERVICES=() # Reset to prevent duplicates on reload + + for svc in "${all_services[@]}"; do + local excluded=false + for ex in "${EXCLUDED_ACL_SERVICES[@]}"; do + if [[ "$svc" == "$ex" ]]; then + excluded=true + break + fi + done + if [[ "$excluded" == false ]]; then + ACL_SERVICES+=("$svc") + fi + done +} + +# Upgraded to support default values for editing, and '--' for gum style +get_valid_input() { + local -n return_var=$1 + local prompt_text=$2 + local regex=$3 + local is_mandatory=$4 + local default_value=$5 + + while true; do + local val + # --value pre-fills the input for easy editing + val=$(gum input --prompt "$prompt_text: " --width 50 --value "$default_value") + + # Handle empty input + if [[ -z "$val" ]]; then + if [[ "$is_mandatory" == true ]]; then + gum style --foreground "#ff0000" -- "āœ– This field is mandatory." + continue + else + return_var="" + break + fi + fi + + # Handle Regex Validation + if [[ -n "$regex" ]]; then + if [[ "$val" =~ $regex ]]; then + return_var="$val" + break + else + gum style --foreground "#ff0000" -- "āœ– Invalid format. Please try again." + fi + else + return_var="$val" + break + fi + done +} + +# ========================================== +# 4. GROUP MANAGEMENT +# ========================================== + +show_groups_table() { + if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then + gum style --italic --foreground "#6272a4" -- "No groups configured." + return + fi + + # We use CSV format with quotes to handle comma-separated services correctly + local csv="Group Name,Allowed Services\n" + for g in "${!ACL_GROUPS[@]}"; do + csv+="\"$g\",\"${ACL_GROUPS[$g]}\"\n" + done + + printf "%b" "$csv" | gum table +} + +add_group() { + if [[ ${#ACL_GROUPS[@]} -ge 10 ]]; then + gum style --foreground "#ffb86c" -- "⚠ Maximum of 10 groups reached." + sleep 2; return + fi + + local group_name + get_valid_input group_name "Group Name" "^[a-zA-Z0-9_-]+$" true "" + + if [[ -n "${ACL_GROUPS[$group_name]}" ]]; then + gum style --foreground "#ff0000" -- "āœ– Group already exists." + sleep 2; return + fi + + gum style --foreground "#50fa7b" -- "Select services for $group_name (Space to select, Enter to confirm):" + local chosen_services + chosen_services=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -) + + ACL_GROUPS["$group_name"]="$chosen_services" + gum style --foreground "#50fa7b" -- "āœ” Group '$group_name' created." + sleep 1 +} + +edit_group() { + if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then return; fi + + local group_keys=("${!ACL_GROUPS[@]}") + gum style -- "Select a group to edit:" + local group_name=$(gum choose "${group_keys[@]}") + + if [[ -z "$group_name" ]]; then return; fi + if [[ "$group_name" == "admin" ]]; then + gum style --foreground "#ff0000" -- "āœ– The admin group cannot be modified." + sleep 2; return + fi + + gum style --foreground "#50fa7b" -- "Select NEW services for $group_name:" + local chosen_services=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -) + + ACL_GROUPS["$group_name"]="$chosen_services" + gum style --foreground "#50fa7b" -- "āœ” Group '$group_name' updated." + sleep 1 +} + +remove_group() { + if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then return; fi + + local group_keys=("${!ACL_GROUPS[@]}") + gum style -- "Select a group to REMOVE:" + local group_name=$(gum choose "${group_keys[@]}") + + if [[ -z "$group_name" ]]; then return; fi + if [[ "$group_name" == "admin" ]]; then + gum style --foreground "#ff0000" -- "āœ– The admin group cannot be removed." + sleep 2; return + fi + + gum style --foreground "#ff5555" --bold -- "Are you sure you want to delete '$group_name'?" + if gum confirm; then + unset ACL_GROUPS["$group_name"] + gum style --foreground "#50fa7b" -- "āœ” Group deleted." + sleep 1 + fi +} + +manage_groups_menu() { + while true; do + clear + gum style --border double --margin "1" --padding "0 1" --border-foreground "#8be9fd" -- "Group Management (${#ACL_GROUPS[@]}/10)" + show_groups_table + + local action=$(gum choose "Add Group" "Edit Group" "Remove Group" "Back") + case "$action" in + "Add Group") add_group ;; + "Edit Group") edit_group ;; + "Remove Group") remove_group ;; + "Back"|"") break ;; + esac + done +} + +# ========================================== +# 5. USER MANAGEMENT +# ========================================== + +show_users_table() { + if [[ ${#ACL_USERS[@]} -eq 0 ]]; then + gum style --italic --foreground "#6272a4" -- "No users configured." + return + fi + + local csv="Username,Name,Email,Health Alerts,ACL Type,ACL Value\n" + for u in "${!ACL_USERS[@]}"; do + IFS='|' read -r name email phone health type val <<< "${ACL_USERS[$u]}" + csv+="\"$u\",\"$name\",\"$email\",\"$health\",\"$type\",\"$val\"\n" + done + + printf "%b" "$csv" | gum table +} + +add_user() { + if [[ ${#ACL_USERS[@]} -ge 20 ]]; then + gum style --foreground "#ffb86c" -- "⚠ Maximum of 20 users reached." + sleep 2; return + fi + + local name username email phone health_alert acl_type acl_value + + get_valid_input username "Username" "^[a-z0-9_-]+$" true "" + if [[ -n "${ACL_USERS[$username]}" ]]; then + gum style --foreground "#ff0000" -- "āœ– Username already exists." + sleep 2; return + fi + + get_valid_input name "Full Name" "" true "" + get_valid_input email "Email Address" "$EMAIL_REGEX" true "" + get_valid_input phone "Phone Number (E.164, optional)" "$PHONE_REGEX" false "" + + gum style -- "Inform about server health?" + if gum confirm; then health_alert="true"; else health_alert="false"; fi + + gum style -- "How should ACL be managed for $username?" + acl_type=$(gum choose "Assign to Group" "Manual Service Selection") + + if [[ "$acl_type" == "Assign to Group" ]]; then + acl_type="group" + local group_keys=("${!ACL_GROUPS[@]}") + acl_value=$(gum choose "${group_keys[@]}") + else + acl_type="manual" + gum style --foreground "#50fa7b" -- "Select services for $username:" + acl_value=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -) + fi + + ACL_USERS["$username"]="$name|$email|$phone|$health_alert|$acl_type|$acl_value" + gum style --foreground "#50fa7b" -- "āœ” User '$username' created." + sleep 1 +} + +edit_user() { + if [[ ${#ACL_USERS[@]} -eq 0 ]]; then return; fi + + local user_keys=("${!ACL_USERS[@]}") + gum style -- "Select a user to edit:" + local username=$(gum choose "${user_keys[@]}") + + if [[ -z "$username" ]]; then return; fi + + # Extract current values + IFS='|' read -r curr_name curr_email curr_phone curr_health curr_type curr_val <<< "${ACL_USERS[$username]}" + local name email phone health_alert acl_type acl_value + + gum style --foreground "#f1fa8c" -- "Editing User: $username" + get_valid_input name "Full Name" "" true "$curr_name" + get_valid_input email "Email Address" "$EMAIL_REGEX" true "$curr_email" + get_valid_input phone "Phone Number" "$PHONE_REGEX" false "$curr_phone" + + if [[ "$username" == "admin" ]]; then + gum style --foreground "#ffb86c" -- "Admin health alerts and ACL settings cannot be changed." + health_alert="true" + acl_type="group" + acl_value="admin" + sleep 2 + else + gum style -- "Inform about server health? (Currently: $curr_health)" + if gum confirm; then health_alert="true"; else health_alert="false"; fi + + gum style -- "How should ACL be managed? (Currently: $curr_type)" + acl_type=$(gum choose "Assign to Group" "Manual Service Selection") + + if [[ "$acl_type" == "Assign to Group" ]]; then + acl_type="group" + local group_keys=("${!ACL_GROUPS[@]}") + acl_value=$(gum choose "${group_keys[@]}") + else + acl_type="manual" + gum style --foreground "#50fa7b" -- "Select services for $username:" + acl_value=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -) + fi + fi + + ACL_USERS["$username"]="$name|$email|$phone|$health_alert|$acl_type|$acl_value" + gum style --foreground "#50fa7b" -- "āœ” User '$username' updated." + sleep 1 +} + +remove_user() { + if [[ ${#ACL_USERS[@]} -eq 0 ]]; then return; fi + + local user_keys=("${!ACL_USERS[@]}") + gum style -- "Select a user to REMOVE:" + local username=$(gum choose "${user_keys[@]}") + + if [[ -z "$username" ]]; then return; fi + if [[ "$username" == "admin" ]]; then + gum style --foreground "#ff0000" -- "āœ– The admin user cannot be removed." + sleep 2; return + fi + + gum style --foreground "#ff5555" --bold -- "Are you sure you want to delete '$username'?" + if gum confirm; then + unset ACL_USERS["$username"] + gum style --foreground "#50fa7b" -- "āœ” User deleted." + sleep 1 + fi +} + +manage_users_menu() { + while true; do + clear + gum style --border double --margin "1" --padding "0 1" --border-foreground "#ff79c6" -- "User Management (${#ACL_USERS[@]}/20)" + show_users_table + + local action=$(gum choose "Add User" "Edit User" "Remove User" "Back") + case "$action" in + "Add User") add_user ;; + "Edit User") edit_user ;; + "Remove User") remove_user ;; + "Back"|"") break ;; + esac + done +} + +# ========================================== +# 6. SETUP & EXPORT +# ========================================== + +setup_admin_user() { + if [[ -n "${ACL_USERS["admin"]}" ]]; then return; fi + + gum style --border rounded --padding "1 2" --margin "1" --border-foreground "#ff79c6" -- "Initial Setup: Administrator User" + + local name email phone + get_valid_input name "Admin Full Name" "" true "" + get_valid_input email "Admin Email Address" "$EMAIL_REGEX" true "" + get_valid_input phone "Admin Phone Number (optional)" "$PHONE_REGEX" false "" + + ACL_USERS["admin"]="$name|$email|$phone|true|group|admin" + gum style --foreground "#50fa7b" -- "āœ” Administrator configured." + sleep 1 +} + +export_data() { + clear + gum style --foreground "#50fa7b" --bold -- "--- Provisioning Data Payload ---" + echo "GROUPS:" + for group in "${!ACL_GROUPS[@]}"; do + echo " $group -> Allowed: ${ACL_GROUPS[$group]}" + done + echo "" + echo "USERS (Name|Email|Phone|Alert|AclType|AclValue):" + for user in "${!ACL_USERS[@]}"; do + echo " $user -> ${ACL_USERS[$user]}" + done +} + +main_menu() { + compute_acl_services + ACL_GROUPS["admin"]=$(printf "%s," "${ACL_SERVICES[@]}" | sed 's/,$//') + setup_admin_user + + while true; do + clear + gum style --border double --margin "1" --padding "1 2" --border-foreground "#bd93f9" -- "Server Deployment - Access Management" + gum style -- "Current state: ${#ACL_GROUPS[@]}/10 Groups | ${#ACL_USERS[@]}/20 Users" + echo "" + + local choice=$(gum choose "1. Manage Groups" "2. Manage Users" "3. Finish & Apply Configuration" "4. Exit without saving") + + case "$choice" in + "1. Manage Groups") manage_groups_menu ;; + "2. Manage Users") manage_users_menu ;; + "3. Finish & Apply Configuration") + export_data + break + ;; + "4. Exit without saving"|"") + echo "Aborting..." + exit 1 + ;; + esac + done +} + + +########################################################################"" + + + + + + + + + + + + + + + + disks_selection() { gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " āš ļø $(gum style --foreground 212 'WARNING:') You will choose the disks to install NixOS on. @@ -392,9 +912,9 @@ disks_selection() { server_config_generation() { echo -e "\n # Server settings" >> ${CONFIGURATION_PATH} - echo -e " time.timeZone = \"${TIMEZONE}\";" >> ${CONFIGURATION_PATH} + echo -e " time.timeZone = \"${INTERNATIONALIZATION_TIMEZONE}\";" >> ${CONFIGURATION_PATH} echo -e " numbus.locale = \"${LOCALE}\";" >> ${CONFIGURATION_PATH} - echo -e " numbus.language = \"${LANGUAGE}\";" >> ${CONFIGURATION_PATH} + echo -e " numbus.language = \"${INTERNATIONALIZATION_LANGUAGE}\";" >> ${CONFIGURATION_PATH} echo -e " numbus.owner = \"${SERVER_OWNER_NAME}\";" >> ${CONFIGURATION_PATH} } @@ -480,9 +1000,9 @@ keys_generation() { echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" ssh_to_host 'bash -s' << EOF -echo "$LIVE_TARGET_PASSWD" | sudo -S mkdir -p /etc/secrets/disks/ -echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}" -echo "$LIVE_TARGET_PASSWD" | sudo -S chmod 600 /etc/secrets/disks/boot-${i} +echo "$LIVE_TARGET_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/ +echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}" +echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/boot-${i} EOF done for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do @@ -490,8 +1010,8 @@ EOF echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" ssh_to_host 'bash -s' << EOF -echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}" -echo "$LIVE_TARGET_PASSWD" | sudo -S chmod 600 /etc/secrets/disks/content-${i} +echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}" +echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/content-${i} EOF done for i in $(seq 1 "$PARITY_DISK_NUMBER"); do @@ -499,8 +1019,8 @@ EOF echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" ssh_to_host 'bash -s' << EOF -echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}" -echo "$LIVE_TARGET_PASSWD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i} +echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}" +echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i} EOF done @@ -752,7 +1272,7 @@ deploy() { postrun_action() { TARGET_USER="numbus-admin" LIVE_TARGET_IP="${HOME_SERVER_IP}" - LIVE_TARGET_PASSWD="changeMe!" + LIVE_TARGET_PASSWORD="changeMe!" echo -e "\n\n Now the remote machine will reboot. You will need to input the boot disk(s) passphrase. This will be the only time you will have to do so, it will be automatic in the future." @@ -789,7 +1309,7 @@ postrun_action() { Do you want to enable automatic disk decryption on boot ?" if gum confirm "āž”ļø I understand, 'yes' to proceed."; then - sshpass -p "${LIVE_TARGET_PASSWD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF + sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF echo "Enrolling boot disk key to TPM..." BOOT_DISKS_NAME=(${BOOT_DISKS_NAME[@]}) @@ -806,14 +1326,14 @@ for i in \${!BOOT_DISKS_NAME[@]}; do DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}2" fi [[ "\${DEBUG}" == "true" ]] && echo "Issuing enroll command for disk \${DISK_PATH}..." - echo ${LIVE_TARGET_PASSWD} | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-\${j} \${DISK_PATH} + echo ${LIVE_TARGET_PASSWORD} | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-\${j} \${DISK_PATH} j=\$((j + 1)) done echo "Getting PCRS 15 hash..." -PCR_HASH=\$(echo ${LIVE_TARGET_PASSWD} | sudo -S systemd-analyze pcrs 15 --json=short) +PCR_HASH=\$(echo ${LIVE_TARGET_PASSWORD} | sudo -S systemd-analyze pcrs 15 --json=short) -echo ${LIVE_TARGET_PASSWD} | sudo -S sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix +echo ${LIVE_TARGET_PASSWORD} | sudo -S sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix EOF else echo "Skipping TPM configuration." @@ -830,7 +1350,7 @@ securely on a hidden sheet of paper or add it to your password manager (locally gum confirm "āž”ļø I understand, 'yes' to proceed." || { echo -e "\n\nāŒ Aborting as requested."; exit 1; } - echo $LIVE_TARGET_PASSWD | sudo -S passwd numbus-admin + echo $LIVE_TARGET_PASSWORD | sudo -S passwd numbus-admin } nix_update() { @@ -839,11 +1359,11 @@ nix_update() { nixos-rebuild --target-host numbus-admin@${LIVE_TARGET_IP} \ --use-remote-sudo switch --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server } -# --- MAIN FUNCTIONS LOGIC ---< +# --- MAIN FUNCTIONS ---< -# --- DEFAULTS ---> +# --- DEFAULT VARIABLES ---> WEBSERVER_PORT=${WEBSERVER_PORT:-8088} LIVE_DATA_PATH="/run/numbus/web/live_settings.json" @@ -854,7 +1374,8 @@ CONFIG_FILE="config/numbus.yaml" TARGET_USER="nixos" -EXTRA_FILES_PATH="/run/numbus/config" +TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")" +EXTRA_FILES_PATH="${TMP_FILES_PATH}/config" if [[ ${DEBUG-0} -eq 1 ]]; then FILES_CP_FLAGS="vau" @@ -870,7 +1391,15 @@ else MKDIR_FLAGS="p" MV_FLAGS="u" fi -# --- DEFAULTS ---< + +IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$' +SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' +DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$' +EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' +PORT_REGEX='^[0-9]{1,5}$' +SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*' +PHONE_REGEX='^\+[1-9][0-9]{7,14}$' +# --- DEFAULTS VARIABLES ---< @@ -896,11 +1425,12 @@ echo """ DEPLOY_MODE=$(gum choose --header "Choose your preferred configuration interface :" "Through my browser (Recommended for beginners)" "Through my terminal (TUI)") if [[ "$DEPLOY_MODE" == "Through my terminal (TUI)" ]]; then + WEB_MODE=0 preparation - hierarchy_preparation - setup_ssh + configuration else + WEB_MODE=1 launch_configurator hierarchy_preparation echod "\n ā³ Waiting for device credentials from web UI..." @@ -908,9 +1438,9 @@ else sleep 5 done echod "\n āœ… Credentials received." - LANGUAGE=$(jq -r '.language' ${LIVE_DATA_PATH}) + INTERNATIONALIZATION_LANGUAGE=$(jq -r '.language' ${LIVE_DATA_PATH}) COUNTRY=$(jq -r '.country' ${LIVE_DATA_PATH}) - TIMEZONE=$(jq -r '.timeZone' ${LIVE_DATA_PATH}) + INTERNATIONALIZATION_TIMEZONE=$(jq -r '.timeZone' ${LIVE_DATA_PATH}) DEVICE_TYPE=$(jq -r '.device' ${LIVE_DATA_PATH}) DEPLOYMENT_MODE=$(jq -r '.deploymentMode' ${LIVE_DATA_PATH}) if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then