From be549d4b340b3e5fc602955b0bca8c21b457526f Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Wed, 24 Sep 2025 09:07:34 +0100 Subject: [PATCH] Added README.md file -- finally ! Added self-hosting easy installer script -- finally ! --- README.md | 366 +++++++++++++ setup.sh | 1583 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1949 insertions(+) create mode 100644 README.md create mode 100644 setup.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..cac98d2 --- /dev/null +++ b/README.md @@ -0,0 +1,366 @@ +## Purpose + +PatchMon provides centralized patch management across diverse server environments. Agents communicate outbound-only to the PatchMon server, eliminating inbound ports on monitored hosts while delivering comprehensive visibility and safe automation. + +## Features + +### Users & Authentication +- Multi-user accounts (admin and standard users) +- Email/username-based login +- Optional Two‑Factor Authentication (TFA/MFA) with verification flow +- First‑time admin bootstrap flow (no default credentials; secure setup) +- Self‑registration toggle in settings (enable/disable public signup) + +### Roles, Permissions & RBAC +- Built‑in roles: `admin`, `user` +- Fine‑grained permission flags (e.g., view/manage hosts, packages, users, reports, settings) +- Server‑side enforcement for protected routes and UI guards per permission + +### Dashboard +- Customisable dashboard with per‑user card layout and ordering +- Role/permission‑aware defaults on first login +- “Reset to Defaults” uses consistent server‑provided defaults +- Cards include: Total Hosts, Needs Updating, Up‑to‑Date Hosts, Host Groups, Outdated Packages, Security Updates, Package Priority, Repositories, Users, OS Distribution (pie/bar), Update Status, Recent Collection, Recent Users, Quick Stats + +### Hosts & Inventory +- Host inventory with key attributes and OS details +- Host grouping (create and manage host groups) +- OS distribution summaries and visualisations +- Recent telemetry collection indicator + +### Packages & Updates +- Package inventory across hosts +- Outdated packages overview and counts +- Security updates highlight +- Update status breakdown (up‑to‑date vs needs updates) + +### Repositories +- Repositories per host tracking +- Repository module pages and totals + +### Agent & Data Collection +- Outbound‑only agent communication (no inbound ports required on hosts) +- Agent version management and script content stored in DB +- Version marking (current/default) with update history + +### Settings & Configuration +- Server URL/protocol/host/port +- Update interval and auto‑update toggle +- Public signup toggle and default user role selection +- Repository settings: GitHub repo URL, repository type, SSH key path +- Rate‑limit windows and thresholds for API/auth/agent + +### Admin & User Management +- Admin user CRUD (create, list, update, delete) +- Password reset (admin‑initiated) +- Role assignment on user create/update + +### Reporting & Analytics +- Dashboard stats and card‑level metrics +- OS distribution charts (pie/bar) +- Update status and recent activity summaries + +### API & Integrations +- REST API under `/api/v1` with JWT auth +- Consistent JSON responses; errors with appropriate status codes +- CORS configured per server settings + +### Security +- JWT‑secured API with short, scoped tokens +- Permissions enforced server‑side on every route +- Rate limiting for general, auth, and agent endpoints +- Outbound‑only agent model reduces attack surface + +### Deployment & Operations +- One‑line self‑host installer (Ubuntu/Debian) +- Automated provisioning: Node.js, PostgreSQL, nginx +- Prisma migrations and client generation +- systemd service for backend lifecycle +- nginx vhost for frontend + API proxy; optional Let’s Encrypt integration +- Consolidated deployment info file with commands and paths + +### UX & Frontend +- Vite + React single‑page app +- Protected routes with permission checks +- Theming and modern components (icons, modals, notifications) + +### Observability & Logging +- Structured server logs +- Deployment logs copied to instance dir for later review + +### Road‑Readiness +- Works for internal (HTTP) and public (HTTPS) deployments +- Defaults safe for first‑time setup; admin created interactively + +## Communication Model + +- Outbound-only agents: servers initiate communication to PatchMon +- No inbound connections required on monitored servers +- Secure server-side API with JWT authentication and rate limiting + +## Architecture + +- Backend: Node.js/Express + Prisma + PostgreSQL +- Frontend: Vite + React +- Reverse proxy: nginx +- Database: PostgreSQL +- System service: systemd-managed backend + +``` ++----------------------+ HTTPS +--------------------+ HTTP +------------------------+ TCP +---------------+ +| End Users (Browser) | ---------> | nginx | --------> | Backend (Node/Express) | ------> | PostgreSQL | +| Admin UI / Frontend | | serve FE, proxy API| | /api, auth, Prisma | | Database | ++----------------------+ +--------------------+ +------------------------+ +---------------+ + +Agents (Outbound Only) ++---------------------------+ HTTPS +------------------------+ +| Agents on your servers | ----------> | Backend API (/api/v1) | ++---------------------------+ +------------------------+ + +Operational +- systemd manages backend service +- certbot/nginx for TLS (public) +- setup.sh bootstraps OS, app, DB, config +``` + +## Getting Started + +### PatchMon Cloud (coming soon) + +Managed, zero-maintenance PatchMon hosting. Stay tuned. + +### Self-hosted Installation + +Run on a clean Ubuntu/Debian server with internet access: + +```bash +curl -fsSL -o setup.sh https://raw.githubusercontent.com/9technologygroup/patchmon.net/main/setup.sh && chmod +x && bash setup.sh +``` + +During setup you’ll be asked: +- Domain/IP: public DNS or local IP (default: `patchmon.internal`) +- SSL/HTTPS: `y` for public deployments with a public IP, `n` for internal networks +- Email: only if SSL is enabled (for Let’s Encrypt) +- Git Branch: default is `main` (press Enter) + +The script will: +- Install prerequisites (Node.js, PostgreSQL, nginx) +- Clone the repo, install dependencies, build the frontend, run migrations +- Create a systemd service and nginx site vhost config +- Start the service and write a consolidated info file at: + - `/opt//deployment-info.txt` + - Copies the full installer log to `/opt//patchmon-install.log` from /var/log/patchmon-install.log + +After installation: +- Visit `http(s)://` and complete first-time admin setup +- See all useful info in `deployment-info.txt` + +## Support + +- Discord: https://discord.gg/S7RXUHwg +- Email: support@patchmon.net + +## Roadmap + +- PatchMon Cloud (managed offering) +- Additional dashboards and reporting widgets +- More OS distributions and agent enhancements +- Advanced workflow automations and approvals + +Roadmap board: https://github.com/users/9technologygroup/projects/1 + +## Security + +- Outbound-only agent communications; no inbound ports on monitored hosts +- JWT-based API auth, rate limiting, role/permission checks +- Follow least-privilege defaults; sensitive operations audited + +## Support Methods + +- Community: Discord for quick questions and feedback +- Email: SLA-backed assistance for incidents and issues +- GitHub Issues: bug reports and feature requests + +## License + +AGPLv3 (More information on this soon) + +## Links + +- Repository: https://github.com/9technologygroup/patchmon.net/ +- Raw installer: https://raw.githubusercontent.com/9technologygroup/patchmon.net/main/setup.sh +--- + + +# PatchMon + +[![Discord](https://img.shields.io/badge/Discord-Join%20Server-blue?style=for-the-badge&logo=discord)](https://discord.gg/S7RXUHwg) +[![GitHub](https://img.shields.io/badge/GitHub-Repository-black?style=for-the-badge&logo=github)](https://github.com/9technologygroup/patchmon.net) +[![Roadmap](https://img.shields.io/badge/Roadmap-View%20Progress-green?style=for-the-badge&logo=github)](https://github.com/users/9technologygroup/projects/1) + + + +--- + +## 🤝 Contributing + +We welcome contributions from the community! Here's how you can get involved: + +### Development Setup +1. **Fork the Repository** + ```bash + # Click the "Fork" button on GitHub, then clone your fork + git clone https://github.com/YOUR_USERNAME/patchmon.net.git + cd patchmon.net + ``` + +2. **Create a Feature Branch** + ```bash + git checkout -b feature/your-feature-name + # or + git checkout -b fix/your-bug-fix + ``` + + +4. **Make Your Changes** + - Write clean, well-documented code + - Follow existing code style and patterns + - Add tests for new functionality + - Update documentation as needed + +5. **Test Your Changes** + ```bash + # Run backend tests + cd backend + npm test + + # Run frontend tests + cd ../frontend + npm test + ``` + +6. **Commit and Push** + ```bash + git add . + git commit -m "Add: descriptive commit message" + git push origin feature/your-feature-name + ``` + +7. **Create a Pull Request** + - Go to your fork on GitHub + - Click "New Pull Request" + - Provide a clear description of your changes + - Link any related issues + +### Contribution Guidelines +- **Code Style**: Follow the existing code patterns and ESLint configuration +- **Commits**: Use conventional commit messages (feat:, fix:, docs:, etc.) +- **Testing**: Ensure all tests pass and add tests for new features +- **Documentation**: Update README and code comments as needed +- **Issues**: Check existing issues before creating new ones + +### Areas We Need Help With +- 🐳 **Docker & Containerization** (led by @Adam20054) +- 🔄 **CI/CD Pipelines** (led by @tigattack) +- 🔒 **Security Improvements** - Security audits, vulnerability assessments, and security feature enhancements +- ⚡ **Performance for Large Scale Deployments** - Database optimization, caching strategies, and horizontal scaling +- 📚 **Documentation** improvements +- 🧪 **Testing** coverage +- 🌐 **Internationalization** (i18n) +- 📱 **Mobile** responsive improvements +- + +--- + +## 🗺️ Roadmap + +Check out our [public roadmap](https://github.com/users/9technologygroup/projects/1) to see what we're working on and what's coming next! + +**Upcoming Features:** +- 🐳 Docker Compose deployment +- 🔄 Automated CI/CD pipelines +- 📊 Advanced reporting and analytics +- 🔔 Enhanced notification system +- 📱 Mobile application +- 🔄 Patch management workflows and policies +- 👥 Users inventory management +- 🔍 Services and ports monitoring +- 🖥️ Proxmox integration for auto LXC discovery and registration +- 📧 Notifications via Slack/Email + +--- + +## 🏢 Enterprise & Custom Solutions + +### PatchMon Cloud +- **Fully Managed**: We handle all infrastructure and maintenance +- **Scalable**: Grows with your organization +- **Secure**: Enterprise-grade security and compliance +- **Support**: Dedicated support team + +### Custom Integrations +- **API Development**: Custom endpoints for your specific needs +- **Third-Party Integrations**: Connect with your existing tools +- **Custom Dashboards**: Tailored reporting and visualization +- **White-Label Solutions**: Brand PatchMon as your own + +### Enterprise Deployment +- **On-Premises**: Deploy in your own data center +- **Air-Gapped**: Support for isolated environments +- **Compliance**: Meet industry-specific requirements +- **Training**: Comprehensive team training and onboarding + +*Contact us at support@patchmon.net for enterprise inquiries* + +--- + + + +--- + +## 📞 Support & Community + +### Get Help +- 💬 **Discord Community**: [Join our Discord](https://discord.gg/S7RXUHwg) for real-time support and discussions +- 📧 **Email Support**: support@patchmon.net +- 📚 **Documentation**: Check our wiki and documentation +- 🐛 **Bug Reports**: Use GitHub Issues + +### Community +- 🌟 **Star the Project**: Show your support by starring this repository +- 🍴 **Fork & Contribute**: Help improve PatchMon +- 📢 **Share**: Tell others about PatchMon +- 💡 **Feature Requests**: Suggest new features via GitHub Issues + +--- + +## 🙏 Acknowledgments + +### Special Thanks +- **Jonathan Higson** - For inspiration, ideas, and valuable feedback +- **@Adam20054** - For working on Docker Compose deployment +- **@tigattack** - For working on GitHub CI/CD pipelines +- **Cloud X** and **Crazy Dead** - For moderating our Discord server and keeping the community awesome + +### Contributors +Thank you to all our contributors who help make PatchMon better every day! + + +## 🔗 Links + +- **Website**: [patchmon.net](https://patchmon.net) +- **Discord**: [discord.gg/S7RXUHwg](https://discord.gg/S7RXUHwg) +- **Roadmap**: [GitHub Projects](https://github.com/users/9technologygroup/projects/1) +- **Documentation**: [Coming Soon] +- **Support**: support@patchmon.net + +--- + +
+ +**Made with ❤️ by the PatchMon Team** + +[![Discord](https://img.shields.io/badge/Discord-Join%20Server-blue?style=for-the-badge&logo=discord)](https://discord.gg/S7RXUHwg) +[![GitHub](https://img.shields.io/badge/GitHub-Repository-black?style=for-the-badge&logo=github)](https://github.com/9technologygroup/patchmon.net) + +
\ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..c354808 --- /dev/null +++ b/setup.sh @@ -0,0 +1,1583 @@ +#!/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_prerequisites() { + print_info "Running and checking prerequisites..." + print_info "Installing updates..." + apt-get update -y + apt-get upgrade -y + + print_info "Installing prerequisite applications..." + apt-get install -y 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/_*$//') + 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 "$FQDN" | sed 's/[^a-zA-Z0-9]/_/g' | sed 's/^_*//' | sed 's/_*$//' | 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..." + apt-get update -y + apt-get upgrade -y +} + +# Install essential tools +install_essential_tools() { + print_info "Installing essential tools..." + apt-get install -y 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 - + apt-get install -y 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 + apt-get install -y 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 + apt-get install -y 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 + apt-get install -y 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" + + # 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;" + + 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 + sudo -u "$INSTANCE_USER" bash -c "cd $APP_DIR && npm cache clean --force" 2>/dev/null || true + + # Install root dependencies as the dedicated user + print_info "Installing root dependencies..." + if ! sudo -u "$INSTANCE_USER" bash -c " + 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 --production --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 ! sudo -u "$INSTANCE_USER" bash -c " + 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 --production --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 ! sudo -u "$INSTANCE_USER" bash -c " + 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 ! sudo -u "$INSTANCE_USER" bash -c " + 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) + sudo -u "$INSTANCE_USER" npx prisma migrate deploy >/dev/null 2>&1 || true + sudo -u "$INSTANCE_USER" 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 + sudo -u "$INSTANCE_USER" 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 + sudo -u "$INSTANCE_USER" 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