Files
aliasvault/install.sh
2025-07-21 19:02:41 +02:00

2736 lines
99 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# @version 0.20.0
# Repository information used for downloading files and images from GitHub
REPO_OWNER="lanedirt"
REPO_NAME="AliasVault"
GITHUB_RAW_URL_REPO="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}"
GITHUB_CONTAINER_REGISTRY="ghcr.io/$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]')/$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')"
# Required files and directories
REQUIRED_DIRS=(
"certificates/ssl"
"certificates/app"
"certificates/letsencrypt"
"certificates/letsencrypt/www"
"database"
"database/postgres"
"logs"
"logs/msbuild"
)
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
MAGENTA='\033[0;35m'
ORANGE='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
# File paths
ENV_FILE=".env"
ENV_EXAMPLE_FILE=".env.example"
# Minimum required versions
MIN_DOCKER_VERSION="20.10.0"
MIN_COMPOSE_VERSION="2.0.0"
MIN_DISK_SPACE_GB=5
# Function to show usage
show_usage() {
print_logo
printf "Usage: $0 [COMMAND] [OPTIONS]\n"
printf "\n"
printf "Commands:\n"
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (recommended)\n"
printf " build Build AliasVault containers locally from source (takes longer and requires sufficient specs)\n"
printf " start Start AliasVault containers\n"
printf " restart Restart AliasVault containers\n"
printf " stop Stop AliasVault containers\n"
printf "\n"
printf " configure-hostname Configure the hostname where AliasVault can be accessed from\n"
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
printf " configure-email Configure email domains for receiving emails\n"
printf " configure-registration Configure new account registration (enable or disable)\n"
printf " configure-ip-logging Configure IP address logging (enable or disable)\n"
printf " reset-admin-password Reset admin password\n"
printf " uninstall Uninstall AliasVault\n"
printf "\n"
printf " update Update AliasVault including install.sh script to the latest version\n"
printf " update-installer Update install.sh script if newer version is available\n"
printf "\n"
printf " db-export Export database to file\n"
printf " db-import Import database from file\n"
printf "\n"
printf " configure-dev-db Enable/disable development database (for local development only)\n"
printf "\n"
printf "Options:\n"
printf " --verbose Show detailed output\n"
printf " -y, --yes Automatic yes to prompts\n"
printf " --dev Target development database for db import/export operations"
printf "\n"
}
# Function to print the logo
print_logo() {
printf "${MAGENTA}" >&2
printf "==================================================\n" >&2
printf " _ _ _ __ __ _ _ \n" >&2
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n" >&2
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n" >&2
printf " / ___ \| | | (_| \__ \ \ / / (_| | |_| | | |_ \n" >&2
printf "/_/ \_\_|_|\__,_|___/ \/ \__,__|\__,_|_|\__|\n" >&2
printf "\n" >&2
printf "==================================================\n${NC}" >&2
}
# Function to parse command line arguments
parse_args() {
COMMAND=""
VERBOSE=false
FORCE_YES=false
COMMAND_ARG=""
DEV_DB=false
if [ $# -eq 0 ]; then
show_usage
exit 0
fi
# First argument is always the command
case $1 in
install|i)
COMMAND="install"
shift
# Check for version argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
COMMAND_ARG="$1"
shift
fi
;;
build|b)
COMMAND="build"
shift
# Check for additional operation argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
case $1 in
start|stop|restart)
COMMAND_ARG="$1"
shift
;;
*)
echo "Invalid build operation: $1"
echo "Valid operations are: start, stop, restart"
exit 1
;;
esac
fi
;;
uninstall|u)
COMMAND="uninstall"
shift
;;
reset-password|reset-admin-password|rp)
COMMAND="reset-admin-password"
shift
;;
configure-hostname|hostname)
COMMAND="configure-hostname"
shift
;;
configure-ssl|ssl)
COMMAND="configure-ssl"
shift
;;
configure-email|email)
COMMAND="configure-email"
shift
;;
configure-registration|registration)
COMMAND="configure-registration"
shift
;;
configure-ip-logging|ip-logging)
COMMAND="configure-ip-logging"
shift
;;
start|s)
COMMAND="start"
shift
;;
stop|st)
COMMAND="stop"
shift
;;
restart|r)
COMMAND="restart"
shift
;;
update|up)
COMMAND="update"
shift
;;
update-installer|cs)
COMMAND="update-installer"
shift
;;
configure-dev-db|dev-db)
COMMAND="configure-dev-db"
shift
# Check for direct option argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
COMMAND_ARG="$1"
shift
fi
;;
migrate-db|migrate)
COMMAND="migrate-db"
shift
;;
db-export)
COMMAND="db-export"
shift
;;
db-import)
COMMAND="db-import"
shift
;;
--help)
show_usage
exit 0
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac
# Parse remaining flags
while [[ $# -gt 0 ]]; do
case $1 in
--verbose)
VERBOSE=true
shift
;;
-y|--yes)
FORCE_YES=true
shift
;;
--dev)
DEV_DB=true
shift
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac
done
}
# Progress and animation functions
show_spinner() {
local pid=$1
local message="$2"
local delay=0.1
local spinstr='|/-\\'
local i=0
printf "${CYAN} %s${NC} " "$message"
while kill -0 "$pid" 2>/dev/null; do
printf "\b%c" "${spinstr:$i:1}"
sleep $delay
((i = (i + 1) % 4))
done
printf "\b ${GREEN}${NC}\n"
}
log_info() {
printf "${CYAN} ${NC}%s\n" "$1"
}
log_warning() {
printf "${YELLOW}${NC}%s\n" "$1"
}
log_error() {
printf "${RED}${NC}%s\n" "$1" >&2
}
# Version comparison function
version_ge() {
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
}
# Network connectivity check
check_connectivity() {
printf "${CYAN} Checking network connectivity...${NC} "
local test_urls=(
"https://api.github.com"
"https://raw.githubusercontent.com"
"https://ghcr.io"
)
local spinstr='|/-\\'
local i=0
for url in "${test_urls[@]}"; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if ! curl -s --connect-timeout 10 --max-time 30 "$url" > /dev/null 2>&1; then
printf "\b ${RED}${NC}\n"
log_error "Cannot reach $url. Please check your internet connection."
return 1
fi
done
printf "\b ${GREEN}${NC}\n"
return 0
}
# Disk space check
check_disk_space() {
printf "${CYAN} Checking disk space...${NC} "
local available_gb=""
local spinstr='|/-\\'
local i=0
if command -v df >/dev/null 2>&1; then
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
# Use portable df command and parse available size in KB
local available_kb
available_kb=$(df -k . 2>/dev/null | awk 'NR==2 {print $4}')
if [ -n "$available_kb" ] && [ "$available_kb" -gt 0 ] 2>/dev/null; then
available_gb=$((available_kb / 1024 / 1024))
fi
if [ -n "$available_gb" ] && [ "$available_gb" -gt 0 ] 2>/dev/null; then
if [ "$available_gb" -lt "$MIN_DISK_SPACE_GB" ]; then
printf "\b ${RED}${NC}\n"
log_error "Insufficient disk space. Required: ${MIN_DISK_SPACE_GB}GB, Available: ${available_gb}GB"
return 1
fi
printf "\b ${GREEN}${NC}\n"
printf " ${GREEN}✓ Disk space verified (${available_gb}GB available)${NC}\n"
else
printf "\b ${YELLOW}${NC}\n"
log_warning "Cannot determine available disk space, skipping check"
fi
else
printf "\b ${YELLOW}${NC}\n"
log_warning "Cannot check disk space (df command not available)"
fi
return 0
}
# Read port configuration from .env file with fallbacks
get_port_config() {
local http_port=80
local https_port=443
local smtp_port=25
if [ -f "$ENV_FILE" ]; then
# Read ports from .env file if it exists
local env_http_port
local env_https_port
local env_smtp_port
env_http_port=$(grep -E "^HTTP_PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
env_https_port=$(grep -E "^HTTPS_PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
env_smtp_port=$(grep -E "^SMTP_PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
# Use .env values if they're valid numbers
if [[ "$env_http_port" =~ ^[0-9]+$ ]] && [ "$env_http_port" -gt 0 ] && [ "$env_http_port" -le 65535 ]; then
http_port="$env_http_port"
fi
if [[ "$env_https_port" =~ ^[0-9]+$ ]] && [ "$env_https_port" -gt 0 ] && [ "$env_https_port" -le 65535 ]; then
https_port="$env_https_port"
fi
if [[ "$env_smtp_port" =~ ^[0-9]+$ ]] && [ "$env_smtp_port" -gt 0 ] && [ "$env_smtp_port" -le 65535 ]; then
smtp_port="$env_smtp_port"
fi
fi
# Return the ports as space-separated values
echo "$http_port $https_port $smtp_port"
}
# Check if ports are available (not in use by non-AliasVault processes)
check_port_availability() {
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
printf "${CYAN} Checking port availability...${NC} "
local ports_config
ports_config=$(get_port_config)
read -r http_port https_port smtp_port <<< "$ports_config"
local ports_to_check=("$http_port" "$https_port" "$smtp_port")
local port_issues=()
local has_issues=false
local spinstr='|/-\\'
local i=0
# Get current directory name as potential project name
local current_project_name
current_project_name=$(basename "$(pwd)" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]//g')
# Get list of running AliasVault containers to exclude from checks
local aliasvault_containers=()
local aliasvault_project_containers=()
if command -v docker > /dev/null 2>&1 && docker info > /dev/null 2>&1; then
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
# Get all running containers
while IFS= read -r container_info; do
if [ -n "$container_info" ]; then
# Parse container name and project
local container_name
local project_name
container_name=$(echo "$container_info" | cut -d'|' -f1)
project_name=$(echo "$container_info" | cut -d'|' -f2)
# Check if it's an AliasVault container by name or project
if [[ "$container_name" =~ aliasvault ]] || [[ "$project_name" =~ aliasvault ]] || [[ "$project_name" == "$current_project_name" ]]; then
aliasvault_containers+=("$container_name")
aliasvault_project_containers+=("$project_name")
fi
fi
done < <(docker ps --format "{{.Names}}|{{.Label \"com.docker.compose.project\"}}" 2>/dev/null | grep -v "^$" || true)
fi
for port in "${ports_to_check[@]}"; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local port_in_use=false
local blocking_process=""
local is_aliasvault_port=false
# Check if port is in use using netstat/ss
if command -v ss > /dev/null 2>&1; then
# Use ss (more modern)
local ss_output
ss_output=$(ss -tuln 2>/dev/null | grep ":${port} " || true)
if [ -n "$ss_output" ]; then
port_in_use=true
# Try to get process info
local process_info
process_info=$(ss -tulpn 2>/dev/null | grep ":${port} " | head -n1 || true)
if [ -n "$process_info" ]; then
blocking_process=$(echo "$process_info" | sed -n 's/.*users:((\"\([^\"]*\)\".*/\1/p' || true)
fi
fi
elif command -v netstat > /dev/null 2>&1; then
# Fallback to netstat
local netstat_output
netstat_output=$(netstat -tuln 2>/dev/null | grep ":${port} " || true)
if [ -n "$netstat_output" ]; then
port_in_use=true
# Try to get process info with netstat -tulpn if available
local process_info
process_info=$(netstat -tulpn 2>/dev/null | grep ":${port} " | head -n1 || true)
if [ -n "$process_info" ]; then
blocking_process=$(echo "$process_info" | awk '{print $7}' | cut -d'/' -f2 || true)
fi
fi
else
# Last resort: try to bind to the port temporarily
if command -v nc > /dev/null 2>&1; then
if ! nc -z localhost "$port" 2>/dev/null; then
port_in_use=false
else
port_in_use=true
fi
else
log_warning "Cannot check port $port availability (no netstat, ss, or nc available)"
continue
fi
fi
# If port is in use, check if it's used by AliasVault containers
if [ "$port_in_use" = true ]; then
# First, check if any AliasVault containers are using this port directly
for container in "${aliasvault_containers[@]}"; do
if command -v docker > /dev/null 2>&1; then
local container_ports
container_ports=$(docker port "$container" 2>/dev/null || true)
if echo "$container_ports" | grep -q ":${port}$" || echo "$container_ports" | grep -q ":${port}->" ; then
is_aliasvault_port=true
break
fi
fi
done
# If not found in direct container ports, check if it's docker-proxy for AliasVault
if [ "$is_aliasvault_port" = false ] && [ "$blocking_process" = "docker-proxy" ]; then
# Check if there are any AliasVault containers running
if [ ${#aliasvault_containers[@]} -gt 0 ]; then
# If we have AliasVault containers and docker-proxy is using the port,
# it's likely for AliasVault (especially during installation/updates)
is_aliasvault_port=true
fi
fi
# Additional check: if we're in an AliasVault directory and docker-proxy is using the port,
# it's very likely for AliasVault
if [ "$is_aliasvault_port" = false ] && [ "$blocking_process" = "docker-proxy" ]; then
# Check if we're in an AliasVault project directory
if [ -f "docker-compose.yml" ] || [ -f ".env" ]; then
# If we have docker-compose.yml or .env file, this is likely an AliasVault project
is_aliasvault_port=true
fi
fi
# Only report as an issue if it's not used by AliasVault
if [ "$is_aliasvault_port" = false ]; then
has_issues=true
local port_name=""
case "$port" in
"$http_port") port_name="HTTP" ;;
"$https_port") port_name="HTTPS" ;;
"$smtp_port") port_name="SMTP" ;;
esac
if [ -n "$blocking_process" ]; then
port_issues+=("Port $port ($port_name) is in use by: $blocking_process")
else
port_issues+=("Port $port ($port_name) is in use by an unknown process")
fi
fi
fi
done
if [ "$has_issues" = true ]; then
printf "\b ${RED}${NC}\n"
log_error "Port availability issues detected:"
for issue in "${port_issues[@]}"; do
printf " ${RED}${NC} %s\n" "$issue"
done
printf "\n${YELLOW}Common solutions:${NC}\n"
# Show specific help based on which ports are in use
local smtp_in_use=false
local http_https_in_use=false
for issue in "${port_issues[@]}"; do
if [[ "$issue" == *"SMTP"* ]]; then
smtp_in_use=true
fi
if [[ "$issue" == *"HTTP"* ]] || [[ "$issue" == *"HTTPS"* ]]; then
http_https_in_use=true
fi
done
if [ "$smtp_in_use" = true ]; then
printf " ${YELLOW}${NC} Try disabling the postfix service with 'sudo systemctl stop postfix && sudo systemctl disable postfix'\n"
fi
if [ "$http_https_in_use" = true ]; then
printf " ${YELLOW}${NC} Try stopping the existing local webserver (e.g. nginx, apache, httpd etc.)\n"
printf " ${YELLOW}${NC} Change the default AliasVault ports (80, 443) by editing the .env file\n"
fi
printf "\nIf this still doesn't work, try finding out which services are running on the specified ports and read documentation for your distribution on how to disable them.\n"
printf "\n"
return 1
fi
printf "\b ${GREEN}${NC}\n"
printf " ${GREEN}✓ Port availability verified (HTTP:$http_port, HTTPS:$https_port, SMTP:$smtp_port)${NC}\n"
return 0
}
# Comprehensive dependency checks
check_dependencies() {
printf "${CYAN} Checking dependencies...${NC} "
local missing_deps=()
local version_issues=()
local has_issues=false
local spinstr='|/-\\'
local i=0
# Check if OS is 64-bit
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local arch=$(uname -m)
case "$arch" in
x86_64|amd64|arm64|aarch64)
# 64-bit architecture - continue
;;
*)
printf "\b ${RED}${NC}\n"
log_error "AliasVault requires a 64-bit operating system."
printf "\n"
printf "${RED}${BOLD}Unsupported architecture detected: $arch${NC}\n"
printf "\n"
printf "AliasVault only supports 64-bit operating systems. Your current architecture ($arch) is not supported.\n"
printf "\n"
printf "${CYAN}Supported architectures:${NC}\n"
printf " ${GREEN}${NC} x86_64 (Intel/AMD 64-bit)\n"
printf " ${GREEN}${NC} arm64/aarch64 (ARM 64-bit)\n"
printf "\n"
printf "${YELLOW}Please upgrade your operating system to a 64-bit version.${NC}\n"
printf "\n"
return 1
;;
esac
# Check Docker
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if ! command -v docker > /dev/null 2>&1; then
missing_deps+=("docker")
has_issues=true
else
local docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
if [ -n "$docker_version" ]; then
if ! version_ge "$docker_version" "$MIN_DOCKER_VERSION"; then
version_issues+=("Docker version $docker_version is below minimum required $MIN_DOCKER_VERSION")
has_issues=true
fi
fi
# Check if Docker daemon is running
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if ! docker info > /dev/null 2>&1; then
printf "\b ${RED}${NC}\n"
log_error "Docker daemon cannot be reached."
printf "\n"
printf "${CYAN}To resolve this issue:${NC}\n"
printf "\n"
printf "${YELLOW}Step 1:${NC} Manually test Docker daemon status:\n"
printf " ${DIM}docker info${NC}\n"
printf "\n"
printf "${YELLOW}Step 2:${NC} If Docker is not installed or not running, follow the official installation instructions:\n"
printf " ${CYAN}https://docs.docker.com/engine/install/${NC}\n"
printf "\n"
printf "Please install the latest version of Docker and ensure the Docker daemon is running.\n"
printf "\n"
return 1
fi
# Test if Docker can actually run containers (lightweight test)
if [ "$has_issues" != true ]; then
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local docker_test_output
docker_test_output=$(docker run --rm alpine:latest echo "test" 2>&1 >/dev/null)
if [ $? -ne 0 ]; then
printf "\b ${RED}${NC}\n"
log_error "Docker cannot run containers properly. Error output:"
printf " ${RED}%s${NC}\\n\\n" "$docker_test_output"
printf " Possible causes:\\n"
printf " ${YELLOW}${NC} Docker daemon configuration issues\\n"
printf " ${YELLOW}${NC} Insufficient permissions\\n"
printf " ${YELLOW}${NC} SELinux/AppArmor restrictions\\n"
printf " ${YELLOW}${NC} Storage driver problems\\n"
printf " ${YELLOW}${NC} Kernel compatibility issues\\n"
printf "\\n"
printf " ${CYAN}To debug, try running:${NC}\\n"
printf " ${DIM}docker run --rm alpine:latest echo \"test\"${NC}\\n"
printf "\\n"
return 1
fi
fi
fi
# Check Docker Compose
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if docker compose version > /dev/null 2>&1; then
local compose_version=$(docker compose version --short 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
if [ -n "$compose_version" ]; then
if ! version_ge "$compose_version" "$MIN_COMPOSE_VERSION"; then
version_issues+=("Docker Compose version $compose_version is below minimum required $MIN_COMPOSE_VERSION")
has_issues=true
fi
fi
elif command -v docker-compose > /dev/null 2>&1; then
printf "\b ${RED}${NC}\n"
local compose_version=$(docker-compose --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
log_error "Docker Compose v1 detected ($compose_version). AliasVault requires Docker Compose v2."
printf "\n"
printf "${RED}${BOLD}Docker Compose v1 is not supported.${NC}\n"
printf "\n"
printf "${CYAN}To upgrade to Docker Compose v2:${NC}\n"
printf " ${YELLOW}${NC} Uninstall docker-compose v1: sudo apt remove docker-compose (Ubuntu/Debian)\n"
printf " ${YELLOW}${NC} Install Docker Compose v2 plugin: https://docs.docker.com/compose/install/linux/#install-using-the-repository\n"
printf "\n"
printf "${CYAN}After installation, verify with:${NC}\n"
printf " docker compose version\n"
printf "\n"
return 1
else
missing_deps+=("docker-compose")
has_issues=true
fi
# Check essential tools
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
for tool in curl openssl grep sed; do
if ! command -v "$tool" > /dev/null 2>&1; then
missing_deps+=("$tool")
has_issues=true
fi
done
# Show final result
if [ "$has_issues" = true ]; then
printf "\b ${RED}${NC}\n"
printf "\n"
if [ ${#missing_deps[@]} -gt 0 ]; then
printf "${RED}${BOLD}Missing required dependencies:${NC}\n"
for dep in "${missing_deps[@]}"; do
case $dep in
"docker")
printf " ${RED}${NC} %s (install manual: https://docs.docker.com/engine/install/)\n" "$dep"
;;
"docker-compose")
printf " ${RED}${NC} %s (install manual: https://docs.docker.com/compose/install/linux/#install-using-the-repository)\n" "$dep"
;;
*)
printf " ${RED}${NC} %s\n" "$dep"
;;
esac
done
printf "\n"
fi
if [ ${#version_issues[@]} -gt 0 ]; then
printf "${YELLOW}${BOLD}Version compatibility warnings:${NC}\n"
for issue in "${version_issues[@]}"; do
printf " ${YELLOW}${NC} %s\n" "$issue"
done
printf "\n"
printf "${YELLOW}AliasVault may still work, but upgrading is recommended.${NC}\n\n"
fi
if [ ${#missing_deps[@]} -gt 0 ]; then
return 1
fi
else
printf "\b ${GREEN}${NC}\n"
fi
return 0
}
# Enhanced error handling with retries
retry_command() {
local max_attempts=$1
local delay=$2
shift 2
local command=("$@")
local attempt=1
while [ $attempt -le $max_attempts ]; do
if "${command[@]}"; then
return 0
fi
if [ $attempt -lt $max_attempts ]; then
log_warning "Attempt $attempt failed, retrying in ${delay}s..."
sleep $delay
fi
((attempt++))
done
log_error "Command failed after $max_attempts attempts: ${command[*]}"
return 1
}
# Enhanced docker pull with progress
enhanced_docker_pull() {
local image="$1"
local image_name=$(basename "$image")
if [ "$VERBOSE" = true ]; then
docker pull "$image"
else
(
docker pull "$image" > /tmp/docker_pull_${image_name//[:\/]/_}.log 2>&1 &
local pull_pid=$!
show_spinner $pull_pid "Pulling $image_name"
wait $pull_pid
local exit_code=$?
if [ $exit_code -ne 0 ]; then
cat /tmp/docker_pull_${image_name//[:\/]/_}.log >&2
fi
rm -f /tmp/docker_pull_${image_name//[:\/]/_}.log
return $exit_code
)
fi
}
# Function to validate semver format
validate_semver() {
local version="$1"
# Check if version is "latest" (special case)
if [ "$version" = "latest" ]; then
return 0
fi
# Validate semver format: x.y.z where x, y, z are non-negative integers
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
return 0
else
return 1
fi
}
# Main function
main() {
parse_args "$@"
# Check if command is empty (should not happen with updated parse_args)
if [ -z "$COMMAND" ]; then
show_usage
exit 1
fi
print_logo
# Skip dependency checks for certain commands that don't require Docker
case $COMMAND in
"update-installer")
# Only check basic network connectivity for installer updates
if ! check_connectivity; then
exit 1
fi
;;
"install"|"build"|"update"|"configure-dev-db")
# Full dependency check for operations that require Docker
if ! check_dependencies; then
exit 1
fi
# Additional checks for installation/build operations
if [[ "$COMMAND" == "install" || "$COMMAND" == "build" || "$COMMAND" == "update" ]]; then
if ! check_connectivity; then
exit 1
fi
if ! check_disk_space; then
exit 1
fi
if ! check_port_availability; then
exit 1
fi
fi
;;
esac
case $COMMAND in
"build")
handle_build
;;
"install")
handle_install "$COMMAND_ARG"
;;
"uninstall")
handle_uninstall
;;
"reset-admin-password")
generate_admin_password
if [ $? -eq 0 ]; then
printf "${CYAN}> Restarting admin container...${NC}\n"
if [ "$VERBOSE" = true ]; then
$(get_docker_compose_command) up -d --force-recreate admin
else
$(get_docker_compose_command) up -d --force-recreate admin > /dev/null 2>&1
fi
print_password_reset_message
fi
;;
"configure-ssl")
handle_ssl_configuration
;;
"configure-email")
handle_email_configuration
;;
"configure-registration")
handle_registration_configuration
;;
"configure-hostname")
handle_hostname_configuration
;;
"configure-ip-logging")
handle_ip_logging_configuration
;;
"start")
handle_start
;;
"stop")
handle_stop
;;
"restart")
handle_restart
;;
"update")
handle_update
;;
"update-installer")
check_install_script_update
exit $?
;;
"configure-dev-db")
configure_dev_database
;;
"db-export")
handle_db_export
;;
"db-import")
handle_db_import
;;
esac
}
# Function to get the latest release version from GitHub
get_latest_version() {
local latest_version=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$latest_version" ]; then
printf "${RED}> Failed to get latest version from GitHub.${NC}\n" >&2
return 1
fi
echo "$latest_version"
}
# Function to initialize workspace and create required directories
initialize_workspace() {
printf "${CYAN} Checking workspace...${NC} ${GREEN}${NC}\n"
local dirs_needed=false
for dir in "${REQUIRED_DIRS[@]}"; do
if [ ! -d "$dir" ]; then
if [ "$dirs_needed" = false ]; then
printf " ${GREEN}> Creating required directories...${NC}\n"
dirs_needed=true
fi
mkdir -p "$dir"
chmod -R 755 "$dir"
if [ $? -ne 0 ]; then
printf " ${RED}> Failed to create directory: $dir${NC}\n"
exit 1
fi
fi
done
}
# Function to handle docker-compose.yml
handle_docker_compose() {
local version_tag="$1"
printf "${CYAN} Downloading docker-compose files for version ${version_tag}...${NC} "
local files_to_download=(
"docker-compose.yml"
"docker-compose.letsencrypt.yml"
)
local spinstr='|/-\\'
local i=0
for file in "${files_to_download[@]}"; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local temp_file="${file}.tmp"
local download_url="${GITHUB_RAW_URL_REPO}/${version_tag}/${file}"
# First, check if the file exists by making a HEAD request
local http_status
http_status=$(curl -s -o /dev/null -w "%{http_code}" "$download_url" 2>/dev/null)
if [ "$http_status" = "404" ]; then
printf "\b ${RED}${NC}\n"
log_error "Version '${version_tag}' does not exist or is not available."
log_error "The requested version may not have been released yet or may be invalid."
printf "\n"
printf "${CYAN}Available options:${NC}\n"
printf " ${YELLOW}${NC} Check available versions at: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases\n"
printf " ${YELLOW}${NC} Use a different version: ./install.sh install <version>\n"
printf "\n"
rm -f "$temp_file"
exit 1
fi
if ! retry_command 3 2 curl -sSf "$download_url" -o "$temp_file"; then
printf "\b ${RED}${NC}\n"
log_error "Failed to download $file from $download_url"
log_error "Please check your internet connection and try again."
log_info "Alternatively, download manually and place in the current directory."
rm -f "$temp_file"
exit 1
fi
# Special handling for docker-compose.yml version replacement
if [ "$file" = "docker-compose.yml" ]; then
if [ -n "$version_tag" ] && [ "$version_tag" != "latest" ]; then
sed "s/:latest/:$version_tag/g" "$temp_file" > "$file"
rm "$temp_file"
else
mv "$temp_file" "$file"
fi
else
mv "$temp_file" "$file"
fi
done
printf "\b ${GREEN}${NC}\n"
return 0
}
# Function to check and update install.sh for specific version
check_install_script_version() {
local target_version="$1"
printf "${CYAN} Checking install script version for ${target_version}...${NC} ${GREEN}${NC}\n"
# First, check if the install.sh file exists for this version
local install_url="${GITHUB_RAW_URL_REPO}/${target_version}/install.sh"
local http_status
http_status=$(curl -s -o /dev/null -w "%{http_code}" "$install_url" 2>/dev/null)
if [ "$http_status" = "404" ]; then
printf " > Install script not found for version ${target_version}. Continuing with current version.\n"
return 1
fi
# Get remote install.sh for target version
if ! curl -sSf "$install_url" -o "install.sh.tmp"; then
printf "${RED}> Failed to check install script version. Continuing with current version.${NC}\n"
rm -f install.sh.tmp
return 1
fi
# Get versions
local current_version=$(extract_version "install.sh")
local target_script_version=$(extract_version "install.sh.tmp")
# Check if versions could be extracted
if [ -z "$current_version" ] || [ -z "$target_script_version" ]; then
printf "\n${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
if ! cmp -s "install.sh" "install.sh.tmp"; then
printf "${YELLOW}> Install script needs updating to match version ${target_version}${NC}\n"
printf " ${GREEN}${NC}\n"
return 2
fi
else
if [ "$current_version" != "$target_script_version" ]; then
printf "${YELLOW}> Install script needs updating to match version ${target_version}${NC}\n"
printf " ${GREEN}${NC}\n"
return 2
fi
fi
rm -f install.sh.tmp
return 0
}
# Function to create .env file
create_env_file() {
printf "${CYAN} Checking .env file...${NC} ${GREEN}${NC}\n"
if [ ! -f "$ENV_FILE" ]; then
if [ ! -f "$ENV_EXAMPLE_FILE" ]; then
# Get latest release version
local latest_version=$(get_latest_version) || {
printf "\n ${YELLOW}> Failed to check latest version. Creating blank .env file.${NC}\n"
touch "$ENV_FILE"
return 0
}
printf " ${CYAN}> Downloading .env.example...${NC}"
# Check if .env.example exists for this version
local env_example_url="${GITHUB_RAW_URL_REPO}/${latest_version}/.env.example"
local http_status
http_status=$(curl -s -o /dev/null -w "%{http_code}" "$env_example_url" 2>/dev/null)
if [ "$http_status" = "404" ]; then
printf "\n ${YELLOW}> .env.example not found for version ${latest_version}. Creating blank .env file.${NC}\n"
touch "$ENV_FILE"
return 0
fi
if curl -sSf "$env_example_url" -o "$ENV_EXAMPLE_FILE" > /dev/null 2>&1; then
printf "\n ${GREEN}> .env.example downloaded successfully.${NC}\n"
else
printf "\n ${YELLOW}> Failed to download .env.example. Creating blank .env file.${NC}\n"
touch "$ENV_FILE"
return 0
fi
fi
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
printf " ${GREEN}> New .env file created from .env.example.${NC}\n"
fi
}
populate_hostname() {
if ! grep -q "^HOSTNAME=" "$ENV_FILE" || [ -z "$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
while true; do
read -p "Enter the (public) hostname where this AliasVault server can be accessed from (e.g. aliasvault.net): " USER_HOSTNAME
if [ -n "$USER_HOSTNAME" ]; then
HOSTNAME="$USER_HOSTNAME"
break
else
printf "${YELLOW}> Hostname cannot be empty. Please enter a valid hostname.${NC}\n"
fi
done
update_env_var "HOSTNAME" "$HOSTNAME"
else
HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
fi
}
# Function to generate admin password
generate_admin_password() {
printf "${CYAN} Generating admin password...${NC} ${GREEN}${NC}\n"
PASSWORD=$(openssl rand -base64 12)
# Build locally if in build mode or if pre-built image is not available
if grep -q "^DEPLOYMENT_MODE=build" "$ENV_FILE" 2>/dev/null || ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:latest > /dev/null 2>&1; then
log_info "Building InstallCli locally..."
if [ "$VERBOSE" = true ]; then
if ! docker build -t installcli -f apps/server/Utilities/AliasVault.InstallCli/Dockerfile .; then
log_error "Failed to build InstallCli Docker image"
exit 1
fi
else
(
docker build -t installcli -f apps/server/Utilities/AliasVault.InstallCli/Dockerfile . > install_build_output.log 2>&1 &
BUILD_PID=$!
show_spinner $BUILD_PID "Building InstallCli image"
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
log_error "Failed to build InstallCli Docker image. Build output:"
cat install_build_output.log >&2
exit $BUILD_EXIT_CODE
fi
rm -f install_build_output.log
)
fi
HASH=$(docker run --rm installcli hash-password "$PASSWORD")
else
HASH=$(docker run --rm ${GITHUB_CONTAINER_REGISTRY}-installcli:latest hash-password "$PASSWORD")
fi
if [ -z "$HASH" ]; then
printf "\n${RED}> Error: Failed to generate password hash${NC}\n"
exit 1
fi
update_env_var "ADMIN_PASSWORD_HASH" "$HASH"
update_env_var "ADMIN_PASSWORD_GENERATED" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
# Helper function to update environment variables
update_env_var() {
local key=$1
local value=$2
if [ -f "$ENV_FILE" ]; then
# Check if key exists
if grep -q "^${key}=" "$ENV_FILE"; then
# Update existing key inline
sed -i.bak "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
else
# Key doesn't exist, append it
echo "$key=$value" >> "$ENV_FILE"
fi
else
# File doesn't exist, create it with the key-value pair
echo "$key=$value" > "$ENV_FILE"
fi
printf " ${GREEN}> $key has been set in $ENV_FILE.${NC}\n"
}
# Helper function to delete environment variables
delete_env_var() {
local key=$1
if [ -f "$ENV_FILE" ]; then
sed -i.bak "/^${key}=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
printf " ${GREEN}> $key has been removed from $ENV_FILE.${NC}\n"
fi
}
# Function to print a success box
print_success_box() {
local message="$1"
local width=70
local header="╔══════════════════════════════════════════════════════════════════════╗"
local footer="╚══════════════════════════════════════════════════════════════════════╝"
local success="✓ SUCCESS!"
local success_line
local message_line
local padding
# Print header
printf "${MAGENTA}%s${NC}\n" "$header"
# Construct second line with centered success text
local success_padding=$(( (width - ${#success}) / 2 ))
printf "${MAGENTA}${NC}%*s${GREEN}%s${NC}%*s${MAGENTA}${NC}\n" \
"$success_padding" "" "$success" "$((width - success_padding - ${#success}))" ""
# Construct third line with centered message
local msg_len=${#message}
local msg_padding=$(( (width - msg_len) / 2 ))
printf "${MAGENTA}${NC}%*s${BOLD}%s${NC}%*s${MAGENTA}${NC}\n" \
"$msg_padding" "" "$message" "$((width - msg_padding - msg_len))" ""
# Print footer
printf "${MAGENTA}%s${NC}\n" "$footer"
}
# Function to print success message
print_install_success_message() {
printf "\n"
print_success_box "AliasVault is successfully installed!"
printf "\n"
printf "${BOLD}To configure the server, login to the admin panel:${NC}\n"
printf "\n"
if [ -n "$PASSWORD" ]; then
printf " ${CYAN}Admin Panel:${NC} https://localhost/admin\n"
printf " ${CYAN}Username:${NC} admin\n"
printf " ${CYAN}Password:${NC} $PASSWORD\n"
printf "\n"
printf "${YELLOW}⚠ IMPORTANT: Make sure to backup the above credentials in a safe place,${NC}\n"
printf "${YELLOW} they won't be shown again!${NC}\n"
else
printf " ${CYAN}Admin Panel:${NC} https://localhost/admin\n"
printf " ${CYAN}Username:${NC} admin\n"
printf " ${CYAN}Password:${NC} (Previously set. Use ./install.sh reset-admin-password to generate new one.)\n"
fi
printf "\n"
printf "${BOLD}To start using AliasVault, log into the client website:${NC}\n"
printf "\n"
printf " ${CYAN}Client Website:${NC} https://localhost/\n"
}
# Function to recreate (restart) Docker containers
recreate_docker_containers() {
printf "${CYAN} (Re)creating Docker containers...${NC}\n"
if [ "$VERBOSE" = true ]; then
printf "\b${NC}\n"
if ! $(get_docker_compose_command) up -d --force-recreate; then
log_error "Failed to recreate Docker containers"
exit 1
fi
else
(
$(get_docker_compose_command) up -d --force-recreate > /tmp/docker_recreate.log 2>&1 &
RECREATE_PID=$!
show_spinner $RECREATE_PID "Recreating containers"
wait $RECREATE_PID
RECREATE_EXIT_CODE=$?
if [ $RECREATE_EXIT_CODE -ne 0 ]; then
log_error "Failed to recreate Docker containers. Output:"
cat /tmp/docker_recreate.log >&2
exit 1
fi
rm -f /tmp/docker_recreate.log
)
fi
printf "${GREEN}✓ Docker containers (re)created successfully${NC}\n"
}
# Function to print password reset success message
print_password_reset_message() {
printf "\n"
print_success_box "The admin password has been successfully reset!"
printf "\n"
printf "${BOLD}New admin credentials:${NC}\n"
printf " ${CYAN}Admin Panel:${NC} https://localhost/admin\n"
printf " ${CYAN}Username:${NC} admin\n"
printf " ${CYAN}Password:${NC} $PASSWORD\n"
printf "\n"
printf "${YELLOW}⚠ IMPORTANT: Make sure to backup the above credentials in a safe place,${NC}\n"
printf "${YELLOW} they won't be shown again!${NC}\n"
}
# Function to get docker compose command with appropriate config files
get_docker_compose_command() {
local base_command="docker compose -f docker-compose.yml"
# Check if using build configuration
if grep -q "^DEPLOYMENT_MODE=build" "$ENV_FILE" 2>/dev/null; then
base_command="$base_command -f docker-compose.build.yml"
fi
# Check if Let's Encrypt is enabled
if grep -q "^LETSENCRYPT_ENABLED=true" "$ENV_FILE" 2>/dev/null; then
base_command="$base_command -f docker-compose.letsencrypt.yml"
fi
echo "$base_command"
}
# Add this new function for handling registration configuration
handle_registration_configuration() {
printf "${YELLOW}+++ Public Registration Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
# Get current registration setting
CURRENT_SETTING=$(grep "^PUBLIC_REGISTRATION_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}About Public Registration:${NC}\n"
printf "Public registration allows new users to create their own accounts on your AliasVault server.\n"
printf "When disabled, no new accounts can be created.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$CURRENT_SETTING" = "true" ]; then
printf "Public Registration: ${GREEN}Enabled${NC}\n"
else
printf "Public Registration: ${RED}Disabled${NC}\n"
fi
printf "\n"
printf "Options:\n"
printf "1) Enable public registration\n"
printf "2) Disable public registration\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " reg_option
case $reg_option in
1)
update_env_var "PUBLIC_REGISTRATION_ENABLED" "true"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "Public registration has been enabled!"
;;
2)
update_env_var "PUBLIC_REGISTRATION_ENABLED" "false"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "Public registration has been disabled!"
;;
3)
printf "${YELLOW}Registration configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to handle initial installation or reinstallation
handle_install() {
local specified_version="$1"
# If version specified, install that version directly
if [ -n "$specified_version" ]; then
handle_install_version "$specified_version"
return
fi
# Check if .env exists before reading
if [ -f "$ENV_FILE" ]; then
if grep -q "^ALIASVAULT_VERSION=" "$ENV_FILE"; then
current_version=$(grep "^ALIASVAULT_VERSION=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}> Current AliasVault version: ${current_version}${NC}\n"
printf "\n"
printf "==================================================\n"
printf "AliasVault is already installed.\n"
printf "==================================================\n"
printf "1. To reinstall the current version (${current_version}), continue with this script\n"
printf "2. To check for updates and to install the latest version, use: ./install.sh update\n"
printf "3. To install a specific version, use: ./install.sh install <version>\n\n"
read -p "Would you like to reinstall the current version? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Installation cancelled.${NC}\n"
exit 0
fi
handle_install_version "$current_version"
return
fi
fi
handle_install_version "latest"
}
# Function to handle build
handle_build() {
printf "\n${YELLOW}+++ Building AliasVault from source +++${NC}\n"
# Set deployment mode to build to ensure container lifecycle uses build configuration
set_deployment_mode "build"
# Initialize workspace which makes sure all required directories and files exist
initialize_workspace
# Check for required build files
if [ ! -f "docker-compose.build.yml" ] || [ ! -d "apps/server" ]; then
printf "${RED}Error: Required files for building from source are missing.${NC}\n"
printf "\n"
printf "To build AliasVault from source, you need:\n"
printf "1. docker-compose.build.yml file\n"
printf "2. apps/server/ directory with the complete source code\n"
printf "\n"
printf "Please clone the complete repository using:\n"
printf "git clone https://github.com/${REPO_OWNER}/${REPO_NAME}.git\n"
printf "\n"
printf "Alternatively, you can use './install.sh install' to pull pre-built images.\n"
exit 1
fi
# Initialize environment with proper error handling
check_and_populate_env
# Only generate admin password if not already set
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
generate_admin_password || { printf "${RED}> Failed to generate admin password${NC}\n"; exit 1; }
fi
printf "\n${YELLOW}+++ Building and starting services +++${NC}\n"
printf "${CYAN} Building Docker Compose stack...${NC}\n"
if [ "$VERBOSE" = true ]; then
printf "\b${NC}\n"
if ! $(get_docker_compose_command) build; then
log_error "Failed to build Docker Compose stack"
exit 1
fi
else
(
$(get_docker_compose_command) build > install_compose_build_output.log 2>&1 &
BUILD_PID=$!
show_spinner $BUILD_PID "Building Docker images"
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
log_error "Failed to build Docker Compose stack. Build output:"
cat install_compose_build_output.log >&2
exit 1
fi
rm -f install_compose_build_output.log
)
fi
printf "${GREEN}✓ Docker Compose stack built successfully${NC}\n"
printf "${CYAN} Starting Docker Compose stack...${NC}\n"
recreate_docker_containers
printf "${GREEN}✓ Docker Compose stack started successfully${NC}\n"
# Only show success message if we made it here without errors
print_install_success_message
}
# Function to handle uninstall
handle_uninstall() {
printf "${YELLOW}+++ Uninstalling AliasVault +++${NC}\n"
printf "\n"
# Check if -y flag was passed
if [ "$FORCE_YES" != "true" ]; then
# Ask for confirmation before proceeding
read -p "Are you sure you want to uninstall AliasVault? This will remove all containers and images. [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Uninstall cancelled.${NC}\n"
exit 0
fi
fi
printf "${CYAN}> Stopping and removing Docker containers...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.yml down -v || {
printf "${RED}> Failed to stop and remove Docker containers${NC}\n"
exit 1
}
else
docker compose -f docker-compose.yml down -v > /dev/null 2>&1 || {
printf "${RED}> Failed to stop and remove Docker containers${NC}\n"
exit 1
}
fi
printf "${GREEN}> Docker containers stopped and removed.${NC}\n"
# Remove version from .env
delete_env_var "ALIASVAULT_VERSION" ""
printf "${CYAN}> Removing Docker images...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.yml down --rmi all || {
printf "${RED}> Failed to remove Docker images${NC}\n"
exit 1
}
else
docker compose -f docker-compose.yml down --rmi all > /dev/null 2>&1 || {
printf "${RED}> Failed to remove Docker images${NC}\n"
exit 1
}
fi
printf "${GREEN}> Docker images removed.${NC}\n"
printf "${CYAN}> Pruning Docker system...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker system prune -af || {
printf "${RED}> Failed to prune Docker system${NC}\n"
exit 1
}
else
docker system prune -af > /dev/null 2>&1 || {
printf "${RED}> Failed to prune Docker system${NC}\n"
exit 1
}
fi
printf "${GREEN}> Docker system pruned.${NC}\n"
# Only show success message if we made it here without errors
printf "\n"
print_success_box "AliasVault has been successfully uninstalled!"
printf "\n"
printf "All Docker containers and images related to AliasVault have been removed.\n"
printf "The current directory, including database, logs and .env files, has been left intact.\n"
printf "\n"
printf "If you wish to remove the remaining files, it's safe to do so now.\n"
}
# Function to handle SSL configuration
handle_ssl_configuration() {
printf "${YELLOW}+++ SSL Certificate Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
# Get the current hostname and SSL config from .env
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
LETSENCRYPT_ENABLED=$(grep "^LETSENCRYPT_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}SSL Certificate Options:${NC}\n"
printf "AliasVault uses a self-signed SSL certificate by default.\n"
printf "This provides encryption but may trigger browser warnings.\n"
printf "\n"
printf "You can switch to a trusted Let's Encrypt certificate, which:\n"
printf " - Avoids browser warnings\n"
printf " - Requires a public domain (not localhost)\n"
printf " - Needs ports 80 and 443 open to the internet\n"
printf "\n"
printf "Let's Encrypt certificates auto-renew before expiry.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$LETSENCRYPT_ENABLED" = "true" ]; then
printf "Using: ${GREEN}Let's Encrypt${NC}\n"
else
printf "Using: ${YELLOW}Self-signed${NC}\n"
fi
printf "Hostname: ${CYAN}${CURRENT_HOSTNAME}${NC} (change via: ./install.sh configure-hostname)\n"
printf "\n"
printf "Choose an option:\n"
printf "1) Use Let's Encrypt certificate (recommended)\n"
printf "2) Use self-signed certificate\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " ssl_option
case $ssl_option in
1)
configure_letsencrypt
;;
2)
generate_self_signed_cert
;;
3)
printf "${YELLOW}SSL configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to handle email server configuration
handle_email_configuration() {
# Setup trap for Ctrl+C and other interrupts
trap 'printf "\n${YELLOW}Configuration cancelled by user.${NC}\n"; exit 1' INT TERM
printf "${YELLOW}+++ Email Server Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
# Get current email domains from .env
CURRENT_DOMAINS=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}About Email Server:${NC}\n"
printf "AliasVault includes a built-in email server for handling virtual email addresses.\n"
printf "When enabled, it can receive emails for one or more configured domains.\n"
printf "Each domain must have an MX DNS record pointing to this server's hostname.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$CURRENT_DOMAINS" = "DISABLED.TLD" ]; then
printf "Email Server Status: ${RED}Disabled${NC}\n"
else
printf "Email Server Status: ${GREEN}Enabled${NC}\n"
printf "Active Domains: ${CYAN}${CURRENT_DOMAINS}${NC}\n"
fi
printf "\n"
printf "Email Server Options:\n"
printf "1) Enable email server / Update domains\n"
printf "2) Disable email server\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " email_option
case $email_option in
1)
while true; do
printf "\n${CYAN}Enter domain(s) for email server${NC}\n"
printf "For multiple domains, separate with commas (e.g. domain1.com,domain2.com)\n"
printf "IMPORTANT: Each domain must have an MX record in DNS pointing to this server.\n"
read -p "Domains: " new_domains
if [ -z "$new_domains" ]; then
printf "${RED}Error: Domains cannot be empty${NC}\n"
continue
fi
printf "\n${CYAN}You entered the following domains:${NC}\n"
IFS=',' read -ra DOMAIN_ARRAY <<< "$new_domains"
for domain in "${DOMAIN_ARRAY[@]}"; do
printf " - ${GREEN}${domain}${NC}\n"
done
printf "\n"
read -p "Are these domains correct? (y/n): " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
break
fi
done
# Update .env file and restart
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "$new_domains"; then
printf "${RED}Failed to update configuration.${NC}\n"
exit 1
fi
printf "${GREEN}Email server configuration updated${NC}\n"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
if ! handle_restart; then
printf "${RED}Failed to restart services.${NC}\n"
exit 1
fi
# Print success message
printf "\n"
print_success_box "The email server is now successfully configured!"
# Print next steps
printf "\n"
printf "To test the email server:\n"
printf " a. Log in to your AliasVault account\n"
printf " b. Create a new alias using one of your configured private domains\n"
printf " c. Send a test email from an external email service (e.g., Gmail)\n"
printf " d. Check if the email appears in your AliasVault inbox\n"
printf "\n"
printf "If emails don't arrive, please verify:\n"
printf " > DNS MX records are correctly configured\n"
printf " > Your server's firewall allows incoming traffic on port 25 and 587\n"
printf " > Your ISP/hosting provider doesn't block SMTP traffic\n"
printf "\n"
;;
2)
printf "\n${YELLOW}Warning: Docker containers need to be restarted after disabling the email server.${NC}\n"
read -p "Continue with disable and restart? (y/n): " disable_confirm
if [ "$disable_confirm" != "y" ] && [ "$disable_confirm" != "Y" ]; then
printf "${YELLOW}Configuration cancelled.${NC}\n"
exit 0
fi
# Disable email server
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "DISABLED.TLD"; then
printf "${RED}Failed to update configuration.${NC}\n"
exit 1
fi
if ! handle_restart; then
printf "${RED}Failed to restart services.${NC}\n"
exit 1
fi
# Print success message
printf "\n"
print_success_box "The email server has been disabled successfully!"
;;
3)
printf "${YELLOW}Email configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
# Remove the trap before normal exit
trap - INT TERM
}
# Function to configure Let's Encrypt
configure_letsencrypt() {
printf "${CYAN}> Configuring Let's Encrypt SSL certificate...${NC}\n"
# Check if hostname is localhost
if [ "$CURRENT_HOSTNAME" = "localhost" ]; then
printf "${RED}Error: Let's Encrypt certificates cannot be issued for 'localhost'.${NC}\n"
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
exit 1
fi
# Check if hostname is a valid domain
if ! [[ "$CURRENT_HOSTNAME" =~ \.[a-zA-Z]{2,}$ ]]; then
printf "${RED}Error: Invalid hostname '${CURRENT_HOSTNAME}'.${NC}\n"
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
exit 1
fi
# Verify DNS is properly configured
printf "\n${YELLOW}Important: Before proceeding, ensure that:${NC}\n"
printf "1. AliasVault is currently running and accessible at ${CYAN}https://${CURRENT_HOSTNAME}${NC}\n"
printf "2. Your domain (${CYAN}${CURRENT_HOSTNAME}${NC}) is externally resolvable to this server's IP address\n"
printf "3. Ports 80 and 443 are open and accessible from the internet\n"
printf "\n"
read -p "Have you completed these steps? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Let's Encrypt configuration cancelled.${NC}\n"
exit 0
fi
# Get contact email for Let's Encrypt
SUPPORT_EMAIL=$(grep "^SUPPORT_EMAIL=" "$ENV_FILE" | cut -d '=' -f2)
LETSENCRYPT_EMAIL=""
while true; do
printf "\nPlease enter a valid email address that will be used for Let's Encrypt certificate notifications:\n"
read -p "Email: " LETSENCRYPT_EMAIL
if [[ "$LETSENCRYPT_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
printf "Confirm using ${CYAN}${LETSENCRYPT_EMAIL}${NC} for Let's Encrypt notifications? [y/N] "
read REPLY
if [[ $REPLY =~ ^[Yy]$ ]]; then
break
fi
else
printf "${RED}Invalid email format. Please try again.${NC}\n"
fi
done
# Create certbot directories
printf "${CYAN}> Creating Let's Encrypt directories...${NC}\n"
mkdir -p ./certificates/letsencrypt/www
# Get absolute path for certificates directory for the docker bind mounts
CERTIFICATES_DIR=$(realpath ./certificates)
# Request certificate using a temporary certbot container
printf "${CYAN}> Requesting Let's Encrypt certificate...${NC}\n"
docker run --rm \
-v "${CERTIFICATES_DIR}/letsencrypt:/etc/letsencrypt:rw" \
-v "${CERTIFICATES_DIR}/letsencrypt/www:/var/www/certbot:rw" \
certbot/certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email "$LETSENCRYPT_EMAIL" \
--agree-tos \
--no-eff-email \
--non-interactive \
--domains ${CURRENT_HOSTNAME} \
--force-renewal
if [ $? -ne 0 ]; then
printf "${RED}Failed to obtain Let's Encrypt certificate.${NC}\n"
exit 1
fi
# Fix permissions on Let's Encrypt directories and files
sudo chmod -R 755 ./certificates/letsencrypt
# Ensure private keys remain secure
sudo find ./certificates/letsencrypt -type f -name "privkey*.pem" -exec chmod 600 {} \;
sudo find ./certificates/letsencrypt -type f -name "fullchain*.pem" -exec chmod 644 {} \;
# Update .env to indicate Let's Encrypt is enabled
update_env_var "LETSENCRYPT_ENABLED" "true"
# Restart only the reverse proxy with new configuration so it loads the new certificate
printf "${CYAN}> Restarting reverse proxy with Let's Encrypt configuration...${NC}\n"
$(get_docker_compose_command) up -d reverse-proxy --force-recreate
# Starting certbot container to renew certificates automatically
printf "${CYAN}> Starting new certbot container to renew certificates automatically...${NC}\n"
$(get_docker_compose_command) up -d certbot
# Print success message
printf "\n"
print_success_box "Let's Encrypt SSL certificate has been configured successfully!"
}
# Function to generate self-signed certificate
generate_self_signed_cert() {
printf "${CYAN}> Generating new self-signed certificate...${NC}\n"
# Disable Let's Encrypt
update_env_var "LETSENCRYPT_ENABLED" "false"
# Stop existing containers
printf "${CYAN}> Stopping existing containers...${NC}\n"
docker compose down
# Remove existing certificates
rm -f ./certificates/ssl/cert.pem ./certificates/ssl/key.pem
# Remove Let's Encrypt directories
rm -rf ./certificates/letsencrypt
# Start containers (which will generate new self-signed certs)
printf "${CYAN}> Restarting services...${NC}\n"
docker compose up -d
# Print success message
printf "\n"
print_success_box "New self-signed certificate has been generated successfully!"
}
# New functions to handle container lifecycle:
handle_start() {
printf "${CYAN}> Starting AliasVault containers...${NC}\n"
$(get_docker_compose_command) up -d
printf "${GREEN}> AliasVault containers started successfully.${NC}\n"
}
handle_stop() {
printf "${CYAN}> Stopping AliasVault containers...${NC}\n"
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
printf "${YELLOW}> No containers are currently running.${NC}\n"
exit 0
fi
$(get_docker_compose_command) down
printf "${GREEN}> AliasVault containers stopped successfully.${NC}\n"
}
handle_restart() {
printf "\n${CYAN}> Restarting AliasVault containers...${NC}\n"
$(get_docker_compose_command) down
$(get_docker_compose_command) up -d
printf "${GREEN}> AliasVault containers restarted successfully.${NC}\n"
}
# Function to handle updates
handle_update() {
printf "\n${YELLOW}+++ Checking for AliasVault updates +++${NC}\n"
# First check for install.sh updates
check_install_script_update || true
# Check current version
if ! grep -q "^ALIASVAULT_VERSION=" "$ENV_FILE"; then
printf "${CYAN}> No version information found. Running first-time update check...${NC}\n"
printf "\n"
handle_install_version "latest"
return
fi
current_version=$(grep "^ALIASVAULT_VERSION=" "$ENV_FILE" | cut -d '=' -f2)
latest_version=$(get_latest_version) || {
printf "${RED}> Failed to check for updates. Please try again later.${NC}\n"
exit 1
}
if [ -z "$latest_version" ]; then
printf "${RED}> Failed to check for updates. Please try again later.${NC}\n"
exit 1
fi
if [ "$current_version" = "$latest_version" ]; then
printf "\n"
printf "You are already running the latest version of AliasVault (${current_version})!\n"
exit 0
fi
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating AliasVault to the latest version (${latest_version})...${NC}\n"
handle_install_version "$latest_version"
printf "${GREEN}> Update completed successfully!${NC}\n"
return
fi
printf "\n"
printf "A new version of AliasVault is available (${latest_version})!\n"
printf "\n"
printf "${MAGENTA}Important:${NC}\n"
printf "1. It's recommended to backup your database before updating (./install.sh db-export > backup.sql.gz)\n"
printf "2. The update process will restart all containers\n"
printf "\n"
read -p "Would you like to continue with the update? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Update cancelled.${NC}\n"
exit 0
fi
printf "${CYAN}> Updating AliasVault...${NC}\n"
printf "\n"
handle_install_version "$latest_version"
}
# Function to extract version
extract_version() {
local file="$1"
local version=$(head -n 2 "$file" | grep '@version' | cut -d' ' -f3)
echo "$version"
}
# Function to compare semantic versions
compare_versions() {
local version1="$1"
local version2="$2"
# Split versions into arrays
IFS='.' read -ra v1_parts <<< "$version1"
IFS='.' read -ra v2_parts <<< "$version2"
# Compare each part numerically
for i in {0..2}; do
# Default to 0 if part doesn't exist
local v1_part=${v1_parts[$i]:-0}
local v2_part=${v2_parts[$i]:-0}
# Compare numerically
if [ "$v1_part" -gt "$v2_part" ]; then
echo "1" # version1 is greater
return
elif [ "$v1_part" -lt "$v2_part" ]; then
echo "-1" # version1 is lesser
return
fi
done
echo "0" # versions are equal
}
# Function to check if install.sh needs updating
check_install_script_update() {
printf "${CYAN}> Checking for install script updates...${NC}\n"
# Get latest release version
local latest_version=$(get_latest_version) || {
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
return 1
}
if [ -z "$latest_version" ]; then
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
return 1
fi
if ! curl -sSf "${GITHUB_RAW_URL_REPO}/${latest_version}/install.sh" -o "install.sh.tmp"; then
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
rm -f install.sh.tmp
return 1
fi
# Get versions
local current_version=$(extract_version "install.sh")
local new_version=$(extract_version "install.sh.tmp")
# Check if versions could be extracted
if [ -z "$current_version" ] || [ -z "$new_version" ]; then
printf "${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
# Fall back to file comparison
if ! cmp -s "install.sh" "install.sh.tmp"; then
printf "${YELLOW}> Changes detected in install script.${NC}\n"
else
printf "${GREEN}> Install script is up to date.${NC}\n"
rm -f install.sh.tmp
return 0
fi
else
# Compare versions using semver comparison
if [ "$current_version" = "$new_version" ]; then
printf "${GREEN}> Install script is up to date.${NC}\n"
rm -f install.sh.tmp
return 0
else
local compare_result=$(compare_versions "$current_version" "$new_version")
if [ "$compare_result" -ge "0" ]; then
printf "${GREEN}> Install script is up to date.${NC}\n"
rm -f install.sh.tmp
return 0
fi
fi
fi
# If we get here, an update is available
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating install script...${NC}\n"
cp "install.sh" "install.sh.backup"
mv "install.sh.tmp" "install.sh"
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
exit 0
fi
printf "${YELLOW}> A new version of the install script is available (${new_version}).${NC}\n"
printf "\n"
printf "Would you like to update the install script? [Y/n]: "
read -r reply
if [[ ! $reply =~ ^[Nn]$ ]]; then
# Create backup of current script
cp "install.sh" "install.sh.backup"
if mv "install.sh.tmp" "install.sh"; then
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
printf "${YELLOW}> Please run the update command again to continue with the update process.${NC}\n"
exit 0
else
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
# Restore from backup if update failed
mv "install.sh.backup" "install.sh"
rm -f install.sh.tmp
return 1
fi
else
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
rm -f install.sh.tmp
return 0
fi
}
# Function to perform the actual installation with specific version
handle_install_version() {
local target_version="$1"
# Validate semver format if a specific version is provided
if [ -n "$target_version" ] && [ "$target_version" != "latest" ]; then
if ! validate_semver "$target_version"; then
printf "${RED}Error: You tried to install AliasVault with version '${target_version}' which is an incorrect value.${NC}\n"
printf "Please check the command you executed and try again.${NC}\n"
printf "\n"
printf "The provided version must follow semantic versioning format (e.g., '0.0.1', '1.0.5') and match an existing version on GitHub.${NC}\n"
printf "Alternatively, you can omit the version to install the latest version.${NC}\n"
exit 1
fi
fi
# If latest, get actual version number from GitHub API
if [ "$target_version" = "latest" ]; then
local actual_version=$(get_latest_version) || {
printf "${RED}> Failed to get latest version. Please try again later.${NC}\n"
exit 1
}
if [ -n "$actual_version" ]; then
target_version="$actual_version"
fi
fi
printf "\n${YELLOW}+++ Installing AliasVault ${target_version} +++${NC}\n"
# Initialize workspace which makes sure all required directories and files exist
initialize_workspace
# Check if install script needs updating for this version
check_install_script_version "$target_version"
local check_result=$?
if [ $check_result -eq 2 ]; then
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating install script to match version ${target_version}...${NC}\n"
else
printf "${YELLOW}> A different version of the install script is required for installing version ${target_version}.${NC}\n"
read -p "Would you like to self-update the install script before proceeding? [Y/n]: " reply
if [[ $reply =~ ^[Nn]$ ]]; then
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
rm -f install.sh.tmp
fi
fi
if [ "$FORCE_YES" = true ] || [[ ! $reply =~ ^[Nn]$ ]]; then
# Create backup of current script
cp "install.sh" "install.sh.backup"
if mv "install.sh.tmp" "install.sh"; then
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
printf "${YELLOW}> Please run the same install command again to continue with the installation.${NC}\n"
exit 2
else
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
mv "install.sh.backup" "install.sh"
rm -f install.sh.tmp
fi
fi
fi
# Update docker-compose files with correct version so we pull the correct images
if ! handle_docker_compose "$target_version"; then
log_error "Failed to download docker-compose files"
exit 1
fi
# Initialize environment
check_and_populate_env
# Set deployment mode to install to ensure container lifecycle uses install configuration
set_deployment_mode "install"
# Only generate admin password if not already set
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
generate_admin_password || { printf "${RED}> Failed to generate admin password${NC}\n"; exit 1; }
fi
# Pull images from GitHub Container Registry
printf "\n${YELLOW}+++ Pulling Docker images +++${NC}\n"
printf "${CYAN} Installing version: ${target_version}${NC}\n"
images=(
"${GITHUB_CONTAINER_REGISTRY}-postgres:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-reverse-proxy:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-api:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-client:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-admin:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-smtp:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-task-runner:${target_version}"
)
for image in "${images[@]}"; do
if ! retry_command 3 5 enhanced_docker_pull "$image"; then
log_warning "Failed to pull image: $image - continuing anyway"
fi
done
printf "${GREEN}✓ Docker image pulling completed${NC}\n"
# Save version to .env
update_env_var "ALIASVAULT_VERSION" "$target_version"
# Start containers
printf "\n${YELLOW}+++ Starting services +++${NC}\n"
if [ "$VERBOSE" = true ]; then
printf "${CYAN} Starting Docker containers...${NC} "
printf "\b${NC}\n"
if ! docker compose up -d --force-recreate; then
log_error "Failed to start Docker containers"
exit 1
fi
else
(
docker compose up -d --force-recreate > /tmp/docker_start.log 2>&1 &
START_PID=$!
show_spinner $START_PID "Starting Docker containers"
wait $START_PID
START_EXIT_CODE=$?
if [ $START_EXIT_CODE -ne 0 ]; then
log_error "Failed to start Docker containers. Output:"
cat /tmp/docker_start.log >&2
exit 1
fi
rm -f /tmp/docker_start.log
)
fi
printf "${GREEN}✓ Docker containers started successfully${NC}\n"
# Only show success message if we made it here without errors
print_install_success_message
}
# Function to handle development database configuration
configure_dev_database() {
printf "${YELLOW}+++ Development Database Configuration +++${NC}\n"
printf "\n"
if [ ! -f "docker-compose.dev.yml" ]; then
printf "${RED}> The docker-compose.dev.yml file is missing. This file is required to start the development database. Please checkout the full GitHub repository and try again.${NC}\n"
return 1
fi
# Check if direct option was provided
if [ -n "$COMMAND_ARG" ]; then
case $COMMAND_ARG in
1|start)
if docker compose -f docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
printf "${YELLOW}> Development database is already running.${NC}\n"
else
printf "${CYAN}> Starting development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml up -d --wait --wait-timeout 60
printf "${GREEN}> Development database started successfully.${NC}\n"
fi
print_dev_db_details
return
;;
0|stop)
if ! docker compose -f docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
printf "${YELLOW}> Development database is already stopped.${NC}\n"
else
printf "${CYAN}> Stopping development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml down
printf "${GREEN}> Development database stopped successfully.${NC}\n"
fi
return
;;
esac
fi
# Check current status
if docker compose -f docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
DEV_DB_STATUS="running"
else
DEV_DB_STATUS="stopped"
fi
printf "${CYAN}About Development Database:${NC}\n"
printf "A separate PostgreSQL instance for development purposes that:\n"
printf " - Runs on port 5433 (to avoid conflicts)\n"
printf " - Uses simple credentials (password: 'password')\n"
printf " - Stores data separately from production\n"
printf "\n"
printf "${CYAN}Current Status:${NC}\n"
if [ "$DEV_DB_STATUS" = "running" ]; then
printf "Development Database: ${GREEN}Running${NC}\n"
else
printf "Development Database: ${YELLOW}Stopped${NC}\n"
fi
printf "\n"
printf "Options:\n"
printf "1) Start development database\n"
printf "2) Stop development database\n"
printf "3) View connection details\n"
printf "4) Cancel\n"
printf "\n"
read -p "Select an option [1-4]: " dev_db_option
case $dev_db_option in
1)
if [ "$DEV_DB_STATUS" = "running" ]; then
printf "${YELLOW}> Development database is already running.${NC}\n"
else
printf "${CYAN}> Starting development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml up -d --wait --wait-timeout 60
printf "${GREEN}> Development database started successfully.${NC}\n"
fi
print_dev_db_details
;;
2)
if [ "$DEV_DB_STATUS" = "stopped" ]; then
printf "${YELLOW}> Development database is already stopped.${NC}\n"
else
printf "${CYAN}> Stopping development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml down
printf "${GREEN}> Development database stopped successfully.${NC}\n"
fi
;;
3)
print_dev_db_details
;;
4)
printf "${YELLOW}Configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to print development database connection details
print_dev_db_details() {
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
printf "\n"
printf "${CYAN}Development Database Connection Details:${NC}\n"
printf "Host: localhost\n"
printf "Port: 5433\n"
printf "Database: aliasvault\n"
printf "Username: aliasvault\n"
printf "Password: password\n"
printf "\n"
printf "Connection string:\n"
printf "Host=localhost;Port=5433;Database=aliasvault;Username=aliasvault;Password=password\n"
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
}
# Function to set deployment mode in .env
set_deployment_mode() {
local mode=$1
if [ "$mode" != "build" ] && [ "$mode" != "install" ]; then
printf "${RED}Invalid deployment mode: $mode${NC}\n"
exit 1
fi
update_env_var "DEPLOYMENT_MODE" "$mode"
}
# Function to handle database export
handle_db_export() {
printf "${YELLOW}+++ Exporting Database +++${NC}\n" >&2
# Check if output redirection is present
if [ -t 1 ]; then
printf "Usage: ./install.sh db-export [--dev] > backup.sql.gz\n" >&2
printf "\n" >&2
printf "Options:\n" >&2
printf " --dev Export from development database\n" >&2
printf "\n" >&2
printf "Example:\n" >&2
printf " ./install.sh db-export > my_backup_$(date +%Y%m%d).sql.gz\n" >&2
printf " ./install.sh db-export --dev > my_dev_backup_$(date +%Y%m%d).sql.gz\n" >&2
exit 1
fi
if [ "$DEV_DB" = true ]; then
# Check if dev containers are running
if ! docker compose -f docker-compose.dev.yml -p aliasvault-dev ps postgres-dev --quiet 2>/dev/null | grep -q .; then
printf "${RED}Error: Development database container is not running. Start it first with: ./install.sh configure-dev-db${NC}\n" >&2
exit 1
fi
# Check if postgres-dev container is healthy
if ! docker compose -f docker-compose.dev.yml -p aliasvault-dev ps postgres-dev | grep -q "healthy"; then
printf "${RED}Error: Development PostgreSQL container is not healthy. Please check the logs.${NC}\n" >&2
exit 1
fi
printf "${CYAN}> Exporting development database...${NC}\n" >&2
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec postgres-dev pg_dump -U aliasvault aliasvault | gzip
else
# Production database export logic
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
printf "${RED}Error: AliasVault containers are not running. Start them first with: ./install.sh start${NC}\n" >&2
exit 1
fi
if ! docker compose ps postgres | grep -q "healthy"; then
printf "${RED}Error: PostgreSQL container is not healthy. Please check the logs with: docker compose logs postgres${NC}\n" >&2
exit 1
fi
printf "${CYAN}> Exporting production database...${NC}\n" >&2
docker compose exec postgres pg_dump -U aliasvault aliasvault | gzip
fi
if [ $? -eq 0 ]; then
printf "${GREEN}> Database exported successfully.${NC}\n" >&2
else
printf "${RED}> Failed to export database.${NC}\n" >&2
exit 1
fi
}
# Function to handle database import
handle_db_import() {
printf "${YELLOW}+++ Importing Database +++${NC}\n"
# Check if containers are running
if [ "$DEV_DB" = true ]; then
if ! docker compose -f docker-compose.dev.yml -p aliasvault-dev ps postgres-dev | grep -q "healthy"; then
printf "${RED}Error: Development PostgreSQL container is not healthy.${NC}\n"
exit 1
fi
else
if ! docker compose ps postgres | grep -q "healthy"; then
printf "${RED}Error: PostgreSQL container is not healthy.${NC}\n"
exit 1
fi
fi
# Check if we're getting input from a pipe
if [ -t 0 ]; then
printf "Usage: ./install.sh db-import [--dev] < backup.sql.gz\n"
exit 1
fi
# Save stdin to file descriptor 3
exec 3<&0
printf "${RED}Warning: This will DELETE ALL EXISTING DATA in the "
if [ "$DEV_DB" = true ]; then
printf "development database"
else
printf "database"
fi
printf ".${NC}\n"
if [ "$FORCE_YES" != true ]; then
# Use /dev/tty to read from terminal even when stdin is redirected
if [ -t 1 ] && [ -t 2 ] && [ -e /dev/tty ]; then
# Temporarily switch stdin to tty for confirmation
exec < /dev/tty
read -p "Continue? [y/N]: " confirm
# Switch back to original stdin
exec 0<&3
if [[ ! $confirm =~ ^[Yy]$ ]]; then
exec 3<&- # Close fd 3
exit 1
fi
else
printf "${RED}Error: Cannot read confirmation from terminal. Use -y flag to bypass confirmation.${NC}\n"
exec 3<&- # Close fd 3
exit 1
fi
fi
if [ "$DEV_DB" != true ]; then
printf "${CYAN}> Stopping dependent services...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose stop api admin task-runner smtp
else
docker compose stop api admin task-runner smtp > /dev/null 2>&1
fi
fi
printf "${CYAN}> Importing "
if [ "$DEV_DB" = true ]; then
printf "development "
fi
printf "database...${NC}\n"
# Create a temporary file to verify the gzip input
temp_file=$(mktemp)
cat <&3 > "$temp_file" # Read from fd 3 instead of stdin
exec 3<&- # Close fd 3
if ! gzip -t "$temp_file" 2>/dev/null; then
printf "${RED}Error: Input is not a valid gzip file${NC}\n"
rm "$temp_file"
exit 1
fi
if [ "$DEV_DB" = true ]; then
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" && \
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" && \
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" && \
gunzip -c "$temp_file" | docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault aliasvault
else
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" > /dev/null 2>&1 && \
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" > /dev/null 2>&1 && \
docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" > /dev/null 2>&1 && \
gunzip -c "$temp_file" | docker compose -f docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault aliasvault > /dev/null 2>&1
fi
else
if [ "$VERBOSE" = true ]; then
docker compose exec -T postgres psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" && \
docker compose exec -T postgres psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" && \
docker compose exec -T postgres psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" && \
gunzip -c "$temp_file" | docker compose exec -T postgres psql -U aliasvault aliasvault
else
docker compose exec -T postgres psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" > /dev/null 2>&1 && \
docker compose exec -T postgres psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" > /dev/null 2>&1 && \
docker compose exec -T postgres psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" > /dev/null 2>&1 && \
gunzip -c "$temp_file" | docker compose exec -T postgres psql -U aliasvault aliasvault > /dev/null 2>&1
fi
fi
import_status=$?
rm "$temp_file"
if [ $import_status -eq 0 ]; then
printf "${GREEN}> Database imported successfully.${NC}\n"
if [ "$DEV_DB" != true ]; then
printf "${CYAN}> Starting services...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose restart api admin task-runner smtp reverse-proxy
else
docker compose restart api admin task-runner smtp reverse-proxy > /dev/null 2>&1
fi
fi
else
printf "${RED}> Import failed. Please check that your backup file is valid.${NC}\n"
exit 1
fi
}
# Function to handle hostname configuration
handle_hostname_configuration() {
printf "${YELLOW}+++ Hostname Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
printf "The hostname is the domain name where your AliasVault server will be accessible.\n"
printf "A valid hostname is required for Let's Encrypt SSL certificate generation.\n"
printf "The hostname must be a real domain that points to this server (not localhost).\n"
printf "\n"
# Get current hostname
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
printf "Current hostname: ${CYAN}${CURRENT_HOSTNAME}${NC}\n"
printf "\n"
# Ask for new hostname
while true; do
read -p "Enter new hostname (e.g. aliasvault.net): " NEW_HOSTNAME
if [ -n "$NEW_HOSTNAME" ]; then
break
else
printf "${YELLOW}> Hostname cannot be empty. Please enter a valid hostname.${NC}\n"
fi
done
# Update the hostname
update_env_var "HOSTNAME" "$NEW_HOSTNAME"
printf "\n"
print_success_box "Hostname updated successfully to ${NEW_HOSTNAME}!"
}
# Function to handle IP logging configuration
handle_ip_logging_configuration() {
# Get current IP logging setting
CURRENT_SETTING=$(grep "^IP_LOGGING_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${YELLOW}+++ Configure IP Address Logging +++${NC}\n"
printf "\n"
printf "Current setting: ${CYAN}${CURRENT_SETTING}${NC}\n"
printf "\n"
printf "1) Enable IP address logging\n"
printf "2) Disable IP address logging\n"
printf "\n"
printf "Choose an option (1-2): "
read -r choice
case $choice in
1)
update_env_var "IP_LOGGING_ENABLED" "true"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "IP address logging has been enabled!"
;;
2)
update_env_var "IP_LOGGING_ENABLED" "false"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "IP address logging has been disabled!"
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
return 1
;;
esac
}
check_and_populate_env() {
printf "${CYAN} Checking .env values...${NC} ${GREEN}${NC}\n"
local any_missing=false
# SUPPORT_EMAIL
if ! grep -q "^SUPPORT_EMAIL=" "$ENV_FILE"; then
read -p "Enter server admin support email address that is shown on contact page (optional, press Enter to skip): " SUPPORT_EMAIL
update_env_var "SUPPORT_EMAIL" "$SUPPORT_EMAIL"
printf " Set SUPPORT_EMAIL\n"
any_missing=true
fi
# JWT_KEY
if ! grep -q "^JWT_KEY=" "$ENV_FILE" || [ -z "$(grep "^JWT_KEY=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
JWT_KEY=$(openssl rand -base64 32)
update_env_var "JWT_KEY" "$JWT_KEY"
printf " Set JWT_KEY\n"
any_missing=true
fi
# DATA_PROTECTION_CERT_PASS
if ! grep -q "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" || [ -z "$(grep "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
CERT_PASS=$(openssl rand -base64 32)
update_env_var "DATA_PROTECTION_CERT_PASS" "$CERT_PASS"
printf " Set DATA_PROTECTION_CERT_PASS\n"
any_missing=true
fi
# POSTGRES_DB
if ! grep -q "^POSTGRES_DB=" "$ENV_FILE" || [ -z "$(grep "^POSTGRES_DB=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "POSTGRES_DB" "aliasvault"
printf " Set POSTGRES_DB\n"
any_missing=true
fi
# POSTGRES_USER
if ! grep -q "^POSTGRES_USER=" "$ENV_FILE" || [ -z "$(grep "^POSTGRES_USER=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "POSTGRES_USER" "aliasvault"
printf " Set POSTGRES_USER\n"
any_missing=true
fi
# POSTGRES_PASSWORD
if ! grep -q "^POSTGRES_PASSWORD=" "$ENV_FILE" || [ -z "$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
POSTGRES_PASS=$(openssl rand -base64 32)
update_env_var "POSTGRES_PASSWORD" "$POSTGRES_PASS"
printf " Generated POSTGRES_PASSWORD\n"
any_missing=true
fi
# PRIVATE_EMAIL_DOMAINS
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "PRIVATE_EMAIL_DOMAINS" "DISABLED.TLD"
printf " Set PRIVATE_EMAIL_DOMAINS\n"
any_missing=true
fi
# SMTP_TLS_ENABLED
if ! grep -q "^SMTP_TLS_ENABLED=" "$ENV_FILE"; then
update_env_var "SMTP_TLS_ENABLED" "false"
printf " Set SMTP_TLS_ENABLED\n"
any_missing=true
fi
# HTTP_PORT
if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTP_PORT" "80"
printf " Set HTTP_PORT\n"
any_missing=true
fi
# HTTPS_PORT
if ! grep -q "^HTTPS_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTPS_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTPS_PORT" "443"
printf " Set HTTPS_PORT\n"
any_missing=true
fi
# SMTP_PORT
if ! grep -q "^SMTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^SMTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "SMTP_PORT" "25"
printf " Set SMTP_PORT\n"
any_missing=true
fi
# SMTP_TLS_PORT
if ! grep -q "^SMTP_TLS_PORT=" "$ENV_FILE" || [ -z "$(grep "^SMTP_TLS_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "SMTP_TLS_PORT" "587"
printf " Set SMTP_TLS_PORT\n"
any_missing=true
fi
}
main "$@"