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:
2026-06-08 00:29:04 +00:00
parent 90ab33ccf0
commit 0ab3d8d584
5 changed files with 624 additions and 0 deletions
+19
View File
@@ -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`;
+100
View File
@@ -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);
}
+252
View File
@@ -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';
}
}
+4
View File
@@ -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
+249
View File
@@ -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 });
};