mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Add full API endpoint suite, lib managers, webmail (Roundcube :8883), and NovaCPX icon/branding assets
- 14 API endpoints: accounts, packages, domains, dns, email, databases, ftp, ssl, cron, php, files, stats, webmail, server_setup - 8 lib managers: AccountManager, VhostManager, DNSManager, EmailManager, DatabaseManager, PHPManager, FTPManager, SSLManager - Roundcube webmail on dedicated port 8883 (sequenced after 8880/8881/8882) - Custom NovaCPX SVG icon sprite (30+ unique icons), logo, mark, favicon - PORT_WEBMAIL=8883 wired into Core.php, install.sh, UFW, Fail2Ban, credentials file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* AccountManager — creates/suspends/terminates Linux hosting accounts
|
||||
* Each account = system user + home dir + vhost + DNS zone + mail domain
|
||||
*/
|
||||
class AccountManager {
|
||||
|
||||
public static function create(array $data): array {
|
||||
$db = DB::getInstance();
|
||||
$username = strtolower(preg_replace('/[^a-z0-9_]/', '', $data['username']));
|
||||
$domain = strtolower(trim($data['domain']));
|
||||
$userId = (int)$data['user_id'];
|
||||
$pkgId = (int)($data['package_id'] ?? 0);
|
||||
$phpVer = $data['php_version'] ?? PHP_DEFAULT;
|
||||
$webSrv = WEB_SERVER;
|
||||
|
||||
if (strlen($username) < 2 || strlen($username) > 32) throw new RuntimeException("Username must be 2-32 chars");
|
||||
if (!filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) throw new RuntimeException("Invalid domain");
|
||||
|
||||
// Check uniqueness
|
||||
if ($db->fetchOne("SELECT id FROM accounts WHERE username = ?", [$username])) throw new RuntimeException("Username taken");
|
||||
if ($db->fetchOne("SELECT id FROM domains WHERE domain = ?", [$domain])) throw new RuntimeException("Domain already hosted");
|
||||
|
||||
$homeDir = "/home/{$username}";
|
||||
$docRoot = "{$homeDir}/public_html";
|
||||
$password = $data['password'] ?? bin2hex(random_bytes(8));
|
||||
|
||||
// Create Linux user
|
||||
self::shell("useradd -m -d {$homeDir} -s /sbin/nologin -G www-data " . escapeshellarg($username));
|
||||
self::shell("echo " . escapeshellarg("{$username}:{$password}") . " | chpasswd");
|
||||
self::shell("mkdir -p {$docRoot} {$homeDir}/logs {$homeDir}/tmp");
|
||||
self::shell("chown -R {$username}:www-data {$homeDir}");
|
||||
self::shell("chmod 750 {$homeDir}; chmod 755 {$docRoot}");
|
||||
|
||||
// Default index page
|
||||
file_put_contents("{$docRoot}/index.html",
|
||||
"<html><body style='font-family:sans-serif;text-align:center;padding:4rem'><h1>Welcome to {$domain}</h1><p>Hosted by NovaCPX</p></body></html>"
|
||||
);
|
||||
|
||||
// Save account to DB
|
||||
$acctId = (int)$db->insert(
|
||||
"INSERT INTO accounts (user_id, username, domain, home_dir, package_id, php_version, web_server) VALUES (?,?,?,?,?,?,?)",
|
||||
[$userId, $username, $domain, $homeDir, $pkgId ?: null, $phpVer, $webSrv]
|
||||
);
|
||||
|
||||
// Save domain
|
||||
$db->insert(
|
||||
"INSERT INTO domains (account_id, domain, type, document_root) VALUES (?,?,?,?)",
|
||||
[$acctId, $domain, 'main', $docRoot]
|
||||
);
|
||||
|
||||
// Create web vhost
|
||||
VhostManager::create($username, $domain, $docRoot, $phpVer);
|
||||
|
||||
// Create DNS zone
|
||||
DNSManager::createZone($acctId, $domain);
|
||||
|
||||
// Create PHP-FPM pool
|
||||
PHPManager::createPool($username, $phpVer);
|
||||
|
||||
novacpx_log('info', "Account created: $username ($domain)");
|
||||
return ['account_id' => $acctId, 'username' => $username, 'domain' => $domain, 'home_dir' => $homeDir];
|
||||
}
|
||||
|
||||
public static function suspend(int $acctId, string $reason = ''): void {
|
||||
$db = DB::getInstance();
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE id = ?", [$acctId]);
|
||||
if (!$acct) throw new RuntimeException("Account not found");
|
||||
|
||||
self::shell("usermod -L " . escapeshellarg($acct['username']));
|
||||
VhostManager::suspend($acct['username'], $acct['domain']);
|
||||
$db->execute("UPDATE accounts SET status = 'suspended', suspended_at = NOW() WHERE id = ?", [$acctId]);
|
||||
novacpx_log('info', "Account suspended: {$acct['username']} — $reason");
|
||||
}
|
||||
|
||||
public static function unsuspend(int $acctId): void {
|
||||
$db = DB::getInstance();
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE id = ?", [$acctId]);
|
||||
if (!$acct) throw new RuntimeException("Account not found");
|
||||
|
||||
self::shell("usermod -U " . escapeshellarg($acct['username']));
|
||||
VhostManager::unsuspend($acct['username'], $acct['domain']);
|
||||
$db->execute("UPDATE accounts SET status = 'active', suspended_at = NULL WHERE id = ?", [$acctId]);
|
||||
}
|
||||
|
||||
public static function terminate(int $acctId): void {
|
||||
$db = DB::getInstance();
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE id = ?", [$acctId]);
|
||||
if (!$acct) throw new RuntimeException("Account not found");
|
||||
|
||||
// Remove vhost
|
||||
VhostManager::remove($acct['username'], $acct['domain']);
|
||||
|
||||
// Remove DNS zone
|
||||
DNSManager::removeZone($acct['domain']);
|
||||
|
||||
// Drop databases
|
||||
$dbs = $db->fetchAll("SELECT * FROM databases WHERE account_id = ?", [$acctId]);
|
||||
foreach ($dbs as $dbe) { DatabaseManager::drop($dbe['db_name'], $dbe['db_user'], $dbe['db_type']); }
|
||||
|
||||
// Remove PHP-FPM pool
|
||||
PHPManager::removePool($acct['username']);
|
||||
|
||||
// Remove Linux user and home dir
|
||||
self::shell("userdel -r " . escapeshellarg($acct['username']) . " 2>/dev/null || true");
|
||||
|
||||
// Remove from DB (cascade handles child tables)
|
||||
$db->execute("DELETE FROM users WHERE id = ?", [$acct['user_id']]);
|
||||
$db->execute("DELETE FROM accounts WHERE id = ?", [$acctId]);
|
||||
novacpx_log('info', "Account terminated: {$acct['username']}");
|
||||
}
|
||||
|
||||
public static function getDiskUsage(string $homeDir): int {
|
||||
$out = trim(shell_exec("du -sm " . escapeshellarg($homeDir) . " 2>/dev/null | awk '{print $1}'") ?: '0');
|
||||
return (int)$out;
|
||||
}
|
||||
|
||||
private static function shell(string $cmd): string {
|
||||
$out = shell_exec($cmd . ' 2>&1');
|
||||
novacpx_log('debug', "shell: $cmd");
|
||||
return $out ?: '';
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -22,6 +22,7 @@ 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('PORT_WEBMAIL', (int)($_cfg['panel']['port_webmail'] ?? 8883));
|
||||
define('WEB_SERVER', $_cfg['web']['server'] ?? 'apache');
|
||||
define('PHP_DEFAULT', $_cfg['web']['php_default'] ?? '8.3');
|
||||
|
||||
@@ -29,7 +30,8 @@ define('PHP_DEFAULT', $_cfg['web']['php_default'] ?? '8.3');
|
||||
$requestPort = (int)($_SERVER['SERVER_PORT'] ?? 0);
|
||||
define('CURRENT_PORTAL',
|
||||
$requestPort === PORT_ADMIN ? 'admin' :
|
||||
($requestPort === PORT_RESELLER ? 'reseller' : 'user')
|
||||
($requestPort === PORT_RESELLER ? 'reseller' :
|
||||
($requestPort === PORT_WEBMAIL ? 'webmail' : 'user'))
|
||||
);
|
||||
|
||||
function novacpx_log(string $level, string $msg, array $ctx = []): void {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* DNSManager — BIND9 zone file generation and management
|
||||
*/
|
||||
class DNSManager {
|
||||
|
||||
private static string $zonesDir = '/etc/bind/novacpx-zones';
|
||||
private static string $namedConf = '/etc/bind/named.conf.novacpx';
|
||||
|
||||
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');
|
||||
$email = 'hostmaster.' . $domain;
|
||||
$ip = self::serverIp();
|
||||
|
||||
$zoneId = (int)$db->insert(
|
||||
"INSERT INTO dns_zones (account_id, domain, serial, primary_ns, secondary_ns, admin_email) VALUES (?,?,?,?,?,?)",
|
||||
[$accountId, $domain, $serial, $ns1, $ns2, $email]
|
||||
);
|
||||
|
||||
// Default records
|
||||
$defaults = [
|
||||
['@', 'A', $ip, 3600, null],
|
||||
['www', 'A', $ip, 3600, null],
|
||||
['mail', 'A', $ip, 3600, null],
|
||||
['@', 'MX', "mail.{$domain}.", 3600, 10],
|
||||
['@', 'TXT', "v=spf1 a mx ~all", 3600, null],
|
||||
];
|
||||
foreach ($defaults as [$name, $type, $content, $ttl, $prio]) {
|
||||
$db->execute(
|
||||
"INSERT INTO dns_records (zone_id, name, type, content, ttl, priority) VALUES (?,?,?,?,?,?)",
|
||||
[$zoneId, $name, $type, $content, $ttl, $prio]
|
||||
);
|
||||
}
|
||||
|
||||
self::writeZoneFile($zoneId);
|
||||
self::reloadBind();
|
||||
}
|
||||
|
||||
public static function removeZone(string $domain): void {
|
||||
$db = DB::getInstance();
|
||||
$zone = $db->fetchOne("SELECT id FROM dns_zones WHERE domain = ?", [$domain]);
|
||||
if ($zone) {
|
||||
$db->execute("DELETE FROM dns_zones WHERE id = ?", [$zone['id']]);
|
||||
}
|
||||
$file = self::$zonesDir . '/' . $domain . '.zone';
|
||||
@unlink($file);
|
||||
self::rebuildNamedConf();
|
||||
self::reloadBind();
|
||||
}
|
||||
|
||||
public static function addRecord(int $zoneId, string $name, string $type, string $content, int $ttl = 3600, ?int $priority = null): int {
|
||||
$db = DB::getInstance();
|
||||
$id = (int)$db->insert(
|
||||
"INSERT INTO dns_records (zone_id, name, type, content, ttl, priority) VALUES (?,?,?,?,?,?)",
|
||||
[$zoneId, $name, $type, $content, $ttl, $priority]
|
||||
);
|
||||
$db->execute("UPDATE dns_zones SET serial = serial + 1, updated_at = NOW() WHERE id = ?", [$zoneId]);
|
||||
self::writeZoneFile($zoneId);
|
||||
self::reloadBind();
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function updateRecord(int $recordId, array $data): void {
|
||||
$db = DB::getInstance();
|
||||
$rec = $db->fetchOne("SELECT zone_id FROM dns_records WHERE id = ?", [$recordId]);
|
||||
if (!$rec) throw new RuntimeException("Record not found");
|
||||
$db->execute(
|
||||
"UPDATE dns_records SET name=?, type=?, content=?, ttl=?, priority=? WHERE id=?",
|
||||
[$data['name'], $data['type'], $data['content'], $data['ttl'] ?? 3600, $data['priority'] ?? null, $recordId]
|
||||
);
|
||||
$db->execute("UPDATE dns_zones SET serial = serial + 1, updated_at = NOW() WHERE id = ?", [$rec['zone_id']]);
|
||||
self::writeZoneFile($rec['zone_id']);
|
||||
self::reloadBind();
|
||||
}
|
||||
|
||||
public static function deleteRecord(int $recordId): void {
|
||||
$db = DB::getInstance();
|
||||
$rec = $db->fetchOne("SELECT zone_id FROM dns_records WHERE id = ?", [$recordId]);
|
||||
if (!$rec) throw new RuntimeException("Record not found");
|
||||
$db->execute("DELETE FROM dns_records WHERE id = ?", [$recordId]);
|
||||
$db->execute("UPDATE dns_zones SET serial = serial + 1 WHERE id = ?", [$rec['zone_id']]);
|
||||
self::writeZoneFile($rec['zone_id']);
|
||||
self::reloadBind();
|
||||
}
|
||||
|
||||
public static function writeZoneFile(int $zoneId): void {
|
||||
$db = DB::getInstance();
|
||||
$zone = $db->fetchOne("SELECT * FROM dns_zones WHERE id = ?", [$zoneId]);
|
||||
if (!$zone) return;
|
||||
$records = $db->fetchAll("SELECT * FROM dns_records WHERE zone_id = ? ORDER BY type, name", [$zoneId]);
|
||||
|
||||
@mkdir(self::$zonesDir, 0755, true);
|
||||
$domain = $zone['domain'];
|
||||
$content = "\$ORIGIN {$domain}.\n\$TTL {$zone['ttl']}\n\n";
|
||||
$content .= "@ IN SOA {$zone['primary_ns']}. {$zone['admin_email']}. (\n";
|
||||
$content .= " {$zone['serial']} ; serial\n";
|
||||
$content .= " {$zone['refresh']} ; refresh\n";
|
||||
$content .= " {$zone['retry']} ; retry\n";
|
||||
$content .= " {$zone['expire']} ; expire\n";
|
||||
$content .= " {$zone['minimum']} ; minimum\n)\n\n";
|
||||
$content .= "@ IN NS {$zone['primary_ns']}.\n";
|
||||
$content .= "@ IN NS {$zone['secondary_ns']}.\n\n";
|
||||
|
||||
foreach ($records as $r) {
|
||||
$name = $r['name'] === '@' ? '@' : $r['name'];
|
||||
$prio = $r['priority'] !== null ? "{$r['priority']} " : '';
|
||||
$val = in_array($r['type'], ['TXT','SPF','DMARC','DKIM']) ? "\"{$r['content']}\"" : $r['content'];
|
||||
$content .= "{$name} {$r['ttl']} IN {$r['type']} {$prio}{$val}\n";
|
||||
}
|
||||
|
||||
file_put_contents(self::$zonesDir . '/' . $domain . '.zone', $content);
|
||||
self::rebuildNamedConf();
|
||||
}
|
||||
|
||||
private static function rebuildNamedConf(): void {
|
||||
@mkdir(self::$zonesDir, 0755, true);
|
||||
$zones = glob(self::$zonesDir . '/*.zone') ?: [];
|
||||
$conf = "// NovaCPX auto-generated zone list\n";
|
||||
foreach ($zones as $zf) {
|
||||
$domain = basename($zf, '.zone');
|
||||
$conf .= "zone \"{$domain}\" { type master; file \"" . self::$zonesDir . "/{$domain}.zone\"; };\n";
|
||||
}
|
||||
file_put_contents(self::$namedConf, $conf);
|
||||
|
||||
// Include in main named.conf if not already there
|
||||
$mainConf = '/etc/bind/named.conf';
|
||||
if (file_exists($mainConf) && !str_contains(file_get_contents($mainConf), 'named.conf.novacpx')) {
|
||||
file_put_contents($mainConf, "\ninclude \"" . self::$namedConf . "\";\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
private static function reloadBind(): void {
|
||||
shell_exec("rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true");
|
||||
}
|
||||
|
||||
private static function serverIp(): string {
|
||||
return trim(shell_exec("hostname -I | awk '{print $1}'") ?: '127.0.0.1');
|
||||
}
|
||||
|
||||
private static function getSetting(string $key, string $default): string {
|
||||
$row = DB::getInstance()->fetchOne("SELECT value FROM settings WHERE `key` = ?", [$key]);
|
||||
return $row['value'] ?? $default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* DatabaseManager — MySQL 8 + PostgreSQL database/user provisioning
|
||||
*/
|
||||
class DatabaseManager {
|
||||
|
||||
public static function createMySQL(int $accountId, string $dbName, string $dbUser, string $dbPass): int {
|
||||
self::validateName($dbName); self::validateName($dbUser);
|
||||
$db = DB::getInstance();
|
||||
$pdo = $db->pdo();
|
||||
|
||||
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
$pdo->exec("CREATE USER IF NOT EXISTS '{$dbUser}'@'localhost' IDENTIFIED BY " . $pdo->quote($dbPass));
|
||||
$pdo->exec("GRANT ALL PRIVILEGES ON `{$dbName}`.* TO '{$dbUser}'@'localhost'");
|
||||
$pdo->exec("FLUSH PRIVILEGES");
|
||||
|
||||
return (int)$db->insert(
|
||||
"INSERT INTO databases (account_id, db_name, db_user, db_pass, db_type) VALUES (?,?,?,?,?)",
|
||||
[$accountId, $dbName, $dbUser, encrypt($dbPass), 'mysql']
|
||||
);
|
||||
}
|
||||
|
||||
public static function createPostgres(int $accountId, string $dbName, string $dbUser, string $dbPass): int {
|
||||
self::validateName($dbName); self::validateName($dbUser);
|
||||
$db = DB::getInstance();
|
||||
$safe = escapeshellarg($dbPass);
|
||||
shell_exec("sudo -u postgres psql -c \"CREATE USER {$dbUser} WITH PASSWORD {$safe}\" 2>/dev/null");
|
||||
shell_exec("sudo -u postgres createdb -O {$dbUser} {$dbName} 2>/dev/null");
|
||||
|
||||
return (int)$db->insert(
|
||||
"INSERT INTO databases (account_id, db_name, db_user, db_pass, db_type) VALUES (?,?,?,?,?)",
|
||||
[$accountId, $dbName, $dbUser, encrypt($dbPass), 'postgresql']
|
||||
);
|
||||
}
|
||||
|
||||
public static function drop(string $dbName, string $dbUser, string $type = 'mysql'): void {
|
||||
if ($type === 'mysql') {
|
||||
$pdo = DB::getInstance()->pdo();
|
||||
$pdo->exec("DROP DATABASE IF EXISTS `{$dbName}`");
|
||||
$pdo->exec("DROP USER IF EXISTS '{$dbUser}'@'localhost'");
|
||||
$pdo->exec("FLUSH PRIVILEGES");
|
||||
} else {
|
||||
shell_exec("sudo -u postgres dropdb --if-exists " . escapeshellarg($dbName) . " 2>/dev/null");
|
||||
shell_exec("sudo -u postgres dropuser --if-exists " . escapeshellarg($dbUser) . " 2>/dev/null");
|
||||
}
|
||||
DB::getInstance()->execute("DELETE FROM databases WHERE db_name = ? AND db_type = ?", [$dbName, $type]);
|
||||
}
|
||||
|
||||
public static function changePassword(int $id, string $newPass): void {
|
||||
$db = DB::getInstance();
|
||||
$dbe = $db->fetchOne("SELECT * FROM databases WHERE id = ?", [$id]);
|
||||
if (!$dbe) throw new RuntimeException("Database not found");
|
||||
if ($dbe['db_type'] === 'mysql') {
|
||||
$pdo = $db->pdo();
|
||||
$pdo->exec("ALTER USER '{$dbe['db_user']}'@'localhost' IDENTIFIED BY " . $pdo->quote($newPass));
|
||||
} else {
|
||||
shell_exec("sudo -u postgres psql -c \"ALTER USER {$dbe['db_user']} WITH PASSWORD " . escapeshellarg($newPass) . "\" 2>/dev/null");
|
||||
}
|
||||
$db->execute("UPDATE databases SET db_pass = ? WHERE id = ?", [encrypt($newPass), $id]);
|
||||
}
|
||||
|
||||
public static function getSize(string $dbName, string $type = 'mysql'): float {
|
||||
if ($type === 'mysql') {
|
||||
$row = DB::getInstance()->fetchOne(
|
||||
"SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size
|
||||
FROM information_schema.tables WHERE table_schema = ?",
|
||||
[$dbName]
|
||||
);
|
||||
return (float)($row['size'] ?? 0);
|
||||
}
|
||||
$out = shell_exec("sudo -u postgres psql -t -c \"SELECT pg_size_pretty(pg_database_size('{$dbName}'))\" 2>/dev/null");
|
||||
return (float)$out;
|
||||
}
|
||||
|
||||
private static function validateName(string $name): void {
|
||||
if (!preg_match('/^[a-zA-Z0-9_]{1,64}$/', $name)) {
|
||||
throw new RuntimeException("Invalid database/user name: must be alphanumeric+underscore, max 64 chars");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function encrypt(string $val): string {
|
||||
$key = SECRET_KEY;
|
||||
$iv = random_bytes(16);
|
||||
$enc = openssl_encrypt($val, 'aes-256-cbc', substr(hash('sha256', $key, true), 0, 32), OPENSSL_RAW_DATA, $iv);
|
||||
return base64_encode($iv . $enc);
|
||||
}
|
||||
|
||||
function decrypt(string $val): string {
|
||||
$key = SECRET_KEY;
|
||||
$data = base64_decode($val);
|
||||
$iv = substr($data, 0, 16);
|
||||
$enc = substr($data, 16);
|
||||
return openssl_decrypt($enc, 'aes-256-cbc', substr(hash('sha256', $key, true), 0, 32), OPENSSL_RAW_DATA, $iv) ?: '';
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* EmailManager — Postfix virtual mailbox + Dovecot user management
|
||||
* Uses MySQL backend for both Postfix and Dovecot
|
||||
*/
|
||||
class EmailManager {
|
||||
|
||||
public static function createAccount(int $accountId, string $email, string $password, int $quotaMb = 500): int {
|
||||
$db = DB::getInstance();
|
||||
$hashed = self::hashPassword($password);
|
||||
$id = (int)$db->insert(
|
||||
"INSERT INTO email_accounts (account_id, email, password, quota_mb) VALUES (?,?,?,?)",
|
||||
[$accountId, $email, $hashed, $quotaMb]
|
||||
);
|
||||
self::syncPostfix();
|
||||
novacpx_log('info', "Email account created: $email");
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function deleteAccount(int $id): void {
|
||||
$db = DB::getInstance();
|
||||
$acc = $db->fetchOne("SELECT email FROM email_accounts WHERE id = ?", [$id]);
|
||||
if (!$acc) throw new RuntimeException("Email account not found");
|
||||
$db->execute("DELETE FROM email_accounts WHERE id = ?", [$id]);
|
||||
self::syncPostfix();
|
||||
}
|
||||
|
||||
public static function changePassword(int $id, string $newPassword): void {
|
||||
$db = DB::getInstance();
|
||||
$db->execute("UPDATE email_accounts SET password = ? WHERE id = ?", [self::hashPassword($newPassword), $id]);
|
||||
}
|
||||
|
||||
public static function suspend(int $id): void {
|
||||
DB::getInstance()->execute("UPDATE email_accounts SET status = 'suspended' WHERE id = ?", [$id]);
|
||||
self::syncPostfix();
|
||||
}
|
||||
|
||||
public static function addForwarder(int $accountId, string $source, string $destination): int {
|
||||
$db = DB::getInstance();
|
||||
return (int)$db->insert(
|
||||
"INSERT INTO email_forwarders (account_id, source, destination) VALUES (?,?,?)",
|
||||
[$accountId, $source, $destination]
|
||||
);
|
||||
}
|
||||
|
||||
public static function removeForwarder(int $id): void {
|
||||
DB::getInstance()->execute("DELETE FROM email_forwarders WHERE id = ?", [$id]);
|
||||
self::syncPostfix();
|
||||
}
|
||||
|
||||
public static function addAutoresponder(int $accountId, string $email, string $subject, string $body): int {
|
||||
$db = DB::getInstance();
|
||||
return (int)$db->insert(
|
||||
"INSERT INTO email_autoresponders (account_id, email, subject, body, is_active) VALUES (?,?,?,?,1)",
|
||||
[$accountId, $email, $subject, $body]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Postfix virtual_mailbox_maps + virtual_alias_maps files from DB
|
||||
* Postfix reads these files (postmap creates .db hash)
|
||||
*/
|
||||
private static function syncPostfix(): void {
|
||||
$db = DB::getInstance();
|
||||
|
||||
// Virtual mailbox map
|
||||
$accounts = $db->fetchAll("SELECT ea.email, a.username FROM email_accounts ea JOIN accounts a ON a.id = ea.account_id WHERE ea.status = 'active'");
|
||||
$mailboxes = '';
|
||||
foreach ($accounts as $a) {
|
||||
$domain = substr(strrchr($a['email'], '@'), 1);
|
||||
$user = strstr($a['email'], '@', true);
|
||||
$mailboxes .= "{$a['email']} {$a['username']}/{$domain}/{$user}/\n";
|
||||
}
|
||||
file_put_contents('/etc/postfix/novacpx_mailboxes', $mailboxes);
|
||||
shell_exec('postmap /etc/postfix/novacpx_mailboxes 2>/dev/null');
|
||||
|
||||
// Virtual alias map (forwarders)
|
||||
$forwarders = $db->fetchAll("SELECT source, destination FROM email_forwarders");
|
||||
$aliases = '';
|
||||
foreach ($forwarders as $f) {
|
||||
$aliases .= "{$f['source']} {$f['destination']}\n";
|
||||
}
|
||||
file_put_contents('/etc/postfix/novacpx_aliases', $aliases);
|
||||
shell_exec('postmap /etc/postfix/novacpx_aliases 2>/dev/null');
|
||||
|
||||
// Virtual domains map
|
||||
$domains = $db->fetchAll("SELECT DISTINCT SUBSTRING_INDEX(email,'@',-1) as domain FROM email_accounts WHERE status='active'");
|
||||
$vdomains = '';
|
||||
foreach ($domains as $d) { $vdomains .= "{$d['domain']} novacpx\n"; }
|
||||
file_put_contents('/etc/postfix/novacpx_domains', $vdomains);
|
||||
shell_exec('postmap /etc/postfix/novacpx_domains 2>/dev/null');
|
||||
shell_exec('systemctl reload postfix 2>/dev/null || true');
|
||||
}
|
||||
|
||||
private static function hashPassword(string $password): string {
|
||||
// Dovecot SHA512-CRYPT compatible
|
||||
return '{SHA512-CRYPT}' . crypt($password, '$6$' . bin2hex(random_bytes(8)) . '$');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* FTPManager — ProFTPD virtual user management via MySQL
|
||||
*/
|
||||
class FTPManager {
|
||||
|
||||
public static function createAccount(int $accountId, string $username, string $password, string $homeDir, int $quotaMb = 0): int {
|
||||
$db = DB::getInstance();
|
||||
$hashed = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
// Create system user for FTP (no shell, no home dir creation)
|
||||
$acct = $db->fetchOne("SELECT username as owner FROM accounts WHERE id = ?", [$accountId]);
|
||||
$ftpUser = strtolower(preg_replace('/[^a-z0-9_]/', '', $username));
|
||||
|
||||
$id = (int)$db->insert(
|
||||
"INSERT INTO ftp_accounts (account_id, username, password, home_dir, quota_mb) VALUES (?,?,?,?,?)",
|
||||
[$accountId, $ftpUser, $hashed, $homeDir, $quotaMb]
|
||||
);
|
||||
|
||||
self::syncProftpd();
|
||||
novacpx_log('info', "FTP account created: $ftpUser");
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function deleteAccount(int $id): void {
|
||||
DB::getInstance()->execute("DELETE FROM ftp_accounts WHERE id = ?", [$id]);
|
||||
self::syncProftpd();
|
||||
}
|
||||
|
||||
public static function changePassword(int $id, string $newPassword): void {
|
||||
DB::getInstance()->execute(
|
||||
"UPDATE ftp_accounts SET password = ? WHERE id = ?",
|
||||
[password_hash($newPassword, PASSWORD_BCRYPT), $id]
|
||||
);
|
||||
self::syncProftpd();
|
||||
}
|
||||
|
||||
public static function suspend(int $id): void {
|
||||
DB::getInstance()->execute("UPDATE ftp_accounts SET status = 'suspended' WHERE id = ?", [$id]);
|
||||
self::syncProftpd();
|
||||
}
|
||||
|
||||
private static function syncProftpd(): void {
|
||||
// Write ProFTPD virtual users file (passwd format)
|
||||
$db = DB::getInstance();
|
||||
$accounts = $db->fetchAll("SELECT f.*, a.username as owner FROM ftp_accounts f JOIN accounts a ON a.id = f.account_id WHERE f.status = 'active'");
|
||||
$passwd = '';
|
||||
foreach ($accounts as $a) {
|
||||
$uid = self::getUid($a['owner']);
|
||||
$gid = self::getGid('www-data');
|
||||
$passwd .= "{$a['username']}:{$a['password']}:{$uid}:{$gid}:NovaCPX FTP:{$a['home_dir']}:/sbin/nologin\n";
|
||||
}
|
||||
file_put_contents('/etc/proftpd/novacpx-users.passwd', $passwd);
|
||||
shell_exec('systemctl reload proftpd 2>/dev/null || true');
|
||||
}
|
||||
|
||||
private static function getUid(string $username): int {
|
||||
$out = trim(shell_exec("id -u " . escapeshellarg($username) . " 2>/dev/null") ?: '33');
|
||||
return (int)$out;
|
||||
}
|
||||
|
||||
private static function getGid(string $group): int {
|
||||
$out = trim(shell_exec("getent group " . escapeshellarg($group) . " | cut -d: -f3 2>/dev/null") ?: '33');
|
||||
return (int)$out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* PHPManager — per-account PHP-FPM pools + version switching
|
||||
*/
|
||||
class PHPManager {
|
||||
|
||||
private static string $poolDir = '/etc/php/{ver}/fpm/pool.d';
|
||||
|
||||
public static function createPool(string $username, string $phpVer): void {
|
||||
$poolFile = str_replace('{ver}', $phpVer, self::$poolDir) . "/{$username}.conf";
|
||||
$homeDir = "/home/{$username}";
|
||||
$sock = "/run/php/php{$phpVer}-fpm-{$username}.sock";
|
||||
|
||||
file_put_contents($poolFile, "[{$username}]
|
||||
user = {$username}
|
||||
group = www-data
|
||||
listen = {$sock}
|
||||
listen.owner = www-data
|
||||
listen.group = www-data
|
||||
listen.mode = 0660
|
||||
|
||||
pm = ondemand
|
||||
pm.max_children = 5
|
||||
pm.process_idle_timeout = 10s
|
||||
pm.max_requests = 500
|
||||
|
||||
php_admin_value[error_log] = {$homeDir}/logs/php.log
|
||||
php_admin_value[open_basedir] = {$homeDir}/:/tmp/
|
||||
php_admin_flag[log_errors] = on
|
||||
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
|
||||
php_value[upload_max_filesize] = 64M
|
||||
php_value[post_max_size] = 64M
|
||||
php_value[memory_limit] = 256M
|
||||
php_value[max_execution_time] = 30
|
||||
");
|
||||
self::reloadFPM($phpVer);
|
||||
}
|
||||
|
||||
public static function removePool(string $username): void {
|
||||
foreach (['7.4','8.1','8.2','8.3'] as $ver) {
|
||||
$file = str_replace('{ver}', $ver, self::$poolDir) . "/{$username}.conf";
|
||||
if (file_exists($file)) { unlink($file); self::reloadFPM($ver); }
|
||||
}
|
||||
}
|
||||
|
||||
public static function switchVersion(int $accountId, string $newVer): void {
|
||||
$db = DB::getInstance();
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE id = ?", [$accountId]);
|
||||
if (!$acct) throw new RuntimeException("Account not found");
|
||||
|
||||
$oldVer = $acct['php_version'];
|
||||
if ($oldVer === $newVer) return;
|
||||
|
||||
// Remove old pool, create new one
|
||||
$oldPool = str_replace('{ver}', $oldVer, self::$poolDir) . "/{$acct['username']}.conf";
|
||||
if (file_exists($oldPool)) { unlink($oldPool); self::reloadFPM($oldVer); }
|
||||
|
||||
self::createPool($acct['username'], $newVer);
|
||||
|
||||
// Update vhost to use new socket
|
||||
VhostManager::create($acct['username'], $acct['domain'], $acct['home_dir'] . '/public_html', $newVer);
|
||||
|
||||
$db->execute("UPDATE accounts SET php_version = ? WHERE id = ?", [$newVer, $accountId]);
|
||||
$db->execute("UPDATE php_configs SET php_version = ?, updated_at = NOW() WHERE account_id = ?", [$newVer, $accountId]);
|
||||
}
|
||||
|
||||
public static function updateConfig(int $accountId, array $cfg): void {
|
||||
$db = DB::getInstance();
|
||||
$acct = $db->fetchOne("SELECT username, php_version FROM accounts WHERE id = ?", [$accountId]);
|
||||
if (!$acct) throw new RuntimeException("Account not found");
|
||||
|
||||
$poolFile = str_replace('{ver}', $acct['php_version'], self::$poolDir) . "/{$acct['username']}.conf";
|
||||
if (!file_exists($poolFile)) throw new RuntimeException("PHP-FPM pool not found");
|
||||
|
||||
$content = file_get_contents($poolFile);
|
||||
$map = [
|
||||
'memory_limit' => 'php_value[memory_limit]',
|
||||
'max_execution_time' => 'php_value[max_execution_time]',
|
||||
'upload_max_filesize' => 'php_value[upload_max_filesize]',
|
||||
'post_max_size' => 'php_value[post_max_size]',
|
||||
];
|
||||
foreach ($map as $key => $iniKey) {
|
||||
if (isset($cfg[$key])) {
|
||||
$content = preg_replace("/{$iniKey}\s*=.*/", "{$iniKey} = {$cfg[$key]}", $content);
|
||||
}
|
||||
}
|
||||
file_put_contents($poolFile, $content);
|
||||
self::reloadFPM($acct['php_version']);
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO php_configs (account_id, php_version, memory_limit, max_execution_time, upload_max_filesize, post_max_size)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE memory_limit=VALUES(memory_limit), max_execution_time=VALUES(max_execution_time),
|
||||
upload_max_filesize=VALUES(upload_max_filesize), post_max_size=VALUES(post_max_size), updated_at=NOW()",
|
||||
[$accountId, $acct['php_version'], $cfg['memory_limit'] ?? '256M', $cfg['max_execution_time'] ?? 30,
|
||||
$cfg['upload_max_filesize'] ?? '64M', $cfg['post_max_size'] ?? '64M']
|
||||
);
|
||||
}
|
||||
|
||||
public static function listExtensions(string $phpVer): array {
|
||||
$out = shell_exec("php{$phpVer} -m 2>/dev/null") ?: '';
|
||||
return array_values(array_filter(explode("\n", $out), fn($l) => $l && !str_starts_with($l, '[')));
|
||||
}
|
||||
|
||||
private static function reloadFPM(string $ver): void {
|
||||
shell_exec("systemctl reload php{$ver}-fpm 2>/dev/null || true");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* SSLManager — Let's Encrypt (Certbot) + custom certificate management
|
||||
*/
|
||||
class SSLManager {
|
||||
|
||||
public static function issueLetsEncrypt(int $accountId, string $domain, string $email = ''): array {
|
||||
$db = DB::getInstance();
|
||||
$webRoot = $db->fetchOne("SELECT d.document_root FROM domains d WHERE d.domain = ? AND d.account_id = ?", [$domain, $accountId]);
|
||||
if (!$webRoot) throw new RuntimeException("Domain not found for this account");
|
||||
|
||||
$docRoot = $webRoot['document_root'];
|
||||
$email = $email ?: "ssl@{$domain}";
|
||||
$cmd = "certbot certonly --webroot -w {$docRoot} -d {$domain} -d www.{$domain}"
|
||||
. " --email " . escapeshellarg($email)
|
||||
. " --agree-tos --non-interactive 2>&1";
|
||||
$out = shell_exec($cmd);
|
||||
|
||||
$certPath = "/etc/letsencrypt/live/{$domain}/fullchain.pem";
|
||||
$keyPath = "/etc/letsencrypt/live/{$domain}/privkey.pem";
|
||||
$chainPath = "/etc/letsencrypt/live/{$domain}/chain.pem";
|
||||
|
||||
if (!file_exists($certPath)) {
|
||||
novacpx_log('error', "Certbot failed for $domain: $out");
|
||||
throw new RuntimeException("Certbot failed. Check DNS propagation and try again.");
|
||||
}
|
||||
|
||||
$cert = file_get_contents($certPath);
|
||||
$key = file_get_contents($keyPath);
|
||||
$chain = file_get_contents($chainPath);
|
||||
|
||||
// Parse expiry
|
||||
$expiryRaw = shell_exec("openssl x509 -enddate -noout -in " . escapeshellarg($certPath));
|
||||
preg_match('/notAfter=(.+)/', $expiryRaw ?: '', $m);
|
||||
$expires = isset($m[1]) ? date('Y-m-d', strtotime($m[1])) : null;
|
||||
|
||||
// Store in DB
|
||||
$certId = self::storeCert($accountId, $domain, 'lets_encrypt', $cert, $key, $chain, $expires);
|
||||
|
||||
// Install on vhost
|
||||
$acct = $db->fetchOne("SELECT username FROM accounts WHERE id = ?", [$accountId]);
|
||||
VhostManager::enableSSL($acct['username'], $domain, $cert, $key, $chain);
|
||||
|
||||
return ['cert_id' => $certId, 'expires' => $expires, 'output' => $out];
|
||||
}
|
||||
|
||||
public static function installCustom(int $accountId, string $domain, string $cert, string $key, string $chain = ''): int {
|
||||
$db = DB::getInstance();
|
||||
$expiryRaw = shell_exec("echo " . escapeshellarg($cert) . " | openssl x509 -enddate -noout 2>/dev/null");
|
||||
preg_match('/notAfter=(.+)/', $expiryRaw ?: '', $m);
|
||||
$expires = isset($m[1]) ? date('Y-m-d', strtotime($m[1])) : null;
|
||||
|
||||
$certId = self::storeCert($accountId, $domain, 'custom', $cert, $key, $chain, $expires);
|
||||
|
||||
$acct = $db->fetchOne("SELECT username FROM accounts WHERE id = ?", [$accountId]);
|
||||
VhostManager::enableSSL($acct['username'], $domain, $cert, $key, $chain);
|
||||
return $certId;
|
||||
}
|
||||
|
||||
public static function renewAll(): void {
|
||||
$db = DB::getInstance();
|
||||
$soon = $db->fetchAll(
|
||||
"SELECT * FROM ssl_certs WHERE auto_renew = 1 AND type = 'lets_encrypt'
|
||||
AND expires_at <= DATE_ADD(NOW(), INTERVAL 30 DAY) AND status = 'active'"
|
||||
);
|
||||
foreach ($soon as $cert) {
|
||||
try {
|
||||
self::issueLetsEncrypt($cert['account_id'], $cert['domain']);
|
||||
novacpx_log('info', "SSL auto-renewed: {$cert['domain']}");
|
||||
} catch (Throwable $e) {
|
||||
novacpx_log('error', "SSL renewal failed for {$cert['domain']}: " . $e->getMessage());
|
||||
$db->execute("UPDATE ssl_certs SET status = 'failed' WHERE id = ?", [$cert['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function storeCert(int $accountId, string $domain, string $type, string $cert, string $key, string $chain, ?string $expires): int {
|
||||
$db = DB::getInstance();
|
||||
$db->execute("DELETE FROM ssl_certs WHERE account_id = ? AND domain = ?", [$accountId, $domain]);
|
||||
return (int)$db->insert(
|
||||
"INSERT INTO ssl_certs (account_id, domain, type, cert, private_key, chain, issued_at, expires_at, status)
|
||||
VALUES (?,?,?,?,?,?,NOW(),?,?)",
|
||||
[$accountId, $domain, $type, $cert, $key, $chain, $expires, 'active']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* VhostManager — creates/removes Apache2 and nginx virtual host configs
|
||||
*/
|
||||
class VhostManager {
|
||||
|
||||
public static function create(string $username, string $domain, string $docRoot, string $phpVer): void {
|
||||
$logDir = "/home/{$username}/logs";
|
||||
if (WEB_SERVER === 'nginx') {
|
||||
self::writeNginx($username, $domain, $docRoot, $phpVer, $logDir);
|
||||
} else {
|
||||
self::writeApache($username, $domain, $docRoot, $phpVer, $logDir);
|
||||
}
|
||||
self::reload();
|
||||
}
|
||||
|
||||
public static function createSubdomain(string $username, string $subdomain, string $docRoot, string $phpVer): void {
|
||||
self::create($username, $subdomain, $docRoot, $phpVer);
|
||||
}
|
||||
|
||||
public static function suspend(string $username, string $domain): void {
|
||||
$suspendedRoot = "/var/novacpx/suspended";
|
||||
@mkdir($suspendedRoot, 0755, true);
|
||||
$suspendPage = "{$suspendedRoot}/{$domain}.html";
|
||||
file_put_contents($suspendPage,
|
||||
"<html><body style='font-family:sans-serif;text-align:center;padding:4rem;background:#0d0f17;color:#e2e4f0'>"
|
||||
. "<h1 style='color:#ef4444'>Account Suspended</h1>"
|
||||
. "<p>This account has been suspended. Please contact support.</p></body></html>"
|
||||
);
|
||||
// Rewrite vhost to serve suspension page
|
||||
if (WEB_SERVER === 'nginx') {
|
||||
$conf = "/etc/nginx/sites-available/novacpx-{$username}.conf";
|
||||
if (file_exists($conf)) {
|
||||
$content = file_get_contents($conf);
|
||||
$content = preg_replace('/root\s+[^;]+;/', "root {$suspendedRoot};", $content);
|
||||
file_put_contents($conf, $content);
|
||||
}
|
||||
} else {
|
||||
$conf = "/etc/apache2/sites-available/novacpx-{$username}.conf";
|
||||
if (file_exists($conf)) {
|
||||
$content = file_get_contents($conf);
|
||||
$content = preg_replace('/DocumentRoot\s+\S+/', "DocumentRoot {$suspendedRoot}", $content);
|
||||
file_put_contents($conf, $content);
|
||||
}
|
||||
}
|
||||
self::reload();
|
||||
}
|
||||
|
||||
public static function unsuspend(string $username, string $domain): void {
|
||||
// Re-create from DB
|
||||
$db = DB::getInstance();
|
||||
$acct = $db->fetchOne("SELECT * FROM accounts WHERE username = ?", [$username]);
|
||||
if ($acct) {
|
||||
self::create($username, $domain, $acct['home_dir'] . '/public_html', $acct['php_version']);
|
||||
}
|
||||
}
|
||||
|
||||
public static function remove(string $username, string $domain): void {
|
||||
if (WEB_SERVER === 'nginx') {
|
||||
$conf = "/etc/nginx/sites-available/novacpx-{$username}.conf";
|
||||
$link = "/etc/nginx/sites-enabled/novacpx-{$username}.conf";
|
||||
@unlink($conf); @unlink($link);
|
||||
} else {
|
||||
$conf = "/etc/apache2/sites-available/novacpx-{$username}.conf";
|
||||
shell_exec("a2dissite novacpx-{$username} 2>/dev/null");
|
||||
@unlink($conf);
|
||||
}
|
||||
self::reload();
|
||||
}
|
||||
|
||||
public static function enableSSL(string $username, string $domain, string $cert, string $key, string $chain = ''): void {
|
||||
$certDir = "/etc/novacpx/ssl/accounts/{$username}";
|
||||
@mkdir($certDir, 0700, true);
|
||||
file_put_contents("{$certDir}/cert.pem", $cert);
|
||||
file_put_contents("{$certDir}/key.pem", $key);
|
||||
if ($chain) file_put_contents("{$certDir}/chain.pem", $chain);
|
||||
|
||||
if (WEB_SERVER === 'nginx') {
|
||||
$conf = file_get_contents("/etc/nginx/sites-available/novacpx-{$username}.conf") ?: '';
|
||||
if (!str_contains($conf, 'ssl_certificate')) {
|
||||
$conf = str_replace('listen 80;', "listen 443 ssl http2;\n listen 80;\n ssl_certificate {$certDir}/cert.pem;\n ssl_certificate_key {$certDir}/key.pem;", $conf);
|
||||
file_put_contents("/etc/nginx/sites-available/novacpx-{$username}.conf", $conf);
|
||||
}
|
||||
} else {
|
||||
$conf = file_get_contents("/etc/apache2/sites-available/novacpx-{$username}.conf") ?: '';
|
||||
if (!str_contains($conf, 'SSLEngine')) {
|
||||
$conf = str_replace('<VirtualHost *:80>', "<VirtualHost *:443>\n SSLEngine on\n SSLCertificateFile {$certDir}/cert.pem\n SSLCertificateKeyFile {$certDir}/key.pem", $conf);
|
||||
$conf .= "\n<VirtualHost *:80>\n ServerName {$domain}\n Redirect permanent / https://{$domain}/\n</VirtualHost>";
|
||||
file_put_contents("/etc/apache2/sites-available/novacpx-{$username}.conf", $conf);
|
||||
}
|
||||
}
|
||||
self::reload();
|
||||
}
|
||||
|
||||
private static function writeNginx(string $username, string $domain, string $docRoot, string $phpVer, string $logDir): void {
|
||||
$sock = "/run/php/php{$phpVer}-fpm-{$username}.sock";
|
||||
$conf = "/etc/nginx/sites-available/novacpx-{$username}.conf";
|
||||
file_put_contents($conf, "server {
|
||||
listen 80;
|
||||
server_name {$domain} www.{$domain};
|
||||
root {$docRoot};
|
||||
index index.php index.html index.htm;
|
||||
access_log {$logDir}/access.log;
|
||||
error_log {$logDir}/error.log;
|
||||
|
||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:{$sock};
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
||||
fastcgi_param PHP_VALUE \"error_log={$logDir}/php.log\";
|
||||
}
|
||||
location ~ /\\.ht { deny all; }
|
||||
location ~* \\.(jpg|jpeg|png|gif|ico|css|js|svg|woff2)$ { expires 30d; add_header Cache-Control public; }
|
||||
}
|
||||
");
|
||||
@symlink($conf, "/etc/nginx/sites-enabled/novacpx-{$username}.conf");
|
||||
}
|
||||
|
||||
private static function writeApache(string $username, string $domain, string $docRoot, string $phpVer, string $logDir): void {
|
||||
$sock = "/run/php/php{$phpVer}-fpm-{$username}.sock";
|
||||
$conf = "/etc/apache2/sites-available/novacpx-{$username}.conf";
|
||||
file_put_contents($conf, "<VirtualHost *:80>
|
||||
ServerName {$domain}
|
||||
ServerAlias www.{$domain}
|
||||
DocumentRoot {$docRoot}
|
||||
ErrorLog {$logDir}/error.log
|
||||
CustomLog {$logDir}/access.log combined
|
||||
|
||||
<Directory {$docRoot}>
|
||||
Options -Indexes +FollowSymLinks +MultiViews
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<FilesMatch \\.php$>
|
||||
SetHandler \"proxy:unix:{$sock}|fcgi://localhost/\"
|
||||
</FilesMatch>
|
||||
</VirtualHost>
|
||||
");
|
||||
shell_exec("a2ensite novacpx-{$username} 2>/dev/null");
|
||||
}
|
||||
|
||||
private static function reload(): void {
|
||||
if (WEB_SERVER === 'nginx') {
|
||||
shell_exec("nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null");
|
||||
} else {
|
||||
shell_exec("apache2ctl configtest 2>/dev/null && systemctl reload apache2 2>/dev/null");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user