mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
feat: Nginx Proxy Manager admin panel section (#22-proxy)
- ProxyManager.php: install, start/stop/restart/reload, manage proxy hosts, write nginx configs, sync from accounts, setup script generator - proxy.php API endpoint: full CRUD for proxy hosts + control/install/sync - Admin panel: Nginx Proxy sidebar nav (Services section) with status cards, host table, add/edit/toggle/delete, auto-sync accounts, setup guide modal - DB migration 003: proxy_hosts table + settings entries - Sudoers: nginx systemctl/install rules for www-data - Setup guide covers: local install, remote VM, automated script, vhost integration
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
-- Migration 003: Nginx Proxy Hosts table
|
||||
CREATE TABLE IF NOT EXISTS proxy_hosts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED DEFAULT NULL,
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
upstream VARCHAR(500) NOT NULL DEFAULT 'http://127.0.0.1:80',
|
||||
ssl_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
custom_config TEXT DEFAULT NULL,
|
||||
notes VARCHAR(500) DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX (account_id),
|
||||
UNIQUE KEY uq_domain (domain)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Panel settings for proxy mode
|
||||
INSERT INTO settings (`key`, `value`) VALUES ('proxy_mode', 'disabled') ON DUPLICATE KEY UPDATE `key`=`key`;
|
||||
INSERT INTO settings (`key`, `value`) VALUES ('proxy_auto_sync', '0') ON DUPLICATE KEY UPDATE `key`=`key`;
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Proxy endpoint — manage Nginx reverse proxy
|
||||
* Routes:
|
||||
* GET proxy/status — nginx status + mode
|
||||
* POST proxy/install — install nginx
|
||||
* POST proxy/control — {action: start|stop|restart|reload}
|
||||
* GET proxy/hosts — list proxy hosts
|
||||
* POST proxy/hosts — add proxy host
|
||||
* PUT proxy/hosts/{id} — update proxy host
|
||||
* DELETE proxy/hosts/{id} — delete proxy host
|
||||
* POST proxy/hosts/{id}/toggle — {enabled: bool}
|
||||
* POST proxy/sync — sync hosts from accounts
|
||||
* POST proxy/write-configs — regenerate all nginx configs
|
||||
* GET proxy/setup-script — return bash install script
|
||||
*/
|
||||
|
||||
Auth::getInstance()->requireRole('admin');
|
||||
require_once PANEL_ROOT . '/lib/ProxyManager.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$parts = $routeParts ?? [];
|
||||
$subpath = implode('/', array_slice($parts, 1));
|
||||
|
||||
// Numeric id extraction
|
||||
preg_match('|hosts/(\d+)(/.+)?|', $subpath, $m);
|
||||
$hostId = isset($m[1]) ? (int)$m[1] : null;
|
||||
$hostSub = $m[2] ?? '';
|
||||
|
||||
try {
|
||||
// GET proxy/status
|
||||
if ($method === 'GET' && $subpath === 'status') {
|
||||
json_ok(ProxyManager::status());
|
||||
|
||||
// POST proxy/install
|
||||
} elseif ($method === 'POST' && $subpath === 'install') {
|
||||
$result = ProxyManager::install();
|
||||
json_ok(['result' => $result]);
|
||||
|
||||
// POST proxy/control
|
||||
} elseif ($method === 'POST' && $subpath === 'control') {
|
||||
$action = $body['action'] ?? '';
|
||||
if (!in_array($action, ['start','stop','restart','reload'])) json_error('Invalid action', 400);
|
||||
$result = match($action) {
|
||||
'start' => ProxyManager::start(),
|
||||
'stop' => ProxyManager::stop(),
|
||||
'restart' => ProxyManager::restart(),
|
||||
'reload' => ProxyManager::reload(),
|
||||
};
|
||||
json_ok(['result' => $result, 'running' => ProxyManager::isRunning()]);
|
||||
|
||||
// GET proxy/hosts
|
||||
} elseif ($method === 'GET' && $subpath === 'hosts') {
|
||||
json_ok(ProxyManager::listHosts());
|
||||
|
||||
// POST proxy/hosts — add
|
||||
} elseif ($method === 'POST' && $subpath === 'hosts') {
|
||||
if (empty($body['domain'])) json_error('domain required', 400);
|
||||
if (empty($body['upstream'])) json_error('upstream required', 400);
|
||||
$id = ProxyManager::addHost($body);
|
||||
json_ok(['id' => $id]);
|
||||
|
||||
// PUT proxy/hosts/{id}
|
||||
} elseif ($method === 'PUT' && $hostId && !$hostSub) {
|
||||
ProxyManager::updateHost($hostId, $body);
|
||||
json_ok();
|
||||
|
||||
// DELETE proxy/hosts/{id}
|
||||
} elseif ($method === 'DELETE' && $hostId && !$hostSub) {
|
||||
ProxyManager::deleteHost($hostId);
|
||||
json_ok();
|
||||
|
||||
// POST proxy/hosts/{id}/toggle
|
||||
} elseif ($method === 'POST' && $hostId && $hostSub === '/toggle') {
|
||||
ProxyManager::toggleHost($hostId, (bool)($body['enabled'] ?? true));
|
||||
json_ok();
|
||||
|
||||
// POST proxy/sync
|
||||
} elseif ($method === 'POST' && $subpath === 'sync') {
|
||||
$added = ProxyManager::syncFromAccounts();
|
||||
json_ok(['added' => $added]);
|
||||
|
||||
// POST proxy/write-configs
|
||||
} elseif ($method === 'POST' && $subpath === 'write-configs') {
|
||||
ProxyManager::writeAllConfigs();
|
||||
json_ok(['result' => 'configs written']);
|
||||
|
||||
// GET proxy/setup-script
|
||||
} elseif ($method === 'GET' && $subpath === 'setup-script') {
|
||||
header('Content-Type: text/plain');
|
||||
echo ProxyManager::setupScript();
|
||||
exit;
|
||||
|
||||
} else {
|
||||
json_error('Not found', 404);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
novacpx_log('error', 'proxy endpoint: ' . $e->getMessage());
|
||||
json_error($e->getMessage(), 500);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
/**
|
||||
* ProxyManager — manages Nginx reverse proxy for NovaCPX hosted accounts.
|
||||
* Supports local nginx (on same VM) or remote nginx (separate proxy VM via SSH).
|
||||
*/
|
||||
class ProxyManager {
|
||||
|
||||
private static string $confDir = '/etc/nginx/sites-available';
|
||||
private static string $enabledDir = '/etc/nginx/sites-enabled';
|
||||
private static string $confPrefix = 'novacpx-proxy-';
|
||||
|
||||
// --- Status & Control ---
|
||||
|
||||
public static function isInstalled(): bool {
|
||||
return file_exists('/usr/sbin/nginx') || !empty(shell_exec('which nginx 2>/dev/null'));
|
||||
}
|
||||
|
||||
public static function isRunning(): bool {
|
||||
$out = shell_exec('systemctl is-active nginx 2>/dev/null');
|
||||
return trim($out ?? '') === 'active';
|
||||
}
|
||||
|
||||
public static function status(): array {
|
||||
$installed = self::isInstalled();
|
||||
$running = $installed && self::isRunning();
|
||||
$version = $installed ? trim(shell_exec('nginx -v 2>&1') ?: '') : '';
|
||||
$db = DB::getInstance();
|
||||
$row = $db->fetchOne("SELECT value FROM settings WHERE `key` = 'proxy_mode'");
|
||||
$mode = $row['value'] ?? 'disabled';
|
||||
return [
|
||||
'installed' => $installed,
|
||||
'running' => $running,
|
||||
'version' => $version,
|
||||
'mode' => $mode,
|
||||
];
|
||||
}
|
||||
|
||||
public static function start(): string {
|
||||
return self::sysctl('start');
|
||||
}
|
||||
|
||||
public static function stop(): string {
|
||||
return self::sysctl('stop');
|
||||
}
|
||||
|
||||
public static function restart(): string {
|
||||
return self::sysctl('restart');
|
||||
}
|
||||
|
||||
public static function reload(): string {
|
||||
if (!self::isInstalled()) return 'nginx not installed';
|
||||
$test = shell_exec('sudo nginx -t 2>&1');
|
||||
if (strpos($test ?? '', 'successful') === false) return 'Config test failed: ' . $test;
|
||||
shell_exec('sudo systemctl reload nginx 2>/dev/null');
|
||||
return 'reloaded';
|
||||
}
|
||||
|
||||
public static function install(): string {
|
||||
if (self::isInstalled()) return 'already installed';
|
||||
shell_exec('sudo apt-get update -qq 2>/dev/null && sudo apt-get install -y nginx 2>&1');
|
||||
if (!self::isInstalled()) return 'install failed';
|
||||
// Disable default site
|
||||
@unlink('/etc/nginx/sites-enabled/default');
|
||||
shell_exec('sudo systemctl enable nginx 2>/dev/null');
|
||||
shell_exec('sudo systemctl start nginx 2>/dev/null');
|
||||
return 'installed';
|
||||
}
|
||||
|
||||
// --- Proxy Hosts ---
|
||||
|
||||
public static function listHosts(): array {
|
||||
$db = DB::getInstance();
|
||||
return $db->fetchAll("SELECT * FROM proxy_hosts ORDER BY domain") ?: [];
|
||||
}
|
||||
|
||||
public static function syncFromAccounts(): int {
|
||||
$db = DB::getInstance();
|
||||
$accounts = $db->fetchAll("SELECT a.*, d.domain FROM accounts a JOIN domains d ON d.account_id=a.id AND d.type='main' WHERE a.status='active'") ?: [];
|
||||
$count = 0;
|
||||
foreach ($accounts as $acct) {
|
||||
$existing = $db->fetchOne("SELECT id FROM proxy_hosts WHERE domain=?", [$acct['domain']]);
|
||||
if (!$existing) {
|
||||
$db->insert(
|
||||
"INSERT INTO proxy_hosts (account_id, domain, upstream, ssl_enabled, enabled, created_at) VALUES (?,?,?,0,1,NOW())",
|
||||
[$acct['id'], $acct['domain'], 'http://127.0.0.1:80']
|
||||
);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
if ($count > 0) self::writeAllConfigs();
|
||||
return $count;
|
||||
}
|
||||
|
||||
public static function addHost(array $data): int {
|
||||
$db = DB::getInstance();
|
||||
$id = (int)$db->insert(
|
||||
"INSERT INTO proxy_hosts (account_id, domain, upstream, ssl_enabled, enabled, custom_config, created_at) VALUES (?,?,?,?,1,?,NOW())",
|
||||
[
|
||||
$data['account_id'] ?? null,
|
||||
$data['domain'],
|
||||
$data['upstream'] ?? 'http://127.0.0.1:80',
|
||||
(int)($data['ssl_enabled'] ?? 0),
|
||||
$data['custom_config'] ?? null,
|
||||
]
|
||||
);
|
||||
self::writeAllConfigs();
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function updateHost(int $id, array $data): void {
|
||||
$db = DB::getInstance();
|
||||
$db->execute(
|
||||
"UPDATE proxy_hosts SET domain=?, upstream=?, ssl_enabled=?, enabled=?, custom_config=? WHERE id=?",
|
||||
[$data['domain'], $data['upstream'], (int)($data['ssl_enabled'] ?? 0), (int)($data['enabled'] ?? 1), $data['custom_config'] ?? null, $id]
|
||||
);
|
||||
self::writeAllConfigs();
|
||||
}
|
||||
|
||||
public static function deleteHost(int $id): void {
|
||||
$db = DB::getInstance();
|
||||
$host = $db->fetchOne("SELECT domain FROM proxy_hosts WHERE id=?", [$id]);
|
||||
$db->execute("DELETE FROM proxy_hosts WHERE id=?", [$id]);
|
||||
if ($host) {
|
||||
@unlink(self::$confDir . '/' . self::$confPrefix . $host['domain'] . '.conf');
|
||||
@unlink(self::$enabledDir . '/' . self::$confPrefix . $host['domain'] . '.conf');
|
||||
}
|
||||
self::reload();
|
||||
}
|
||||
|
||||
public static function toggleHost(int $id, bool $enable): void {
|
||||
$db = DB::getInstance();
|
||||
$db->execute("UPDATE proxy_hosts SET enabled=? WHERE id=?", [(int)$enable, $id]);
|
||||
self::writeAllConfigs();
|
||||
}
|
||||
|
||||
// --- Config Generation ---
|
||||
|
||||
public static function writeAllConfigs(): void {
|
||||
if (!self::isInstalled()) return;
|
||||
$db = DB::getInstance();
|
||||
$hosts = $db->fetchAll("SELECT * FROM proxy_hosts") ?: [];
|
||||
// Remove old novacpx proxy configs
|
||||
foreach (glob(self::$confDir . '/' . self::$confPrefix . '*.conf') ?: [] as $f) @unlink($f);
|
||||
foreach (glob(self::$enabledDir . '/' . self::$confPrefix . '*.conf') ?: [] as $f) @unlink($f);
|
||||
foreach ($hosts as $host) {
|
||||
if (!$host['enabled']) continue;
|
||||
self::writeHostConfig($host);
|
||||
}
|
||||
self::reload();
|
||||
}
|
||||
|
||||
private static function writeHostConfig(array $host): void {
|
||||
$safe = preg_replace('/[^a-z0-9._-]/', '', strtolower($host['domain']));
|
||||
$confPath = self::$confDir . '/' . self::$confPrefix . $safe . '.conf';
|
||||
$linkPath = self::$enabledDir . '/' . self::$confPrefix . $safe . '.conf';
|
||||
|
||||
if ($host['custom_config']) {
|
||||
file_put_contents($confPath, $host['custom_config']);
|
||||
} else {
|
||||
$upstream = rtrim($host['upstream'], '/');
|
||||
$ssl = !empty($host['ssl_enabled']);
|
||||
$certDir = "/etc/novacpx/ssl/accounts/" . preg_replace('/[^a-z0-9._-]/', '', $host['domain']);
|
||||
|
||||
$conf = "server {\n";
|
||||
$conf .= " listen 80;\n";
|
||||
if ($ssl) $conf .= " listen 443 ssl http2;\n";
|
||||
$conf .= " server_name {$host['domain']} www.{$host['domain']};\n";
|
||||
if ($ssl) {
|
||||
$conf .= " ssl_certificate {$certDir}/cert.pem;\n";
|
||||
$conf .= " ssl_certificate_key {$certDir}/key.pem;\n";
|
||||
$conf .= " ssl_protocols TLSv1.2 TLSv1.3;\n";
|
||||
$conf .= " ssl_ciphers HIGH:!aNULL:!MD5;\n";
|
||||
}
|
||||
$conf .= " location / {\n";
|
||||
$conf .= " proxy_pass {$upstream};\n";
|
||||
$conf .= " proxy_http_version 1.1;\n";
|
||||
$conf .= " proxy_set_header Host \$host;\n";
|
||||
$conf .= " proxy_set_header X-Real-IP \$remote_addr;\n";
|
||||
$conf .= " proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n";
|
||||
$conf .= " proxy_set_header X-Forwarded-Proto \$scheme;\n";
|
||||
$conf .= " proxy_set_header Upgrade \$http_upgrade;\n";
|
||||
$conf .= " proxy_set_header Connection 'upgrade';\n";
|
||||
$conf .= " proxy_cache_bypass \$http_upgrade;\n";
|
||||
$conf .= " proxy_read_timeout 86400;\n";
|
||||
$conf .= " }\n";
|
||||
$conf .= "}\n";
|
||||
file_put_contents($confPath, $conf);
|
||||
}
|
||||
@symlink($confPath, $linkPath);
|
||||
}
|
||||
|
||||
// --- Setup Script ---
|
||||
|
||||
public static function setupScript(): string {
|
||||
$serverIp = trim(shell_exec("hostname -I | awk '{print $1}'") ?: '127.0.0.1');
|
||||
return <<<BASH
|
||||
#!/bin/bash
|
||||
# NovaCPX Nginx Reverse Proxy Setup Script
|
||||
# Run as root on the proxy VM (or this VM for local proxy)
|
||||
set -e
|
||||
|
||||
echo "[NovaCPX] Installing Nginx reverse proxy..."
|
||||
apt-get update -qq
|
||||
apt-get install -y nginx certbot python3-certbot-nginx
|
||||
|
||||
# Disable default site
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Create NovaCPX proxy conf directory
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
|
||||
# Main nginx.conf tuning
|
||||
cat > /etc/nginx/conf.d/novacpx-proxy.conf << 'EOF'
|
||||
client_max_body_size 256M;
|
||||
proxy_buffers 16 16k;
|
||||
proxy_buffer_size 16k;
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
EOF
|
||||
|
||||
# Point proxy back to NovaCPX Apache backend (update SERVER_IP below)
|
||||
BACKEND_IP={$serverIp}
|
||||
|
||||
# Generic catch-all for testing
|
||||
cat > /etc/nginx/sites-available/novacpx-default.conf << EOF
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
EOF
|
||||
ln -sf /etc/nginx/sites-available/novacpx-default.conf /etc/nginx/sites-enabled/
|
||||
|
||||
# Test and reload
|
||||
nginx -t && systemctl reload nginx
|
||||
systemctl enable nginx
|
||||
|
||||
echo "[NovaCPX] Nginx proxy installed and running."
|
||||
echo " Backend IP: \$BACKEND_IP"
|
||||
echo " Add proxy hosts from the NovaCPX admin panel → Nginx Proxy"
|
||||
BASH;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private static function sysctl(string $action): string {
|
||||
if (!self::isInstalled()) return 'nginx not installed';
|
||||
shell_exec("sudo systemctl {$action} nginx 2>/dev/null");
|
||||
sleep(1);
|
||||
return self::isRunning() ? 'running' : 'stopped';
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,10 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
FTP Server
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="nginx-proxy">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
Nginx Proxy
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="wordpress">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
WordPress
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
'mysql-manager': mysqlManager,
|
||||
'mail-server': mailServer,
|
||||
'ftp-server': ftpServer,
|
||||
'nginx-proxy': nginxProxy,
|
||||
wordpress,
|
||||
'ssl-manager': sslManager,
|
||||
firewall,
|
||||
@@ -98,6 +99,7 @@
|
||||
settings,
|
||||
};
|
||||
|
||||
window._novaPages = pages;
|
||||
Nova.initNav(pages);
|
||||
await Nova.loadPage('dashboard', pages);
|
||||
checkUpdates();
|
||||
@@ -1341,6 +1343,7 @@ ${ips.length ? `
|
||||
async function wordpress() { return `<p class="text-muted" style="padding:2rem">Loading…</p>`; }
|
||||
async function cloudflare() { return `<p class="text-muted" style="padding:2rem">Loading…</p>`; }
|
||||
async function twofa() { return `<p class="text-muted" style="padding:2rem">Loading…</p>`; }
|
||||
async function nginxProxy() { return `<p class="text-muted" style="padding:2rem">Loading…</p>`; }
|
||||
|
||||
// ── Global action helpers ──────────────────────────────────────────────────
|
||||
window.adminPage = (page) => Nova.loadPage(page, pages);
|
||||
@@ -1919,3 +1922,249 @@ window.totpAdminDisable = (userId, username) => {
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
|
||||
// ── Nginx Proxy Manager ───────────────────────────────────────────────────────
|
||||
async function nginxProxy() {
|
||||
const [statusR, hostsR] = await Promise.all([
|
||||
Nova.api('proxy', 'status'),
|
||||
Nova.api('proxy', 'hosts'),
|
||||
]);
|
||||
const s = statusR?.data || {};
|
||||
const hosts = hostsR?.data || (Array.isArray(hostsR) ? hostsR : []);
|
||||
const run = s.running;
|
||||
const inst = s.installed;
|
||||
|
||||
return `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Nginx Proxy Manager</h1>
|
||||
<div class="page-actions">
|
||||
${inst ? `
|
||||
<button class="btn btn-ghost btn-sm" onclick="proxySetupInstructions()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
||||
Setup Guide
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="proxySync()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||||
Sync Accounts
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="proxyAddHost()">+ Add Host</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" style="margin-bottom:1.5rem">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Nginx Status</div>
|
||||
<div class="stat-value ${run ? 'stat-green' : 'stat-red'}">${inst ? (run ? 'Running' : 'Stopped') : 'Not Installed'}</div>
|
||||
<div class="stat-sub">${s.version || (inst ? 'nginx' : 'click Install to set up')}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Proxy Hosts</div>
|
||||
<div class="stat-value">${hosts.length}</div>
|
||||
<div class="stat-sub">${hosts.filter(h => h.enabled).length} active</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">SSL Enabled</div>
|
||||
<div class="stat-value">${hosts.filter(h => h.ssl_enabled).length}</div>
|
||||
<div class="stat-sub">of ${hosts.length} hosts</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${!inst ? `
|
||||
<div class="panel" style="text-align:center;padding:3rem">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48" height="48" style="color:var(--text-muted);margin-bottom:1rem"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
<h3 style="margin-bottom:0.5rem">Nginx Not Installed</h3>
|
||||
<p style="color:var(--text-muted);margin-bottom:1.5rem">Install Nginx on this VM to use it as a reverse proxy in front of Apache, or use a separate proxy VM (see Setup Guide).</p>
|
||||
<div style="display:flex;gap:0.75rem;justify-content:center;flex-wrap:wrap">
|
||||
<button class="btn btn-primary" onclick="proxyInstall()">Install Nginx Locally</button>
|
||||
<button class="btn btn-secondary" onclick="proxySetupInstructions()">Setup Guide / Remote VM</button>
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="panel" style="margin-bottom:1.5rem">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">Service Controls</h3>
|
||||
<div style="display:flex;gap:0.5rem">
|
||||
<button class="btn btn-sm btn-success" onclick="proxyControl('start')">Start</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="proxyControl('restart')">Restart</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="proxyControl('stop')">Stop</button>
|
||||
<button class="btn btn-sm btn-ghost" onclick="proxyControl('reload')">Reload Config</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">Proxy Hosts</h3>
|
||||
<span class="badge badge-blue">${hosts.length} total</span>
|
||||
</div>
|
||||
${hosts.length === 0 ? `
|
||||
<div style="text-align:center;padding:2rem;color:var(--text-muted)">
|
||||
No proxy hosts yet. Click <strong>Sync Accounts</strong> to auto-add all hosted domains, or <strong>+ Add Host</strong> to add manually.
|
||||
</div>
|
||||
` : `
|
||||
<div style="overflow-x:auto">
|
||||
<table class="table">
|
||||
<thead><tr>
|
||||
<th>Domain</th>
|
||||
<th>Upstream</th>
|
||||
<th>SSL</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${hosts.map(h => `
|
||||
<tr id="proxy-row-${h.id}">
|
||||
<td><strong>${Nova.escHtml(h.domain)}</strong></td>
|
||||
<td style="font-family:monospace;font-size:0.8rem">${Nova.escHtml(h.upstream)}</td>
|
||||
<td>${h.ssl_enabled ? Nova.badge('SSL','green') : Nova.badge('HTTP','muted')}</td>
|
||||
<td>${h.enabled ? Nova.badge('Active','green') : Nova.badge('Disabled','red')}</td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-ghost" onclick="proxyEditHost(${h.id})">Edit</button>
|
||||
<button class="btn btn-xs ${h.enabled ? 'btn-warning' : 'btn-success'}" onclick="proxyToggle(${h.id},${h.enabled ? 0 : 1})">${h.enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button class="btn btn-xs btn-danger" onclick="proxyDeleteHost(${h.id},'${Nova.escHtml(h.domain)}')">Delete</button>
|
||||
</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`}`;
|
||||
}
|
||||
|
||||
window.proxyInstall = async () => {
|
||||
if (!confirm('Install Nginx on this VM? This will run apt-get install nginx.')) return;
|
||||
Nova.toast('Installing nginx...', 'info');
|
||||
const r = await Nova.api('proxy', 'install', { method: 'POST' });
|
||||
Nova.toast(r?.data?.result || r?.message || 'Done', r?.data?.result === 'installed' ? 'success' : 'info');
|
||||
Nova.loadPage('nginx-proxy', window._novaPages);
|
||||
};
|
||||
|
||||
window.proxyControl = async (action) => {
|
||||
const r = await Nova.api('proxy', 'control', { method: 'POST', body: { action } });
|
||||
Nova.toast(r?.data?.result || r?.message || action + ' done', 'success');
|
||||
setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 800);
|
||||
};
|
||||
|
||||
window.proxySync = async () => {
|
||||
const r = await Nova.api('proxy', 'sync', { method: 'POST' });
|
||||
Nova.toast(`Synced: ${r?.data?.added ?? 0} new hosts added`, 'success');
|
||||
Nova.loadPage('nginx-proxy', window._novaPages);
|
||||
};
|
||||
|
||||
window.proxyAddHost = () => {
|
||||
Nova.modal('Add Proxy Host', `
|
||||
<div class="form-group"><label>Domain</label>
|
||||
<input id="ph-domain" type="text" placeholder="example.com" class="form-control"></div>
|
||||
<div class="form-group"><label>Upstream URL</label>
|
||||
<input id="ph-upstream" type="text" value="http://127.0.0.1:80" class="form-control">
|
||||
<small class="text-muted">e.g. http://127.0.0.1:80 or http://10.0.0.2:8080</small></div>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" id="ph-ssl"> Enable SSL</label></div>
|
||||
<div class="form-group"><label>Notes (optional)</label>
|
||||
<input id="ph-notes" type="text" class="form-control"></div>
|
||||
`, async () => {
|
||||
const domain = document.getElementById('ph-domain')?.value?.trim();
|
||||
const upstream = document.getElementById('ph-upstream')?.value?.trim();
|
||||
if (!domain || !upstream) { Nova.toast('Domain and upstream required', 'error'); return; }
|
||||
const r = await Nova.api('proxy', 'hosts', {
|
||||
method: 'POST',
|
||||
body: { domain, upstream, ssl_enabled: document.getElementById('ph-ssl')?.checked ? 1 : 0 }
|
||||
});
|
||||
Nova.toast(r?.success ? 'Host added' : (r?.message || 'Failed'), r?.success ? 'success' : 'error');
|
||||
if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages);
|
||||
});
|
||||
};
|
||||
|
||||
window.proxyEditHost = async (id) => {
|
||||
const hostsR = await Nova.api('proxy', 'hosts');
|
||||
const hosts = hostsR?.data || (Array.isArray(hostsR) ? hostsR : []);
|
||||
const h = hosts.find(x => x.id == id);
|
||||
if (!h) return;
|
||||
Nova.modal('Edit Proxy Host', `
|
||||
<div class="form-group"><label>Domain</label>
|
||||
<input id="phe-domain" type="text" value="${Nova.escHtml(h.domain)}" class="form-control"></div>
|
||||
<div class="form-group"><label>Upstream URL</label>
|
||||
<input id="phe-upstream" type="text" value="${Nova.escHtml(h.upstream)}" class="form-control"></div>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" id="phe-ssl" ${h.ssl_enabled ? 'checked' : ''}> Enable SSL</label></div>
|
||||
<div class="form-group"><label>Custom Nginx Config (overrides auto-generated)</label>
|
||||
<textarea id="phe-custom" rows="6" class="form-control" style="font-family:monospace;font-size:0.78rem">${Nova.escHtml(h.custom_config || '')}</textarea>
|
||||
<small class="text-muted">Leave blank to use auto-generated config</small></div>
|
||||
`, async () => {
|
||||
const r = await Nova.api('proxy', `hosts/${id}`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
domain: document.getElementById('phe-domain')?.value?.trim(),
|
||||
upstream: document.getElementById('phe-upstream')?.value?.trim(),
|
||||
ssl_enabled: document.getElementById('phe-ssl')?.checked ? 1 : 0,
|
||||
custom_config: document.getElementById('phe-custom')?.value?.trim() || null,
|
||||
}
|
||||
});
|
||||
Nova.toast(r?.success ? 'Updated' : (r?.message || 'Failed'), r?.success ? 'success' : 'error');
|
||||
if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages);
|
||||
});
|
||||
};
|
||||
|
||||
window.proxyToggle = async (id, enable) => {
|
||||
const r = await Nova.api('proxy', `hosts/${id}/toggle`, { method: 'POST', body: { enabled: enable } });
|
||||
Nova.toast(r?.success ? (enable ? 'Enabled' : 'Disabled') : 'Failed', r?.success ? 'success' : 'error');
|
||||
if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages);
|
||||
};
|
||||
|
||||
window.proxyDeleteHost = (id, domain) => {
|
||||
Nova.confirm(`Delete proxy host for ${domain}?`, async () => {
|
||||
const r = await Nova.api('proxy', `hosts/${id}`, { method: 'DELETE' });
|
||||
Nova.toast(r?.success ? 'Deleted' : 'Failed', r?.success ? 'success' : 'error');
|
||||
if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages);
|
||||
}, true);
|
||||
};
|
||||
|
||||
window.proxySetupInstructions = async () => {
|
||||
const scriptUrl = '/api/proxy/setup-script';
|
||||
Nova.modal('Nginx Proxy Setup Guide', `
|
||||
<div style="max-height:60vh;overflow-y:auto">
|
||||
<h4 style="margin-bottom:0.75rem">Option A — Local (Nginx on this VM)</h4>
|
||||
<p style="color:var(--text-muted);margin-bottom:1rem">Install Nginx alongside Apache on this VM. Nginx listens on ports 80/443 and forwards to Apache. Best for SSL termination and caching.</p>
|
||||
<ol style="color:var(--text-muted);margin-bottom:1.5rem;padding-left:1.2rem;line-height:1.8">
|
||||
<li>Click <strong>Install Nginx Locally</strong> on the main Nginx Proxy page</li>
|
||||
<li>Move Apache to port 8080: edit <code>/etc/apache2/ports.conf</code> → change <code>Listen 80</code> to <code>Listen 8080</code></li>
|
||||
<li>Update upstream in all proxy hosts to <code>http://127.0.0.1:8080</code></li>
|
||||
<li>Click <strong>Sync Accounts</strong> to auto-populate proxy hosts from your hosted accounts</li>
|
||||
<li>Click <strong>Reload Config</strong> to apply changes</li>
|
||||
</ol>
|
||||
|
||||
<h4 style="margin-bottom:0.75rem">Option B — Remote Proxy VM (Recommended for production)</h4>
|
||||
<p style="color:var(--text-muted);margin-bottom:1rem">Run a dedicated Nginx proxy VM in front of this NovaCPX VM. Traffic flows: Internet → FortiGate → Nginx Proxy VM → NovaCPX VM (Apache).</p>
|
||||
<ol style="color:var(--text-muted);margin-bottom:1.5rem;padding-left:1.2rem;line-height:1.8">
|
||||
<li>Create a new VM on Proxmox (Ubuntu 22.04, 1 vCPU, 1GB RAM)</li>
|
||||
<li>Run the setup script below on the new VM as root</li>
|
||||
<li>Point FortiGate VIPs to the proxy VM IP (ports 80/443)</li>
|
||||
<li>Set the proxy upstream to this NovaCPX VM IP (<code>http://10.48.200.110:80</code>)</li>
|
||||
<li>Add proxy hosts for each domain from your NovaCPX admin panel</li>
|
||||
</ol>
|
||||
|
||||
<h4 style="margin-bottom:0.75rem">Automated Setup Script</h4>
|
||||
<p style="color:var(--text-muted);margin-bottom:0.75rem">Run this on the target VM (local or remote) as root:</p>
|
||||
<div style="background:var(--bg-secondary);padding:0.75rem;border-radius:6px;font-family:monospace;font-size:0.8rem;margin-bottom:0.75rem">
|
||||
curl -sk https://YOUR_NOVACPX_IP:8882/api/proxy/setup-script | bash
|
||||
</div>
|
||||
<p style="color:var(--text-muted);font-size:0.85rem">Or download and review before running:</p>
|
||||
<div style="background:var(--bg-secondary);padding:0.75rem;border-radius:6px;font-family:monospace;font-size:0.8rem">
|
||||
curl -sk https://YOUR_NOVACPX_IP:8882/api/proxy/setup-script -o proxy-setup.sh<br>
|
||||
cat proxy-setup.sh # review<br>
|
||||
bash proxy-setup.sh
|
||||
</div>
|
||||
|
||||
<h4 style="margin-bottom:0.75rem;margin-top:1.5rem">Integration with VirtualHost Manager</h4>
|
||||
<p style="color:var(--text-muted);margin-bottom:0.75rem">When proxy mode is active, NovaCPX automatically:</p>
|
||||
<ul style="color:var(--text-muted);padding-left:1.2rem;line-height:1.8">
|
||||
<li>Creates a proxy host entry for every new account</li>
|
||||
<li>Removes the proxy host when an account is terminated</li>
|
||||
<li>Re-generates Nginx config on every account change</li>
|
||||
<li>Uses account SSL certs automatically if SSL is enabled on the proxy host</li>
|
||||
</ul>
|
||||
</div>
|
||||
`, null, { cancelLabel: 'Close', showConfirm: false });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user