mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Fix #4-#8: mail virtual domains, DNS verified, reseller isolation, missing DB tables
#4: Postfix virtual mailbox config (virtual_mailbox_domains/maps, vmail user, maildir at /var/mail/vhosts/%d/%n). Dovecot SQL backend pointed at novacpx.email_accounts with SHA512-CRYPT passdb and per-domain Maildir userdb. #5: BIND9 confirmed working — dig @localhost resolves testdomain1.com correctly. #6: Certbot 2.9.0 confirmed installed; domains.document_root wired; infrastructure ready for live domain issuance (testdomain1.com not publicly resolvable so dry-run expected to fail). #7: Fixed all broken user-panel API queries — missing tables (databases, ftp_accounts, ssl_certs, cron_jobs, php_configs, notifications) created; `databases` reserved-word backtick-quoted across DatabaseManager+endpoints; domains.php is_primary→type=main, doc_root→document_root column fixes; DNSManager::createZone call signature fixed; stats/account auto-resolves account_id for user role. #8: assert_account_access() helper added to api/index.php; reseller ownership check wired into email, ftp, databases, domains, dns, ssl endpoints. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ match ($action) {
|
||||
p.name as package_name,
|
||||
(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
|
||||
(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
|
||||
@@ -132,7 +132,7 @@ match ($action) {
|
||||
'disk_limit_mb' => $pkg['disk_mb'] ?? 0,
|
||||
'email_count' => $db->fetchOne("SELECT COUNT(*) c FROM email_accounts WHERE account_id = ?", [$id])['c'],
|
||||
'email_limit' => $pkg['max_email'] ?? 0,
|
||||
'db_count' => $db->fetchOne("SELECT COUNT(*) c FROM databases WHERE account_id = ?", [$id])['c'],
|
||||
'db_count' => $db->fetchOne("SELECT COUNT(*) c FROM `databases` WHERE account_id = ?", [$id])['c'],
|
||||
'db_limit' => $pkg['max_databases'] ?? 0,
|
||||
'domain_count' => $db->fetchOne("SELECT COUNT(*) c FROM domains WHERE account_id = ?", [$id])['c'],
|
||||
'domain_limit' => $pkg['max_domains'] ?? 0,
|
||||
|
||||
@@ -4,15 +4,18 @@ $body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
require_once NOVACPX_LIB . '/DatabaseManager.php';
|
||||
|
||||
$user = Auth::getInstance()->user();
|
||||
$accountId = $user['role'] === 'user'
|
||||
? (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0)
|
||||
: (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($user['role'] === 'user') {
|
||||
$accountId = (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0);
|
||||
} else {
|
||||
$accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($accountId && $user['role'] === 'reseller') assert_account_access($accountId);
|
||||
}
|
||||
|
||||
match ($action) {
|
||||
|
||||
'list' => (function() use ($db, $accountId) {
|
||||
if (!$accountId) Response::error("account_id required");
|
||||
$rows = $db->fetchAll("SELECT id, db_name, db_user, db_type, size_mb, created_at FROM databases WHERE account_id = ?", [$accountId]);
|
||||
$rows = $db->fetchAll("SELECT id, db_name, db_user, db_type, size_mb, created_at FROM `databases` WHERE account_id = ?", [$accountId]);
|
||||
foreach ($rows as &$r) { $r['size_mb'] = DatabaseManager::getSize($r['db_name'], $r['db_type']); }
|
||||
Response::success($rows);
|
||||
})(),
|
||||
@@ -22,7 +25,7 @@ match ($action) {
|
||||
// Package limit check
|
||||
$acctPkg = $db->fetchOne("SELECT p.max_databases FROM accounts a LEFT JOIN packages p ON p.id=a.package_id WHERE a.id=?", [$accountId]);
|
||||
if ($acctPkg && $acctPkg['max_databases'] > 0) {
|
||||
$count = (int)$db->fetchOne("SELECT COUNT(*) c FROM databases WHERE account_id=?", [$accountId])['c'];
|
||||
$count = (int)$db->fetchOne("SELECT COUNT(*) c FROM `databases` WHERE account_id=?", [$accountId])['c'];
|
||||
if ($count >= (int)$acctPkg['max_databases']) Response::error("Database limit ({$acctPkg['max_databases']}) reached for this package", 403);
|
||||
}
|
||||
$type = $body['type'] ?? 'mysql';
|
||||
@@ -47,7 +50,7 @@ match ($action) {
|
||||
|
||||
'drop' => (function() use ($db, $body) {
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
$dbe = $db->fetchOne("SELECT db_name, db_user, db_type FROM databases WHERE id = ?", [$id]);
|
||||
$dbe = $db->fetchOne("SELECT db_name, db_user, db_type FROM `databases` WHERE id = ?", [$id]);
|
||||
if (!$dbe) Response::error("Database not found", 404);
|
||||
DatabaseManager::drop($dbe['db_name'], $dbe['db_user'], $dbe['db_type']);
|
||||
audit('database.drop', $dbe['db_name']);
|
||||
|
||||
@@ -12,6 +12,7 @@ if ($user['role'] === 'user') {
|
||||
$accountId = $acct ? (int)$acct['id'] : null;
|
||||
} else {
|
||||
$accountId = (int)($_GET['account_id'] ?? $body['account_id'] ?? 0) ?: null;
|
||||
if ($accountId && $user['role'] === 'reseller') assert_account_access($accountId);
|
||||
}
|
||||
|
||||
match ($action) {
|
||||
|
||||
@@ -8,9 +8,12 @@ require_once NOVACPX_LIB . '/VhostManager.php';
|
||||
require_once NOVACPX_LIB . '/DNSManager.php';
|
||||
|
||||
$user = Auth::getInstance()->user();
|
||||
$accountId = $user['role'] === 'user'
|
||||
? (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0)
|
||||
: (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($user['role'] === 'user') {
|
||||
$accountId = (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0);
|
||||
} else {
|
||||
$accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($accountId && $user['role'] === 'reseller') assert_account_access($accountId);
|
||||
}
|
||||
|
||||
if (!$accountId) Response::error("account_id required");
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE id = ?", [$accountId]);
|
||||
@@ -18,7 +21,7 @@ if (!$acct) Response::error("Account not found", 404);
|
||||
|
||||
match ($action) {
|
||||
'list' => (function() use ($db, $accountId) {
|
||||
$rows = $db->fetchAll("SELECT * FROM domains WHERE account_id = ? ORDER BY is_primary DESC, domain", [$accountId]);
|
||||
$rows = $db->fetchAll("SELECT * FROM domains WHERE account_id = ? ORDER BY (type='main') DESC, domain", [$accountId]);
|
||||
Response::success($rows);
|
||||
})(),
|
||||
|
||||
@@ -30,11 +33,11 @@ match ($action) {
|
||||
$exists = $db->fetchOne("SELECT id FROM domains WHERE domain = ?", [$domain]);
|
||||
if ($exists) Response::error("Domain already exists");
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO domains (account_id, domain, type, doc_root, created_at) VALUES (?,?,?,?,NOW())",
|
||||
[$accountId, $domain, 'addon', $acct['home_dir'] . '/public_html/' . $domain]
|
||||
);
|
||||
$docRoot = $acct['home_dir'] . '/public_html/' . $domain;
|
||||
$db->execute(
|
||||
"INSERT INTO domains (account_id, domain, type, document_root, created_at) VALUES (?,?,?,?,NOW())",
|
||||
[$accountId, $domain, 'addon', $docRoot]
|
||||
);
|
||||
@mkdir($docRoot, 0755, true);
|
||||
file_put_contents("$docRoot/index.html", "<html><body><h1>$domain is ready!</h1><p>Upload your website files.</p></body></html>");
|
||||
|
||||
@@ -45,7 +48,7 @@ match ($action) {
|
||||
'doc_root' => $docRoot,
|
||||
'php_ver' => $acct['php_version'] ?? PHP_DEFAULT,
|
||||
]);
|
||||
DNSManager::createZone($domain, gethostbyname(gethostname()));
|
||||
DNSManager::createZone($accountId, $domain);
|
||||
audit('domains.add-addon', $domain);
|
||||
Response::success(null, "Addon domain $domain added");
|
||||
})(),
|
||||
@@ -61,11 +64,18 @@ match ($action) {
|
||||
file_put_contents("$docRoot/index.html", "<html><body><h1>$full</h1></body></html>");
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO domains (account_id, domain, type, doc_root, created_at) VALUES (?,?,?,?,NOW())",
|
||||
"INSERT INTO domains (account_id, domain, type, document_root, created_at) VALUES (?,?,?,?,NOW())",
|
||||
[$accountId, $full, 'subdomain', $docRoot]
|
||||
);
|
||||
VhostManager::createSubdomain($full, $acct['username'], $docRoot, $acct['php_version'] ?? PHP_DEFAULT);
|
||||
DNSManager::addRecord($parent, $sub, 'A', gethostbyname(gethostname()));
|
||||
VhostManager::create([
|
||||
'domain' => $full,
|
||||
'username' => $acct['username'],
|
||||
'home_dir' => $acct['home_dir'],
|
||||
'doc_root' => $docRoot,
|
||||
'php_ver' => $acct['php_version'] ?? PHP_DEFAULT,
|
||||
]);
|
||||
$zone = DB::getInstance()->fetchOne("SELECT id FROM dns_zones WHERE domain = ? AND account_id = ?", [$parent, $accountId]);
|
||||
if ($zone) DNSManager::addRecord((int)$zone['id'], $sub, 'A', gethostbyname(gethostname()));
|
||||
audit('domains.add-subdomain', $full);
|
||||
Response::success(null, "Subdomain $full created");
|
||||
})(),
|
||||
@@ -76,10 +86,9 @@ match ($action) {
|
||||
if (!$alias) Response::error("domain required");
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO domains (account_id, domain, type, doc_root, created_at) VALUES (?,?,?,?,NOW())",
|
||||
"INSERT INTO domains (account_id, domain, type, document_root, created_at) VALUES (?,?,?,?,NOW())",
|
||||
[$accountId, $alias, 'alias', $acct['home_dir'] . '/public_html']
|
||||
);
|
||||
// Create vhost that serves same doc root as primary
|
||||
VhostManager::create([
|
||||
'domain' => $alias,
|
||||
'username' => $acct['username'],
|
||||
@@ -87,7 +96,7 @@ match ($action) {
|
||||
'doc_root' => $acct['home_dir'] . '/public_html',
|
||||
'php_ver' => $acct['php_version'] ?? PHP_DEFAULT,
|
||||
]);
|
||||
DNSManager::createZone($alias, gethostbyname(gethostname()));
|
||||
DNSManager::createZone($accountId, $alias);
|
||||
audit('domains.add-alias', $alias);
|
||||
Response::success(null, "Domain alias $alias added");
|
||||
})(),
|
||||
@@ -97,7 +106,7 @@ match ($action) {
|
||||
$url = trim($body['redirect_url'] ?? '');
|
||||
$code = in_array((int)($body['code'] ?? 301), [301, 302]) ? (int)$body['code'] : 301;
|
||||
if (!$url) Response::error("redirect_url required");
|
||||
$db->execute("UPDATE domains SET redirect_url=?, redirect_code=? WHERE id=? AND account_id=?", [$url, $code, $id, $accountId]);
|
||||
$db->execute("UPDATE domains SET redirect_to=? WHERE id=? AND account_id=?", [$url, $id, $accountId]);
|
||||
// Update vhost to add Redirect directive
|
||||
$dom = $db->fetchOne("SELECT * FROM domains WHERE id = ?", [$id]);
|
||||
if ($dom) {
|
||||
@@ -110,7 +119,7 @@ match ($action) {
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
$dom = $db->fetchOne("SELECT * FROM domains WHERE id = ? AND account_id = ?", [$id, $accountId]);
|
||||
if (!$dom) Response::error("Domain not found", 404);
|
||||
if ($dom['is_primary']) Response::error("Cannot remove primary domain");
|
||||
if ($dom['type'] === 'main') Response::error("Cannot remove primary domain");
|
||||
|
||||
VhostManager::remove($dom['domain']);
|
||||
if ($dom['type'] !== 'subdomain') DNSManager::removeZone($dom['domain']);
|
||||
|
||||
@@ -11,7 +11,9 @@ function self_account_id($db, $user): ?int {
|
||||
$a = $db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']]);
|
||||
return $a ? (int)$a['id'] : null;
|
||||
}
|
||||
return (int)(request_param('account_id') ?? 0) ?: null;
|
||||
$id = (int)(request_param('account_id') ?? 0);
|
||||
if ($id && $user['role'] === 'reseller') assert_account_access($id);
|
||||
return $id ?: null;
|
||||
}
|
||||
function request_param(string $k): mixed { return $_GET[$k] ?? (json_decode(file_get_contents('php://input'), true) ?? [])[$k] ?? null; }
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ $body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
require_once NOVACPX_LIB . '/FTPManager.php';
|
||||
|
||||
$user = Auth::getInstance()->user();
|
||||
$accountId = $user['role'] === 'user'
|
||||
? (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0)
|
||||
: (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($user['role'] === 'user') {
|
||||
$accountId = (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0);
|
||||
} else {
|
||||
$accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($accountId && $user['role'] === 'reseller') assert_account_access($accountId);
|
||||
}
|
||||
|
||||
match ($action) {
|
||||
'list' => (function() use ($db, $accountId) {
|
||||
|
||||
@@ -5,9 +5,12 @@ require_once NOVACPX_LIB . '/SSLManager.php';
|
||||
require_once NOVACPX_LIB . '/VhostManager.php';
|
||||
|
||||
$user = Auth::getInstance()->user();
|
||||
$accountId = $user['role'] === 'user'
|
||||
? (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0)
|
||||
: (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($user['role'] === 'user') {
|
||||
$accountId = (int)($db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']])['id'] ?? 0);
|
||||
} else {
|
||||
$accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
if ($accountId && $user['role'] === 'reseller') assert_account_access($accountId);
|
||||
}
|
||||
|
||||
match ($action) {
|
||||
'list' => (function() use ($db, $accountId) {
|
||||
|
||||
@@ -49,7 +49,13 @@ match ($action) {
|
||||
})(),
|
||||
|
||||
'account' => (function() use ($db, $body) {
|
||||
$accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
$user = Auth::getInstance()->user();
|
||||
if ($user['role'] === 'user') {
|
||||
$acctRow = $db->fetchOne("SELECT id FROM accounts WHERE user_id = ?", [$user['uid']]);
|
||||
$accountId = $acctRow ? (int)$acctRow['id'] : 0;
|
||||
} else {
|
||||
$accountId = (int)($body['account_id'] ?? $_GET['account_id'] ?? 0);
|
||||
}
|
||||
if (!$accountId) Response::error("account_id required");
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE id = ?", [$accountId]);
|
||||
if (!$acct) Response::error("Account not found", 404);
|
||||
@@ -62,7 +68,7 @@ match ($action) {
|
||||
$inodes = (int)trim(shell_exec("find " . escapeshellarg($acct['home_dir']) . " 2>/dev/null | wc -l") ?: 0);
|
||||
|
||||
// DB count & size
|
||||
$dbs = $db->fetchAll("SELECT id FROM databases WHERE account_id = ?", [$accountId]);
|
||||
$dbs = $db->fetchAll("SELECT id FROM `databases` WHERE account_id = ?", [$accountId]);
|
||||
$dbCount = count($dbs);
|
||||
|
||||
// Email count
|
||||
|
||||
Reference in New Issue
Block a user