#!/bin/bash # ============================================================ # Nexus Autoparts POS — Automated Installer v2.0 # Works on: Debian 12+, Ubuntu 22.04+, Raspberry Pi OS (64-bit) # ============================================================ 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" 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 v2.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 # Generate a secure random password generate_secret() { openssl rand -hex 32 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(32))" } # ============================================================ # 1. CHECK PREREQUISITES # ============================================================ check_prerequisites() { info "Checking prerequisites..." if [[ "$(uname -s)" != "Linux" ]]; then fatal "This installer only supports Linux (Debian/Ubuntu/Raspberry Pi OS)." fi if [[ $EUID -ne 0 ]]; then fatal "This script must be run as root. Use: sudo bash install.sh" fi if [[ -f /etc/os-release ]]; then . /etc/os-release info "Detected OS: ${PRETTY_NAME:-$ID}" log "OS: ${PRETTY_NAME:-$ID}" fi 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." fi ARCH=$(uname -m) info "Architecture: $ARCH" if ! ping -c 1 -W 3 8.8.8.8 &>/dev/null; then warn "No internet connection detected. Some features may not work." 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 redis-server git nginx curl libpq-dev gcc python3-dev ) if [[ "$IS_RPI" == true ]]; then PACKAGES+=(libxml2-dev libxslt1-dev zlib1g-dev) fi info "Installing system packages..." DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}" >> "$LOG_FILE" 2>&1 # Install Node.js for WhatsApp bridge (LTS) if ! command -v node &>/dev/null; then info "Installing Node.js LTS..." curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >> "$LOG_FILE" 2>&1 apt-get install -y -qq nodejs >> "$LOG_FILE" 2>&1 fi ok "System packages installed." } # ============================================================ # 3. CONFIGURE POSTGRESQL # ============================================================ configure_postgresql() { info "Configuring PostgreSQL..." systemctl enable postgresql >> "$LOG_FILE" 2>&1 systemctl start postgresql >> "$LOG_FILE" 2>&1 info "Configuring Redis..." systemctl enable redis-server >> "$LOG_FILE" 2>&1 systemctl start redis-server >> "$LOG_FILE" 2>&1 # Generate random DB password DB_PASS=$(generate_secret) DB_USER="nexus" DB_NAME="nexus_autoparts" # Save credentials for later echo "DB_USER=$DB_USER" > /tmp/nexus_install_vars echo "DB_PASS=$DB_PASS" >> /tmp/nexus_install_vars echo "DB_NAME=$DB_NAME" >> /tmp/nexus_install_vars # Create user if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then info "PostgreSQL user '${DB_USER}' exists. Updating password..." sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;" >> "$LOG_FILE" 2>&1 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 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 # Ensure md5 auth for local connections PG_HBA=$(sudo -u postgres psql -tAc "SHOW hba_file" 2>/dev/null | head -1 | xargs) if [[ -n "$PG_HBA" ]] && ! grep -q "local.*all.*${DB_USER}.*scram-sha-256" "$PG_HBA" 2>/dev/null; then # Add scram-sha-256 line before the first peer/trust line sed -i "/^local.*all.*all.*peer/i local all ${DB_USER} scram-sha-256" "$PG_HBA" 2>/dev/null || true systemctl reload postgresql >> "$LOG_FILE" 2>&1 fi ok "PostgreSQL configured." } # ============================================================ # 4. SETUP APPLICATION # ============================================================ setup_app() { info "Setting up application in ${INSTALL_DIR}..." if [[ -d "${INSTALL_DIR}" ]]; then warn "${INSTALL_DIR} already exists. Updating in place..." else mkdir -p "${INSTALL_DIR}" fi # Copy from local source (the repo where this script lives) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "${SCRIPT_DIR}/pos/app.py" ]]; then info "Copying from local source: ${SCRIPT_DIR}" rsync -a --delete --exclude='venv' --exclude='__pycache__' --exclude='.git' \ "${SCRIPT_DIR}/" "${INSTALL_DIR}/" >> "$LOG_FILE" 2>&1 else fatal "Could not find local source. Run this script from the project root." fi # Start Meilisearch and Metabase (optional but recommended) if command -v docker &>/dev/null; then info "Starting Meilisearch..." cd "${INSTALL_DIR}/docker" && docker compose -f docker-compose.meilisearch.yml up -d >> "$LOG_FILE" 2>&1 ok "Meilisearch container started." info "Starting Metabase..." cd "${INSTALL_DIR}/docker" && docker compose -f docker-compose.metabase.yml up -d >> "$LOG_FILE" 2>&1 ok "Metabase container started." else warn "Docker not found. Meilisearch and Metabase will not be available." fi # Create virtual environment python3 -m venv "${INSTALL_DIR}/venv" >> "$LOG_FILE" 2>&1 "${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 "Application files and Python dependencies ready." } # ============================================================ # 5. INTERACTIVE SETUP # ============================================================ interactive_setup() { echo "" echo -e "${BOLD}${CYAN}--- Business Setup ---${NC}" echo "" echo -en "${BOLD} Business name${NC} (e.g., Refaccionaria Lopez): " read -r BUSINESS_NAME [[ -z "$BUSINESS_NAME" ]] && BUSINESS_NAME="Mi Refaccionaria" echo -en "${BOLD} RFC${NC} (optional): " read -r BUSINESS_RFC echo -en "${BOLD} Owner name${NC}: " read -r OWNER_NAME [[ -z "$OWNER_NAME" ]] && OWNER_NAME="Administrador" while true; do echo -en "${BOLD} Owner PIN${NC} (4 digits): " read -rs OWNER_PIN echo "" [[ "$OWNER_PIN" =~ ^[0-9]{4}$ ]] && break warn "PIN must be exactly 4 digits." done DEFAULT_IP=$(hostname -I 2>/dev/null | awk '{print $1}') echo -en "${BOLD} Domain or IP${NC} [${DEFAULT_IP:-localhost}]: " read -r ACCESS_HOST [[ -z "$ACCESS_HOST" ]] && ACCESS_HOST="${DEFAULT_IP:-localhost}" 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 [[ "${confirm,,}" == "n" ]] && fatal "Installation cancelled." } # ============================================================ # 6. LOAD MASTER SCHEMA & SEED DATA # ============================================================ load_master_schema() { info "Loading master database schema (vehicles + catalog)..." source /tmp/nexus_install_vars # Load PostgreSQL schema (no TecDoc) sudo -u postgres psql "${DB_NAME}" -f "${INSTALL_DIR}/sql/schema_master_postgres.sql" >> "$LOG_FILE" 2>&1 # Load initial catalog seed (categories, brands, models, years) sudo -u postgres psql "${DB_NAME}" -f "${INSTALL_DIR}/pos/seed/initial_catalog.sql" >> "$LOG_FILE" 2>&1 ok "Master schema and seed data loaded." } # ============================================================ # 7. PROVISION TENANT # ============================================================ provision_tenant() { info "Provisioning tenant database..." source /tmp/nexus_install_vars cd "${INSTALL_DIR}/pos" export MASTER_DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}" export TENANT_DB_URL_TEMPLATE="postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}" export POS_JWT_SECRET="$(generate_secret)" export DATABASE_URL="${MASTER_DB_URL}" export JWT_SECRET="${POS_JWT_SECRET}" # Build provision script cat > /tmp/_nexus_provision.py << 'PYEOF' import sys, os sys.path.insert(0, os.environ['INSTALL_DIR'] + '/pos') os.chdir(os.environ['INSTALL_DIR'] + '/pos') from services.tenant_manager import provision_tenant result = provision_tenant( name=os.environ['NX_BUSINESS'], rfc=os.environ.get('NX_RFC') or None, owner_name=os.environ['NX_OWNER'], owner_pin=os.environ['NX_PIN'] ) print(f"Tenant created: id={result['tenant_id']}, db={result['db_name']}, subdomain={result['subdomain']}") PYEOF INSTALL_DIR="${INSTALL_DIR}" \ 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 TO ALL TENANTS # ============================================================ apply_migrations() { info "Applying database migrations..." source /tmp/nexus_install_vars export MASTER_DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}" export TENANT_DB_URL_TEMPLATE="postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}" cd "${INSTALL_DIR}/pos" # Run master migrations "${INSTALL_DIR}/venv/bin/python3" -c " import sys sys.path.insert(0, '${INSTALL_DIR}/pos') from migrations.runner_master import run_master_migrations run_master_migrations() " >> "$LOG_FILE" 2>&1 # Run tenant migrations "${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 "All migrations applied." } # ============================================================ # 9. GENERATE .env FILE # ============================================================ generate_env() { info "Generating environment configuration..." source /tmp/nexus_install_vars POS_SECRET=$(generate_secret) WEB_SECRET=$(generate_secret) WPP_SECRET=$(generate_secret) cat > "${INSTALL_DIR}/.env" << ENVEOF # Nexus Autoparts — Generated on $(date) # DO NOT COMMIT THIS FILE TO GIT DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME} MASTER_DB_URL=postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME} TENANT_DB_URL_TEMPLATE=postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name} JWT_SECRET=${WEB_SECRET} POS_JWT_SECRET=${POS_SECRET} WHATSAPP_BRIDGE_KEY=${WPP_SECRET} REDIS_URL=redis://localhost:6379/0 REDIS_ENABLED=true REDIS_STOCK_TTL=300 MEILI_URL=http://localhost:7700 MEILI_API_KEY=$(generate_secret) METABASE_URL=http://localhost:3000 METABASE_ADMIN_EMAIL=admin@${SUBDOMAIN}.local METABASE_ADMIN_PASS=$(generate_secret) METABASE_DB_PASS=$(generate_secret) DEFAULT_CURRENCY=MXN EXCHANGE_RATE_USD_MXN=17.5 ENVEOF chmod 600 "${INSTALL_DIR}/.env" ok ".env file created with secure secrets." } # ============================================================ # 10. CREATE SYSTEMD SERVICES # ============================================================ create_systemd_services() { info "Creating systemd services..." source /tmp/nexus_install_vars # Create nexus user if ! id -u nexus &>/dev/null; then useradd -r -s /bin/false -d "${INSTALL_DIR}" nexus >> "$LOG_FILE" 2>&1 fi chown -R nexus:nexus "${INSTALL_DIR}" # POS Service cat > /etc/systemd/system/nexus-pos.service << SERVICEEOF [Unit] Description=Nexus Autoparts POS After=network.target postgresql.service redis-server.service Wants=postgresql.service redis-server.service [Service] Type=notify User=nexus Group=nexus WorkingDirectory=${INSTALL_DIR}/pos Environment=PATH=${INSTALL_DIR}/venv/bin:/usr/bin:/bin Environment=PYTHONUNBUFFERED=1 EnvironmentFile=${INSTALL_DIR}/.env ExecStartPre=/bin/mkdir -p /var/log/nexus-pos ExecStart=${INSTALL_DIR}/venv/bin/gunicorn -c ${INSTALL_DIR}/pos/gunicorn.conf.py "app:create_app()" Restart=always RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target SERVICEEOF # Web Dashboard Service cat > /etc/systemd/system/nexus-web.service << SERVICEEOF [Unit] Description=Nexus Autoparts Web Publica After=network.target postgresql.service redis-server.service Wants=postgresql.service redis-server.service [Service] Type=simple User=nexus Group=nexus WorkingDirectory=${INSTALL_DIR}/dashboard Environment=PATH=${INSTALL_DIR}/venv/bin:/usr/bin:/bin Environment=PYTHONUNBUFFERED=1 EnvironmentFile=${INSTALL_DIR}/.env ExecStart=${INSTALL_DIR}/venv/bin/python3 server.py Restart=always RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target SERVICEEOF # WhatsApp Bridge Service cat > /etc/systemd/system/nexus-whatsapp.service << SERVICEEOF [Unit] Description=Nexus WhatsApp Bridge (Baileys) After=network.target [Service] Type=simple User=nexus Group=nexus WorkingDirectory=${INSTALL_DIR} Environment=NODE_ENV=production ExecStart=/usr/bin/node ${INSTALL_DIR}/pos/whatsapp-bridge-server.js Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target SERVICEEOF systemctl daemon-reload >> "$LOG_FILE" 2>&1 ok "Systemd services created: nexus-pos, nexus-web, nexus-whatsapp" } # ============================================================ # 11. CONFIGURE NGINX # ============================================================ configure_nginx() { info "Configuring nginx..." rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true cat > /etc/nginx/sites-available/nexus << 'NGINXEOF' # Rate limiting zone limit_req_zone $binary_remote_addr zone=pos_login:10m rate=10r/s; upstream nexus_main { server 127.0.0.1:5000; } upstream nexus_pos { server 127.0.0.1:5001; } server { listen 80; server_name _; # Catch-all client_max_body_size 20M; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; # Web publica / landing location / { proxy_pass http://nexus_main; 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; } # POS paths location /pos/ { proxy_pass http://nexus_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; } # Rate limit login endpoint location /pos/api/auth/login { limit_req zone=pos_login burst=5 nodelay; proxy_pass http://nexus_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; } # Health check location /health { proxy_pass http://nexus_pos/pos/health; } } NGINXEOF ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/nexus if nginx -t >> "$LOG_FILE" 2>&1; then ok "Nginx configuration valid." else err "Nginx configuration test failed. Check $LOG_FILE." return 1 fi } # ============================================================ # 12. START SERVICES # ============================================================ start_services() { info "Starting services..." systemctl enable postgresql >> "$LOG_FILE" 2>&1 systemctl enable nginx >> "$LOG_FILE" 2>&1 systemctl restart nginx >> "$LOG_FILE" 2>&1 ok "Nginx started." systemctl enable nexus-pos >> "$LOG_FILE" 2>&1 systemctl start nexus-pos >> "$LOG_FILE" 2>&1 ok "Nexus POS service started." systemctl enable nexus-web >> "$LOG_FILE" 2>&1 systemctl start nexus-web >> "$LOG_FILE" 2>&1 ok "Nexus Web service started." systemctl enable nexus-whatsapp >> "$LOG_FILE" 2>&1 systemctl start nexus-whatsapp >> "$LOG_FILE" 2>&1 ok "Nexus WhatsApp bridge started." sleep 3 # Verify POS is running if systemctl is-active --quiet nexus-pos; then ok "All services are running." else warn "nexus-pos may not have started correctly. Check: journalctl -u nexus-pos" fi } # ============================================================ # 13. HEALTH CHECK # ============================================================ run_health_check() { info "Running post-installation health check..." source /tmp/nexus_install_vars export MASTER_DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}" export TENANT_DB_URL_TEMPLATE="postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}" export DATABASE_URL="${MASTER_DB_URL}" cd "${INSTALL_DIR}" "${INSTALL_DIR}/venv/bin/pip" install requests >> "$LOG_FILE" 2>&1 || true if "${INSTALL_DIR}/venv/bin/python3" "${INSTALL_DIR}/scripts/health_check.py"; then ok "Health check passed." else warn "Health check found issues. Review output above." fi } # ============================================================ # 14. PRINT SUCCESS # ============================================================ print_success() { source /tmp/nexus_install_vars echo "" echo -e "${BOLD}${GREEN}" echo " ========================================" echo " Installation Complete!" echo " ========================================" echo -e "${NC}" echo "" echo -e " ${BOLD}POS Login:${NC} http://${ACCESS_HOST}/pos/login" echo -e " ${BOLD}Web Catalog:${NC} http://${ACCESS_HOST}/" echo -e " ${BOLD}Business:${NC} ${BUSINESS_NAME}" echo -e " ${BOLD}Owner:${NC} ${OWNER_NAME}" echo -e " ${BOLD}PIN:${NC} ${OWNER_PIN}" echo "" echo -e " ${BOLD}Services:${NC}" echo " POS: systemctl status nexus-pos" echo " Web: systemctl status nexus-web" echo " WhatsApp: systemctl status nexus-whatsapp" echo " Logs: journalctl -u nexus-pos -f" echo "" echo -e " ${BOLD}Database:${NC}" echo " Host: localhost" echo " User: ${DB_USER}" echo " Pass: ${DB_PASS}" echo " Master DB: ${DB_NAME}" echo "" echo -e " ${BOLD}Files:${NC} ${INSTALL_DIR}/" echo -e " ${BOLD}.env:${NC} ${INSTALL_DIR}/.env" echo "" echo -e " ${YELLOW}Save the database password securely!${NC}" echo "" } # ============================================================ # MAIN # ============================================================ main() { banner mkdir -p "$(dirname "$LOG_FILE")" echo "=== Nexus POS Install v2.0 started at $(date) ===" > "$LOG_FILE" check_prerequisites install_packages configure_postgresql setup_app interactive_setup load_master_schema provision_tenant apply_migrations generate_env create_systemd_services configure_nginx start_services run_health_check print_success # Cleanup temp vars rm -f /tmp/nexus_install_vars log "Installation completed successfully." } main "$@"