mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 16:13:57 +00:00
Frontend package.json fixes: - react-router-dom: ^6.31.0 → ^6.30.1 (6.31.0 doesn't exist) - postcss: ^8.5.1 → ^8.5.6 (use latest stable version) Setup script fixes: - Replace deprecated --production flag with --omit=dev - Resolves npm warning: 'npm WARN config production Use --omit=dev instead' Fixes npm install errors: - 'No matching version found for react-router-dom@^6.31.0' - Deprecated npm configuration warnings
1668 lines
53 KiB
Bash
1668 lines
53 KiB
Bash
#!/bin/bash
|
||
# PatchMon Self-Hosting Installation Script
|
||
# Automated deployment script for self-hosted PatchMon instances
|
||
# Usage: ./self-hosting-install.sh
|
||
# Interactive self-hosting installation script
|
||
|
||
set -e
|
||
|
||
# Create main installation log file
|
||
INSTALL_LOG="/var/log/patchmon-install.log"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] === PatchMon Self-Hosting Installation Started ===" >> "$INSTALL_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script PID: $$" >> "$INSTALL_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Running as user: $(whoami)" >> "$INSTALL_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Current directory: $(pwd)" >> "$INSTALL_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script arguments: $@" >> "$INSTALL_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script path: $0" >> "$INSTALL_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ======================================" >> "$INSTALL_LOG"
|
||
|
||
# Create immediate debug log for troubleshooting
|
||
DEBUG_LOG="/tmp/patchmon_debug_$(date +%Y%m%d_%H%M%S).log"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] === PatchMon Script Started ===" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script PID: $$" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Running as user: $(whoami)" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Current directory: $(pwd)" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script arguments: $@" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script path: $0" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ======================================" >> "$DEBUG_LOG"
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Global variables
|
||
SCRIPT_VERSION="self-hosting-install.sh v1.2.6-selfhost-2025-01-20-1"
|
||
DEFAULT_GITHUB_REPO="https://github.com/9technologygroup/patchmon.net.git"
|
||
FQDN=""
|
||
CUSTOM_FQDN=""
|
||
EMAIL=""
|
||
|
||
# Logging function
|
||
function log_message() {
|
||
local message="$1"
|
||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||
local log_file="/var/log/patchmon-install.log"
|
||
|
||
echo "[${timestamp}] ${message}" >> "$log_file"
|
||
echo "[${timestamp}] ${message}"
|
||
}
|
||
DEPLOYMENT_BRANCH="main"
|
||
GITHUB_REPO=""
|
||
DB_SAFE_DB_DB_USER=""
|
||
DB_PASS=""
|
||
JWT_SECRET=""
|
||
BACKEND_PORT=""
|
||
APP_DIR=""
|
||
SERVICE_USE_LETSENCRYPT="true" # Will be set based on user input
|
||
SERVER_PROTOCOL_SEL="https"
|
||
SERVER_PORT_SEL="" # Will be set to BACKEND_PORT in init_instance_vars
|
||
SETUP_NGINX="true"
|
||
|
||
# Functions
|
||
print_status() {
|
||
echo -e "${GREEN}✅ $1${NC}"
|
||
}
|
||
|
||
print_info() {
|
||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "${RED}❌ $1${NC}"
|
||
}
|
||
|
||
print_warning() {
|
||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||
}
|
||
|
||
print_question() {
|
||
echo -e "${BLUE}❓ $1${NC}"
|
||
}
|
||
|
||
print_success() {
|
||
echo -e "${GREEN}🎉 $1${NC}"
|
||
}
|
||
|
||
# Interactive input functions
|
||
read_input() {
|
||
local prompt="$1"
|
||
local var_name="$2"
|
||
local default_value="$3"
|
||
|
||
if [ -n "$default_value" ]; then
|
||
echo -n -e "${BLUE}$prompt${NC} [${YELLOW}$default_value${NC}]: "
|
||
else
|
||
echo -n -e "${BLUE}$prompt${NC}: "
|
||
fi
|
||
|
||
read -r input
|
||
if [ -z "$input" ] && [ -n "$default_value" ]; then
|
||
eval "$var_name='$default_value'"
|
||
else
|
||
eval "$var_name='$input'"
|
||
fi
|
||
}
|
||
|
||
read_yes_no() {
|
||
local prompt="$1"
|
||
local var_name="$2"
|
||
local default_value="$3"
|
||
|
||
while true; do
|
||
if [ -n "$default_value" ]; then
|
||
echo -n -e "${BLUE}$prompt${NC} [${YELLOW}$default_value${NC}]: "
|
||
else
|
||
echo -n -e "${BLUE}$prompt${NC} (y/n): "
|
||
fi
|
||
read -r input
|
||
|
||
if [ -z "$input" ] && [ -n "$default_value" ]; then
|
||
input="$default_value"
|
||
fi
|
||
|
||
case $input in
|
||
[Yy]|[Yy][Ee][Ss])
|
||
eval "$var_name='y'"
|
||
break
|
||
;;
|
||
[Nn]|[Nn][Oo])
|
||
eval "$var_name='n'"
|
||
break
|
||
;;
|
||
*)
|
||
print_error "Please answer yes (y) or no (n)"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
print_banner() {
|
||
echo -e "${BLUE}====================================================${NC}"
|
||
echo -e "${BLUE} PatchMon Self-Hosting Installation${NC}"
|
||
echo -e "${BLUE}Running: $SCRIPT_VERSION${NC}"
|
||
echo -e "${BLUE}====================================================${NC}"
|
||
}
|
||
|
||
# Interactive setup functions
|
||
check_timezone() {
|
||
print_info "Checking current timezone..."
|
||
current_tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")
|
||
|
||
if [ "$current_tz" != "Unknown" ]; then
|
||
current_datetime=$(date)
|
||
print_info "Current timezone: $current_tz"
|
||
print_info "Current date/time: $current_datetime"
|
||
read_yes_no "Is this timezone and date/time correct?" TIMEZONE_CORRECT "y"
|
||
|
||
if [ "$TIMEZONE_CORRECT" = "n" ]; then
|
||
print_info "Available timezones:"
|
||
timedatectl list-timezones | head -20
|
||
print_warning "Showing first 20 timezones. Use 'timedatectl list-timezones' to see all."
|
||
read_input "Enter your timezone (e.g., America/New_York, Europe/London)" NEW_TIMEZONE
|
||
|
||
if [ -n "$NEW_TIMEZONE" ]; then
|
||
print_info "Setting timezone to $NEW_TIMEZONE..."
|
||
timedatectl set-timezone "$NEW_TIMEZONE"
|
||
print_status "Timezone updated to $NEW_TIMEZONE"
|
||
|
||
# Show updated date/time
|
||
updated_datetime=$(date)
|
||
print_info "Updated date/time: $updated_datetime"
|
||
fi
|
||
fi
|
||
else
|
||
print_warning "Could not detect timezone. Please set it manually if needed."
|
||
current_datetime=$(date)
|
||
print_info "Current date/time: $current_datetime"
|
||
fi
|
||
}
|
||
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
print_error "This script must be run as root"
|
||
print_info "Please run: sudo $0"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Function to run commands as a specific user with better error handling
|
||
run_as_user() {
|
||
local user="$1"
|
||
local command="$2"
|
||
|
||
if ! command -v sudo >/dev/null 2>&1; then
|
||
print_error "sudo is required but not installed. Please install sudo first."
|
||
exit 1
|
||
fi
|
||
|
||
if ! id "$user" &>/dev/null; then
|
||
print_error "User '$user' does not exist"
|
||
exit 1
|
||
fi
|
||
|
||
sudo -u "$user" bash -c "$command"
|
||
}
|
||
|
||
# Detect and use the best available package manager
|
||
detect_package_manager() {
|
||
# Prefer apt over apt-get for modern Debian/Ubuntu systems
|
||
if command -v apt >/dev/null 2>&1; then
|
||
PKG_MANAGER="apt"
|
||
PKG_UPDATE="apt update"
|
||
PKG_UPGRADE="apt upgrade -y"
|
||
PKG_INSTALL="apt install -y"
|
||
elif command -v apt-get >/dev/null 2>&1; then
|
||
PKG_MANAGER="apt-get"
|
||
PKG_UPDATE="apt-get update"
|
||
PKG_UPGRADE="apt-get upgrade -y"
|
||
PKG_INSTALL="apt-get install -y"
|
||
else
|
||
print_error "No supported package manager found (apt or apt-get required)"
|
||
print_info "This script requires a Debian/Ubuntu-based system"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Using package manager: $PKG_MANAGER"
|
||
}
|
||
|
||
check_prerequisites() {
|
||
print_info "Running and checking prerequisites..."
|
||
|
||
# Check if running as root
|
||
check_root
|
||
|
||
# Detect package manager
|
||
detect_package_manager
|
||
|
||
print_info "Installing updates..."
|
||
$PKG_UPDATE -y
|
||
$PKG_UPGRADE
|
||
|
||
print_info "Installing prerequisite applications..."
|
||
# Install sudo if not present (needed for user switching)
|
||
if ! command -v sudo >/dev/null 2>&1; then
|
||
print_info "Installing sudo (required for user switching)..."
|
||
$PKG_INSTALL sudo
|
||
fi
|
||
|
||
$PKG_INSTALL wget curl jq git netcat-openbsd
|
||
|
||
print_status "Prerequisites installed successfully"
|
||
}
|
||
|
||
select_branch() {
|
||
print_info "Fetching available branches from GitHub repository..."
|
||
|
||
# Create temporary directory for git operations
|
||
TEMP_DIR="/tmp/patchmon_branches_$$"
|
||
mkdir -p "$TEMP_DIR"
|
||
cd "$TEMP_DIR"
|
||
|
||
# Try to clone the repository normally
|
||
if git clone "$DEFAULT_GITHUB_REPO" . 2>/dev/null; then
|
||
# Get list of remote branches and trim whitespace
|
||
branches=$(git branch -r | grep -v HEAD | sed 's/origin\///' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | sort -u)
|
||
|
||
if [ -n "$branches" ]; then
|
||
print_info "Available branches with details:"
|
||
echo ""
|
||
|
||
# Get branch information
|
||
branch_count=1
|
||
while IFS= read -r branch; do
|
||
if [ -n "$branch" ]; then
|
||
# Get last commit date for this branch
|
||
last_commit=$(git log -1 --format="%ci" "origin/$branch" 2>/dev/null || echo "Unknown")
|
||
|
||
# Get release tag associated with this branch (if any)
|
||
release_tag=$(git describe --tags --exact-match "origin/$branch" 2>/dev/null || echo "")
|
||
|
||
# Format the date
|
||
if [ "$last_commit" != "Unknown" ]; then
|
||
formatted_date=$(date -d "$last_commit" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$last_commit")
|
||
else
|
||
formatted_date="Unknown"
|
||
fi
|
||
|
||
# Display branch info
|
||
printf "%2d. %-20s" "$branch_count" "$branch"
|
||
printf " (Last commit: %s)" "$formatted_date"
|
||
|
||
if [ -n "$release_tag" ]; then
|
||
printf " [Release: %s]" "$release_tag"
|
||
fi
|
||
|
||
echo ""
|
||
branch_count=$((branch_count + 1))
|
||
fi
|
||
done <<< "$branches"
|
||
|
||
echo ""
|
||
|
||
# Determine default selection: prefer 'main' if present
|
||
main_index=$(echo "$branches" | nl -w1 -s':' | awk -F':' '$2=="main"{print $1}' | head -1)
|
||
if [ -z "$main_index" ]; then
|
||
main_index=1
|
||
fi
|
||
|
||
while true; do
|
||
read_input "Select branch number" BRANCH_NUMBER "$main_index"
|
||
|
||
if [[ "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
|
||
selected_branch=$(echo "$branches" | sed -n "${BRANCH_NUMBER}p" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
|
||
if [ -n "$selected_branch" ]; then
|
||
DEPLOYMENT_BRANCH="$selected_branch"
|
||
|
||
# Show additional info for selected branch
|
||
last_commit=$(git log -1 --format="%ci" "origin/$selected_branch" 2>/dev/null || echo "Unknown")
|
||
release_tag=$(git describe --tags --exact-match "origin/$selected_branch" 2>/dev/null || echo "")
|
||
|
||
if [ "$last_commit" != "Unknown" ]; then
|
||
formatted_date=$(date -d "$last_commit" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$last_commit")
|
||
else
|
||
formatted_date="Unknown"
|
||
fi
|
||
|
||
print_status "Selected branch: $DEPLOYMENT_BRANCH"
|
||
print_info "Last commit: $formatted_date"
|
||
if [ -n "$release_tag" ]; then
|
||
print_info "Release tag: $release_tag"
|
||
fi
|
||
break
|
||
else
|
||
print_error "Invalid branch number. Please try again."
|
||
fi
|
||
else
|
||
print_error "Please enter a valid number."
|
||
fi
|
||
done
|
||
else
|
||
print_warning "No branches found, using default: main"
|
||
DEPLOYMENT_BRANCH="main"
|
||
fi
|
||
else
|
||
print_warning "Could not connect to GitHub repository"
|
||
print_warning "This might be due to:"
|
||
print_warning " • Network connectivity issues"
|
||
print_warning " • Firewall blocking git access"
|
||
print_warning " • GitHub repository access restrictions"
|
||
print_warning "Using default branch: main"
|
||
DEPLOYMENT_BRANCH="main"
|
||
fi
|
||
|
||
# Clean up
|
||
cd /
|
||
rm -rf "$TEMP_DIR"
|
||
}
|
||
|
||
interactive_setup() {
|
||
print_banner
|
||
|
||
print_info "Welcome to PatchMon Self-Hosting Installation!"
|
||
print_info "This script will guide you through the installation process."
|
||
echo ""
|
||
|
||
# Check prerequisites
|
||
check_prerequisites
|
||
echo ""
|
||
|
||
# Check timezone
|
||
check_timezone
|
||
echo ""
|
||
|
||
# Get basic information
|
||
print_question "Let's gather some information about your installation:"
|
||
echo ""
|
||
|
||
read_input "Enter your domain name or IP address (e.g., patchmon.yourdomain.com or 192.168.1.100)" FQDN "patchmon.internal"
|
||
|
||
echo ""
|
||
print_info "🔒 SSL/HTTPS Configuration:"
|
||
print_info " • Public hosting (accessible from internet): Enable SSL for security"
|
||
print_info " • Local hosting (internal network only): SSL not required"
|
||
echo ""
|
||
read_yes_no "Are you hosting this publicly on the internet and want SSL/HTTPS with Let's Encrypt?" SSL_ENABLED "n"
|
||
|
||
if [ "$SSL_ENABLED" = "y" ]; then
|
||
read_input "Enter your email address for Let's Encrypt SSL certificate" EMAIL
|
||
else
|
||
EMAIL=""
|
||
fi
|
||
|
||
|
||
# Select branch
|
||
echo ""
|
||
select_branch
|
||
echo ""
|
||
|
||
# Confirm settings
|
||
print_info "Please confirm your settings:"
|
||
echo " Domain/IP: $FQDN"
|
||
echo " SSL Enabled: $SSL_ENABLED"
|
||
if [ "$SSL_ENABLED" = "y" ]; then
|
||
echo " Email: $EMAIL"
|
||
fi
|
||
echo " Branch: $DEPLOYMENT_BRANCH"
|
||
echo ""
|
||
|
||
read_yes_no "Proceed with installation?" CONFIRM_INSTALL "y"
|
||
|
||
if [ "$CONFIRM_INSTALL" = "n" ]; then
|
||
print_info "Installation cancelled by user."
|
||
exit 0
|
||
fi
|
||
|
||
print_success "Starting installation process..."
|
||
echo ""
|
||
}
|
||
|
||
# Generate random password
|
||
generate_password() {
|
||
openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
|
||
}
|
||
|
||
# Generate JWT secret
|
||
generate_jwt_secret() {
|
||
openssl rand -base64 64 | tr -d "=+/" | cut -c1-50
|
||
}
|
||
|
||
# Initialize instance variables
|
||
init_instance_vars() {
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] init_instance_vars function started" >> "$DEBUG_LOG"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating safe database name from FQDN: $FQDN" >> "$DEBUG_LOG"
|
||
|
||
# Create safe database name from FQDN
|
||
DB_SAFE_NAME=$(echo "$FQDN" | sed 's/[^a-zA-Z0-9]/_/g' | sed 's/^_*//' | sed 's/_*$//')
|
||
|
||
# Check if FQDN starts with a digit (likely an IP address)
|
||
if [[ "$FQDN" =~ ^[0-9] ]]; then
|
||
# Generate 2 random letters for IP address prefixing
|
||
RANDOM_PREFIX=$(tr -dc 'a-z' < /dev/urandom | head -c 2)
|
||
DB_SAFE_NAME="${RANDOM_PREFIX}${DB_SAFE_NAME}"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] IP address detected, prefixed with: $RANDOM_PREFIX" >> "$DEBUG_LOG"
|
||
print_info "IP address detected ($FQDN), using prefix '$RANDOM_PREFIX' for database/service names"
|
||
fi
|
||
|
||
DB_NAME="${DB_SAFE_NAME}"
|
||
DB_USER="${DB_SAFE_NAME}"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DB_SAFE_NAME: $DB_SAFE_NAME" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DB_NAME: $DB_NAME" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DB_USER: $DB_USER" >> "$DEBUG_LOG"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Generating password..." >> "$DEBUG_LOG"
|
||
DB_PASS=$(generate_password)
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Generating JWT secret..." >> "$DEBUG_LOG"
|
||
JWT_SECRET=$(generate_jwt_secret)
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Generating random backend port..." >> "$DEBUG_LOG"
|
||
|
||
# Generate random backend port (3001-3999)
|
||
BACKEND_PORT=$((3001 + RANDOM % 999))
|
||
|
||
# Set SERVER_PORT_SEL to 443 for HTTPS (external port) or backend port for HTTP
|
||
if [ "$SERVER_PROTOCOL_SEL" = "https" ]; then
|
||
SERVER_PORT_SEL=443
|
||
else
|
||
SERVER_PORT_SEL=$BACKEND_PORT
|
||
fi
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] BACKEND_PORT: $BACKEND_PORT" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SERVER_PORT_SEL: $SERVER_PORT_SEL" >> "$DEBUG_LOG"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Setting application directory and service name..." >> "$DEBUG_LOG"
|
||
|
||
# Set application directory and service name
|
||
APP_DIR="/opt/${FQDN}"
|
||
SERVICE_NAME="${FQDN}"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] APP_DIR: $APP_DIR" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SERVICE_NAME: $SERVICE_NAME" >> "$DEBUG_LOG"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating dedicated user name..." >> "$DEBUG_LOG"
|
||
|
||
# Create dedicated user name (safe for system users)
|
||
INSTANCE_USER=$(echo "$DB_SAFE_NAME" | cut -c1-32)
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INSTANCE_USER: $INSTANCE_USER" >> "$DEBUG_LOG"
|
||
|
||
print_info "Initialized variables for $FQDN"
|
||
print_info "Database: $DB_NAME"
|
||
print_info "Backend Port: $BACKEND_PORT"
|
||
print_info "App Directory: $APP_DIR"
|
||
print_info "Instance User: $INSTANCE_USER"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] init_instance_vars function completed successfully" >> "$DEBUG_LOG"
|
||
}
|
||
|
||
# Update system packages
|
||
update_system() {
|
||
print_info "Updating system packages..."
|
||
$PKG_UPDATE -y
|
||
$PKG_UPGRADE
|
||
}
|
||
|
||
# Install essential tools
|
||
install_essential_tools() {
|
||
print_info "Installing essential tools..."
|
||
$PKG_INSTALL curl netcat-openbsd git jq
|
||
}
|
||
|
||
# Install Node.js (if not already installed)
|
||
install_nodejs() {
|
||
# Force PATH refresh to ensure we get the latest Node.js
|
||
export PATH="/usr/bin:/usr/local/bin:$PATH"
|
||
hash -r # Clear bash command cache
|
||
|
||
NODE_VERSION=""
|
||
if command -v node >/dev/null 2>&1; then
|
||
NODE_VERSION=$(node --version 2>/dev/null | sed 's/v//')
|
||
print_info "Node.js already installed: v$NODE_VERSION"
|
||
|
||
# Check if version is 18 or higher
|
||
if [ "$(echo "$NODE_VERSION" | cut -d. -f1)" -ge 18 ]; then
|
||
print_status "Node.js version is sufficient (v$NODE_VERSION)"
|
||
# Clean npm cache to avoid issues
|
||
npm cache clean --force 2>/dev/null || true
|
||
return 0
|
||
else
|
||
print_warning "Node.js version $NODE_VERSION is too old, updating..."
|
||
fi
|
||
fi
|
||
|
||
print_info "Installing Node.js 20.x..."
|
||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||
$PKG_INSTALL nodejs
|
||
|
||
# Verify installation
|
||
NODE_VERSION=$(node --version | sed 's/v//')
|
||
NPM_VERSION=$(npm --version)
|
||
print_status "Node.js installed: v$NODE_VERSION"
|
||
print_status "npm installed: v$NPM_VERSION"
|
||
|
||
# Clean npm cache to avoid issues
|
||
npm cache clean --force 2>/dev/null || true
|
||
}
|
||
|
||
# Install PostgreSQL
|
||
install_postgresql() {
|
||
print_info "Installing PostgreSQL..."
|
||
|
||
if systemctl is-active --quiet postgresql; then
|
||
print_status "PostgreSQL already running"
|
||
else
|
||
$PKG_INSTALL postgresql postgresql-contrib
|
||
systemctl start postgresql
|
||
systemctl enable postgresql
|
||
print_status "PostgreSQL installed and started"
|
||
fi
|
||
}
|
||
|
||
# Install nginx
|
||
install_nginx() {
|
||
print_info "Installing nginx..."
|
||
|
||
if systemctl is-active --quiet nginx; then
|
||
print_status "nginx already running"
|
||
else
|
||
$PKG_INSTALL nginx
|
||
systemctl start nginx
|
||
systemctl enable nginx
|
||
print_status "nginx installed and started"
|
||
fi
|
||
}
|
||
|
||
# Install certbot for Let's Encrypt
|
||
install_certbot() {
|
||
print_info "Installing certbot for Let's Encrypt..."
|
||
|
||
if command -v certbot >/dev/null 2>&1; then
|
||
print_status "certbot already installed"
|
||
else
|
||
$PKG_INSTALL certbot python3-certbot-nginx
|
||
print_status "certbot installed"
|
||
fi
|
||
}
|
||
|
||
# Create dedicated user for this instance
|
||
create_instance_user() {
|
||
print_info "Creating dedicated user: $INSTANCE_USER"
|
||
|
||
# Create application directory first (as root)
|
||
mkdir -p "$APP_DIR"
|
||
|
||
# Check if user already exists
|
||
if id "$INSTANCE_USER" &>/dev/null; then
|
||
print_warning "User $INSTANCE_USER already exists, skipping creation"
|
||
# Ensure directory ownership is correct for existing user
|
||
chown -R "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR"
|
||
chmod 755 "$APP_DIR"
|
||
return 0
|
||
fi
|
||
|
||
# Create user with no login shell and no home directory
|
||
useradd --system --no-create-home --shell /bin/false "$INSTANCE_USER"
|
||
|
||
# Set ownership and permissions
|
||
chown -R "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR"
|
||
chmod 755 "$APP_DIR"
|
||
|
||
print_status "Dedicated user $INSTANCE_USER created successfully"
|
||
}
|
||
|
||
# Setup Node.js environment isolation for this instance
|
||
setup_nodejs_isolation() {
|
||
print_info "Setting up Node.js environment isolation for $INSTANCE_USER..."
|
||
|
||
# Create npm directories as root first
|
||
mkdir -p "$APP_DIR/.npm" "$APP_DIR/.npm-global"
|
||
|
||
# Create .npmrc file with proper configuration
|
||
cat > "$APP_DIR/.npmrc" << EOF
|
||
cache=$APP_DIR/.npm
|
||
prefix=$APP_DIR/.npm-global
|
||
init-module=$APP_DIR/.npm-global/.npm-init.js
|
||
tmp=$APP_DIR/.npm/tmp
|
||
EOF
|
||
|
||
# Set ownership to the dedicated user
|
||
chown -R "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR/.npm" "$APP_DIR/.npm-global" "$APP_DIR/.npmrc"
|
||
|
||
print_status "Node.js environment isolation configured for $INSTANCE_USER"
|
||
}
|
||
|
||
# Setup database for instance
|
||
setup_database() {
|
||
print_info "Creating database: $DB_NAME"
|
||
|
||
# Check if sudo is available for user switching
|
||
if command -v sudo >/dev/null 2>&1; then
|
||
# Drop and recreate database and user for clean state
|
||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;" || true
|
||
sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;" || true
|
||
|
||
# Create database and user
|
||
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';"
|
||
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
|
||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
|
||
else
|
||
# Alternative method for systems without sudo (run as postgres user directly)
|
||
print_warning "sudo not available, using alternative method for PostgreSQL setup"
|
||
|
||
# Switch to postgres user using su
|
||
su - postgres -c "psql -c \"DROP DATABASE IF EXISTS $DB_NAME;\"" || true
|
||
su - postgres -c "psql -c \"DROP USER IF EXISTS $DB_USER;\"" || true
|
||
su - postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\""
|
||
su - postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\""
|
||
su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;\""
|
||
fi
|
||
|
||
print_status "Database $DB_NAME created with user $DB_USER"
|
||
}
|
||
|
||
# Clone application repository
|
||
clone_application() {
|
||
print_info "Cloning PatchMon application..."
|
||
|
||
if [ -d "$APP_DIR" ]; then
|
||
print_warning "Directory $APP_DIR already exists, removing..."
|
||
rm -rf "$APP_DIR"
|
||
fi
|
||
|
||
git clone -b "$DEPLOYMENT_BRANCH" "$GITHUB_REPO" "$APP_DIR"
|
||
|
||
# Set ownership to the dedicated user
|
||
chown -R "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR"
|
||
|
||
cd "$APP_DIR"
|
||
|
||
print_status "Application cloned to $APP_DIR with ownership set to $INSTANCE_USER"
|
||
}
|
||
|
||
# Setup Node.js environment
|
||
setup_node_environment() {
|
||
print_info "Setting up Node.js environment..."
|
||
|
||
cd "$APP_DIR"
|
||
|
||
# Set Node.js environment
|
||
export NODE_ENV=production
|
||
export PATH="/usr/bin:/usr/local/bin:$PATH"
|
||
|
||
print_status "Node.js environment configured"
|
||
}
|
||
|
||
# Install dependencies
|
||
install_dependencies() {
|
||
print_info "Installing dependencies as user $INSTANCE_USER..."
|
||
|
||
cd "$APP_DIR"
|
||
|
||
# Clean up any existing node_modules to avoid conflicts
|
||
rm -rf node_modules
|
||
|
||
# Create tmp directory for npm
|
||
mkdir -p "$APP_DIR/.npm/tmp"
|
||
|
||
# Fix npm cache ownership issues (common problem)
|
||
chown -R "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR/.npm"
|
||
|
||
# Clean npm cache to avoid permission issues
|
||
run_as_user "$INSTANCE_USER" "cd $APP_DIR && npm cache clean --force" 2>/dev/null || true
|
||
|
||
# Install root dependencies as the dedicated user
|
||
print_info "Installing root dependencies..."
|
||
if ! run_as_user "$INSTANCE_USER" "
|
||
cd $APP_DIR
|
||
export NPM_CONFIG_CACHE=$APP_DIR/.npm
|
||
export NPM_CONFIG_PREFIX=$APP_DIR/.npm-global
|
||
export NPM_CONFIG_TMP=$APP_DIR/.npm/tmp
|
||
npm install --omit=dev --no-audit --no-fund --no-save
|
||
"; then
|
||
print_error "Failed to install root dependencies"
|
||
return 1
|
||
fi
|
||
|
||
# Install backend dependencies as the dedicated user
|
||
print_info "Installing backend dependencies..."
|
||
cd backend
|
||
rm -rf node_modules
|
||
if ! run_as_user "$INSTANCE_USER" "
|
||
cd $APP_DIR/backend
|
||
export NPM_CONFIG_CACHE=$APP_DIR/.npm
|
||
export NPM_CONFIG_PREFIX=$APP_DIR/.npm-global
|
||
export NPM_CONFIG_TMP=$APP_DIR/.npm/tmp
|
||
npm install --omit=dev --no-audit --no-fund --no-save
|
||
"; then
|
||
print_error "Failed to install backend dependencies"
|
||
return 1
|
||
fi
|
||
cd ..
|
||
|
||
# Install frontend dependencies as the dedicated user (including dev dependencies for build)
|
||
print_info "Installing frontend dependencies..."
|
||
cd frontend
|
||
rm -rf node_modules
|
||
if ! run_as_user "$INSTANCE_USER" "
|
||
cd $APP_DIR/frontend
|
||
export NPM_CONFIG_CACHE=$APP_DIR/.npm
|
||
export NPM_CONFIG_PREFIX=$APP_DIR/.npm-global
|
||
export NPM_CONFIG_TMP=$APP_DIR/.npm/tmp
|
||
npm install --no-audit --no-fund --no-save
|
||
"; then
|
||
print_error "Failed to install frontend dependencies"
|
||
return 1
|
||
fi
|
||
|
||
# Build frontend
|
||
print_info "Building frontend..."
|
||
if ! run_as_user "$INSTANCE_USER" "
|
||
cd $APP_DIR/frontend
|
||
export NPM_CONFIG_CACHE=$APP_DIR/.npm
|
||
export NPM_CONFIG_PREFIX=$APP_DIR/.npm-global
|
||
export NPM_CONFIG_TMP=$APP_DIR/.npm/tmp
|
||
npm run build
|
||
"; then
|
||
print_error "Failed to build frontend"
|
||
return 1
|
||
fi
|
||
cd ..
|
||
|
||
# Ensure ownership is maintained
|
||
chown -R "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR"
|
||
|
||
print_status "Dependencies installed and frontend built as $INSTANCE_USER"
|
||
}
|
||
|
||
# Create environment files
|
||
create_env_files() {
|
||
print_info "Creating environment files..."
|
||
|
||
cd "$APP_DIR"
|
||
|
||
# Backend .env
|
||
cat > backend/.env << EOF
|
||
# Database Configuration
|
||
DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
|
||
|
||
# JWT Configuration
|
||
JWT_SECRET="$JWT_SECRET"
|
||
|
||
# Server Configuration
|
||
PORT=$BACKEND_PORT
|
||
NODE_ENV=production
|
||
|
||
# API Configuration
|
||
API_VERSION=v1
|
||
|
||
# CORS Configuration
|
||
CORS_ORIGIN="$SERVER_PROTOCOL_SEL://$FQDN"
|
||
|
||
# Rate Limiting (times in milliseconds)
|
||
RATE_LIMIT_WINDOW_MS=900000
|
||
RATE_LIMIT_MAX=5000
|
||
AUTH_RATE_LIMIT_WINDOW_MS=600000
|
||
AUTH_RATE_LIMIT_MAX=500
|
||
AGENT_RATE_LIMIT_WINDOW_MS=60000
|
||
AGENT_RATE_LIMIT_MAX=1000
|
||
|
||
# Logging
|
||
LOG_LEVEL=info
|
||
EOF
|
||
|
||
# Frontend .env
|
||
cat > frontend/.env << EOF
|
||
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
|
||
VITE_APP_NAME=PatchMon
|
||
VITE_APP_VERSION=1.2.6
|
||
EOF
|
||
|
||
print_status "Environment files created"
|
||
}
|
||
|
||
# Run database migrations
|
||
run_migrations() {
|
||
print_info "Running database migrations as user $INSTANCE_USER..."
|
||
|
||
cd "$APP_DIR/backend"
|
||
# Suppress Prisma CLI output (still logged to install log via tee)
|
||
run_as_user "$INSTANCE_USER" "cd $APP_DIR/backend && npx prisma migrate deploy" >/dev/null 2>&1 || true
|
||
run_as_user "$INSTANCE_USER" "cd $APP_DIR/backend && npx prisma generate" >/dev/null 2>&1 || true
|
||
|
||
print_status "Database migrations completed as $INSTANCE_USER"
|
||
}
|
||
|
||
# Admin account creation removed - handled by application's first-time setup
|
||
|
||
# Create systemd service
|
||
create_systemd_service() {
|
||
print_info "Creating systemd service for user $INSTANCE_USER..."
|
||
|
||
cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF
|
||
[Unit]
|
||
Description=PatchMon Service for $FQDN
|
||
After=network.target postgresql.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$INSTANCE_USER
|
||
Group=$INSTANCE_USER
|
||
WorkingDirectory=$APP_DIR/backend
|
||
ExecStart=/usr/bin/node src/server.js
|
||
Restart=always
|
||
RestartSec=10
|
||
Environment=NODE_ENV=production
|
||
Environment=PATH=/usr/bin:/usr/local/bin
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ReadWritePaths=$APP_DIR
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable "$SERVICE_NAME"
|
||
|
||
print_status "Systemd service created: $SERVICE_NAME (running as $INSTANCE_USER)"
|
||
}
|
||
|
||
# Setup nginx configuration
|
||
setup_nginx() {
|
||
print_info "Setting up nginx configuration..."
|
||
log_message "Setting up nginx configuration for $FQDN"
|
||
|
||
if [ "$USE_LETSENCRYPT" = "true" ]; then
|
||
# HTTP-only config first for Certbot challenge
|
||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||
server {
|
||
listen 80;
|
||
server_name $FQDN;
|
||
|
||
location /.well-known/acme-challenge/ {
|
||
root /var/www/html;
|
||
}
|
||
|
||
location / {
|
||
return 301 https://\$server_name\$request_uri;
|
||
}
|
||
}
|
||
EOF
|
||
else
|
||
# HTTP-only configuration for local hosting
|
||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||
server {
|
||
listen 80;
|
||
server_name $FQDN;
|
||
|
||
# Frontend
|
||
location / {
|
||
root $APP_DIR/frontend/dist;
|
||
try_files \$uri \$uri/ /index.html;
|
||
|
||
# Security headers
|
||
add_header X-Frame-Options DENY;
|
||
add_header X-Content-Type-Options nosniff;
|
||
add_header X-XSS-Protection "1; mode=block";
|
||
}
|
||
|
||
# API routes
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection 'upgrade';
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_cache_bypass \$http_upgrade;
|
||
proxy_read_timeout 300s;
|
||
proxy_connect_timeout 75s;
|
||
}
|
||
|
||
# Health check
|
||
location /health {
|
||
proxy_pass http://127.0.0.1:$BACKEND_PORT/health;
|
||
access_log off;
|
||
}
|
||
}
|
||
EOF
|
||
fi
|
||
|
||
# Enable site
|
||
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
||
|
||
# Remove default site if it exists
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
|
||
# Test nginx configuration
|
||
nginx -t
|
||
|
||
# Reload nginx
|
||
systemctl reload nginx
|
||
|
||
print_status "nginx configuration created for $FQDN"
|
||
}
|
||
|
||
# Setup Let's Encrypt SSL
|
||
setup_letsencrypt() {
|
||
print_info "Setting up Let's Encrypt SSL certificate..."
|
||
|
||
# Check if a valid certificate already exists
|
||
if certbot certificates 2>/dev/null | grep -q "$FQDN" && certbot certificates 2>/dev/null | grep -A 10 "$FQDN" | grep -q "VALID"; then
|
||
print_status "Valid SSL certificate already exists for $FQDN, skipping certificate generation"
|
||
|
||
# Update Nginx config with existing HTTPS configuration
|
||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||
server {
|
||
listen 80;
|
||
server_name $FQDN;
|
||
return 301 https://\$server_name\$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name $FQDN;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
|
||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||
|
||
# Security headers
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||
add_header X-Frame-Options DENY;
|
||
add_header X-Content-Type-Options nosniff;
|
||
add_header X-XSS-Protection "1; mode=block";
|
||
|
||
# Frontend
|
||
location / {
|
||
root $APP_DIR/frontend/dist;
|
||
try_files \$uri \$uri/ /index.html;
|
||
|
||
# Security headers
|
||
add_header X-Frame-Options DENY;
|
||
add_header X-Content-Type-Options nosniff;
|
||
add_header X-XSS-Protection "1; mode=block";
|
||
}
|
||
|
||
# API proxy
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection 'upgrade';
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_cache_bypass \$http_upgrade;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
# Enable the site
|
||
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
||
|
||
# Test nginx configuration
|
||
if nginx -t; then
|
||
print_status "Nginx configuration updated for existing SSL certificate"
|
||
systemctl reload nginx
|
||
else
|
||
print_error "Nginx configuration test failed"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
fi
|
||
|
||
print_info "No valid certificate found, generating new SSL certificate..."
|
||
|
||
# Wait a moment for nginx to be ready
|
||
sleep 5
|
||
|
||
# Obtain SSL certificate
|
||
log_message "Obtaining SSL certificate for $FQDN using Let's Encrypt"
|
||
certbot --nginx -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" --redirect
|
||
log_message "SSL certificate obtained successfully"
|
||
|
||
# Update Nginx config with full HTTPS configuration
|
||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||
server {
|
||
listen 80;
|
||
server_name $FQDN;
|
||
return 301 https://\$server_name\$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name $FQDN;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
|
||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||
|
||
# Frontend
|
||
location / {
|
||
root $APP_DIR/frontend/dist;
|
||
try_files \$uri \$uri/ /index.html;
|
||
|
||
# Security headers
|
||
add_header X-Frame-Options DENY;
|
||
add_header X-Content-Type-Options nosniff;
|
||
add_header X-XSS-Protection "1; mode=block";
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||
}
|
||
|
||
# API routes
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade \$http_upgrade;
|
||
proxy_set_header Connection 'upgrade';
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
proxy_cache_bypass \$http_upgrade;
|
||
proxy_read_timeout 300s;
|
||
proxy_connect_timeout 75s;
|
||
}
|
||
|
||
# Health check
|
||
location /health {
|
||
proxy_pass http://127.0.0.1:$BACKEND_PORT/health;
|
||
access_log off;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
nginx -t
|
||
nginx -s reload
|
||
|
||
# Setup auto-renewal
|
||
echo "0 12 * * * /usr/bin/certbot renew --quiet" | crontab -
|
||
|
||
print_status "SSL certificate obtained and auto-renewal configured"
|
||
}
|
||
|
||
# Start services
|
||
start_services() {
|
||
print_info "Starting services..."
|
||
|
||
# Start PatchMon service
|
||
systemctl start "$SERVICE_NAME"
|
||
|
||
# Wait for service to start
|
||
sleep 10
|
||
|
||
# Check if service is running
|
||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||
print_status "PatchMon service started successfully"
|
||
else
|
||
print_error "Failed to start PatchMon service"
|
||
systemctl status "$SERVICE_NAME"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Populate server settings in database
|
||
populate_server_settings() {
|
||
print_info "Populating server settings in database..."
|
||
|
||
cd "$APP_DIR/backend"
|
||
|
||
# Create settings update script
|
||
cat > update_settings.js << EOF
|
||
const { PrismaClient } = require('@prisma/client');
|
||
const prisma = new PrismaClient();
|
||
|
||
async function updateSettings() {
|
||
try {
|
||
// Check if settings record exists, create or update
|
||
const existingSettings = await prisma.settings.findFirst();
|
||
|
||
const settingsData = {
|
||
server_url: '$SERVER_PROTOCOL_SEL://$FQDN',
|
||
server_protocol: '$SERVER_PROTOCOL_SEL',
|
||
server_host: '$FQDN',
|
||
server_port: $SERVER_PORT_SEL,
|
||
update_interval: 60,
|
||
auto_update: true
|
||
};
|
||
|
||
if (existingSettings) {
|
||
// Update existing settings
|
||
await prisma.settings.update({
|
||
where: { id: existingSettings.id },
|
||
data: settingsData
|
||
});
|
||
} else {
|
||
// Create new settings record
|
||
await prisma.settings.create({
|
||
data: settingsData
|
||
});
|
||
}
|
||
|
||
console.log('✅ Database settings updated successfully');
|
||
} catch (error) {
|
||
console.error('❌ Error updating settings:', error.message);
|
||
process.exit(1);
|
||
} finally {
|
||
await prisma.\$disconnect();
|
||
}
|
||
}
|
||
|
||
updateSettings();
|
||
EOF
|
||
|
||
# Run the settings update script as the dedicated user
|
||
run_as_user "$INSTANCE_USER" "cd $APP_DIR/backend && node update_settings.js"
|
||
|
||
# Clean up temporary script
|
||
rm -f update_settings.js
|
||
|
||
print_status "Server settings populated successfully"
|
||
}
|
||
|
||
# Create agent version
|
||
create_agent_version() {
|
||
echo -e "${BLUE}🤖 Creating agent version...${NC}"
|
||
log_message "Creating agent version in database..."
|
||
cd $APP_DIR/backend
|
||
|
||
# Priority 1: Get version from agent script (most accurate for agent versions)
|
||
local current_version="N/A"
|
||
if [ -f "$APP_DIR/agents/patchmon-agent.sh" ]; then
|
||
current_version=$(grep '^AGENT_VERSION=' "$APP_DIR/agents/patchmon-agent.sh" | cut -d'"' -f2 2>/dev/null || echo "N/A")
|
||
if [ "$current_version" != "N/A" ] && [ -n "$current_version" ]; then
|
||
print_info "Detected agent version from script: $current_version"
|
||
fi
|
||
fi
|
||
|
||
# Priority 2: Use fallback version if not found
|
||
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
|
||
current_version="1.2.6"
|
||
print_warning "Could not determine version, using fallback: $current_version"
|
||
fi
|
||
|
||
print_info "Creating/updating agent version: $current_version"
|
||
print_info "This will ensure the latest agent script is available in the database"
|
||
|
||
# Test connection before creating agent version
|
||
if ! PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
|
||
print_error "Cannot connect to database before creating agent version"
|
||
exit 1
|
||
fi
|
||
|
||
# Copy agent script to backend directory
|
||
if [ -f "$APP_DIR/agents/patchmon-agent.sh" ]; then
|
||
cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/"
|
||
|
||
# Create agent version using Node.js script
|
||
node -e "
|
||
require('dotenv').config();
|
||
const { PrismaClient } = require('@prisma/client');
|
||
const fs = require('fs');
|
||
const crypto = require('crypto');
|
||
|
||
async function createAgentVersion() {
|
||
let prisma;
|
||
try {
|
||
// Initialize Prisma client with proper error handling
|
||
prisma = new PrismaClient();
|
||
|
||
// Test database connection
|
||
await prisma.\$connect();
|
||
console.log('✅ Database connection established');
|
||
|
||
// Debug: Check what models are available
|
||
console.log('Available Prisma models:', Object.keys(prisma).filter(key => !key.startsWith('\$')));
|
||
|
||
// Check if agent_versions model exists
|
||
if (!prisma.agent_versions) {
|
||
console.log('❌ agent_versions model not found in Prisma client');
|
||
console.log('Available models:', Object.keys(prisma).filter(key => !key.startsWith('\$')));
|
||
console.log('Skipping agent version creation...');
|
||
return;
|
||
}
|
||
|
||
const currentVersion = '$current_version';
|
||
const agentScript = fs.readFileSync('./patchmon-agent.sh', 'utf8');
|
||
|
||
// Check if current version already exists
|
||
const existingVersion = await prisma.agent_versions.findUnique({
|
||
where: { version: currentVersion }
|
||
});
|
||
|
||
if (existingVersion) {
|
||
// Version exists, always update the script content during updates
|
||
console.log('📝 Updating existing agent version ' + currentVersion + ' with latest script content...');
|
||
await prisma.agent_versions.update({
|
||
where: { version: currentVersion },
|
||
data: {
|
||
script_content: agentScript,
|
||
is_current: true,
|
||
is_default: true,
|
||
release_notes: 'Version ' + currentVersion + ' - Initial Deployment\\n\\nThis version contains the latest agent script from the deployment.'
|
||
}
|
||
});
|
||
console.log('✅ Agent version ' + currentVersion + ' updated successfully with latest script');
|
||
} else {
|
||
// Version doesn't exist, create it
|
||
console.log('🆕 Creating new agent version ' + currentVersion + '...');
|
||
await prisma.agent_versions.create({
|
||
data: {
|
||
id: crypto.randomUUID(),
|
||
version: currentVersion,
|
||
script_content: agentScript,
|
||
is_current: true,
|
||
is_default: true,
|
||
release_notes: 'Version ' + currentVersion + ' - Initial Deployment\\n\\nThis version contains the latest agent script from the deployment.',
|
||
updated_at: new Date()
|
||
}
|
||
});
|
||
console.log('✅ Agent version ' + currentVersion + ' created successfully');
|
||
}
|
||
|
||
// Set all other versions to not be current/default
|
||
await prisma.agent_versions.updateMany({
|
||
where: { version: { not: currentVersion } },
|
||
data: { is_current: false, is_default: false }
|
||
});
|
||
|
||
console.log('✅ Agent version management completed successfully');
|
||
} catch (error) {
|
||
console.error('❌ Error creating agent version:', error.message);
|
||
console.error('❌ Error details:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
if (prisma) {
|
||
await prisma.\$disconnect();
|
||
}
|
||
}
|
||
}
|
||
|
||
createAgentVersion();
|
||
"
|
||
|
||
# Clean up
|
||
rm -f "$APP_DIR/backend/patchmon-agent.sh"
|
||
|
||
print_status "Agent version created"
|
||
else
|
||
print_warning "Agent script not found, skipping agent version creation"
|
||
fi
|
||
}
|
||
|
||
# Create deployment summary
|
||
create_deployment_summary() {
|
||
print_info "Writing deployment summary into deployment-info.txt..."
|
||
|
||
# Reuse the unified deployment info file
|
||
SUMMARY_FILE="$APP_DIR/deployment-info.txt"
|
||
|
||
cat >> "$SUMMARY_FILE" << EOF
|
||
|
||
----------------------------------------------------
|
||
Deployment Summary (Appended)
|
||
----------------------------------------------------
|
||
|
||
Deployment Information:
|
||
- Email: $EMAIL
|
||
- Branch: $DEPLOYMENT_BRANCH
|
||
- Deployed: $(date)
|
||
- Deployment Duration: $(($(date +%s) - $DEPLOYMENT_START_TIME)) seconds
|
||
|
||
Service Status:
|
||
- PatchMon Service: $(systemctl is-active $SERVICE_NAME)
|
||
- Nginx Service: $(systemctl is-active nginx)
|
||
- PostgreSQL Service: $(systemctl is-active postgresql)
|
||
- SSL Certificate: $(if [ "$USE_LETSENCRYPT" = "true" ]; then echo "Enabled"; else echo "Disabled"; fi)
|
||
|
||
Diagnostic Commands:
|
||
- Service Status: systemctl status $SERVICE_NAME
|
||
- Service Logs: journalctl -u $SERVICE_NAME -f
|
||
- Nginx Status: systemctl status nginx
|
||
- Nginx Logs: journalctl -u nginx -f
|
||
- Database Status: systemctl status postgresql
|
||
- SSL Certificate: certbot certificates
|
||
- Disk Usage: df -h $APP_DIR
|
||
- Process Status: ps aux | grep $SERVICE_NAME
|
||
|
||
Troubleshooting:
|
||
- Check deployment log: cat $APP_DIR/patchmon-install.log
|
||
- Check service logs: journalctl -u $SERVICE_NAME --since "1 hour ago"
|
||
- Check nginx config: nginx -t
|
||
- Check database connection: sudo -u $DB_USER psql -d $DB_NAME -c "SELECT 1;"
|
||
- Check port binding: netstat -tlnp | grep $BACKEND_PORT
|
||
|
||
====================================================
|
||
EOF
|
||
|
||
# Ensure permissions
|
||
chmod 644 "$SUMMARY_FILE"
|
||
chown "$INSTANCE_USER:$INSTANCE_USER" "$SUMMARY_FILE"
|
||
|
||
# Copy the entire installation log into the instance folder
|
||
if [ -f "$INSTALL_LOG" ]; then
|
||
cp "$INSTALL_LOG" "$APP_DIR/patchmon-install.log" || true
|
||
chown "$INSTANCE_USER:$INSTANCE_USER" "$APP_DIR/patchmon-install.log" || true
|
||
chmod 644 "$APP_DIR/patchmon-install.log" || true
|
||
fi
|
||
|
||
print_status "Unified deployment info saved to: $SUMMARY_FILE"
|
||
}
|
||
|
||
# Email notification function removed for self-hosting deployment
|
||
|
||
# Save deployment information to file
|
||
save_deployment_info() {
|
||
print_info "Saving deployment information to file..."
|
||
|
||
# Create deployment info file
|
||
INFO_FILE="$APP_DIR/deployment-info.txt"
|
||
|
||
cat > "$INFO_FILE" << EOF
|
||
====================================================
|
||
PatchMon Deployment Information
|
||
====================================================
|
||
|
||
Instance Details:
|
||
- FQDN: $FQDN
|
||
- URL: $SERVER_PROTOCOL_SEL://$FQDN
|
||
- Deployed: $(date)
|
||
- Deployment Type: $(if [ "$USE_LETSENCRYPT" = "true" ]; then echo "Public with SSL"; else echo "Local/Internal"; fi)
|
||
- SSL Enabled: $USE_LETSENCRYPT
|
||
- Service Name: $SERVICE_NAME
|
||
|
||
Directories:
|
||
- App Directory: $APP_DIR
|
||
- Backend: $APP_DIR/backend
|
||
- Frontend (built): $APP_DIR/frontend/dist
|
||
- Node.js isolation dir: $APP_DIR/.npm
|
||
|
||
Database Information:
|
||
- Name: $DB_NAME
|
||
- User: $DB_USER
|
||
- Password: $DB_PASS
|
||
- Host: localhost
|
||
- Port: 5432
|
||
|
||
Networking:
|
||
- Backend Port: $BACKEND_PORT
|
||
- Nginx Config: /etc/nginx/sites-available/$FQDN
|
||
|
||
Logs & Files:
|
||
- Deployment Log: $LOG_FILE
|
||
- Systemd Service: /etc/systemd/system/$SERVICE_NAME.service
|
||
|
||
Common Commands:
|
||
- Restart backend service: sudo systemctl restart $SERVICE_NAME
|
||
- Check backend status: systemctl status $SERVICE_NAME
|
||
- Tail backend logs: journalctl -u $SERVICE_NAME -f
|
||
- Test nginx config: nginx -t && systemctl reload nginx
|
||
- Check DB connection: sudo -u $DB_USER psql -d $DB_NAME -c "SELECT 1;"
|
||
|
||
First-Time Setup:
|
||
- Visit the web interface: $SERVER_PROTOCOL_SEL://$FQDN
|
||
- Create the admin account through the web UI (no pre-created credentials)
|
||
|
||
Notes:
|
||
- Default role permissions (admin/user) are created automatically on backend startup
|
||
- Keep this file for future reference of your environment
|
||
|
||
====================================================
|
||
EOF
|
||
|
||
# Set permissions (readable by root and instance user)
|
||
chmod 644 "$INFO_FILE"
|
||
chown "$INSTANCE_USER:$INSTANCE_USER" "$INFO_FILE"
|
||
|
||
print_status "Deployment information saved to: $INFO_FILE"
|
||
}
|
||
|
||
# Restart PatchMon service
|
||
restart_patchmon() {
|
||
print_info "Restarting PatchMon service..."
|
||
|
||
# Restart PatchMon service
|
||
systemctl restart "$SERVICE_NAME"
|
||
|
||
# Wait for service to restart
|
||
sleep 5
|
||
|
||
# Check if service is running
|
||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||
print_status "PatchMon service restarted successfully"
|
||
else
|
||
print_error "Failed to restart PatchMon service"
|
||
systemctl status "$SERVICE_NAME"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Setup logging for deployment
|
||
setup_deployment_logging() {
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] setup_deployment_logging function started" >> "$DEBUG_LOG"
|
||
|
||
print_info "Setting up deployment logging..."
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] APP_DIR variable: $APP_DIR" >> "$DEBUG_LOG"
|
||
|
||
# Use the main installation log file
|
||
LOG_FILE="$INSTALL_LOG"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Using main log file: $LOG_FILE" >> "$DEBUG_LOG"
|
||
|
||
print_info "Deployment log: $LOG_FILE"
|
||
|
||
# Function to log with timestamp
|
||
log_output() {
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
|
||
}
|
||
|
||
# Redirect all output to both terminal and log file
|
||
exec > >(tee -a "$LOG_FILE")
|
||
exec 2>&1
|
||
|
||
log_output "=== PatchMon Deployment Started ==="
|
||
log_output "Script started at: $(date)"
|
||
log_output "Script PID: $$"
|
||
log_output "Running as user: $(whoami)"
|
||
log_output "Current directory: $(pwd)"
|
||
log_output "Script arguments: $@"
|
||
log_output "FQDN: $FQDN"
|
||
log_output "Email: $EMAIL"
|
||
log_output "Branch: $DEPLOYMENT_BRANCH"
|
||
log_output "SSL Enabled: $USE_LETSENCRYPT"
|
||
log_output "====================================="
|
||
}
|
||
|
||
# Main deployment function
|
||
deploy_instance() {
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] deploy_instance function started" >> "$DEBUG_LOG"
|
||
|
||
log_message "=== SELF-HOSTING-INSTALL.SH DEPLOYMENT STARTED ==="
|
||
log_message "Script version: $SCRIPT_VERSION"
|
||
log_message "FQDN: $FQDN"
|
||
log_message "Email: $EMAIL"
|
||
log_message "SSL Enabled: $USE_LETSENCRYPT"
|
||
|
||
print_banner
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Skipping early logging setup - will do after variables initialized" >> "$DEBUG_LOG"
|
||
|
||
# Record deployment start time
|
||
DEPLOYMENT_START_TIME=$(date +%s)
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] About to validate parameters" >> "$DEBUG_LOG"
|
||
|
||
# Parameters are already validated in interactive_setup
|
||
print_info "All parameters validated successfully"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Parameter validation passed" >> "$DEBUG_LOG"
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Checking if instance already exists at /opt/$FQDN" >> "$DEBUG_LOG"
|
||
|
||
# Check if instance already exists
|
||
if [ -d "/opt/$FQDN" ]; then
|
||
print_error "Instance for $FQDN already exists at /opt/$FQDN"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Instance already exists" >> "$DEBUG_LOG"
|
||
exit 1
|
||
fi
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Instance check passed - no existing instance found" >> "$DEBUG_LOG"
|
||
|
||
print_info "🚀 Deploying PatchMon instance for $FQDN"
|
||
print_info "📧 Email: $EMAIL"
|
||
print_info "🌿 Branch: $DEPLOYMENT_BRANCH"
|
||
print_info "🔒 SSL: $USE_LETSENCRYPT"
|
||
if [ "$USE_LETSENCRYPT" = "true" ]; then
|
||
print_info "📧 SSL Email: $EMAIL"
|
||
fi
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] About to call init_instance_vars function" >> "$DEBUG_LOG"
|
||
|
||
# Initialize variables
|
||
init_instance_vars
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] init_instance_vars function completed" >> "$DEBUG_LOG"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Variables initialized, APP_DIR: $APP_DIR" >> "$DEBUG_LOG"
|
||
|
||
# Setup logging (after variables are initialized)
|
||
setup_deployment_logging
|
||
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deployment logging setup completed" >> "$DEBUG_LOG"
|
||
|
||
# Display generated credentials
|
||
echo -e "${BLUE}🔐 Auto-generated credentials:${NC}"
|
||
echo -e "${YELLOW}Database Name: $DB_NAME${NC}"
|
||
echo -e "${YELLOW}Database User: $DB_USER${NC}"
|
||
echo -e "${YELLOW}Database Password: $DB_PASS${NC}"
|
||
echo -e "${YELLOW}JWT Secret: $JWT_SECRET${NC}"
|
||
echo -e "${YELLOW}Backend Port: $BACKEND_PORT${NC}"
|
||
echo -e "${YELLOW}Instance User: $INSTANCE_USER${NC}"
|
||
echo -e "${YELLOW}Node.js Isolation: $APP_DIR/.npm${NC}"
|
||
echo ""
|
||
|
||
# System setup (prerequisites already installed in interactive_setup)
|
||
install_nodejs
|
||
install_postgresql
|
||
install_nginx
|
||
|
||
# Only install certbot if SSL is enabled
|
||
if [ "$USE_LETSENCRYPT" = "true" ]; then
|
||
install_certbot
|
||
fi
|
||
|
||
# Instance-specific setup
|
||
create_instance_user
|
||
setup_nodejs_isolation
|
||
setup_database
|
||
clone_application
|
||
setup_node_environment
|
||
install_dependencies
|
||
create_env_files
|
||
run_migrations
|
||
# Admin account creation removed - handled by application's first-time setup
|
||
|
||
# Service and web server setup
|
||
create_systemd_service
|
||
setup_nginx
|
||
|
||
# SSL setup (if enabled)
|
||
if [ "$USE_LETSENCRYPT" = "true" ]; then
|
||
setup_letsencrypt
|
||
else
|
||
print_info "SSL disabled - skipping SSL certificate setup"
|
||
fi
|
||
|
||
# Start services
|
||
start_services
|
||
|
||
# Populate server settings in database
|
||
populate_server_settings
|
||
|
||
# Create agent version in database
|
||
create_agent_version
|
||
|
||
# Restart PatchMon service to ensure it's running properly
|
||
restart_patchmon
|
||
|
||
# Save deployment information to file
|
||
save_deployment_info
|
||
|
||
# Create deployment summary
|
||
create_deployment_summary
|
||
|
||
# Email notifications removed for self-hosting deployment
|
||
|
||
# Final status
|
||
log_message "=== DEPLOYMENT COMPLETED SUCCESSFULLY ==="
|
||
log_message "Instance URL: $SERVER_PROTOCOL_SEL://$FQDN"
|
||
log_message "Service name: $SERVICE_NAME"
|
||
log_message "Backend port: $BACKEND_PORT"
|
||
log_message "SSL enabled: $USE_LETSENCRYPT"
|
||
|
||
print_status "🎉 PatchMon instance deployed successfully!"
|
||
echo ""
|
||
print_info "Next steps:"
|
||
echo " • Visit your URL: $SERVER_PROTOCOL_SEL://$FQDN (ensure DNS is configured)"
|
||
echo " • Useful deployment information is stored in: $APP_DIR/deployment-info.txt"
|
||
echo ""
|
||
|
||
# Suppress JSON echo to terminal; details already logged and saved to summary/credentials files
|
||
:
|
||
}
|
||
|
||
# Main script execution
|
||
main() {
|
||
# Log script entry
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Interactive installation started" >> "$DEBUG_LOG"
|
||
|
||
# Run interactive setup
|
||
interactive_setup
|
||
|
||
# Set GitHub repo (always use public repo for self-hosted deployments)
|
||
GITHUB_REPO="$DEFAULT_GITHUB_REPO"
|
||
|
||
# Validate SSL setting
|
||
if [ "$SSL_ENABLED" = "y" ] || [ "$SSL_ENABLED" = "yes" ]; then
|
||
USE_LETSENCRYPT="true"
|
||
SERVER_PROTOCOL_SEL="https"
|
||
print_info "SSL enabled - will use Let's Encrypt for HTTPS"
|
||
|
||
# Validate email for SSL
|
||
if [ -z "$EMAIL" ]; then
|
||
print_error "Email is required when SSL is enabled for Let's Encrypt"
|
||
exit 1
|
||
fi
|
||
else
|
||
USE_LETSENCRYPT="false"
|
||
SERVER_PROTOCOL_SEL="http"
|
||
print_info "SSL disabled - will use HTTP only"
|
||
fi
|
||
|
||
# Log before calling deploy_instance
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] About to call deploy_instance function" >> "$DEBUG_LOG"
|
||
|
||
# Run deployment
|
||
deploy_instance
|
||
|
||
# Log after deploy_instance completes
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] deploy_instance function completed" >> "$DEBUG_LOG"
|
||
}
|
||
|
||
# Run main function (no arguments needed for interactive mode)
|
||
main
|