mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
feat: dedicated ports per panel tier (8880/8881/8882)
Each panel now has its own dedicated port and is fully self-contained: - Port 8880: User panel (end-user hosting dashboard) - Port 8881: Reseller panel (account/package management) - Port 8882: Admin panel (datacenter/server manager) Changes: - install.sh: PORT_USER/PORT_RESELLER/PORT_ADMIN constants; three separate nginx/Apache vhosts; UFW opens all three ports; Fail2Ban jail per port; credentials file shows all three URLs - config.ini: stores port_user/port_reseller/port_admin - Core.php: defines PORT_USER/RESELLER/ADMIN, detects CURRENT_PORTAL from SERVER_PORT so the API knows which tier is being accessed - Auth.php: portalUrl() maps role → correct port for cross-portal redirects - auth.php endpoint: returns portal_url on login so JS redirects to right port - index.php login: uses portal_url from API response (no hardcoded paths) - admin/index.php: inline login form (port 8882 is self-contained, no redirect) - user/index.php: inline login form (port 8880 self-contained) - reseller/index.php: new full reseller panel with inline login (port 8881); sidebar with accounts, packages, DNS, branding, bandwidth report sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+127
-24
@@ -14,6 +14,11 @@ DB_NAME="novacpx"
|
||||
DB_USER="novacpx_user"
|
||||
PHP_DEFAULT="8.3"
|
||||
|
||||
# ── Panel ports (each tier has its own port) ──────────────────────────────────
|
||||
PORT_USER=8880 # End-user panel
|
||||
PORT_RESELLER=8881 # Reseller panel
|
||||
PORT_ADMIN=8882 # Admin / datacenter panel
|
||||
|
||||
# ── Colors ────────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
@@ -106,7 +111,9 @@ mkdir -p /root/.novacpx
|
||||
cat > /root/.novacpx/credentials.txt <<CREDS
|
||||
NovaCPX Installation Credentials — $(date)
|
||||
==========================================
|
||||
Panel URL: https://$(hostname -I | awk '{print $1}'):2083
|
||||
User Panel: https://$(hostname -I | awk '{print $1}'):${PORT_USER}
|
||||
Reseller Panel: https://$(hostname -I | awk '{print $1}'):${PORT_RESELLER}
|
||||
Admin Panel: https://$(hostname -I | awk '{print $1}'):${PORT_ADMIN}
|
||||
Admin User: admin
|
||||
Admin Pass: $ADMIN_PASS
|
||||
DB Name: $DB_NAME
|
||||
@@ -165,53 +172,131 @@ if [[ "$WEB_SERVER" == "nginx" ]]; then
|
||||
apt-get install -y -qq nginx >> "$LOG" 2>&1
|
||||
systemctl enable nginx >> "$LOG" 2>&1
|
||||
log "nginx installed"
|
||||
|
||||
PANEL_WEB_CONF="/etc/nginx/sites-available/novacpx"
|
||||
cat > "$PANEL_WEB_CONF" <<NGXCONF
|
||||
server {
|
||||
listen 2083 ssl http2;
|
||||
server_name _;
|
||||
root $WEB_ROOT;
|
||||
index index.php;
|
||||
# NovaCPX — three panels on three dedicated ports
|
||||
|
||||
# ── User Panel (8880) ─────────────────────────────────────────────────────────
|
||||
server {
|
||||
listen ${PORT_USER} ssl http2;
|
||||
server_name _;
|
||||
root ${WEB_ROOT}/user;
|
||||
index index.php;
|
||||
ssl_certificate /etc/novacpx/ssl/novacpx.crt;
|
||||
ssl_certificate_key /etc/novacpx/ssl/novacpx.key;
|
||||
|
||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
||||
location /api/ { fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME ${WEB_ROOT}/api/index.php; }
|
||||
location ~ \.php$ { fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; }
|
||||
location /assets/ { root ${WEB_ROOT}; }
|
||||
location ~ /\.ht { deny all; }
|
||||
}
|
||||
|
||||
# ── Reseller Panel (8881) ─────────────────────────────────────────────────────
|
||||
server {
|
||||
listen ${PORT_RESELLER} ssl http2;
|
||||
server_name _;
|
||||
root ${WEB_ROOT}/reseller;
|
||||
index index.php;
|
||||
ssl_certificate /etc/novacpx/ssl/novacpx.crt;
|
||||
ssl_certificate_key /etc/novacpx/ssl/novacpx.key;
|
||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||
location /api/ { fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME ${WEB_ROOT}/api/index.php; }
|
||||
location ~ \.php$ { fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; }
|
||||
location /assets/ { root ${WEB_ROOT}; }
|
||||
location ~ /\.ht { deny all; }
|
||||
}
|
||||
|
||||
# ── Admin Panel (8882) ────────────────────────────────────────────────────────
|
||||
server {
|
||||
listen ${PORT_ADMIN} ssl http2;
|
||||
server_name _;
|
||||
root ${WEB_ROOT}/admin;
|
||||
index index.php;
|
||||
ssl_certificate /etc/novacpx/ssl/novacpx.crt;
|
||||
ssl_certificate_key /etc/novacpx/ssl/novacpx.key;
|
||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||
location /api/ { fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME ${WEB_ROOT}/api/index.php; }
|
||||
location ~ \.php$ { fastcgi_pass unix:/run/php/php${PHP_DEFAULT}-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; }
|
||||
location /assets/ { root ${WEB_ROOT}; }
|
||||
location ~ /\.ht { deny all; }
|
||||
}
|
||||
NGXCONF
|
||||
ln -sf "$PANEL_WEB_CONF" /etc/nginx/sites-enabled/novacpx
|
||||
|
||||
else
|
||||
apt-get install -y -qq apache2 libapache2-mod-fcgid >> "$LOG" 2>&1
|
||||
a2enmod ssl rewrite proxy_fcgi setenvif headers >> "$LOG" 2>&1
|
||||
systemctl enable apache2 >> "$LOG" 2>&1
|
||||
log "Apache2 installed"
|
||||
|
||||
# Tell Apache to listen on all three panel ports
|
||||
for PORT in $PORT_USER $PORT_RESELLER $PORT_ADMIN; do
|
||||
grep -q "Listen $PORT" /etc/apache2/ports.conf 2>/dev/null || echo "Listen $PORT" >> /etc/apache2/ports.conf
|
||||
done
|
||||
|
||||
PANEL_WEB_CONF="/etc/apache2/sites-available/novacpx.conf"
|
||||
cat > "$PANEL_WEB_CONF" <<APCONF
|
||||
<VirtualHost *:2083>
|
||||
DocumentRoot $WEB_ROOT
|
||||
# NovaCPX — three panels on three dedicated ports
|
||||
|
||||
# ── User Panel (8880) ─────────────────────────────────────────────────────────
|
||||
<VirtualHost *:${PORT_USER}>
|
||||
DocumentRoot ${WEB_ROOT}/user
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/novacpx/ssl/novacpx.crt
|
||||
SSLCertificateKeyFile /etc/novacpx/ssl/novacpx.key
|
||||
|
||||
<Directory $WEB_ROOT>
|
||||
Alias /assets ${WEB_ROOT}/assets
|
||||
Alias /api ${WEB_ROOT}/api
|
||||
<Directory ${WEB_ROOT}>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<FilesMatch "\.php$">
|
||||
<FilesMatch "\.php\$">
|
||||
SetHandler "proxy:unix:/run/php/php${PHP_DEFAULT}-fpm.sock|fcgi://localhost/"
|
||||
</FilesMatch>
|
||||
Header always set X-NovaCPX-Portal "user"
|
||||
</VirtualHost>
|
||||
|
||||
# ── Reseller Panel (8881) ─────────────────────────────────────────────────────
|
||||
<VirtualHost *:${PORT_RESELLER}>
|
||||
DocumentRoot ${WEB_ROOT}/reseller
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/novacpx/ssl/novacpx.crt
|
||||
SSLCertificateKeyFile /etc/novacpx/ssl/novacpx.key
|
||||
Alias /assets ${WEB_ROOT}/assets
|
||||
Alias /api ${WEB_ROOT}/api
|
||||
<Directory ${WEB_ROOT}>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<FilesMatch "\.php\$">
|
||||
SetHandler "proxy:unix:/run/php/php${PHP_DEFAULT}-fpm.sock|fcgi://localhost/"
|
||||
</FilesMatch>
|
||||
Header always set X-NovaCPX-Portal "reseller"
|
||||
</VirtualHost>
|
||||
|
||||
# ── Admin Panel (8882) ────────────────────────────────────────────────────────
|
||||
<VirtualHost *:${PORT_ADMIN}>
|
||||
DocumentRoot ${WEB_ROOT}/admin
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/novacpx/ssl/novacpx.crt
|
||||
SSLCertificateKeyFile /etc/novacpx/ssl/novacpx.key
|
||||
Alias /assets ${WEB_ROOT}/assets
|
||||
Alias /api ${WEB_ROOT}/api
|
||||
<Directory ${WEB_ROOT}>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<FilesMatch "\.php\$">
|
||||
SetHandler "proxy:unix:/run/php/php${PHP_DEFAULT}-fpm.sock|fcgi://localhost/"
|
||||
</FilesMatch>
|
||||
Header always set X-NovaCPX-Portal "admin"
|
||||
</VirtualHost>
|
||||
APCONF
|
||||
a2ensite novacpx >> "$LOG" 2>&1
|
||||
# Enable PHP-FPM for default version
|
||||
a2enconf php${PHP_DEFAULT}-fpm >> "$LOG" 2>&1 || true
|
||||
fi
|
||||
|
||||
@@ -314,7 +399,9 @@ pass = ${DB_PASS}
|
||||
|
||||
[panel]
|
||||
secret = ${SECRET_KEY}
|
||||
port = 2083
|
||||
port_user = ${PORT_USER}
|
||||
port_reseller = ${PORT_RESELLER}
|
||||
port_admin = ${PORT_ADMIN}
|
||||
webroot = ${WEB_ROOT}
|
||||
version = ${NOVACPX_VERSION}
|
||||
|
||||
@@ -345,7 +432,9 @@ ufw default allow outgoing >> "$LOG" 2>&1
|
||||
ufw allow ssh >> "$LOG" 2>&1
|
||||
ufw allow 80/tcp >> "$LOG" 2>&1 # HTTP
|
||||
ufw allow 443/tcp >> "$LOG" 2>&1 # HTTPS
|
||||
ufw allow 2083/tcp >> "$LOG" 2>&1 # NovaCPX panel
|
||||
ufw allow ${PORT_USER}/tcp >> "$LOG" 2>&1 # NovaCPX user panel
|
||||
ufw allow ${PORT_RESELLER}/tcp >> "$LOG" 2>&1 # NovaCPX reseller panel
|
||||
ufw allow ${PORT_ADMIN}/tcp >> "$LOG" 2>&1 # NovaCPX admin panel
|
||||
ufw allow 21/tcp >> "$LOG" 2>&1 # FTP
|
||||
ufw allow 20/tcp >> "$LOG" 2>&1 # FTP data
|
||||
ufw allow 25/tcp >> "$LOG" 2>&1 # SMTP
|
||||
@@ -371,11 +460,23 @@ maxretry = 5
|
||||
[sshd]
|
||||
enabled = true
|
||||
|
||||
[novacpx-panel]
|
||||
[novacpx-user]
|
||||
enabled = true
|
||||
port = 2083
|
||||
port = ${PORT_USER}
|
||||
logpath = /var/log/novacpx/access.log
|
||||
maxretry = 10
|
||||
|
||||
[novacpx-reseller]
|
||||
enabled = true
|
||||
port = ${PORT_RESELLER}
|
||||
logpath = /var/log/novacpx/access.log
|
||||
maxretry = 10
|
||||
|
||||
[novacpx-admin]
|
||||
enabled = true
|
||||
port = ${PORT_ADMIN}
|
||||
logpath = /var/log/novacpx/access.log
|
||||
maxretry = 5
|
||||
F2B
|
||||
systemctl enable fail2ban >> "$LOG" 2>&1
|
||||
systemctl restart fail2ban >> "$LOG" 2>&1
|
||||
@@ -411,11 +512,13 @@ cat <<DONE
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ NovaCPX Installation Complete! ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Panel URL: https://${SERVER_IP}:2083
|
||||
║ User Panel: https://${SERVER_IP}:${PORT_USER}
|
||||
║ Reseller Panel: https://${SERVER_IP}:${PORT_RESELLER}
|
||||
║ Admin Panel: https://${SERVER_IP}:${PORT_ADMIN}
|
||||
║ Username: admin
|
||||
║ Password: ${ADMIN_PASS}
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Credentials saved to: /root/.novacpx/credentials.txt ║
|
||||
║ Credentials: /root/.novacpx/credentials.txt ║
|
||||
║ Install log: ${LOG}
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ match ($action) {
|
||||
audit('login', 'auth');
|
||||
Response::success([
|
||||
'token' => $token,
|
||||
'portal_url' => Auth::portalUrl($user['role']),
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
|
||||
@@ -97,4 +97,19 @@ class Auth {
|
||||
Response::error('Forbidden', 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the correct panel URL for a given role
|
||||
* Used by login redirect so each role lands on the right port
|
||||
*/
|
||||
public static function portalUrl(string $role, string $path = '/'): string {
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$hostname = preg_replace('/:\d+$/', '', $host);
|
||||
$port = match($role) {
|
||||
'admin' => PORT_ADMIN,
|
||||
'reseller' => PORT_RESELLER,
|
||||
default => PORT_USER,
|
||||
};
|
||||
return "https://{$hostname}:{$port}{$path}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,19 @@ define('DB_USER', $_cfg['database']['user'] ?? '');
|
||||
define('DB_PASS', $_cfg['database']['pass'] ?? '');
|
||||
define('SECRET_KEY', $_cfg['panel']['secret'] ?? '');
|
||||
define('PANEL_VER', $_cfg['panel']['version'] ?? NOVACPX_VERSION);
|
||||
define('PORT_USER', (int)($_cfg['panel']['port_user'] ?? 8880));
|
||||
define('PORT_RESELLER', (int)($_cfg['panel']['port_reseller'] ?? 8881));
|
||||
define('PORT_ADMIN', (int)($_cfg['panel']['port_admin'] ?? 8882));
|
||||
define('WEB_SERVER', $_cfg['web']['server'] ?? 'apache');
|
||||
define('PHP_DEFAULT', $_cfg['web']['php_default'] ?? '8.3');
|
||||
|
||||
// Detect which portal is being accessed by the request port
|
||||
$requestPort = (int)($_SERVER['SERVER_PORT'] ?? 0);
|
||||
define('CURRENT_PORTAL',
|
||||
$requestPort === PORT_ADMIN ? 'admin' :
|
||||
($requestPort === PORT_RESELLER ? 'reseller' : 'user')
|
||||
);
|
||||
|
||||
function novacpx_log(string $level, string $msg, array $ctx = []): void {
|
||||
$line = sprintf("[%s] [%s] %s %s\n",
|
||||
date('Y-m-d H:i:s'), strtoupper($level), $msg,
|
||||
|
||||
@@ -165,9 +165,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth guard -->
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh">
|
||||
<div style="text-align:center;color:var(--text-muted)">Verifying session…</div>
|
||||
<!-- Auth guard / Login overlay (shown when not authenticated) -->
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--bg)">
|
||||
<div style="width:100%;max-width:400px;padding:1.5rem">
|
||||
<div style="text-align:center;margin-bottom:1.5rem">
|
||||
<svg viewBox="0 0 40 40" fill="none" style="width:40px;height:40px;margin:0 auto 1rem">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#alg1)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#alg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#alg2)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="alg1" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
<linearGradient id="alg2" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div style="font-size:1.4rem;font-weight:300">Nova<strong style="font-weight:700;background:linear-gradient(135deg,#6366f1,#0ea5e9);-webkit-background-clip:text;-webkit-text-fill-color:transparent">CPX</strong></div>
|
||||
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.25rem;text-transform:uppercase;letter-spacing:.1em">Admin Panel · Port 8882</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="login-err" class="alert alert-error" style="display:none"></div>
|
||||
<form id="login-form">
|
||||
<div class="form-group"><label>Username or Email</label><input type="text" id="l-user" autofocus required></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" id="l-pass" required></div>
|
||||
<button type="submit" class="btn btn-primary btn-full" id="l-btn">Sign In to Admin</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/nova.js"></script>
|
||||
|
||||
@@ -3,9 +3,31 @@
|
||||
*/
|
||||
(async () => {
|
||||
// ── Auth guard ─────────────────────────────────────────────────────────────
|
||||
// Inline login handler on port 8882
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('l-btn');
|
||||
const err = document.getElementById('login-err');
|
||||
btn.disabled = true; btn.textContent = 'Signing in…'; err.style.display = 'none';
|
||||
const res = await Nova.api('auth', 'login', {
|
||||
method: 'POST',
|
||||
body: { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value }
|
||||
});
|
||||
if (res?.success && res.data?.user?.role === 'admin') {
|
||||
location.reload();
|
||||
} else {
|
||||
err.textContent = res?.message || 'Invalid credentials or insufficient role';
|
||||
err.style.display = '';
|
||||
btn.disabled = false; btn.textContent = 'Sign In to Admin';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const me = await Nova.api('auth', 'me');
|
||||
if (!me?.success || me.data.role !== 'admin') {
|
||||
location.href = '/?redirect=/admin/';
|
||||
// Already showing the login form in #auth-check
|
||||
return;
|
||||
}
|
||||
document.getElementById('auth-check').style.display = 'none';
|
||||
|
||||
@@ -96,8 +96,8 @@ document.getElementById('login-form').addEventListener('submit', async e => {
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message || 'Login failed');
|
||||
|
||||
const role = data.data.user.role;
|
||||
const dest = REDIRECT || (role === 'admin' ? '/admin/' : role === 'reseller' ? '/reseller/' : '/user/');
|
||||
// Each role redirects to its dedicated port
|
||||
const dest = REDIRECT || data.data.portal_url || '/';
|
||||
location.href = dest;
|
||||
} catch (ex) {
|
||||
err.textContent = ex.message;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
// NovaCPX Reseller Panel — port 8881
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NovaCPX Reseller</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/css/nova.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="panel-layout" id="app" style="display:none">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<svg class="logo-icon" viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#rlg1)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#rlg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#rlg2)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="rlg1" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
<linearGradient id="rlg2" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="logo-text">Nova<strong>CPX</strong> <small style="font-size:.65rem;color:var(--text-muted)">Reseller</small></span>
|
||||
</div>
|
||||
<nav>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Overview</div>
|
||||
<a href="#" class="sidebar-link active" data-page="dashboard">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg> Dashboard
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="resource-usage">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> Resource Usage
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Accounts</div>
|
||||
<a href="#" class="sidebar-link" data-page="accounts">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> My Accounts
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="create-account">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg> Create Account
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="suspended">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg> Suspended
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Plans</div>
|
||||
<a href="#" class="sidebar-link" data-page="packages">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> My Packages
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="create-package">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Create Package
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">DNS & Domains</div>
|
||||
<a href="#" class="sidebar-link" data-page="dns">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 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> DNS Zones
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="nameservers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> Nameservers
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Branding</div>
|
||||
<a href="#" class="sidebar-link" data-page="branding">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg> White Label
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="emails">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> Email Templates
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Reports</div>
|
||||
<a href="#" class="sidebar-link" data-page="bandwidth">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> Bandwidth Report
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="logs">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/></svg> Account Logs
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-info">
|
||||
<div class="avatar" id="user-avatar">R</div>
|
||||
<div><div class="user-name" id="user-name">Reseller</div><div class="user-role">Reseller Account</div></div>
|
||||
<a href="#" id="logout-btn" class="btn btn-ghost btn-sm btn-icon" title="Logout" style="margin-left:auto">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-content">
|
||||
<header class="topbar">
|
||||
<div class="topbar-title" id="page-title">Reseller Dashboard</div>
|
||||
</header>
|
||||
<div class="page-content" id="page-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth guard / Inline login -->
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--bg)">
|
||||
<div style="width:100%;max-width:400px;padding:1.5rem">
|
||||
<div style="text-align:center;margin-bottom:1.5rem">
|
||||
<svg viewBox="0 0 40 40" fill="none" style="width:40px;height:40px;margin:0 auto 1rem">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#rlga)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#rlgb)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#rlgb)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="rlga" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
<linearGradient id="rlgb" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div style="font-size:1.4rem;font-weight:300">Nova<strong style="font-weight:700;background:linear-gradient(135deg,#6366f1,#0ea5e9);-webkit-background-clip:text;-webkit-text-fill-color:transparent">CPX</strong></div>
|
||||
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.25rem;text-transform:uppercase;letter-spacing:.1em">Reseller Panel · Port 8881</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="login-err" class="alert alert-error" style="display:none"></div>
|
||||
<form id="login-form">
|
||||
<div class="form-group"><label>Username or Email</label><input type="text" id="l-user" autofocus required></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" id="l-pass" required></div>
|
||||
<button type="submit" class="btn btn-primary btn-full" id="l-btn">Sign In to Reseller Panel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/nova.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
// Inline login on port 8881
|
||||
document.getElementById('login-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('l-btn');
|
||||
const err = document.getElementById('login-err');
|
||||
btn.disabled = true; btn.textContent = 'Signing in…'; err.style.display = 'none';
|
||||
const res = await Nova.api('auth', 'login', {
|
||||
method: 'POST',
|
||||
body: { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value }
|
||||
});
|
||||
if (res?.success && ['reseller','admin'].includes(res.data?.user?.role)) {
|
||||
location.reload();
|
||||
} else {
|
||||
err.textContent = res?.message || 'Invalid credentials or insufficient role';
|
||||
err.style.display = '';
|
||||
btn.disabled = false; btn.textContent = 'Sign In to Reseller Panel';
|
||||
}
|
||||
});
|
||||
|
||||
const me = await Nova.api('auth', 'me');
|
||||
if (!me?.success || !['reseller','admin'].includes(me.data.role)) { return; }
|
||||
|
||||
document.getElementById('auth-check').style.display = 'none';
|
||||
document.getElementById('app').style.display = '';
|
||||
document.getElementById('user-name').textContent = me.data.username;
|
||||
document.getElementById('user-avatar').textContent = me.data.username[0].toUpperCase();
|
||||
|
||||
document.getElementById('logout-btn').addEventListener('click', async e => {
|
||||
e.preventDefault();
|
||||
await Nova.api('auth', 'logout', { method: 'POST' });
|
||||
location.reload();
|
||||
});
|
||||
|
||||
// Simple dashboard
|
||||
document.getElementById('page-content').innerHTML = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card"><div class="stat-label">Total Accounts</div><div class="stat-value stat-blue">0</div><div class="stat-sub">Active hosting accounts</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Disk Used</div><div class="stat-value">0 GB</div><div class="stat-sub">of allocated quota</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Bandwidth</div><div class="stat-value">0 GB</div><div class="stat-sub">this month</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Packages</div><div class="stat-value stat-green">0</div><div class="stat-sub">Active plans</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Quick Actions</span></div>
|
||||
<div class="card-body flex gap-2">
|
||||
<button class="btn btn-primary">Create Account</button>
|
||||
<button class="btn btn-ghost">Create Package</button>
|
||||
<button class="btn btn-ghost">View DNS Zones</button>
|
||||
</div>
|
||||
</div>`;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -167,15 +167,61 @@ svg.ring circle { transition: stroke-dashoffset .5s; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh">
|
||||
<div style="text-align:center;color:var(--text-muted)">Loading…</div>
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--bg)">
|
||||
<div style="width:100%;max-width:400px;padding:1.5rem">
|
||||
<div style="text-align:center;margin-bottom:1.5rem">
|
||||
<svg viewBox="0 0 40 40" fill="none" style="width:40px;height:40px;margin:0 auto 1rem">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#ulg3)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#ulg4)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#ulg4)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="ulg3" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
<linearGradient id="ulg4" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div style="font-size:1.4rem;font-weight:300">Nova<strong style="font-weight:700;background:linear-gradient(135deg,#6366f1,#0ea5e9);-webkit-background-clip:text;-webkit-text-fill-color:transparent">CPX</strong></div>
|
||||
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.25rem;text-transform:uppercase;letter-spacing:.1em">My Hosting · Port 8880</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="login-err" class="alert alert-error" style="display:none"></div>
|
||||
<form id="login-form">
|
||||
<div class="form-group"><label>Username or Email</label><input type="text" id="l-user" autofocus required></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" id="l-pass" required></div>
|
||||
<button type="submit" class="btn btn-primary btn-full" id="l-btn">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/nova.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
// Inline login on port 8880
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('l-btn');
|
||||
const err = document.getElementById('login-err');
|
||||
btn.disabled = true; btn.textContent = 'Signing in…'; err.style.display = 'none';
|
||||
const res = await Nova.api('auth', 'login', {
|
||||
method: 'POST',
|
||||
body: { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value }
|
||||
});
|
||||
if (res?.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
err.textContent = res?.message || 'Invalid credentials';
|
||||
err.style.display = '';
|
||||
btn.disabled = false; btn.textContent = 'Sign In';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const me = await Nova.api('auth', 'me');
|
||||
if (!me?.success) { location.href = '/?redirect=/user/'; return; }
|
||||
if (!me?.success) { return; } // Login form already visible
|
||||
|
||||
document.getElementById('auth-check').style.display = 'none';
|
||||
document.getElementById('app').style.display = '';
|
||||
|
||||
Reference in New Issue
Block a user