feat: feature registry, auto-deploy, IP management, Docker support

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 05:11:36 +00:00
parent e802443d4a
commit e94dc719c8
6 changed files with 668 additions and 0 deletions
+261
View File
@@ -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);
+92
View File
@@ -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"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Run once after install to configure the auto-deploy system
# Usage: bash setup-deploy.sh <github_webhook_secret>
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 - <<PYEOF
import configparser, os
cfg = configparser.ConfigParser()
cfg.read('/etc/novacpx/config.ini')
if 'deploy' not in cfg: cfg['deploy'] = {}
cfg['deploy']['webhook_secret'] = '$SECRET'
cfg['deploy']['repo_path'] = '$REPO_PATH'
cfg['deploy']['web_root'] = '$WEB_ROOT'
cfg['deploy']['branch'] = 'main'
with open('/etc/novacpx/config.ini', 'w') as f: cfg.write(f)
print('Config updated')
PYEOF
# Install deploy runner
cp "$REPO_PATH/deploy/deploy-runner.sh" /usr/local/bin/novacpx-deploy
chmod +x /usr/local/bin/novacpx-deploy
# Install webhook handler into web root
mkdir -p "$WEB_ROOT/deploy"
cp "$REPO_PATH/deploy/webhook.php" "$WEB_ROOT/deploy/webhook.php"
chown www-data:www-data "$WEB_ROOT/deploy/webhook.php"
# Add cron job (every minute)
(crontab -l 2>/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"
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* NovaCPX Auto-Deploy Webhook Handler
* Place at: https://<panel-ip>: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]);
+80
View File
@@ -0,0 +1,80 @@
<?php
/**
* NovaCPX Feature Registry & Toggle API
* Features can be enabled/disabled/installed on the fly
* Covers: Docker, IPs, WordPress, Node.js, Python, Ruby, Git deploy,
* Cloudflare, Redis, Memcached, Varnish, ModSecurity, ImunifyAV,
* Webmail, phpMyAdmin, phpPgAdmin, and more
*/
Auth::getInstance()->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),
};
+133
View File
@@ -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 '<p class="text-muted">Failed to load features.</p>';
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 `
<div class="flex justify-between items-center mb-3">
<h2 style="font-size:1.1rem;font-weight:700">Feature Manager</h2>
<div class="flex gap-1">
<input type="text" id="feat-search" placeholder="Search features…" style="width:220px;padding:.45rem .85rem;font-size:.85rem">
<select id="feat-cat-filter" style="padding:.45rem .7rem;font-size:.85rem">
<option value="">All Categories</option>
${Object.keys(grouped).map(c => `<option value="${c}">${c}</option>`).join('')}
</select>
</div>
</div>
<div id="features-container">
${Object.entries(grouped).map(([cat, feats]) => `
<div class="feat-category" data-cat="${cat}">
<div class="flex items-center gap-1 mb-2 mt-3">
<span style="font-size:1.1rem">${categoryIcons[cat] || '🔧'}</span>
<h3 style="font-size:.9rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)">${cat}</h3>
<span class="badge badge-gray" style="margin-left:.5rem">${feats.length}</span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.75rem">
${feats.map(f => `
<div class="feat-card card" data-id="${f.id}" data-name="${f.name.toLowerCase()}" data-cat="${cat}">
<div class="card-body" style="padding:1rem">
<div class="flex justify-between items-center">
<div>
<div style="font-weight:600;font-size:.88rem">${f.name}</div>
<div style="font-size:.75rem;color:var(--text-muted);margin-top:.15rem;line-height:1.4">${f.description}</div>
${f.min_ram_mb > 0 ? `<div style="font-size:.72rem;color:var(--text-muted);margin-top:.2rem">Requires ${f.min_ram_mb}MB RAM</div>` : ''}
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:.4rem;margin-left:1rem;flex-shrink:0">
${f.installed
? `<label class="toggle-switch" title="${f.enabled ? 'Disable' : 'Enable'}">
<input type="checkbox" ${f.enabled ? 'checked' : ''} onchange="FeaturesManager.toggle(${f.id}, this.checked)">
<span class="toggle-slider"></span>
</label>`
: `<button class="btn btn-primary btn-sm" onclick="FeaturesManager.install(${f.id})">Install</button>`
}
${f.installed ? `<span class="badge badge-green" style="font-size:.65rem">Installed</span>` : `<span class="badge badge-gray" style="font-size:.65rem">Not installed</span>`}
</div>
</div>
</div>
</div>`).join('')}
</div>
</div>`).join('')}
</div>
<style>
.toggle-switch { position:relative; display:inline-block; width:40px; height:22px; }
.toggle-switch input { opacity:0; width:0; height:0; }
.toggle-slider {
position:absolute; cursor:pointer; inset:0;
background:var(--border); border-radius:999px; transition:.2s;
}
.toggle-slider:before {
content:''; position:absolute; width:16px; height:16px;
left:3px; bottom:3px; background:#fff; border-radius:50%; transition:.2s;
}
input:checked + .toggle-slider { background:var(--primary); }
input:checked + .toggle-slider:before { transform:translateX(18px); }
</style>`;
},
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 = `<div class="terminal" id="install-log-${id}" style="min-height:100px">Starting installation…</div>`;
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 => `<div>${l}</div>`).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);
},
};