Virtualmin Rootless Podman Discourse: Post-Install Master Initializer
Virtualmin has recently made its Podman plugin available in dev, opening up clean containerized deployments directly within the Virtualmin control panel. However, out-of-the-box Discourse deployments currently face two critical bootstrapping hurdles:
- First-Run Authentication Lockout: Upon initial installation, Discourse prompts for an admin account and forces
admin@[domain]. If you create this, the container cannot send out the required activation email, leaving you locked out. - Outbound Crawling & Gateway Networking: Internal link previews fail, and Postfix relay authorization for the container’s gateway IP (
10.88.0.x) needs manual configuration to allow outbound transactional emails.
To resolve these issues cleanly without manually digging into Rails consoles or container shell instances, I wrote an interactive Post-Install Master Initializer.
Key Features
- Rootless Podman Instance Scanning: Autodetects rootless Podman users running Discourse web containers, mapping system owners, domain names, and container IDs automatically.
- Database User Management: Syncs, adds, edits, or deletes Discourse users directly via internal Rails passes—instantly activating new accounts as fully approved administrators.
- Persistent Credential Caching: Optionally caches admin credentials to a secure
.envfile (600permissions) for rapid deployment across multiple containers. - Network & Gateway Alignment: Automatically detects container gateway IPs and updates Discourse’s
allowed_internal_hostsalongside host Postfixmynetworksconfigurations. - Dry-Run & Diagnostic Logging: Inspects live database state and network configurations without altering system settings. Includes built-in log viewing (
--log) and in-place binary updating (--update-script).
CLI Parameter Reference
| Parameter | Function |
|---|---|
--domain DOMAIN |
Target a specific domain directly (bypasses the instance menu). |
--user USERNAME |
Target a specific system owner. |
--update-script |
In-place self-update with editor choice and automated syntax check. |
--update-cache |
Refreshes the local discovery cache (/root/.discourse_containers.cache). |
--log |
Displays the session log from the most recent execution pass. |
--dry-run |
Read-only inspection of network settings and database tables. |
-h, --help |
Displays command syntax and options. |
Step 1: Bootstrapper & Installation Script
Run this initial snippet directly in Bash as root. It handles path creation, binary placement, editor detection (nano, micro, vim, vi), and launches your chosen editor so you can paste the main script binary in Step 2.
cd /tmp
clear
echo "=========================================================="
echo " Virtualmin Discourse Initializer - Installation "
echo "=========================================================="
echo ""
echo "Select installation target directory:"
echo " [1] /usr/local/bin (Recommended - Already in \$PATH)"
echo " [2] /root/scripts (Isolated directory)"
echo " [3] Custom Directory"
echo ""
read -p "Selection [1-3] (Default: 1): " DIR_CHOICE
case $DIR_CHOICE in
2) TARGET_DIR="/root/scripts" ;;
3) read -p "Enter custom directory path: " TARGET_DIR ;;
*) TARGET_DIR="/usr/local/bin" ;;
esac
mkdir -p "$TARGET_DIR"
SCRIPT_PATH="$TARGET_DIR/virtualmin-discourse-init.sh"
if [ -f "$SCRIPT_PATH" ]; then
TS=$(date +%s)
BACKUP_PATH="${TARGET_DIR}/virtualmin-discourse-init-${TS}.sh"
mv "$SCRIPT_PATH" "$BACKUP_PATH"
echo ""
echo "[-] Existing script backed up to: $BACKUP_PATH"
fi
if ! echo "$PATH" \vert{} grep -E -q "(^\vert{}:)${TARGET_DIR}(:|\$)"; then
echo ""
echo "[!] Notice: $TARGET_DIR is not currently in your system \$PATH."
read -p "Would you like to add $TARGET_DIR to /root/.bashrc? [y/N]: " ADD_PATH
if [[ "$ADD_PATH" =~ ^[Yy]$ ]]; then
echo "export PATH=\"\$PATH:$TARGET_DIR\"" >> /root/.bashrc
export PATH="$PATH:$TARGET_DIR"
echo " -> Added $TARGET_DIR to /root/.bashrc"
fi
fi
AVAILABLE_EDITORS=()
for ed in "${EDITOR:-}" nano micro vim vi emacs pico; do
if [ -n "$ed" ] && command -v "$ed" &>/dev/null; then
if [[ ! " ${AVAILABLE_EDITORS[*]} " =~ " ${ed} " ]]; then
AVAILABLE_EDITORS+=("$ed")
fi
fi
done
CHOSEN_EDITOR="${AVAILABLE_EDITORS[0]:-nano}"
if [ ${#AVAILABLE_EDITORS[@]} -gt 1 ]; then
echo ""
echo "Detected text editors on your system:"
for i in "${!AVAILABLE_EDITORS[@]}"; do
echo " [$((i+1))] ${AVAILABLE_EDITORS[$i]}"
done
echo ""
read -p "Select editor [1-${#AVAILABLE_EDITORS[@]}] (Default: 1 - ${CHOSEN_EDITOR}): " ED_CHOICE
if [[ "$ED_CHOICE" =~ ^[0-9]+$ ]] && [ "$ED_CHOICE" -ge 1 ] && [ "$ED_CHOICE" -le "${#AVAILABLE_EDITORS[@]}" ]; then
CHOSEN_EDITOR="${AVAILABLE_EDITORS[$((ED_CHOICE-1))]}"
fi
fi
echo ""
echo "[-] Launching ${CHOSEN_EDITOR} on${SCRIPT_PATH}..."
"$CHOSEN_EDITOR" "$SCRIPT_PATH" && chmod +x "$SCRIPT_PATH"
echo ""
echo "=========================================================="
echo " Installation Complete! "
echo "=========================================================="
echo " Script Location: $SCRIPT_PATH"
echo ""
echo " Usage Options:"
echo " 1. Interactive Admin & Network Tool:"
echo " virtualmin-discourse-init.sh"
echo ""
echo " 2. Self-Update / In-Place Upgrade:"
echo " virtualmin-discourse-init.sh --update-script"
echo ""
echo " 3. Read-Only Pass / Display Help:"
echo " virtualmin-discourse-init.sh --dry-run"
echo " virtualmin-discourse-init.sh --help"
echo "=========================================================="
Step 2: Main Operational Script
Paste the code block below into the editor window launched by the installation script above, then save and exit.
#!/usr/bin/env bash
set -eo pipefail
DRY_RUN=0
UPDATE_CACHE_ONLY=0
CLI_DOMAIN=""
CLI_USER=""
REAL_SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || echo "$0")"
SCRIPT_DIR="$(dirname "$REAL_SCRIPT_PATH")"
CLI_ARGS="$*"
CACHE_FILE="/root/.discourse_containers.cache"
LOG_FILE="${SCRIPT_DIR}/virtualmin-discourse-init.log"
[ "$SCRIPT_DIR" == "/usr/local/bin" ] && LOG_FILE="/root/virtualmin-discourse-init.log"
rotate_log() {
> "$LOG_FILE"
}
log_diag() {
echo "$@" | sed -r 's/\x1B\[[0-9;]*[mK]//g' >> "$LOG_FILE"
}
show_help() {
echo "=========================================================="
echo " Virtualmin Discourse Post-Install Master Initializer "
echo "=========================================================="
echo "Usage: virtualmin-discourse-init.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --domain DOMAIN Target a specific domain directly (skips selection menu)."
echo " --user USERNAME Target a specific system owner (narrows menu if user owns multiple)."
echo " --update-script Perform an in-place script update. Backs up running binary,"
echo " launches preferred editor to paste update, and validates."
echo " --update-cache Perform a fresh scan for containers, refresh local cache"
echo " file ($CACHE_FILE), and exit cleanly."
echo " --log Display the session log from the most recent run."
echo " --dry-run Run in read-only mode to inspect container network settings,"
echo " cached .env values, and Discourse database user tables."
echo " -h, --help Display this help message and exit."
echo "=========================================================="
exit 0
}
display_last_log() {
if [ -f "$LOG_FILE" ]; then
echo "=========================================================="
echo " LAST RUN LOG FILE: $LOG_FILE"
echo "=========================================================="
cat "$LOG_FILE"
echo "=========================================================="
else
echo "[!] No previous log file found at: $LOG_FILE"
fi
exit 0
}
perform_self_update() {
echo "=========================================================="
echo " Virtualmin Discourse Initializer - Self-Update "
echo "=========================================================="
echo ""
echo "Current binary location: $REAL_SCRIPT_PATH"
TS=$(date +%s)
BACKUP_PATH="${REAL_SCRIPT_PATH}-${TS}.bak"
mv "$REAL_SCRIPT_PATH" "$BACKUP_PATH"
echo "[-] Running script backed up to: $BACKUP_PATH"
AVAILABLE_EDITORS=()
for ed in "${EDITOR:-}" nano micro vim vi emacs pico; do
if [ -n "$ed" ] && command -v "$ed" &>/dev/null; then
if [[ ! " ${AVAILABLE_EDITORS[*]} " =~ " ${ed} " ]]; then
AVAILABLE_EDITORS+=("$ed")
fi
fi
done
CHOSEN_EDITOR="${AVAILABLE_EDITORS[0]:-nano}"
if [ ${#AVAILABLE_EDITORS[@]} -gt 1 ]; then
echo ""
echo "Detected text editors:"
for i in "${!AVAILABLE_EDITORS[@]}"; do
echo " [$((i+1))] ${AVAILABLE_EDITORS[$i]}"
done
echo ""
read -p "Select editor [1-${#AVAILABLE_EDITORS[@]}] (Default: 1 - ${CHOSEN_EDITOR}): " ED_CHOICE
if [[ "$ED_CHOICE" =~ ^[0-9]+$ ]] && [ "$ED_CHOICE" -ge 1 ] && [ "$ED_CHOICE" -le "${#AVAILABLE_EDITORS[@]}" ]; then
CHOSEN_EDITOR="${AVAILABLE_EDITORS[$((ED_CHOICE-1))]}"
fi
fi
echo ""
echo "[-] Opening editor for update..."
"$CHOSEN_EDITOR" "$REAL_SCRIPT_PATH" && chmod +x "$REAL_SCRIPT_PATH"
echo ""
echo "[-] Self-update complete! Verifying updated script:"
echo "----------------------------------------------------------"
"$REAL_SCRIPT_PATH" --help
exit 0
}
user_podman() {
local target_user="$1"
shift
local user_uid
user_uid=$(id -u "$target_user" 2>/dev/null || true)
if [ -z "$user_uid" ]; then
log_diag "[LOG-DIAG] Failed to resolve UID for user: $target_user"
return 1
fi
loginctl enable-linger "$target_user" 2>/dev/null || true
systemctl start "user@$user_uid.service" 2>/dev/null || true
if [ ! -d "/run/user/$user_uid" ]; then
mkdir -p "/run/user/$user_uid"
chown "$target_user:$target_user" "/run/user/$user_uid"
chmod 700 "/run/user/$user_uid"
fi
(cd / && sudo -u "$target_user" XDG_RUNTIME_DIR="/run/user/$user_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$user_uid/bus" podman "$@")
}
scan_containers() {
echo "[-] Scanning system for active Virtualmin Discourse instances..."
log_diag "[-] Scanning system for active Virtualmin Discourse instances..."
DISCOVERED_USERS=()
DISCOVERED_DOMAINS=()
DISCOVERED_CONTAINERS=()
log_diag "[LOG-DIAG] Fetching active users..."
local candidate_uids
candidate_uids=$(loginctl list-users --no-legend 2>/dev/null | awk '{print $1}')
if [ -z "$candidate_uids" ]; then
log_diag "[LOG-DIAG] loginctl returned empty list. Fallback to /etc/passwd users with UID >= 1000..."
candidate_uids=$(awk -F: '$3 >= 1000 && $3 < 60000 {print $3}' /etc/passwd)
fi
for user_uid in $candidate_uids; do
username=$(id -nu "$user_uid" 2>/dev/null || true)
if [ -n "$username" ] && [ "$username" != "root" ]; then
log_diag "[LOG-DIAG] Testing user: $username (UID: $user_uid)..."
local ps_output
ps_output=$(user_podman "$username" ps --format "{{.Names}}" 2>&1 || true)
log_diag "[LOG-DIAG] Podman ps result for $username:"
log_diag "$ps_output"
# Match any web container for Discourse
while IFS= read -r web_container; do
if [ -n "$web_container" ]; then
dom_name=""
if command -v virtualmin &>/dev/null; then
dom_name=$(virtualmin list-domains --user "$username" --name-only 2>/dev/null | grep -i "discourse" | head -n 1 || true)
if [ -z "$dom_name" ]; then
dom_name=$(virtualmin list-domains --user "$username" --name-only 2>/dev/null | head -n 1 || true)
fi
fi
[ -z "$dom_name" ] && dom_name="discourse.example.com ($username)"
DISCOVERED_USERS+=("$username")
DISCOVERED_DOMAINS+=("$dom_name")
DISCOVERED_CONTAINERS+=("$web_container")
echo " Checking user: $username ... [FOUND: $dom_name ($web_container)]"
log_diag "[LOG-MATCH] User $username -> Domain: $dom_name | Container: $web_container"
fi
done < <(echo "$ps_output" | grep -E "_discourse-web$|discourse.*web" || true)
if ! echo "$ps_output" | grep -qE "_discourse-web$|discourse.*web"; then
echo " Checking user: $username ... (No Discourse container)"
fi
fi
done
if [ ${#DISCOVERED_CONTAINERS[@]} -eq 0 ]; then
echo ""
echo "[!] Error: No Discourse web containers detected on this server."
log_diag "[!] Error: No Discourse web containers detected on this server."
rm -f "$CACHE_FILE"
exit 1
fi
> "$CACHE_FILE"
for i in "${!DISCOVERED_CONTAINERS[@]}"; do
echo "${DISCOVERED_DOMAINS[$i]}|${DISCOVERED_USERS[$i]}|${DISCOVERED_CONTAINERS[$i]}" >> "$CACHE_FILE"
done
chmod 600 "$CACHE_FILE"
echo ""
echo "[+] Discovery complete! Cached ${#DISCOVERED_CONTAINERS[@]} container(s) to $CACHE_FILE"
log_diag "[-] Updated container discovery cache: $CACHE_FILE"
}
# Parse CLI Arguments
while [[ $# -gt 0 ]]; do
case $1 in
--domain)
CLI_DOMAIN="$2"
shift 2
;;
--user)
CLI_USER="$2"
shift 2
;;
--update-script|--update)
perform_self_update
;;
--update-cache)
UPDATE_CACHE_ONLY=1
shift
;;
--log|--display-log)
display_last_log
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
show_help
;;
*)
shift
;;
esac
done
if [ "$EUID" -ne 0 ]; then
echo "[!] Error: This script must be run as root (or with sudo)."
exit 1
fi
rotate_log
log_diag "=========================================================="
log_diag " Session Start : $(date '+%Y-%m-%d %H:%M:%S %Z')"
log_diag " Executing Binary : $REAL_SCRIPT_PATH"
log_diag " CLI Flags Passed : ${CLI_ARGS:-[None]}"
log_diag " Target Domain Flag: ${CLI_DOMAIN:-[None]}"
log_diag " Target User Flag : ${CLI_USER:-[None]}"
log_diag " Active Log Target : $LOG_FILE"
log_diag " Environment User : $(whoami) (EUID: $EUID)"
log_diag " Cache File Target : $CACHE_FILE"
log_diag "=========================================================="
if [ "$UPDATE_CACHE_ONLY" -eq 1 ]; then
scan_containers
exit 0
fi
clear
echo "=========================================================="
if [ "$DRY_RUN" -eq 1 ]; then
echo " Virtualmin Discourse Initializer (DRY-RUN MODE) "
else
echo " Virtualmin Discourse Post-Install Master Initializer "
fi
echo "=========================================================="
echo ""
load_cached_containers() {
DISCOVERED_DOMAINS=()
DISCOVERED_USERS=()
DISCOVERED_CONTAINERS=()
while IFS='|' read -r dom u_owner cont; do
if [ -n "$dom" ] && [ -n "$u_owner" ] && [ -n "$cont" ]; then
DISCOVERED_DOMAINS+=("$dom")
DISCOVERED_USERS+=("$u_owner")
DISCOVERED_CONTAINERS+=("$cont")
fi
done < "$CACHE_FILE"
}
if [ -f "$CACHE_FILE" ]; then
echo "[-] Loading instances from discovery cache ($CACHE_FILE)..."
log_diag "[-] Loading instances from discovery cache ($CACHE_FILE)..."
load_cached_containers
if [ ${#DISCOVERED_CONTAINERS[@]} -eq 0 ]; then
echo "[!] Cache file empty or invalid. Triggering rescan..."
log_diag "[!] Cache file empty or invalid. Triggering rescan..."
scan_containers
load_cached_containers
fi
else
scan_containers
fi
# Multi-Match Container Filtering Logic
FILTERED_INDICES=()
find_target_matches() {
FILTERED_INDICES=()
for i in "${!DISCOVERED_CONTAINERS[@]}"; do
if [ -n "$CLI_DOMAIN" ] && [ -n "$CLI_USER" ]; then
if [[ "${DISCOVERED_DOMAINS[$i]}" =~ $CLI_DOMAIN ]] && [ "${DISCOVERED_USERS[$i]}" == "$CLI_USER" ]; then
FILTERED_INDICES+=("$i")
fi
elif [ -n "$CLI_DOMAIN" ]; then
if [[ "${DISCOVERED_DOMAINS[$i]}" =~ $CLI_DOMAIN ]]; then
FILTERED_INDICES+=("$i")
fi
elif [ -n "$CLI_USER" ]; then
if [ "${DISCOVERED_USERS[$i]}" == "$CLI_USER" ]; then
FILTERED_INDICES+=("$i")
fi
else
FILTERED_INDICES+=("$i")
fi
done
}
if [ -n "$CLI_DOMAIN" ] || [ -n "$CLI_USER" ]; then
echo "[-] Filtering instances using CLI parameters..."
log_diag "[-] Filtering instances using CLI parameters..."
find_target_matches
if [ ${#FILTERED_INDICES[@]} -eq 0 ]; then
echo "[!] No cached match found for domain '$CLI_DOMAIN' / user '$CLI_USER'. Triggering fresh rescan..."
log_diag "[!] Rescanning due to no cache match for CLI params."
scan_containers
load_cached_containers
find_target_matches
fi
if [ ${#FILTERED_INDICES[@]} -eq 0 ]; then
echo "[!] Error: No Discourse containers found matching domain '$CLI_DOMAIN' or user '$CLI_USER'."
exit 1
fi
else
for i in "${!DISCOVERED_CONTAINERS[@]}"; do
FILTERED_INDICES+=("$i")
done
fi
TARGET_INDEX="-1"
if [ ${#FILTERED_INDICES[@]} -eq 1 ]; then
TARGET_INDEX="${FILTERED_INDICES[0]}"
echo "[+] Uniquely identified target instance: ${DISCOVERED_DOMAINS[$TARGET_INDEX]} (Owner: ${DISCOVERED_USERS[$TARGET_INDEX]})"
log_diag "[+] Unique target matched: ${DISCOVERED_DOMAINS[$TARGET_INDEX]}"
else
while true; do
echo ""
if [ -n "$CLI_USER" ]; then
echo "Found multiple Discourse instances owned by user '$CLI_USER':"
else
echo "Found the following Discourse installation(s):"
fi
echo " [0] Rescan system for new containers"
for idx in "${!FILTERED_INDICES[@]}"; do
real_i="${FILTERED_INDICES[$idx]}"
echo " [$((idx+1))] Domain: ${DISCOVERED_DOMAINS[$real_i]} | Owner: ${DISCOVERED_USERS[$real_i]} | Container: ${DISCOVERED_CONTAINERS[$real_i]}"
done
echo ""
read -p "Select installation number [0-${#FILTERED_INDICES[@]}] (or press ENTER to exit): " SELECTION
if [ -z "$SELECTION" ]; then
echo "No selection made. Exiting safely."
exit 0
fi
if [ "$SELECTION" == "0" ]; then
echo ""
scan_containers
load_cached_containers
find_target_matches
continue
fi
if [[ "$SELECTION" =~ ^[0-9]+$ ]] && [ "$SELECTION" -ge 1 ] && [ "$SELECTION" -le "${#FILTERED_INDICES[@]}" ]; then
TARGET_INDEX="${FILTERED_INDICES[$((SELECTION-1))]}"
break
else
echo "[!] Invalid selection."
fi
done
fi
TARGET_USER="${DISCOVERED_USERS[$TARGET_INDEX]}"
TARGET_DOMAIN="${DISCOVERED_DOMAINS[$TARGET_INDEX]}"
TARGET_CONTAINER="${DISCOVERED_CONTAINERS[$TARGET_INDEX]}"
TARGET_UID=$(id -u "$TARGET_USER")
ENV_FILE=""
if [ -f "./.discourse_admin.env" ]; then
ENV_FILE="./.discourse_admin.env"
elif [ -f "/root/.discourse_admin.env" ]; then
ENV_FILE="/root/.discourse_admin.env"
fi
if [ -n "$ENV_FILE" ]; then
source "$ENV_FILE"
fi
fetch_user_table() {
echo "[-] Querying Discourse database for active users..."
log_diag "[-] Querying Discourse database for active users..."
user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "
User.includes(:primary_email).order(:id).each do |u|
em = u.primary_email&.email || 'No email'
adm = u.admin? ? 'Yes' : 'No'
act = u.active? ? 'Yes' : 'No'
puts \"#{u.id}|#{u.username}|#{em}|#{adm}|#{act}\"
end
" 2>/dev/null || echo ""
}
USER_TABLE_RAW=$(fetch_user_table)
if [ "$DRY_RUN" -eq 1 ]; then
echo "[-] Inspecting container gateway & host network settings..."
log_diag "[-] Inspecting container gateway & host network settings..."
HOST_IP=$(hostname -I | awk '{print $1}')
GATEWAY_IP=$(user_podman "$TARGET_USER" inspect "$TARGET_CONTAINER" --format '{{.NetworkSettings.Gateway}}' || true)
[ -z "$GATEWAY_IP" ] && GATEWAY_IP="10.88.0.1"
CURRENT_HOSTS=$(user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "puts SiteSetting.allowed_internal_hosts.presence || '[Unset]'" 2>/dev/null || echo "Unknown")
echo "=========================================================="
echo " DRY-RUN INSPECTION REPORT "
echo "=========================================================="
echo " Selected Domain : $TARGET_DOMAIN ($TARGET_USER)"
echo " Host Primary IP : $HOST_IP"
echo " Gateway IP : $GATEWAY_IP"
echo " Allowed Internal : $CURRENT_HOSTS"
echo " Config File (.env) : ${ENV_FILE:-[Not Found]}"
if [ -n "$ENV_FILE" ]; then
echo " -> Email : $ADMIN_EMAIL"
echo " -> Username : $ADMIN_USER"
echo " -> Password : ${#ADMIN_PASS} characters"
fi
echo "----------------------------------------------------------"
echo " Existing Users in Discourse Database:"
printf " %-4s | %-20s | %-30s | %-6s | %-6s\n" "ID" "Username" "Email Address" "Admin" "Active"
echo " ----------------------------------------------------------------------------------"
while IFS='|' read -r uid uname uemail uadmin uactive; do
if [ -n "$uid" ]; then
printf " %-4s | %-20s | %-30s | %-6s | %-6s\n" "$uid" "$uname" "$uemail" "$uadmin" "$uactive"
fi
done <<< "$USER_TABLE_RAW"
echo "=========================================================="
echo ""
echo "Dry-run pass complete. Log written to $LOG_FILE."
log_diag "Dry-run pass complete."
exit 0
fi
ACTIONS_LOG=()
# Main Instance Hub Menu Loop
while true; do
echo ""
echo "=========================================================="
echo " INSTANCE OPERATIONS HUB: $TARGET_DOMAIN"
echo "=========================================================="
echo " System Owner : $TARGET_USER (UID $TARGET_UID)"
echo " Web Container: $TARGET_CONTAINER"
echo "----------------------------------------------------------"
echo " Choose task to perform:"
echo " [1] Manage User Accounts (Sync / Edit / Add / Delete)"
echo " [2] Manage Network & Gateway Settings (Link Preview / Postfix)"
echo " (Press ENTER to exit)"
echo "=========================================================="
echo ""
read -p "Select hub option [1-2 or ENTER]: " HUB_CHOICE
if [ -z "$HUB_CHOICE" ]; then
break
fi
case $HUB_CHOICE in
1)
while true; do
USER_TABLE_RAW=$(fetch_user_table)
echo ""
echo "=========================================================="
echo " DISCOURSE USER DATABASE & ENV "
echo "=========================================================="
if [ -n "$ENV_FILE" ]; then
echo " Cached .env File : $ENV_FILE"
echo " -> Default User : $ADMIN_USER"
echo " -> Default Email : $ADMIN_EMAIL"
echo " -> Password : ${#ADMIN_PASS} characters configured"
else
echo " Cached .env File : [None found]"
fi
echo "----------------------------------------------------------"
echo " Active Users in Discourse Database:"
printf " %-4s | %-20s | %-30s | %-6s | %-6s\n" "ID" "Username" "Email Address" "Admin" "Active"
echo " ----------------------------------------------------------------------------------"
while IFS='|' read -r uid uname uemail uadmin uactive; do
if [ -n "$uid" ]; then
printf " %-4s | %-20s | %-30s | %-6s | %-6s\n" "$uid" "$uname" "$uemail" "$uadmin" "$uactive"
fi
done <<< "$USER_TABLE_RAW"
echo "=========================================================="
echo ""
echo "User Management Actions:"
echo " [1] Sync/Provision Admin Account using .env credentials"
echo " [2] Edit an existing user account"
echo " [3] Add a new user account"
echo " [4] Delete a user account"
echo " [5] Return to Main Instance Operations Hub"
echo ""
read -p "Select action [1-5] (Default: 5): " USER_ACTION
[ -z "$USER_ACTION" ] && USER_ACTION="5"
if [ "$USER_ACTION" -eq 5 ]; then
break
fi
case $USER_ACTION in
1)
if [ -z "$ENV_FILE" ]; then
echo ""
echo "[-] No .env cached. Enter admin credentials to sync:"
read -p "Admin Email: " ADMIN_EMAIL
read -p "Admin Username: " ADMIN_USER
read -sp "Admin Password: " ADMIN_PASS
echo ""
read -p "Save these credentials to /root/.discourse_admin.env? [y/N]: " SAVE_ENV
if [[ "$SAVE_ENV" =~ ^[Yy]$ ]]; then
cat << ENV_EOF > /root/.discourse_admin.env
ADMIN_EMAIL="$ADMIN_EMAIL"
ADMIN_USER="$ADMIN_USER"
ADMIN_PASS="$ADMIN_PASS"
ENV_EOF
chmod 600 /root/.discourse_admin.env
ENV_FILE="/root/.discourse_admin.env"
fi
fi
echo "[-] Executing Rails pass: Syncing Admin account ($ADMIN_USER / $ADMIN_EMAIL)..."
log_diag "[-] Syncing Admin account ($ADMIN_USER / $ADMIN_EMAIL)"
user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "
ue = UserEmail.find_by(email: '$ADMIN_EMAIL')
u = ue&.user || User.find_by(username: '$ADMIN_USER') || User.new
u.username = '$ADMIN_USER'
u.email = '$ADMIN_EMAIL'
u.password = '$ADMIN_PASS'
u.active = true
u.admin = true
u.approved = true
u.save(validate: false)
rec = u.user_emails.find_by(email: '$ADMIN_EMAIL')
rec ? rec.update!(primary: true) : u.user_emails.create!(email: '$ADMIN_EMAIL', primary: true)
EmailToken.where(user_id: u.id).destroy_all
u.update_columns(active: true, approved: true)
" 2>&1 || true
ACTIONS_LOG+=("Synced admin account '$ADMIN_USER' ($ADMIN_EMAIL) from .env")
;;
2)
read -p "Enter User ID to edit: " TARGET_UID_ARG
if [ -z "$TARGET_UID_ARG" ]; then
echo "[!] No User ID entered. Returning to menu."
continue
fi
echo "[-] Querying user parameters for User ID $TARGET_UID_ARG..."
log_diag "[-] Querying user parameters for User ID $TARGET_UID_ARG"
USER_INFO=$(user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "
u = User.find_by(id: $TARGET_UID_ARG)
if u
puts u.username + '|' + (u.primary_email&.email || '')
end
" 2>/dev/null || echo "")
if [ -z "$USER_INFO" ]; then
echo "[!] User ID $TARGET_UID_ARG not found in database."
continue
fi
WORK_USER=$(echo "$USER_INFO" | cut -d'|' -f1)
WORK_EMAIL=$(echo "$USER_INFO" | cut -d'|' -f2)
WORK_PASS=""
while true; do
echo ""
echo "--- Editing User ID $TARGET_UID_ARG ---"
echo " 1. Username : ${WORK_USER:-[Not Set]}"
echo " 2. Email : ${WORK_EMAIL:-[Not Set]}"
echo " 3. Password : $( [ -n "$WORK_PASS" ] && echo "${#WORK_PASS} characters set" || echo "[Unchanged]" )"
echo "---------------------------------"
[ -n "$ENV_FILE" ] && echo " 0. Overwrite all fields from .env file ($ADMIN_USER / $ADMIN_EMAIL)"
echo " 4. Prompt for ALL THREE fields manually"
echo " (Press ENTER to apply changes)"
echo ""
read -p "Select option [0-4 or ENTER]: " SUB_CHOICE
[ -z "$SUB_CHOICE" ] && break
case $SUB_CHOICE in
0)
if [ -n "$ENV_FILE" ]; then
WORK_USER="$ADMIN_USER"
WORK_EMAIL="$ADMIN_EMAIL"
WORK_PASS="$ADMIN_PASS"
fi
;;
1) read -p "Enter Username: " WORK_USER ;;
2) read -p "Enter Email: " WORK_EMAIL ;;
3) read -sp "Enter Password: " WORK_PASS; echo "" ;;
4)
read -p "Enter Username: " WORK_USER
read -p "Enter Email: " WORK_EMAIL
read -sp "Enter Password: " WORK_PASS
echo ""
;;
esac
done
echo "[-] Executing Rails pass: Applying edits to User ID $TARGET_UID_ARG..."
log_diag "[-] Applying edits to User ID $TARGET_UID_ARG ($WORK_USER / $WORK_EMAIL)"
user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "
u = User.find_by(id: $TARGET_UID_ARG)
if u
u.username = '$WORK_USER'
u.email = '$WORK_EMAIL'
u.password = '$WORK_PASS' if '$WORK_PASS'.length > 0
u.active = true
u.admin = true
u.approved = true
u.save(validate: false)
rec = u.user_emails.find_by(email: '$WORK_EMAIL')
rec ? rec.update!(primary: true) : u.user_emails.create!(email: '$WORK_EMAIL', primary: true)
EmailToken.where(user_id: u.id).destroy_all
u.update_columns(active: true, approved: true)
end
" 2>&1 || true
ACTIONS_LOG+=("Updated User ID $TARGET_UID_ARG ('$WORK_USER' / '$WORK_EMAIL')")
;;
3)
echo ""
echo "--- Add New User ---"
if [ -n "$ENV_FILE" ]; then
echo " Cached .env Config Available: Username: $ADMIN_USER | Email: $ADMIN_EMAIL"
echo " [0] Create new user using .env credentials"
echo " [1] Enter new user details manually"
echo ""
read -p "Select option [0-1] (Default: 1): " ADD_CHOICE
[ -z "$ADD_CHOICE" ] && ADD_CHOICE="1"
else
ADD_CHOICE="1"
fi
if [ "$ADD_CHOICE" -eq 0 ] && [ -n "$ENV_FILE" ]; then
WORK_USER="$ADMIN_USER"
WORK_EMAIL="$ADMIN_EMAIL"
WORK_PASS="$ADMIN_PASS"
else
read -p "Enter Username: " WORK_USER
read -p "Enter Email: " WORK_EMAIL
read -sp "Enter Password: " WORK_PASS
echo ""
fi
if [ -z "$WORK_USER" ] || [ -z "$WORK_EMAIL" ] || [ -z "$WORK_PASS" ]; then
echo "[!] Error: Username, Email, and Password cannot be blank."
continue
fi
echo "[-] Executing Rails pass: Creating user '$WORK_USER'..."
log_diag "[-] Creating user '$WORK_USER' ($WORK_EMAIL)"
user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "
ue = UserEmail.find_by(email: '$WORK_EMAIL')
u = ue&.user || User.find_by(username: '$WORK_USER') || User.new
u.username = '$WORK_USER'
u.email = '$WORK_EMAIL'
u.password = '$WORK_PASS'
u.active = true
u.admin = true
u.approved = true
u.save(validate: false)
rec = u.user_emails.find_by(email: '$WORK_EMAIL')
rec ? rec.update!(primary: true) : u.user_emails.create!(email: '$WORK_EMAIL', primary: true)
EmailToken.where(user_id: u.id).destroy_all
u.update_columns(active: true, approved: true)
" 2>&1 || true
ACTIONS_LOG+=("Created user '$WORK_USER' ($WORK_EMAIL)")
;;
4)
read -p "Enter User ID to delete: " DELETE_UID
if [ -n "$DELETE_UID" ]; then
read -p "Are you sure you want to permanently delete User ID $DELETE_UID? [y/N]: " CONFIRM_DEL
if [[ "$CONFIRM_DEL" =~ ^[Yy]$ ]]; then
echo "[-] Executing Rails pass: Deleting User ID $DELETE_UID..."
log_diag "[-] Deleting User ID $DELETE_UID"
user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "
u = User.find_by(id: $DELETE_UID)
if u
u.user_emails.destroy_all
EmailToken.where(user_id: u.id).destroy_all
u.destroy!
puts 'User deleted.'
end
" 2>&1 || true
ACTIONS_LOG+=("Deleted User ID $DELETE_UID")
fi
fi
;;
*)
echo "[!] Invalid action selected."
;;
esac
done
;;
2)
echo ""
echo "[-] Detecting Network & Gateway Parameters..."
log_diag "[-] Detecting Network & Gateway Parameters"
HOST_IP=$(hostname -I | awk '{print $1}')
GATEWAY_IP=$(user_podman "$TARGET_USER" inspect "$TARGET_CONTAINER" --format '{{.NetworkSettings.Gateway}}' || true)
[ -z "$GATEWAY_IP" ] && GATEWAY_IP="10.88.0.1"
TARGET_HOSTS_STRING="$GATEWAY_IP|127.0.0.1|host.containers.internal|$HOST_IP"
echo "[-] Querying current link preview settings..."
log_diag "[-] Querying current link preview settings"
CURRENT_INTERNAL_HOSTS=$(user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "puts SiteSetting.allowed_internal_hosts.presence || '[Unset]'" 2>/dev/null || echo "[Unset]")
CURRENT_POSTFIX_MYNETWORKS="N/A"
if command -v postconf &>/dev/null; then
CURRENT_POSTFIX_MYNETWORKS=$(postconf -h mynetworks 2>/dev/null || echo "N/A")
fi
echo ""
echo "=========================================================="
echo " NETWORK & CRAWLING CONFIGURATION "
echo "=========================================================="
echo " Current Link Preview Hosts : $CURRENT_INTERNAL_HOSTS"
echo " Target Link Preview Hosts : $TARGET_HOSTS_STRING"
echo ""
echo " Current Postfix mynetworks : $CURRENT_POSTFIX_MYNETWORKS"
echo " Gateway IP to authorize : $GATEWAY_IP"
echo "=========================================================="
echo ""
if [ "$CURRENT_INTERNAL_HOSTS" != "$TARGET_HOSTS_STRING" ] || ! echo "$CURRENT_POSTFIX_MYNETWORKS" | grep -q "$GATEWAY_IP"; then
read -p "Network configuration differences detected. Update networking settings now? [Y/n]: " NET_CONFIRM
if [[ ! "$NET_CONFIRM" =~ ^[Nn]$ ]]; then
echo "[-] Executing Network Updates..."
log_diag "[-] Executing Network Updates ($TARGET_HOSTS_STRING / $GATEWAY_IP)"
user_podman "$TARGET_USER" exec -i "$TARGET_CONTAINER" rails r "SiteSetting.allowed_internal_hosts = '$TARGET_HOSTS_STRING'" 2>&1 || true
if command -v postfix &>/dev/null && [ -f /etc/postfix/main.cf ]; then
if ! grep -q "$GATEWAY_IP" /etc/postfix/main.cf; then
echo "[-] Updating Postfix mynetworks parameter..."
log_diag "[-] Updating Postfix mynetworks parameter ($GATEWAY_IP)"
postconf -e "mynetworks = 127.0.0.0/8 [::1]/128 $HOST_IP/32 $GATEWAY_IP/16"
systemctl reload postfix
fi
fi
ACTIONS_LOG+=("Updated allowed_internal_hosts and Postfix mynetworks ($GATEWAY_IP)")
echo " -> Network settings updated successfully."
fi
else
echo "[+] Network and Postfix configurations are already aligned."
fi
read -p "Press ENTER to return to Instance Operations Hub..."
;;
*)
echo "[!] Invalid hub selection."
;;
esac
done
echo ""
echo "=========================================================="
echo " FINAL INITIALIZATION SUMMARY "
echo "=========================================================="
echo " Managed Target Instance: $TARGET_DOMAIN ($TARGET_USER)"
echo " Session Log Written To : $LOG_FILE"
echo ""
echo " Actions Completed During Session:"
if [ ${#ACTIONS_LOG[@]} -gt 0 ]; then
for act in "${ACTIONS_LOG[@]}"; do
echo " -> $act"
done
else
echo " -> Session completed with no active modifications recorded."
fi
echo "=========================================================="
echo ""
echo "Initialization completed successfully!"