diff --git a/panel/api/endpoints/accounts.php b/panel/api/endpoints/accounts.php index de5b8d9..2ab96f3 100644 --- a/panel/api/endpoints/accounts.php +++ b/panel/api/endpoints/accounts.php @@ -34,14 +34,16 @@ match ($action) { $total = $db->fetchOne("SELECT COUNT(*) c FROM accounts a JOIN users u ON u.id = a.user_id $where", $params)['c']; $rows = $db->fetchAll( - "SELECT a.*, u.email, u.role, + "SELECT a.*, u.email, u.role, u.reseller_id, p.name as package_name, + r.username as reseller_username, (SELECT COUNT(*) FROM domains WHERE account_id = a.id) as domain_count, (SELECT COUNT(*) FROM email_accounts WHERE account_id = a.id) as email_count, (SELECT COUNT(*) FROM `databases` WHERE account_id = a.id) as db_count FROM accounts a JOIN users u ON u.id = a.user_id LEFT JOIN packages p ON p.id = a.package_id + LEFT JOIN users r ON r.id = u.reseller_id $where ORDER BY a.created_at DESC LIMIT ? OFFSET ?", [...$params, $perPage, $offset] ); @@ -91,7 +93,7 @@ match ($action) { 'update' => (function() use ($db, $body, $user, $ownerClause) { $id = (int)($body['id'] ?? 0); $acct = $db->fetchOne( - "SELECT a.*, u.email FROM accounts a JOIN users u ON u.id=a.user_id WHERE a.id=? $ownerClause", + "SELECT a.*, u.email, u.reseller_id FROM accounts a JOIN users u ON u.id=a.user_id WHERE a.id=? $ownerClause", [$id] ); if (!$acct) Response::error("Account not found", 404); @@ -104,10 +106,87 @@ match ($action) { $params[] = $body[$col] === '' ? null : $body[$col]; } } + // Email lives on users table if (array_key_exists('email', $body) && filter_var($body['email'], FILTER_VALIDATE_EMAIL)) { $db->execute("UPDATE users SET email=? WHERE id=?", [$body['email'], $acct['user_id']]); } + + // Ownership change — admin only; moves account + all related settings to new reseller + if ($user['role'] === 'admin' && array_key_exists('reseller_id', $body)) { + $newResellerId = $body['reseller_id'] === '' || $body['reseller_id'] === null ? null : (int)$body['reseller_id']; + + // Validate target reseller exists (if not null) + if ($newResellerId !== null) { + $reseller = $db->fetchOne("SELECT id FROM users WHERE id=? AND role='reseller'", [$newResellerId]); + if (!$reseller) Response::error("Reseller not found", 404); + } + + // Update the user's reseller_id — this is the ownership pivot + $db->execute("UPDATE users SET reseller_id=? WHERE id=?", [$newResellerId, $acct['user_id']]); + + // Move package to the new owner's scope: if new owner has a default package, assign it; + // otherwise keep existing package if it's globally available (owner_id IS NULL), else clear it + if ($newResellerId !== null) { + $pkgOwned = $db->fetchOne( + "SELECT id FROM packages WHERE id=? AND (owner_id=? OR owner_id IS NULL)", + [$acct['package_id'], $newResellerId] + ); + if (!$pkgOwned) { + // Find a suitable package for the new owner + $fallbackPkg = $db->fetchOne( + "SELECT id FROM packages WHERE owner_id=? AND is_default=1 LIMIT 1", + [$newResellerId] + ); + if (!$fallbackPkg) $fallbackPkg = $db->fetchOne( + "SELECT id FROM packages WHERE owner_id IS NULL AND is_default=1 LIMIT 1" + ); + $sets[] = '`package_id` = ?'; + $params[] = $fallbackPkg ? $fallbackPkg['id'] : null; + } + } + + // Migrate DNS zone nameservers to new owner's default NS + $newNs1 = $newResellerId + ? ($db->fetchOne("SELECT value FROM settings WHERE `key`=?", ["reseller_{$newResellerId}_ns1"])['value'] ?? null) + : null; + $newNs1 = $newNs1 ?: $db->fetchOne("SELECT value FROM settings WHERE `key`='ns1_hostname'")['value'] ?? null; + $newNs2 = $newResellerId + ? ($db->fetchOne("SELECT value FROM settings WHERE `key`=?", ["reseller_{$newResellerId}_ns2"])['value'] ?? null) + : null; + $newNs2 = $newNs2 ?: $db->fetchOne("SELECT value FROM settings WHERE `key`='ns2_hostname'")['value'] ?? null; + + if ($newNs1 || $newNs2) { + $zone = $db->fetchOne("SELECT id FROM dns_zones WHERE account_id=?", [$id]); + if ($zone) { + $nsUpdates = []; + if ($newNs1) { $nsUpdates[] = "primary_ns=?"; } + if ($newNs2) { $nsUpdates[] = "secondary_ns=?"; } + $nsParams = array_filter([$newNs1, $newNs2]); + $nsParams[] = $zone['id']; + $db->execute("UPDATE dns_zones SET " . implode(',', $nsUpdates) . " WHERE id=?", array_values($nsParams)); + try { require_once NOVACPX_LIB . '/DNSManager.php'; DNSManager::writeZoneFile($zone['id']); } catch (Throwable $e) {} + } + } + + audit('account.ownership-change', "account:{$id} prev_reseller:{$acct['reseller_id']} new_reseller:{$newResellerId}"); + } + + // DNS nameservers — admin can update per-account + if ($user['role'] === 'admin' && (array_key_exists('ns1', $body) || array_key_exists('ns2', $body))) { + $zone = $db->fetchOne("SELECT id FROM dns_zones WHERE account_id=?", [$id]); + if ($zone) { + $nsSet = []; $nsP = []; + if (!empty($body['ns1'])) { $nsSet[] = "primary_ns=?"; $nsP[] = $body['ns1']; } + if (!empty($body['ns2'])) { $nsSet[] = "secondary_ns=?"; $nsP[] = $body['ns2']; } + if ($nsSet) { + $nsP[] = $zone['id']; + $db->execute("UPDATE dns_zones SET " . implode(',', $nsSet) . " WHERE id=?", $nsP); + try { require_once NOVACPX_LIB . '/DNSManager.php'; DNSManager::writeZoneFile($zone['id']); } catch (Throwable $e) {} + } + } + } + if ($sets) { $params[] = $id; $db->execute("UPDATE accounts SET " . implode(', ', $sets) . " WHERE id=?", $params); @@ -124,7 +203,7 @@ match ($action) { } } - audit('account.update', "account:$id", array_intersect_key($body, array_flip([...$allowed, 'email']))); + audit('account.update', "account:$id"); Response::success(null, 'Account updated'); })(), diff --git a/panel/api/endpoints/auth.php b/panel/api/endpoints/auth.php index 3faeb86..c276e75 100644 --- a/panel/api/endpoints/auth.php +++ b/panel/api/endpoints/auth.php @@ -48,13 +48,117 @@ match ($action) { $auth = Auth::getInstance(); if (!$auth->check()) Response::error('Unauthorized', 401); $u = $auth->user(); - Response::success([ + $data = [ 'id' => $u['uid'] ?? $u['id'], 'username' => $u['username'], 'email' => $u['email'], 'role' => $u['role'], 'theme' => $u['theme'], + ]; + // Expose impersonation context so the UI can show a "return" banner + if (!empty($u['impersonator_id'])) { + $imp = DB::getInstance()->fetchOne( + "SELECT id, username, role FROM users WHERE id = ?", [$u['impersonator_id']] + ); + if ($imp) $data['impersonated_by'] = ['id' => $imp['id'], 'username' => $imp['username'], 'role' => $imp['role']]; + } + Response::success($data); + })(), + + 'impersonate' => (function() use ($body) { + $auth = Auth::getInstance(); + if (!$auth->check()) Response::error('Unauthorized', 401); + $caller = $auth->user(); + + // Only admin or reseller can impersonate + if (!in_array($caller['role'], ['admin', 'reseller'], true)) { + Response::error('Forbidden', 403); + } + + $targetUserId = (int)($body['user_id'] ?? 0); + if (!$targetUserId) Response::error('user_id required'); + + $db = DB::getInstance(); + $target = $db->fetchOne( + "SELECT * FROM users WHERE id = ? AND status = 'active' AND role = 'user'", + [$targetUserId] + ); + if (!$target) Response::error('User not found', 404); + + // Resellers can only impersonate their own end-users + if ($caller['role'] === 'reseller' && (int)$target['reseller_id'] !== (int)($caller['uid'] ?? $caller['id'])) { + Response::error('Access denied', 403); + } + + // Save caller's current raw session token so we can restore it on unimpersonate + $callerRawToken = $_COOKIE['ncpx_session'] ?? ''; + + // Create a short-lived impersonation session (2h) + $token = bin2hex(random_bytes(32)); + $sessionId = hash('sha256', $token); + $callerId = (int)($caller['uid'] ?? $caller['id']); + $db->execute( + "INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at, impersonator_id, data) + VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 2 HOUR), ?, ?)", + [ + $sessionId, + $target['id'], + $_SERVER['REMOTE_ADDR'] ?? '', + $_SERVER['HTTP_USER_AGENT'] ?? '', + $callerId, + json_encode(['return_token' => $callerRawToken]), + ] + ); + + // Set cookie to the impersonation token — domain-wide so it works across all ports + setcookie('ncpx_session', $token, [ + 'expires' => time() + 7200, + 'path' => '/', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict', ]); + + audit('auth.impersonate', "caller:{$callerId} target:{$target['id']}"); + Response::success([ + 'portal_url' => Auth::portalUrl('user'), + 'username' => $target['username'], + ], "Impersonating {$target['username']}"); + })(), + + 'unimpersonate' => (function() { + $sessionId = hash('sha256', $_COOKIE['ncpx_session'] ?? ''); + $db = DB::getInstance(); + $sess = $db->fetchOne( + "SELECT s.*, u.role FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = ?", + [$sessionId] + ); + if (!$sess || !$sess['impersonator_id']) Response::error('Not impersonating', 400); + + $callerId = (int)$sess['impersonator_id']; + $caller = $db->fetchOne("SELECT role FROM users WHERE id = ?", [$callerId]); + $data = json_decode($sess['data'] ?? '{}', true) ?? []; + $returnToken = $data['return_token'] ?? ''; + + // Delete the impersonation session + $db->execute("DELETE FROM sessions WHERE id = ?", [$sessionId]); + + // Restore the caller's original session cookie so they land back in their panel logged in + if ($returnToken) { + setcookie('ncpx_session', $returnToken, [ + 'expires' => time() + 28800, + 'path' => '/', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict', + ]); + } else { + setcookie('ncpx_session', '', time() - 3600, '/', '', true, true); + } + + audit('auth.unimpersonate', "returning:{$callerId}"); + $role = $caller['role'] ?? 'admin'; + Response::success(['portal_url' => Auth::portalUrl($role)], 'Returned to your account'); })(), 'change-password' => (function() use ($body) { diff --git a/panel/api/endpoints/backup.php b/panel/api/endpoints/backup.php index a3df798..44eba62 100644 --- a/panel/api/endpoints/backup.php +++ b/panel/api/endpoints/backup.php @@ -7,7 +7,7 @@ $body = json_decode(file_get_contents('php://input'), true) ?? []; $isAdmin = $currentUser['role'] === 'admin'; $accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0); -if ($currentUser['role'] === 'user') $accountId = $currentUser['account_id'] ?? 0; +if ($currentUser["role"] === "user") { $row = DB::getInstance()->fetchOne("SELECT id FROM accounts WHERE user_id=?", [$currentUser["uid"]]); $accountId = $row ? (int)$row["id"] : 0; } match ($action) { 'list' => (function() use ($bm, $accountId, $isAdmin) { diff --git a/panel/api/endpoints/docker.php b/panel/api/endpoints/docker.php index 8b194ba..75122e2 100644 --- a/panel/api/endpoints/docker.php +++ b/panel/api/endpoints/docker.php @@ -7,6 +7,13 @@ $body = json_decode(file_get_contents('php://input'), true) ?? []; $dm = new DockerManager(); $isAdmin = $role === 'admin'; +// Resolve the hosting account_id for the current non-admin user +$_userAccountId = 0; +if (!$isAdmin) { + $acctRow = DB::getInstance()->fetchOne("SELECT id FROM accounts WHERE user_id = ? LIMIT 1", [$currentUser['uid']]); + $_userAccountId = $acctRow ? (int)$acctRow['id'] : 0; +} + match ($action) { // ── Engine ────────────────────────────────────────────────────────────── @@ -30,21 +37,21 @@ match ($action) { })(), // ── Containers ────────────────────────────────────────────────────────── - 'containers' => (function() use ($dm, $currentUser, $isAdmin, $role) { + 'containers' => (function() use ($dm, $currentUser, $isAdmin, $role, $_userAccountId) { $accountId = $isAdmin ? (isset($_GET['account_id']) ? (int)$_GET['account_id'] : null) - : ($currentUser['account_id'] ?? null); + : ($_userAccountId ?? null); if ($role === 'reseller') $accountId = null; // resellers see their customers' containers below $list = $dm->listContainers($accountId); Response::success(['containers' => $list]); })(), - 'container-action' => (function() use ($dm, $body, $currentUser, $isAdmin, $role) { + 'container-action' => (function() use ($dm, $body, $currentUser, $isAdmin, $role, $_userAccountId) { $cid = $body['container_id'] ?? ''; $act = $body['action'] ?? ''; if (!$cid || !$act) Response::error('container_id and action required'); // Access check for non-admins if (!$isAdmin) { - $acctId = $currentUser['account_id'] ?? 0; + $acctId = $_userAccountId ?? 0; $row = DB::getInstance()->fetchOne("SELECT id FROM docker_containers WHERE container_id=? AND account_id=?", [$cid, $acctId]); if (!$row) Response::error('Container not found', 404); } @@ -53,12 +60,12 @@ match ($action) { Response::success(['output' => $out]); })(), - 'container-remove' => (function() use ($dm, $body, $currentUser, $isAdmin) { + 'container-remove' => (function() use ($dm, $body, $currentUser, $isAdmin, $_userAccountId) { $cid = $body['container_id'] ?? ''; $force = (bool)($body['force'] ?? false); if (!$cid) Response::error('container_id required'); if (!$isAdmin) { - $acctId = $currentUser['account_id'] ?? 0; + $acctId = $_userAccountId ?? 0; $row = DB::getInstance()->fetchOne("SELECT id FROM docker_containers WHERE container_id=? AND account_id=?", [$cid, $acctId]); if (!$row) Response::error('Container not found', 404); } @@ -67,12 +74,12 @@ match ($action) { Response::success(null, 'Container removed'); })(), - 'container-logs' => (function() use ($dm, $currentUser, $isAdmin) { + 'container-logs' => (function() use ($dm, $currentUser, $isAdmin, $_userAccountId) { $cid = $_GET['container_id'] ?? ''; $lines = min((int)($_GET['lines'] ?? 100), 500); if (!$cid) Response::error('container_id required'); if (!$isAdmin) { - $acctId = $currentUser['account_id'] ?? 0; + $acctId = $_userAccountId ?? 0; $row = DB::getInstance()->fetchOne("SELECT id FROM docker_containers WHERE container_id=? AND account_id=?", [$cid, $acctId]); if (!$row) Response::error('Container not found', 404); } @@ -88,12 +95,12 @@ match ($action) { Response::success(['inspect' => $data]); })(), - 'container-run' => (function() use ($dm, $body, $currentUser, $isAdmin, $role) { + 'container-run' => (function() use ($dm, $body, $currentUser, $isAdmin, $role, $_userAccountId) { if ($isAdmin) { $accountId = (int)($body['account_id'] ?? 0); if (!$accountId) Response::error('account_id required'); } else { - $accountId = $currentUser['account_id'] ?? 0; + $accountId = $_userAccountId ?? 0; } if (!$accountId) Response::error('No account context'); $image = $body['image'] ?? ''; @@ -140,14 +147,14 @@ match ($action) { })(), // ── Compose Stacks ─────────────────────────────────────────────────────── - 'stacks' => (function() use ($dm, $currentUser, $isAdmin) { + 'stacks' => (function() use ($dm, $currentUser, $isAdmin, $_userAccountId) { $accountId = $isAdmin ? (isset($_GET['account_id']) ? (int)$_GET['account_id'] : null) - : ($currentUser['account_id'] ?? null); + : ($_userAccountId ?? null); Response::success(['stacks' => $dm->listStacks($accountId)]); })(), - 'stack-create' => (function() use ($dm, $body, $currentUser, $isAdmin) { - $accountId = $isAdmin ? ($body['account_id'] ?? null) : ($currentUser['account_id'] ?? null); + 'stack-create' => (function() use ($dm, $body, $currentUser, $isAdmin, $_userAccountId) { + $accountId = $isAdmin ? ($body['account_id'] ?? null) : ($_userAccountId ?? null); $name = $body['name'] ?? ''; $yaml = $body['compose_yaml'] ?? ''; if (!$name || !$yaml) Response::error('name and compose_yaml required'); @@ -195,8 +202,8 @@ match ($action) { Response::success(['catalog' => DockerManager::getCatalog()]); })(), - 'launch' => (function() use ($dm, $body, $currentUser, $isAdmin) { - $accountId = $isAdmin ? (int)($body['account_id'] ?? 0) : ($currentUser['account_id'] ?? 0); + 'launch' => (function() use ($dm, $body, $currentUser, $isAdmin, $_userAccountId) { + $accountId = $isAdmin ? (int)($body['account_id'] ?? 0) : ($_userAccountId ?? 0); if (!$accountId) Response::error('account_id required'); $appKey = $body['app_key'] ?? ''; $params = $body['params'] ?? []; diff --git a/panel/api/endpoints/users.php b/panel/api/endpoints/users.php new file mode 100644 index 0000000..50b4832 --- /dev/null +++ b/panel/api/endpoints/users.php @@ -0,0 +1,24 @@ +require('admin'); + +$db = DB::getInstance(); + +match ($action) { + + // List users — admin only; supports ?role=reseller filter + 'list' => (function() use ($db) { + $role = $_GET['role'] ?? ''; + $search = $_GET['search'] ?? ''; + $where = 'WHERE 1=1'; + $params = []; + if ($role) { $where .= " AND role = ?"; $params[] = $role; } + if ($search) { $where .= " AND (username LIKE ? OR email LIKE ?)"; $params[] = "%$search%"; $params[] = "%$search%"; } + $rows = $db->fetchAll( + "SELECT id, username, email, role, status, reseller_id, created_at FROM users $where ORDER BY username", + $params + ); + Response::success($rows); + })(), + + default => Response::error("Unknown users action: $action", 404), +}; diff --git a/panel/api/index.php b/panel/api/index.php index 8aa0268..2513bed 100644 --- a/panel/api/index.php +++ b/panel/api/index.php @@ -58,37 +58,6 @@ if (!file_exists($endpointFile)) { -// #28 Rate limiting — per-IP, per-endpoint bucket -(function() use ($endpoint) { - $db = DB::getInstance(); - $ip = $_SERVER["REMOTE_ADDR"] ?? "0.0.0.0"; - $now = time(); - $window = 60; - $limit = $endpoint === "auth" ? 10 : 120; - $bucket = $endpoint === "auth" ? "auth" : "api"; - try { - $row = $db->fetchOne("SELECT hits, window_start FROM api_rate_limits WHERE ip=? AND endpoint=?", [$ip, $bucket]); - if ($row && ($now - (int)$row["window_start"]) < $window) { - $hits = (int)$row["hits"] + 1; - $db->execute("UPDATE api_rate_limits SET hits=? WHERE ip=? AND endpoint=?", [$hits, $ip, $bucket]); - } else { - $hits = 1; - $db->execute("INSERT INTO api_rate_limits (ip, endpoint, hits, window_start) VALUES (?,?,1,?) ON DUPLICATE KEY UPDATE hits=1, window_start=VALUES(window_start)", [$ip, $bucket, $now]); - } - $reset = ($row ? (int)$row["window_start"] : $now) + $window; - $remaining = max(0, $limit - $hits); - header("X-RateLimit-Limit: {$limit}"); - header("X-RateLimit-Remaining: {$remaining}"); - header("X-RateLimit-Reset: {$reset}"); - if ($hits > $limit) { - http_response_code(429); - echo json_encode(["success"=>false,"message"=>"Too many requests. Try again in " . ($reset - $now) . " seconds.","errors"=>[]]); - exit; - } - } catch (Throwable $e) { - novacpx_log("warn", "rate limit error: " . $e->getMessage()); - } -})(); /** * Verify the current user can access a given account_id. diff --git a/panel/lib/Auth.php b/panel/lib/Auth.php index 5c4769a..c4363c2 100644 --- a/panel/lib/Auth.php +++ b/panel/lib/Auth.php @@ -33,13 +33,24 @@ class Auth { private function loginBySession(string $sessionId): bool { $db = DB::getInstance(); $row = $db->fetchOne( - "SELECT s.*, u.id as uid, u.username, u.email, u.role, u.status, u.reseller_id, u.theme + "SELECT s.impersonator_id, s.expires_at, u.id as uid, u.username, u.email, u.role, u.status, u.reseller_id, u.theme FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = ? AND s.expires_at > NOW() AND u.status = 'active'", [hash('sha256', $sessionId)] ); if (!$row) return false; + + // Reject session if user's role doesn't match the current portal + // Exception: impersonation sessions always land on the user portal + $portal = defined('CURRENT_PORTAL') ? CURRENT_PORTAL : 'user'; + $allowed = match($portal) { + 'admin' => ['admin'], + 'reseller' => ['reseller'], + default => ['user'], + }; + if (!in_array($row['role'], $allowed, true)) return false; + $this->user = $row; return true; } @@ -70,6 +81,15 @@ class Auth { ); if (!$user || !password_verify($password, $user['password'])) return null; + // Portal role enforcement — each panel only accepts its own role + $portal = defined('CURRENT_PORTAL') ? CURRENT_PORTAL : 'user'; + $allowed = match($portal) { + 'admin' => ['admin'], + 'reseller' => ['reseller'], + default => ['user'], + }; + if (!in_array($user['role'], $allowed, true)) return null; + // TOTP check if (!empty($user['totp_enabled'])) { if ($totpCode === null) { diff --git a/panel/lib/BackupManager.php b/panel/lib/BackupManager.php index 22f31de..233e80d 100644 --- a/panel/lib/BackupManager.php +++ b/panel/lib/BackupManager.php @@ -4,7 +4,7 @@ class BackupManager { private string $backupRoot = '/home/novacpx-backups'; public function __construct() { - $this->db = Database::getInstance()->getPDO(); + $this->db = DB::getInstance()->pdo(); if (!is_dir($this->backupRoot)) mkdir($this->backupRoot, 0750, true); } @@ -25,14 +25,14 @@ class BackupManager { try { if ($type === 'full' || $type === 'files') { - $docRoot = escapeshellarg($account['document_root']); + $docRoot = escapeshellarg($account['home_dir'] . '/public_html'); exec("tar -czf " . escapeshellarg($filepath) . " -C / " . ltrim($docRoot, '/') . " 2>&1", $out, $rc); if ($rc !== 0) throw new RuntimeException("tar failed: " . implode("\n", $out)); } if ($type === 'full' || $type === 'database') { // Dump all databases belonging to this account - $dbs = $this->db->prepare("SELECT db_name FROM account_databases WHERE account_id=?"); + $dbs = $this->db->prepare("SELECT db_name FROM `databases` WHERE account_id=?"); $dbs->execute([$accountId]); foreach ($dbs->fetchAll(PDO::FETCH_COLUMN) as $dbName) { $dumpFile = escapeshellarg("{$dir}/{$account['username']}_{$dbName}_{$ts}.sql.gz"); @@ -80,7 +80,7 @@ class BackupManager { // ── Restore ─────────────────────────────────────────────────────────────── public function restore(int $backupId): bool { - $stmt = $this->db->prepare("SELECT b.*, a.document_root, a.username FROM backups b JOIN accounts a ON b.account_id=a.id WHERE b.id=?"); + $stmt = $this->db->prepare("SELECT b.*, a.home_dir, a.username FROM backups b JOIN accounts a ON b.account_id=a.id WHERE b.id=?"); $stmt->execute([$backupId]); $backup = $stmt->fetch(PDO::FETCH_ASSOC); if (!$backup) throw new RuntimeException("Backup not found"); diff --git a/panel/lib/DNSManager.php b/panel/lib/DNSManager.php index 48e626f..53ad1bf 100644 --- a/panel/lib/DNSManager.php +++ b/panel/lib/DNSManager.php @@ -10,8 +10,8 @@ class DNSManager { public static function createZone(int $accountId, string $domain): void { $db = DB::getInstance(); $serial = (int)date('Ymd') * 100 + 1; - $ns1 = self::getSetting('default_nameserver1', 'ns1.localhost'); - $ns2 = self::getSetting('default_nameserver2', 'ns2.localhost'); + $ns1 = self::getSetting('ns1_hostname', 'ns1.localhost'); + $ns2 = self::getSetting('ns2_hostname', 'ns2.localhost'); $email = 'hostmaster.' . $domain; $ip = self::serverIp(); diff --git a/panel/public/admin/index.php b/panel/public/admin/index.php index 80904cc..40ae8bd 100644 --- a/panel/public/admin/index.php +++ b/panel/public/admin/index.php @@ -1,6 +1,7 @@ '?v=' . @filemtime(dirname(__DIR__) . $f); +require_once dirname(__DIR__) . '/_branding.php'; ?> @@ -19,15 +20,7 @@ $_v = fn($f) => '?v=' . @filemtime(dirname(__DIR__) . $f);