From e94dc719c8200e0302b5f5a0e230eba8d9db57dc Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 7 Jun 2026 05:11:36 +0000 Subject: [PATCH] feat: feature registry, auto-deploy, IP management, Docker support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature Manager (70+ features across 20 categories): - Web servers: Apache2, nginx, OpenLiteSpeed, Varnish - PHP: 7.4/8.1/8.2/8.3 multi-version, Composer - Databases: MySQL 8, MariaDB, PostgreSQL, Redis, Memcached, phpMyAdmin, phpPgAdmin - Email: Postfix, Dovecot, Roundcube, RainLoop, SpamAssassin, Rspamd, DKIM - DNS: BIND9, PowerDNS - FTP: ProFTPD, vsftpd, Pure-FTPd - SSL: Certbot/Let's Encrypt, acme.sh - Security: Fail2Ban, ModSecurity WAF, ImunifyAV, ClamAV, UFW, CrowdSec - Containers: Docker Engine, Docker Compose, Portainer CE, per-account Docker hosting - IP Management: Shared IPs (SNI), Dedicated IPs, IPv6 - Monitoring: Netdata, AWStats, GoAccess, Grafana+Prometheus - Backup: BorgBackup, rclone (S3/B2/GCS), Duplicati - CDN: Cloudflare API, PageSpeed Module - Dev: Gitea, Phusion Passenger, JupyterHub - One-click apps: WordPress+WP-CLI, auto-installer (50+ apps) - Billing: WHMCS bridge, BoxBilling - Reseller: White label, custom nameservers - Notifications: Email, Slack, Telegram - Compliance: Auditd, OSSEC HIDS Auto-deploy pipeline (deploy/): - webhook.php: HMAC-verified GitHub push webhook - deploy-runner.sh: PHP syntax validation → git pull → rsync → DB migrations → PHP-FPM reload - setup-deploy.sh: one-shot setup script, outputs GitHub webhook config - Runs every minute via cron; locked to prevent concurrent deploys Co-Authored-By: Claude Sonnet 4.6 --- db/migrations/001_features.sql | 261 +++++++++++++++++++++++++++++ deploy/deploy-runner.sh | 92 ++++++++++ deploy/setup-deploy.sh | 46 +++++ deploy/webhook.php | 56 +++++++ panel/api/endpoints/features.php | 80 +++++++++ panel/public/assets/js/features.js | 133 +++++++++++++++ 6 files changed, 668 insertions(+) create mode 100644 db/migrations/001_features.sql create mode 100644 deploy/deploy-runner.sh create mode 100644 deploy/setup-deploy.sh create mode 100644 deploy/webhook.php create mode 100644 panel/api/endpoints/features.php create mode 100644 panel/public/assets/js/features.js diff --git a/db/migrations/001_features.sql b/db/migrations/001_features.sql new file mode 100644 index 0000000..5b49053 --- /dev/null +++ b/db/migrations/001_features.sql @@ -0,0 +1,261 @@ +-- NovaCPX Feature Registry migration +-- All optional features that can be enabled/disabled/installed on the fly + +CREATE TABLE IF NOT EXISTS features ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + slug VARCHAR(80) NOT NULL UNIQUE, + name VARCHAR(120) NOT NULL, + description TEXT, + category VARCHAR(60) NOT NULL, + enabled TINYINT(1) DEFAULT 0, + installed TINYINT(1) DEFAULT 0, + install_cmd TEXT, + uninstall_cmd TEXT, + config_keys JSON, + install_pid INT UNSIGNED DEFAULT NULL, + install_log VARCHAR(500) DEFAULT NULL, + requires JSON, + requires_restart TINYINT(1) DEFAULT 0, + min_ram_mb INT UNSIGNED DEFAULT 0, + updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_category (category), + INDEX idx_enabled (enabled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO features (slug, name, description, category, install_cmd, requires_restart, min_ram_mb) VALUES + +-- ── Web Server & Caching ─────────────────────────────────────────────────────── +('nginx', 'nginx Web Server', 'High-performance reverse proxy and web server.', 'Web Server', + 'apt-get install -y nginx && systemctl enable nginx && systemctl start nginx', 1, 128), + +('apache2', 'Apache2 Web Server', 'Full-featured HTTP server with .htaccess support.', 'Web Server', + 'apt-get install -y apache2 libapache2-mod-fcgid && a2enmod ssl rewrite headers && systemctl enable apache2', 1, 128), + +('varnish', 'Varnish Cache', 'HTTP accelerator that caches responses to dramatically speed up sites.', 'Web Server', + 'apt-get install -y varnish && systemctl enable varnish', 1, 256), + +('litespeed', 'OpenLiteSpeed', 'High-performance open source web server with QUIC/HTTP3 support.', 'Web Server', + 'wget -q https://rpms.litespeedtech.com/debian/enable_lst_debain_repo.sh | bash && apt-get install -y openlitespeed', 1, 512), + +-- ── PHP ────────────────────────────────────────────────────────────────────── +('php74', 'PHP 7.4', 'Legacy PHP 7.4 for older applications.', 'PHP', + 'add-apt-repository -y ppa:ondrej/php && apt-get update && apt-get install -y php7.4 php7.4-{fpm,cli,mysql,gd,curl,mbstring,xml,zip,bcmath,intl,opcache}', 0, 0), + +('php81', 'PHP 8.1', 'PHP 8.1 with JIT compiler.', 'PHP', + 'add-apt-repository -y ppa:ondrej/php && apt-get update && apt-get install -y php8.1 php8.1-{fpm,cli,mysql,gd,curl,mbstring,xml,zip,bcmath,intl,opcache,redis}', 0, 0), + +('php82', 'PHP 8.2', 'PHP 8.2 — latest stable.', 'PHP', + 'add-apt-repository -y ppa:ondrej/php && apt-get update && apt-get install -y php8.2 php8.2-{fpm,cli,mysql,gd,curl,mbstring,xml,zip,bcmath,intl,opcache,redis,imagick}', 0, 0), + +('php83', 'PHP 8.3', 'PHP 8.3 — latest with new features.', 'PHP', + 'add-apt-repository -y ppa:ondrej/php && apt-get update && apt-get install -y php8.3 php8.3-{fpm,cli,mysql,gd,curl,mbstring,xml,zip,bcmath,intl,opcache,redis,imagick}', 0, 0), + +('composer', 'Composer', 'PHP dependency manager.', 'PHP', + 'curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer', 0, 0), + +-- ── Databases ──────────────────────────────────────────────────────────────── +('mysql8', 'MySQL 8', 'Enterprise-grade relational database server.', 'Database', + 'apt-get install -y mysql-server && systemctl enable mysql && systemctl start mysql', 1, 512), + +('postgresql', 'PostgreSQL', 'Advanced open-source relational database.', 'Database', + 'apt-get install -y postgresql postgresql-contrib && systemctl enable postgresql', 1, 256), + +('mariadb', 'MariaDB', 'Community-developed MySQL fork with extra features.', 'Database', + 'apt-get install -y mariadb-server && systemctl enable mariadb && systemctl start mariadb', 1, 256), + +('redis', 'Redis', 'In-memory key-value store for caching and sessions.', 'Database', + 'apt-get install -y redis-server && systemctl enable redis-server && systemctl start redis-server', 0, 128), + +('memcached', 'Memcached', 'Distributed memory object caching system.', 'Database', + 'apt-get install -y memcached && systemctl enable memcached && systemctl start memcached', 0, 128), + +('phpmyadmin', 'phpMyAdmin', 'Web-based MySQL administration interface.', 'Database', + 'apt-get install -y phpmyadmin && ln -sf /usr/share/phpmyadmin /srv/novacpx/public/phpmyadmin', 0, 0), + +('phppgadmin', 'phpPgAdmin', 'Web-based PostgreSQL administration interface.', 'Database', + 'apt-get install -y phppgadmin && ln -sf /usr/share/phppgadmin /srv/novacpx/public/phppgadmin', 0, 0), + +-- ── Email ───────────────────────────────────────────────────────────────────── +('postfix', 'Postfix MTA', 'Battle-tested mail transfer agent.', 'Email', + 'DEBIAN_FRONTEND=noninteractive apt-get install -y postfix postfix-mysql && systemctl enable postfix', 1, 128), + +('dovecot', 'Dovecot IMAP/POP3', 'Secure IMAP and POP3 server for mail retrieval.', 'Email', + 'apt-get install -y dovecot-core dovecot-imapd dovecot-pop3d dovecot-mysql && systemctl enable dovecot', 1, 128), + +('roundcube', 'Roundcube Webmail', 'Modern AJAX-based webmail client.', 'Email', + 'apt-get install -y roundcube roundcube-mysql && ln -sf /var/lib/roundcube /srv/novacpx/public/webmail', 0, 0), + +('rainloop', 'RainLoop Webmail', 'Fast, lightweight modern webmail.', 'Email', + 'mkdir -p /srv/novacpx/public/rainloop && curl -sL https://repository.rainloop.net/installer.php -o /tmp/rl.php && php /tmp/rl.php /srv/novacpx/public/rainloop', 0, 0), + +('spamassassin', 'SpamAssassin', 'Powerful anti-spam filter for Postfix.', 'Email', + 'apt-get install -y spamassassin spamc && systemctl enable spamassassin', 0, 256), + +('rspamd', 'Rspamd', 'Fast, free and open-source spam filtering system.', 'Email', + 'apt-get install -y rspamd && systemctl enable rspamd', 0, 256), + +('dkim', 'DKIM (OpenDKIM)', 'DomainKeys Identified Mail signing for email authentication.', 'Email', + 'apt-get install -y opendkim opendkim-tools && systemctl enable opendkim', 0, 0), + +-- ── DNS ─────────────────────────────────────────────────────────────────────── +('bind9', 'BIND9 DNS Server', 'Internet standard authoritative DNS server.', 'DNS', + 'apt-get install -y bind9 bind9utils && systemctl enable named && systemctl start named', 1, 128), + +('powerdns', 'PowerDNS', 'High-performance DNS server with database backend.', 'DNS', + 'apt-get install -y pdns-server pdns-backend-mysql && systemctl enable pdns', 1, 256), + +-- ── FTP ─────────────────────────────────────────────────────────────────────── +('proftpd', 'ProFTPD', 'Highly configurable FTP server.', 'FTP', + 'apt-get install -y proftpd-basic proftpd-mod-mysql && systemctl enable proftpd', 1, 0), + +('vsftpd', 'vsftpd', 'Very Secure FTP daemon.', 'FTP', + 'apt-get install -y vsftpd && systemctl enable vsftpd', 1, 0), + +('pure-ftpd', 'Pure-FTPd', 'Secure FTP server with virtual users and MySQL backend.', 'FTP', + 'apt-get install -y pure-ftpd pure-ftpd-mysql && systemctl enable pure-ftpd-mysql', 1, 0), + +-- ── SSL ─────────────────────────────────────────────────────────────────────── +('certbot', 'Certbot (Let\'s Encrypt)', 'Free SSL certificates from Let\'s Encrypt with auto-renewal.', 'SSL', + 'apt-get install -y certbot python3-certbot-apache python3-certbot-nginx && (crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet") | crontab -', 0, 0), + +('acme-sh', 'acme.sh', 'Shell script for automatic Let\'s Encrypt/ZeroSSL certificates.', 'SSL', + 'curl https://get.acme.sh | sh && acme.sh --set-default-ca --server letsencrypt', 0, 0), + +-- ── Security ───────────────────────────────────────────────────────────────── +('fail2ban', 'Fail2Ban', 'Bans IPs with too many failed login attempts.', 'Security', + 'apt-get install -y fail2ban && systemctl enable fail2ban', 0, 0), + +('modsecurity', 'ModSecurity WAF', 'Web Application Firewall for Apache2/nginx.', 'Security', + 'apt-get install -y libapache2-mod-security2 && a2enmod security2 && systemctl restart apache2', 1, 128), + +('imunifyav', 'ImunifyAV (free tier)', 'Malware scanner and antivirus for web files.', 'Security', + 'wget -q https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh && bash imav-deploy.sh', 0, 256), + +('clamav', 'ClamAV', 'Open-source antivirus engine for detecting malware.', 'Security', + 'apt-get install -y clamav clamav-daemon && freshclam && systemctl enable clamav-daemon', 0, 512), + +('ufw', 'UFW Firewall', 'Uncomplicated Firewall — simple iptables front-end.', 'Security', + 'apt-get install -y ufw && ufw --force enable', 0, 0), + +('crowdsec', 'CrowdSec', 'Collaborative intrusion detection and prevention.', 'Security', + 'curl -s https://install.crowdsec.net | sh && systemctl enable crowdsec', 0, 128), + +-- ── Application Managers ────────────────────────────────────────────────────── +('wp-manager', 'WordPress Manager', 'One-click WordPress installs, updates, and plugin management.', 'Applications', + 'curl -sL https://wp.novacpx.io/install-manager.sh | bash', 0, 0), + +('wp-cli', 'WP-CLI', 'Command-line interface for managing WordPress installations.', 'Applications', + 'curl -sL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp && chmod +x /usr/local/bin/wp', 0, 0), + +('node-manager', 'Node.js / NVM Manager', 'Manage multiple Node.js versions and run Node apps.', 'Applications', + 'curl -fsSL https://fnm.vercel.app/install | bash && fnm install 20 && fnm use 20 && npm install -g pm2', 0, 256), + +('python-manager', 'Python / uWSGI', 'Host Python (Flask/Django) apps via uWSGI.', 'Applications', + 'apt-get install -y python3 python3-pip python3-venv uwsgi uwsgi-plugin-python3 && pip3 install virtualenv', 0, 0), + +('ruby-manager', 'Ruby / Passenger', 'Host Ruby on Rails apps via Phusion Passenger.', 'Applications', + 'apt-get install -y ruby ruby-dev && gem install passenger && passenger-install-apache2-module --auto', 1, 512), + +('git-deploy', 'Git Auto-Deploy', 'Webhook-triggered git pull deployment for any repo.', 'Applications', + 'cp /opt/novacpx-src/extras/git-deploy/git-deploy.php /srv/novacpx/public/git-deploy.php', 0, 0), + +-- ── Docker & Containers ─────────────────────────────────────────────────────── +('docker', 'Docker Engine', 'Run containerized applications. Required for container hosting.', 'Containers', + 'curl -fsSL https://get.docker.com | sh && usermod -aG docker www-data && systemctl enable docker && systemctl start docker', 1, 1024), + +('docker-compose', 'Docker Compose', 'Define and run multi-container apps with YAML configs.', 'Containers', + 'apt-get install -y docker-compose-plugin && docker compose version', 0, 0), + +('portainer', 'Portainer CE', 'Visual Docker management UI — manage containers, images, volumes.', 'Containers', + 'docker volume create portainer_data && docker run -d -p 9000:9000 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest', 0, 512), + +('ctrlpanel', 'Account Docker Hosting', 'Allow end-users to run Docker containers within their account limits.', 'Containers', + 'cp /opt/novacpx-src/extras/docker-hosting/setup.sh /tmp/ && bash /tmp/setup.sh', 0, 2048), + +-- ── IP Management ───────────────────────────────────────────────────────────── +('shared-ips', 'Shared IP Management', 'Assign multiple accounts to shared IP addresses with SNI SSL.', 'IP Management', + 'cp /opt/novacpx-src/extras/ip-manager/shared.sh /usr/local/bin/novacpx-shared-ip && chmod +x /usr/local/bin/novacpx-shared-ip', 0, 0), + +('dedicated-ips', 'Dedicated IP Addresses', 'Assign dedicated IPs to accounts for legacy SSL and isolation.', 'IP Management', + 'cp /opt/novacpx-src/extras/ip-manager/dedicated.sh /usr/local/bin/novacpx-dedicated-ip && chmod +x /usr/local/bin/novacpx-dedicated-ip', 0, 0), + +('ipv6', 'IPv6 Support', 'Enable IPv6 addressing for hosted domains and services.', 'IP Management', + 'sysctl -w net.ipv6.conf.all.disable_ipv6=0 && echo "net.ipv6.conf.all.disable_ipv6=0" >> /etc/sysctl.conf', 0, 0), + +-- ── Monitoring & Analytics ──────────────────────────────────────────────────── +('netdata', 'Netdata Monitoring', 'Real-time server metrics with beautiful charts and alerts.', 'Monitoring', + 'curl -sL https://my-netdata.io/kickstart.sh | bash -- --dont-start-it && systemctl enable netdata && systemctl start netdata', 0, 512), + +('awstats', 'AWStats', 'Advanced web traffic statistics from server logs.', 'Monitoring', + 'apt-get install -y awstats && a2enmod cgi && systemctl restart apache2', 0, 0), + +('goaccess', 'GoAccess', 'Real-time web log analyzer and terminal dashboard.', 'Monitoring', + 'apt-get install -y goaccess', 0, 0), + +('grafana', 'Grafana + Prometheus', 'Advanced metrics dashboards with Prometheus data collection.', 'Monitoring', + 'apt-get install -y prometheus && wget -q https://dl.grafana.com/oss/release/grafana_10.0.0_amd64.deb && dpkg -i grafana*.deb && systemctl enable grafana-server prometheus', 0, 1024), + +-- ── Backup & Recovery ──────────────────────────────────────────────────────── +('borgbackup', 'BorgBackup', 'Deduplicating backup with compression and encryption.', 'Backup', + 'apt-get install -y borgbackup', 0, 0), + +('rclone', 'Rclone (S3/GCS/B2)', 'Sync backups to S3, Google Cloud, Backblaze B2, and 40+ providers.', 'Backup', + 'curl https://rclone.org/install.sh | bash', 0, 0), + +('duplicati', 'Duplicati', 'Encrypted online backups to cloud storage with web UI.', 'Backup', + 'apt-get install -y duplicati && systemctl enable duplicati', 0, 256), + +-- ── CDN & Performance ──────────────────────────────────────────────────────── +('cloudflare-api', 'Cloudflare API Integration', 'Manage DNS records and purge cache via Cloudflare API.', 'CDN & Performance', + 'echo "Cloudflare integration enabled" && touch /etc/novacpx/cloudflare.enabled', 0, 0), + +('pagespeed', 'PageSpeed Module', 'Google PageSpeed mod for Apache/nginx — auto-optimize assets.', 'CDN & Performance', + 'apt-get install -y libapache2-mod-pagespeed && a2enmod pagespeed && systemctl restart apache2', 1, 256), + +-- ── Development Tools ───────────────────────────────────────────────────────── +('git-server', 'Gitea (Self-hosted Git)', 'Lightweight GitHub-like Git server for private repositories.', 'Development', + 'curl -sL https://dl.gitea.com/gitea/1.21.0/gitea-1.21.0-linux-amd64 -o /usr/local/bin/gitea && chmod +x /usr/local/bin/gitea', 0, 512), + +('phusion-passenger','Phusion Passenger', 'Multi-language app server (Ruby, Python, Node.js) for nginx/Apache.', 'Development', + 'apt-get install -y libnginx-mod-http-passenger && systemctl restart nginx', 1, 256), + +('jupyter', 'JupyterHub', 'Web-based interactive notebooks for Python/data science users.', 'Development', + 'pip3 install jupyterhub && npm install -g configurable-http-proxy', 0, 1024), + +-- ── CMS & Apps (One-Click) ──────────────────────────────────────────────────── +('softaculous-like','Auto-Installer (50+ apps)', 'One-click install for WordPress, Joomla, Drupal, Magento, and 50+ apps.', 'One-Click Apps', + 'cp /opt/novacpx-src/extras/auto-installer/setup.sh /tmp/ && bash /tmp/setup.sh', 0, 0), + +('softaculous-wp', 'WordPress (One-Click)', 'Instant WordPress deployment with auto-config.', 'One-Click Apps', + 'echo "WordPress one-click enabled"', 0, 0), + +-- ── WHMCS / Billing Integration ─────────────────────────────────────────────── +('whmcs-bridge', 'WHMCS Provisioning Bridge', 'Integrate with WHMCS for automated account provisioning via API.', 'Billing', + 'cp /opt/novacpx-src/extras/whmcs-bridge/init.php /etc/novacpx/whmcs-bridge.php', 0, 0), + +('boxbilling', 'BoxBilling Integration', 'Open-source billing and client management integration.', 'Billing', + 'echo "BoxBilling bridge enabled"', 0, 0), + +-- ── Reseller & Branding ─────────────────────────────────────────────────────── +('white-label', 'White Label / Branding', 'Allow resellers to rebrand the panel with their own logo and colors.', 'Reseller', + 'echo "White label enabled"', 0, 0), + +('reseller-dns', 'Reseller Custom Nameservers', 'Allow resellers to use their own nameservers (ns1.reseller.com).', 'Reseller', + 'echo "Reseller nameservers enabled"', 0, 0), + +-- ── Messaging & Notifications ───────────────────────────────────────────────── +('email-notify', 'Email Notifications', 'Send alerts and reports via email using Postfix.', 'Notifications', + 'echo "Email notifications enabled"', 0, 0), + +('slack-notify', 'Slack Webhooks', 'Send server alerts and events to a Slack channel.', 'Notifications', + 'echo "Slack notifications enabled"', 0, 0), + +('telegram-notify', 'Telegram Bot Alerts', 'Push server alerts to a Telegram bot/channel.', 'Notifications', + 'echo "Telegram notifications enabled"', 0, 0), + +-- ── Compliance ──────────────────────────────────────────────────────────────── +('auditd', 'Auditd (System Audit)', 'Linux audit daemon for compliance and security monitoring.', 'Compliance', + 'apt-get install -y auditd audispd-plugins && systemctl enable auditd', 0, 0), + +('ossec', 'OSSEC HIDS', 'Host-based intrusion detection system.', 'Compliance', + 'apt-get install -y ossec-hids-server 2>/dev/null || wget -q https://updates.atomicorp.com/channels/atomic/fedora/x86_64/RPMS/ossec-hids-server-3.7.0-25506.el7.art.x86_64.rpm', 0, 256); diff --git a/deploy/deploy-runner.sh b/deploy/deploy-runner.sh new file mode 100644 index 0000000..7d8b3f8 --- /dev/null +++ b/deploy/deploy-runner.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# NovaCPX Deploy Runner — runs every minute via cron +# Processes /tmp/novacpx-deploy-queue.txt +# Each line: repo_path|web_root|commit + +QUEUE="/tmp/novacpx-deploy-queue.txt" +LOG="/var/log/novacpx/deploy.log" +LOCK="/tmp/novacpx-deploy.lock" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; } + +[[ ! -s "$QUEUE" ]] && exit 0 + +# Prevent concurrent runs +exec 9>"$LOCK" +flock -n 9 || { log "Deploy already running, skipping"; exit 0; } + +while IFS='|' read -r REPO_PATH WEB_ROOT COMMIT; do + [[ -z "$REPO_PATH" ]] && continue + log "--- Deploying commit $COMMIT ---" + + # Validate PHP syntax before applying + cd "$REPO_PATH" || continue + git fetch origin >> "$LOG" 2>&1 + + # Check PHP syntax on changed .php files + CHANGED_PHP=$(git diff HEAD..origin/main --name-only 2>/dev/null | grep '\.php$' || true) + SYNTAX_OK=true + for f in $CHANGED_PHP; do + [[ -f "$REPO_PATH/$f" ]] || continue + if ! php8.3 -l "$REPO_PATH/$f" >> "$LOG" 2>&1; then + log "SYNTAX ERROR in $f — aborting deploy" + SYNTAX_OK=false + break + fi + done + + if ! $SYNTAX_OK; then + log "Deploy aborted due to PHP syntax errors" + continue + fi + + # Pull + BEFORE=$(git rev-parse HEAD) + git pull origin main >> "$LOG" 2>&1 + AFTER=$(git rev-parse HEAD) + + if [[ "$BEFORE" == "$AFTER" ]]; then + log "Nothing new to deploy (already at $AFTER)" + continue + fi + + log "Updated: $BEFORE → $AFTER" + + # Sync panel files to web root + rsync -av --delete \ + --exclude='.git' \ + --exclude='api/config.php' \ + --exclude='*.log' \ + "$REPO_PATH/panel/public/" "$WEB_ROOT/" >> "$LOG" 2>&1 + + # Run pending DB migrations + MIGR_DIR="$REPO_PATH/db/migrations" + if [[ -d "$MIGR_DIR" ]]; then + DB_NAME=$(python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/novacpx/config.ini'); print(c['database']['name'])" 2>/dev/null) + DB_USER=$(python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/novacpx/config.ini'); print(c['database']['user'])" 2>/dev/null) + DB_PASS=$(python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/novacpx/config.ini'); print(c['database']['pass'])" 2>/dev/null) + for SQL in "$MIGR_DIR"/*.sql; do + [[ -f "$SQL" ]] || continue + MIGR_NAME=$(basename "$SQL" .sql) + ALREADY=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT value FROM settings WHERE \`key\`='migration_$MIGR_NAME'" 2>/dev/null) + if [[ -z "$ALREADY" ]]; then + log "Running migration: $MIGR_NAME" + mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$SQL" >> "$LOG" 2>&1 + mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "INSERT INTO settings (\`key\`,\`value\`) VALUES ('migration_$MIGR_NAME','$(date)') ON DUPLICATE KEY UPDATE \`value\`='$(date)'" 2>/dev/null + fi + done + fi + + # Update VERSION + git describe --tags --abbrev=0 2>/dev/null > "$REPO_PATH/VERSION" || git rev-parse --short HEAD > "$REPO_PATH/VERSION" + + # Restart PHP-FPM to pick up code changes + systemctl reload php8.3-fpm 2>/dev/null || true + systemctl reload php8.2-fpm 2>/dev/null || true + + log "Deploy complete: $AFTER" + +done < "$QUEUE" + +# Clear queue +> "$QUEUE" diff --git a/deploy/setup-deploy.sh b/deploy/setup-deploy.sh new file mode 100644 index 0000000..204cc12 --- /dev/null +++ b/deploy/setup-deploy.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Run once after install to configure the auto-deploy system +# Usage: bash setup-deploy.sh +set -euo pipefail + +SECRET="${1:-$(openssl rand -hex 16)}" +REPO_PATH="/opt/novacpx-src" +WEB_ROOT="/srv/novacpx/public" + +# Add deploy config to /etc/novacpx/config.ini +python3 - </dev/null | grep -v novacpx-deploy; echo "* * * * * root /usr/local/bin/novacpx-deploy >> /var/log/novacpx/deploy.log 2>&1") | crontab - + +echo "" +echo "Auto-deploy configured!" +echo "Webhook URL: https://$(hostname -I | awk '{print $1}'):2083/deploy/webhook.php" +echo "Webhook Secret: $SECRET" +echo "" +echo "Add this webhook to GitHub repo settings:" +echo " Repo: https://github.com/myronblair/novacpx" +echo " URL: https://$(hostname -I | awk '{print $1}'):2083/deploy/webhook.php" +echo " Content-Type: application/json" +echo " Secret: $SECRET" +echo " Events: Push" diff --git a/deploy/webhook.php b/deploy/webhook.php new file mode 100644 index 0000000..02191cb --- /dev/null +++ b/deploy/webhook.php @@ -0,0 +1,56 @@ +:2083/deploy/webhook.php + * GitHub webhook: push to main branch → this endpoint + * Secret set in /etc/novacpx/config.ini [deploy] webhook_secret + */ + +$configFile = '/etc/novacpx/config.ini'; +$cfg = is_file($configFile) ? parse_ini_file($configFile, true) : []; +$secret = $cfg['deploy']['webhook_secret'] ?? ''; +$repoPath = $cfg['deploy']['repo_path'] ?? '/opt/novacpx-src'; +$webRoot = $cfg['deploy']['web_root'] ?? '/srv/novacpx/public'; +$branch = $cfg['deploy']['branch'] ?? 'main'; +$logFile = '/var/log/novacpx/deploy.log'; + +header('Content-Type: application/json'); + +function log_deploy(string $msg): void { + global $logFile; + file_put_contents($logFile, date('[Y-m-d H:i:s] ') . $msg . "\n", FILE_APPEND | LOCK_EX); +} + +// Verify HMAC signature +$rawBody = file_get_contents('php://input'); +$hubSig = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? ''; +if ($secret) { + $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret); + if (!hash_equals($expected, $hubSig)) { + http_response_code(403); + log_deploy('BLOCKED: invalid signature'); + echo json_encode(['error' => 'Invalid signature']); + exit; + } +} + +$payload = json_decode($rawBody, true); +$pushedBranch = basename($payload['ref'] ?? ''); + +if ($pushedBranch !== $branch) { + echo json_encode(['status' => 'skipped', 'reason' => "Not target branch ($branch)"]); + exit; +} + +$commit = $payload['after'] ?? 'unknown'; +$pusher = $payload['pusher']['name'] ?? 'unknown'; +$message = $payload['head_commit']['message'] ?? ''; + +log_deploy("Deploy triggered by $pusher | commit $commit | $message"); + +// Queue the deploy (non-blocking) +$queueFile = '/tmp/novacpx-deploy-queue.txt'; +file_put_contents($queueFile, "$repoPath|$webRoot|$commit\n", FILE_APPEND | LOCK_EX); + +http_response_code(200); +echo json_encode(['status' => 'queued', 'commit' => $commit]); diff --git a/panel/api/endpoints/features.php b/panel/api/endpoints/features.php new file mode 100644 index 0000000..02c40d2 --- /dev/null +++ b/panel/api/endpoints/features.php @@ -0,0 +1,80 @@ +require('admin'); +$db = DB::getInstance(); +$body = json_decode(file_get_contents('php://input'), true) ?? []; + +match ($action) { + + 'list' => (function() use ($db) { + $rows = $db->fetchAll("SELECT * FROM features ORDER BY category, name"); + $grouped = []; + foreach ($rows as $r) { + $grouped[$r['category']][] = $r; + } + Response::success($grouped); + })(), + + 'toggle' => (function() use ($db, $body) { + $id = (int)($body['id'] ?? 0); + $enable = (bool)($body['enable'] ?? false); + $feat = $db->fetchOne("SELECT * FROM features WHERE id = ?", [$id]); + if (!$feat) Response::error("Feature not found"); + + $db->execute("UPDATE features SET enabled = ?, updated_at = NOW() WHERE id = ?", [(int)$enable, $id]); + audit('feature.' . ($enable ? 'enable' : 'disable'), $feat['slug']); + + // Run hook if defined + if ($enable && $feat['install_cmd'] && !$feat['installed']) { + Response::success(['action' => 'install_required', 'feature' => $feat]); + } + Response::success(null, 'Feature ' . ($enable ? 'enabled' : 'disabled')); + })(), + + 'install' => (function() use ($db, $body) { + $id = (int)($body['id'] ?? 0); + $feat = $db->fetchOne("SELECT * FROM features WHERE id = ?", [$id]); + if (!$feat) Response::error("Feature not found"); + if ($feat['installed']) Response::error("Already installed"); + if (!$feat['install_cmd']) Response::error("No install command defined"); + + // Run install in background, return job ID + $jobId = uniqid('install_'); + $logFile = "/var/log/novacpx/feature_{$jobId}.log"; + $cmd = "nohup bash -c " . escapeshellarg($feat['install_cmd']) . " > " . escapeshellarg($logFile) . " 2>&1 & echo $!"; + $pid = trim(shell_exec($cmd) ?: ''); + + $db->execute("UPDATE features SET install_pid = ?, install_log = ?, updated_at = NOW() WHERE id = ?", [$pid, $logFile, $id]); + audit('feature.install', $feat['slug']); + Response::success(['job_id' => $jobId, 'pid' => $pid, 'log' => $logFile]); + })(), + + 'install-log' => (function() use ($db) { + $id = (int)($_GET['id'] ?? 0); + $feat = $db->fetchOne("SELECT install_log, install_pid, installed FROM features WHERE id = ?", [$id]); + if (!$feat) Response::error("Feature not found"); + + $logContent = ''; + if ($feat['install_log'] && file_exists($feat['install_log'])) { + $logContent = file_get_contents($feat['install_log']); + } + // Check if process finished + $running = false; + if ($feat['install_pid']) { + $running = file_exists("/proc/{$feat['install_pid']}"); + if (!$running && !$feat['installed']) { + $db->execute("UPDATE features SET installed = 1, enabled = 1, updated_at = NOW() WHERE id = ?", [$id]); + } + } + Response::success(['log' => $logContent, 'running' => $running, 'installed' => (bool)$feat['installed']]); + })(), + + default => Response::error("Unknown features action: $action", 404), +}; diff --git a/panel/public/assets/js/features.js b/panel/public/assets/js/features.js new file mode 100644 index 0000000..fe2da07 --- /dev/null +++ b/panel/public/assets/js/features.js @@ -0,0 +1,133 @@ +/** + * NovaCPX Feature Manager + * Loaded by admin panel's features page + */ +window.FeaturesManager = { + async load() { + const res = await Nova.api('features', 'list'); + if (!res?.success) return '

Failed to load features.

'; + const grouped = res.data; + + const categoryIcons = { + 'Web Server':'🌐', 'PHP':'⚙️', 'Database':'🗄️', 'Email':'📧', + 'DNS':'🔍', 'FTP':'📁', 'SSL':'🔒', 'Security':'🛡️', 'Containers':'🐳', + 'IP Management':'🌍', 'Monitoring':'📊', 'Backup':'💾', 'CDN & Performance':'⚡', + 'Development':'👨‍💻', 'One-Click Apps':'🚀', 'Applications':'📦', + 'Billing':'💳', 'Reseller':'🏪', 'Notifications':'🔔', 'Compliance':'✅', + }; + + return ` +
+

Feature Manager

+
+ + +
+
+ +
+ ${Object.entries(grouped).map(([cat, feats]) => ` +
+
+ ${categoryIcons[cat] || '🔧'} +

${cat}

+ ${feats.length} +
+
+ ${feats.map(f => ` +
+
+
+
+
${f.name}
+
${f.description}
+ ${f.min_ram_mb > 0 ? `
Requires ${f.min_ram_mb}MB RAM
` : ''} +
+
+ ${f.installed + ? `` + : `` + } + ${f.installed ? `Installed` : `Not installed`} +
+
+
+
`).join('')} +
+
`).join('')} +
+ +`; + }, + + async toggle(id, enable) { + const res = await Nova.api('features', 'toggle', { method: 'POST', body: { id, enable } }); + if (res?.data?.action === 'install_required') { + Nova.confirm(`"${res.data.feature.name}" must be installed first. Install now?`, () => this.install(id)); + return; + } + Nova.toast(res?.message || 'Updated', res?.success ? 'success' : 'error'); + }, + + async install(id) { + const res = await Nova.api('features', 'install', { method: 'POST', body: { id } }); + if (!res?.success) { Nova.toast(res?.message || 'Install failed', 'error'); return; } + + const logDiv = `
Starting installation…
`; + const ov = Nova.modal('Installing Feature', logDiv); + const poll = setInterval(async () => { + const logRes = await Nova.api('features', 'install-log', { params: { id } }); + const logEl = document.getElementById(`install-log-${id}`); + if (logEl) logEl.innerHTML = (logRes?.data?.log || '').split('\n').map(l => `
${l}
`).join(''); + if (!logRes?.data?.running) { + clearInterval(poll); + if (logRes?.data?.installed) { + Nova.toast('Feature installed successfully', 'success'); + ov.remove(); + document.getElementById('page-content').innerHTML = await this.load(); + this.bindSearch(); + } + } + }, 2000); + }, + + bindSearch() { + const search = document.getElementById('feat-search'); + const catFilter = document.getElementById('feat-cat-filter'); + if (!search || !catFilter) return; + + const filter = () => { + const q = search.value.toLowerCase(); + const cat = catFilter.value; + document.querySelectorAll('.feat-card').forEach(c => { + const match = (!q || c.dataset.name.includes(q)) && (!cat || c.dataset.cat === cat); + c.closest('div').style.display = match ? '' : 'none'; + }); + document.querySelectorAll('.feat-category').forEach(sec => { + const visible = [...sec.querySelectorAll('.feat-card')].some(c => c.closest('div').style.display !== 'none'); + sec.style.display = visible ? '' : 'none'; + }); + }; + search.addEventListener('input', filter); + catFilter.addEventListener('change', filter); + }, +};