#!/bin/bash # ============================================================ # Nexus Autoparts POS — Automated Installer # Works on: Debian 12+, Ubuntu 22.04+, Raspberry Pi OS (64-bit) # Usage: curl -s https://raw.githubusercontent.com/.../install.sh | bash # ============================================================ set -euo pipefail # ----- Colors & helpers ----- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' INSTALL_DIR="/opt/nexus-pos" SERVICE_NAME="nexus-pos" DB_USER="nexus" DB_PASS="nexus_autoparts_2026" DB_NAME="nexus_autoparts" POS_PORT=5001 LOG_FILE="/var/log/nexus-pos-install.log" info() { echo -e "${CYAN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; } fatal() { err "$*"; exit 1; } banner() { echo "" echo -e "${BOLD}${CYAN}" echo " ========================================" echo " Nexus Autoparts POS — Installer v1.0" echo " ========================================" echo -e "${NC}" } log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; } cleanup_on_error() { err "Installation failed. Check $LOG_FILE for details." exit 1 } trap cleanup_on_error ERR # ============================================================ # 1. CHECK PREREQUISITES # ============================================================ check_prerequisites() { info "Checking prerequisites..." # Must be Linux if [[ "$(uname -s)" != "Linux" ]]; then fatal "This installer only supports Linux (Debian/Ubuntu/Raspberry Pi OS)." fi # Must be root if [[ $EUID -ne 0 ]]; then fatal "This script must be run as root. Use: sudo bash install.sh" fi # Check distro if [[ -f /etc/os-release ]]; then . /etc/os-release info "Detected OS: ${PRETTY_NAME:-$ID}" log "OS: ${PRETTY_NAME:-$ID}" else warn "Could not detect OS version. Proceeding anyway." fi # Detect Raspberry Pi IS_RPI=false if grep -qi "raspberry" /proc/cpuinfo 2>/dev/null || grep -qi "raspberry" /sys/firmware/devicetree/base/model 2>/dev/null; then IS_RPI=true info "Raspberry Pi detected." log "Raspberry Pi detected" fi # Check architecture ARCH=$(uname -m) info "Architecture: $ARCH" if [[ "$ARCH" != "x86_64" && "$ARCH" != "aarch64" && "$ARCH" != "armv7l" ]]; then warn "Untested architecture: $ARCH. Proceeding with caution." fi # Check internet if ! ping -c 1 -W 3 8.8.8.8 &>/dev/null; then fatal "No internet connection detected. Please connect and retry." fi ok "Prerequisites check passed." } # ============================================================ # 2. INSTALL SYSTEM PACKAGES # ============================================================ install_packages() { info "Updating package lists..." apt-get update -qq >> "$LOG_FILE" 2>&1 PACKAGES=( python3 python3-pip python3-venv postgresql postgresql-client git nginx libpq-dev gcc python3-dev curl ) # On Raspberry Pi, add some extras for lxml if [[ "$IS_RPI" == true ]]; then PACKAGES+=(libxml2-dev libxslt1-dev zlib1g-dev) fi info "Installing system packages: ${PACKAGES[*]}" DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}" >> "$LOG_FILE" 2>&1 ok "System packages installed." } # ============================================================ # 3. CONFIGURE POSTGRESQL # ============================================================ configure_postgresql() { info "Configuring PostgreSQL..." # Ensure PostgreSQL is running systemctl enable postgresql >> "$LOG_FILE" 2>&1 systemctl start postgresql >> "$LOG_FILE" 2>&1 # Create user if not exists if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then info "PostgreSQL user '${DB_USER}' already exists." else sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;" >> "$LOG_FILE" 2>&1 ok "PostgreSQL user '${DB_USER}' created." fi # Create master database if not exists if sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then info "Database '${DB_NAME}' already exists." else sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" >> "$LOG_FILE" 2>&1 ok "Database '${DB_NAME}' created." fi # Grant CREATEDB to user (idempotent) sudo -u postgres psql -c "ALTER USER ${DB_USER} CREATEDB;" >> "$LOG_FILE" 2>&1 # Ensure pg_hba.conf allows md5 auth for local connections PG_HBA=$(sudo -u postgres psql -tAc "SHOW hba_file" 2>/dev/null | head -1) if [[ -n "$PG_HBA" ]] && ! grep -q "nexus" "$PG_HBA" 2>/dev/null; then # Add md5 auth line for nexus user before the first local line sed -i "/^# TYPE/a local all ${DB_USER} md5" "$PG_HBA" 2>/dev/null || true systemctl reload postgresql >> "$LOG_FILE" 2>&1 fi ok "PostgreSQL configured." } # ============================================================ # 4. CLONE REPOSITORY # ============================================================ clone_repo() { info "Setting up application in ${INSTALL_DIR}..." if [[ -d "${INSTALL_DIR}" ]]; then warn "${INSTALL_DIR} already exists." echo -en "${YELLOW} Overwrite? [y/N]: ${NC}" read -r overwrite if [[ "${overwrite,,}" == "y" ]]; then rm -rf "${INSTALL_DIR}" else info "Keeping existing installation. Will update in place." fi fi if [[ ! -d "${INSTALL_DIR}" ]]; then # If running from the repo itself, copy it SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "${SCRIPT_DIR}/pos/app.py" ]]; then info "Copying from local source: ${SCRIPT_DIR}" cp -a "${SCRIPT_DIR}" "${INSTALL_DIR}" else info "Cloning from GitHub..." git clone https://github.com/consultoria-as/nexus-autoparts.git "${INSTALL_DIR}" >> "$LOG_FILE" 2>&1 fi fi ok "Application files ready at ${INSTALL_DIR}." } # ============================================================ # 5. INSTALL PYTHON DEPENDENCIES # ============================================================ install_python_deps() { info "Creating Python virtual environment..." python3 -m venv "${INSTALL_DIR}/venv" >> "$LOG_FILE" 2>&1 info "Installing Python dependencies..." "${INSTALL_DIR}/venv/bin/pip" install --upgrade pip >> "$LOG_FILE" 2>&1 "${INSTALL_DIR}/venv/bin/pip" install -r "${INSTALL_DIR}/pos/requirements.txt" >> "$LOG_FILE" 2>&1 "${INSTALL_DIR}/venv/bin/pip" install gunicorn >> "$LOG_FILE" 2>&1 ok "Python dependencies installed." } # ============================================================ # 6. INTERACTIVE SETUP # ============================================================ interactive_setup() { echo "" echo -e "${BOLD}${CYAN}--- Business Setup ---${NC}" echo "" # Business name echo -en "${BOLD} Business name${NC} (e.g., Refaccionaria Lopez): " read -r BUSINESS_NAME if [[ -z "$BUSINESS_NAME" ]]; then BUSINESS_NAME="Mi Refaccionaria" warn "Using default: ${BUSINESS_NAME}" fi # RFC echo -en "${BOLD} RFC${NC} (optional, press Enter to skip): " read -r BUSINESS_RFC if [[ -z "$BUSINESS_RFC" ]]; then BUSINESS_RFC="" info "RFC skipped." fi # Owner name echo -en "${BOLD} Owner name${NC}: " read -r OWNER_NAME if [[ -z "$OWNER_NAME" ]]; then OWNER_NAME="Administrador" warn "Using default: ${OWNER_NAME}" fi # Owner PIN while true; do echo -en "${BOLD} Owner PIN${NC} (4 digits): " read -rs OWNER_PIN echo "" if [[ "$OWNER_PIN" =~ ^[0-9]{4}$ ]]; then break else warn "PIN must be exactly 4 digits. Try again." fi done # Domain/IP DEFAULT_IP=$(hostname -I 2>/dev/null | awk '{print $1}') echo -en "${BOLD} Domain or IP${NC} for access [${DEFAULT_IP:-localhost}]: " read -r ACCESS_HOST if [[ -z "$ACCESS_HOST" ]]; then ACCESS_HOST="${DEFAULT_IP:-localhost}" fi echo "" echo -e "${BOLD} Summary:${NC}" echo " Business: ${BUSINESS_NAME}" echo " RFC: ${BUSINESS_RFC:-N/A}" echo " Owner: ${OWNER_NAME}" echo " PIN: ****" echo " Access: http://${ACCESS_HOST}" echo "" echo -en "${BOLD} Proceed? [Y/n]: ${NC}" read -r confirm if [[ "${confirm,,}" == "n" ]]; then fatal "Installation cancelled by user." fi } # ============================================================ # 7. PROVISION TENANT # ============================================================ provision_tenant() { info "Provisioning tenant database..." cd "${INSTALL_DIR}/pos" # Build a small Python script to avoid quoting issues in bash cat > /tmp/_nexus_provision.py << PYEOF import sys, os sys.path.insert(0, '${INSTALL_DIR}/pos') os.chdir('${INSTALL_DIR}/pos') from services.tenant_manager import provision_tenant rfc_val = os.environ.get('NX_RFC') or None result = provision_tenant( name=os.environ['NX_BUSINESS'], rfc=rfc_val, owner_name=os.environ['NX_OWNER'], owner_pin=os.environ['NX_PIN'] ) print(f"Tenant created: id={result['tenant_id']}, db={result['db_name']}") PYEOF NX_BUSINESS="$BUSINESS_NAME" \ NX_RFC="$BUSINESS_RFC" \ NX_OWNER="$OWNER_NAME" \ NX_PIN="$OWNER_PIN" \ "${INSTALL_DIR}/venv/bin/python3" /tmp/_nexus_provision.py >> "$LOG_FILE" 2>&1 rm -f /tmp/_nexus_provision.py ok "Tenant '${BUSINESS_NAME}' provisioned." } # ============================================================ # 8. APPLY MIGRATIONS (v1.1) # ============================================================ apply_migrations() { info "Applying database migrations..." cd "${INSTALL_DIR}/pos" "${INSTALL_DIR}/venv/bin/python3" -c " import sys sys.path.insert(0, '${INSTALL_DIR}/pos') from migrations.runner import run_migrations run_migrations() " >> "$LOG_FILE" 2>&1 ok "Migrations applied." } # ============================================================ # 9. CREATE SYSTEMD SERVICE # ============================================================ create_systemd_service() { info "Creating systemd service..." cat > /etc/systemd/system/${SERVICE_NAME}.service << SERVICEEOF [Unit] Description=Nexus Autoparts POS After=network.target postgresql.service Requires=postgresql.service [Service] Type=simple User=www-data Group=www-data WorkingDirectory=${INSTALL_DIR}/pos Environment=PATH=${INSTALL_DIR}/venv/bin:/usr/bin:/bin Environment=MASTER_DB_URL=postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME} Environment=TENANT_DB_URL_TEMPLATE=postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name} ExecStart=${INSTALL_DIR}/venv/bin/gunicorn --bind 127.0.0.1:${POS_PORT} --workers 3 --timeout 120 "app:create_app()" Restart=always RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target SERVICEEOF # Set ownership chown -R www-data:www-data "${INSTALL_DIR}" systemctl daemon-reload >> "$LOG_FILE" 2>&1 ok "Systemd service created: ${SERVICE_NAME}.service" } # ============================================================ # 10. CONFIGURE NGINX # ============================================================ configure_nginx() { info "Configuring nginx reverse proxy..." # Remove default site rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true cat > /etc/nginx/sites-available/nexus-pos << NGINXEOF server { listen 80; server_name ${ACCESS_HOST}; client_max_body_size 20M; # POS application location /pos/ { proxy_pass http://127.0.0.1:${POS_PORT}/pos/; 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_read_timeout 300s; } # API endpoints location /api/ { proxy_pass http://127.0.0.1:${POS_PORT}/api/; 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; } # Redirect root to POS login location = / { return 302 /pos/login; } # Health check location /health { proxy_pass http://127.0.0.1:${POS_PORT}/pos/health; } } NGINXEOF ln -sf /etc/nginx/sites-available/nexus-pos /etc/nginx/sites-enabled/nexus-pos # Test nginx config if nginx -t >> "$LOG_FILE" 2>&1; then ok "Nginx configuration valid." else err "Nginx configuration test failed. Check $LOG_FILE." return 1 fi } # ============================================================ # 11. START SERVICES # ============================================================ start_services() { info "Starting services..." systemctl enable nginx >> "$LOG_FILE" 2>&1 systemctl restart nginx >> "$LOG_FILE" 2>&1 ok "Nginx started." systemctl enable "${SERVICE_NAME}" >> "$LOG_FILE" 2>&1 systemctl start "${SERVICE_NAME}" >> "$LOG_FILE" 2>&1 ok "Nexus POS service started." # Wait a moment and verify sleep 2 if systemctl is-active --quiet "${SERVICE_NAME}"; then ok "Service is running." else warn "Service may not have started correctly. Check: journalctl -u ${SERVICE_NAME}" fi } # ============================================================ # 12. PRINT SUCCESS # ============================================================ print_success() { echo "" echo -e "${BOLD}${GREEN}" echo " ========================================" echo " Installation Complete!" echo " ========================================" echo -e "${NC}" echo "" echo -e " ${BOLD}Access URL:${NC} http://${ACCESS_HOST}/pos/login" echo -e " ${BOLD}Business:${NC} ${BUSINESS_NAME}" echo -e " ${BOLD}Owner:${NC} ${OWNER_NAME}" echo -e " ${BOLD}PIN:${NC} **** (the 4-digit PIN you entered)" echo "" echo -e " ${BOLD}Service:${NC} systemctl status ${SERVICE_NAME}" echo -e " ${BOLD}Logs:${NC} journalctl -u ${SERVICE_NAME} -f" echo -e " ${BOLD}Install log:${NC} ${LOG_FILE}" echo "" echo -e " ${BOLD}Database:${NC}" echo " Host: localhost" echo " User: ${DB_USER}" echo " Master DB: ${DB_NAME}" echo "" echo -e " ${BOLD}Files:${NC} ${INSTALL_DIR}/" echo "" echo -e " ${YELLOW}To uninstall:${NC} sudo bash ${INSTALL_DIR}/uninstall.sh" echo "" } # ============================================================ # MAIN # ============================================================ main() { banner # Init log mkdir -p "$(dirname "$LOG_FILE")" echo "=== Nexus POS Install started at $(date) ===" > "$LOG_FILE" check_prerequisites install_packages configure_postgresql clone_repo install_python_deps interactive_setup provision_tenant apply_migrations create_systemd_service configure_nginx start_services print_success log "Installation completed successfully." } main "$@"