mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
feat: NovaCPX v1.0.0 initial scaffold
Full hosting control panel with 3 tiers: Admin, Reseller, User. - install.sh: unattended installer for Ubuntu 20/22/24 + Debian 11/12 - PHP multi-version (7.4/8.1/8.2/8.3), Apache2/nginx choice, MySQL, PostgreSQL - BIND9 DNS, Postfix+Dovecot mail, ProFTPD, Certbot SSL, UFW, Fail2Ban - 18-table DB schema with audit log and version tracking - PHP REST API (auth, system/updates, server stats, service control) - Admin panel: dark dashboard, service manager, git-based update system - User panel: usage rings + feature card grid (distinct from cPanel) - VERSION file: git-tracked; Admin > Updates panel shows/applies git commits Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
class Auth {
|
||||
private static ?Auth $instance = null;
|
||||
private ?array $user = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function getInstance(): self {
|
||||
if (!self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function check(): bool {
|
||||
// Bearer token (API)
|
||||
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if (str_starts_with($header, 'Bearer ')) {
|
||||
$token = substr($header, 7);
|
||||
return $this->loginByToken($token);
|
||||
}
|
||||
// Session cookie
|
||||
$sessionId = $_COOKIE['ncpx_session'] ?? '';
|
||||
if ($sessionId) {
|
||||
return $this->loginBySession($sessionId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
$this->user = $row;
|
||||
return true;
|
||||
}
|
||||
|
||||
private function loginByToken(string $token): bool {
|
||||
$db = DB::getInstance();
|
||||
$row = $db->fetchOne(
|
||||
"SELECT t.permissions, u.id as uid, u.username, u.email, u.role, u.status
|
||||
FROM api_tokens t
|
||||
JOIN users u ON u.id = t.user_id
|
||||
WHERE t.token = ? AND (t.expires_at IS NULL OR t.expires_at > NOW()) AND u.status = 'active'",
|
||||
[hash('sha256', $token)]
|
||||
);
|
||||
if (!$row) return false;
|
||||
$db->execute("UPDATE api_tokens SET last_used = NOW() WHERE token = ?", [hash('sha256', $token)]);
|
||||
$this->user = $row;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function attempt(string $username, string $password): ?string {
|
||||
$db = DB::getInstance();
|
||||
$user = $db->fetchOne(
|
||||
"SELECT * FROM users WHERE (username = ? OR email = ?) AND status = 'active'",
|
||||
[$username, $username]
|
||||
);
|
||||
if (!$user || !password_verify($password, $user['password'])) return null;
|
||||
|
||||
// Create session
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$sessionId = hash('sha256', $token);
|
||||
$db->execute(
|
||||
"INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at)
|
||||
VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 8 HOUR))",
|
||||
[$sessionId, $user['id'], $_SERVER['REMOTE_ADDR'] ?? '', $_SERVER['HTTP_USER_AGENT'] ?? '']
|
||||
);
|
||||
$db->execute("UPDATE users SET last_login = NOW() WHERE id = ?", [$user['id']]);
|
||||
|
||||
setcookie('ncpx_session', $token, [
|
||||
'expires' => time() + 28800,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
]);
|
||||
$this->user = $user;
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function logout(): void {
|
||||
$sessionId = hash('sha256', $_COOKIE['ncpx_session'] ?? '');
|
||||
DB::getInstance()->execute("DELETE FROM sessions WHERE id = ?", [$sessionId]);
|
||||
setcookie('ncpx_session', '', time() - 3600, '/', '', true, true);
|
||||
}
|
||||
|
||||
public function user(): ?array { return $this->user; }
|
||||
|
||||
public function require(string ...$roles): void {
|
||||
$user = $this->user();
|
||||
if (!$user || !in_array($user['role'], $roles)) {
|
||||
Response::error('Forbidden', 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* NovaCPX Core — constants, config loader, helpers
|
||||
*/
|
||||
|
||||
define('NOVACPX_VERSION', trim(file_get_contents(NOVACPX_ROOT . '/VERSION') ?: '1.0.0'));
|
||||
define('NOVACPX_CONFIG', '/etc/novacpx/config.ini');
|
||||
|
||||
// Load config
|
||||
$_cfg = parse_ini_file(NOVACPX_CONFIG, true);
|
||||
if (!$_cfg) {
|
||||
http_response_code(503);
|
||||
die(json_encode(['error' => 'NovaCPX not configured. Run the installer.']));
|
||||
}
|
||||
|
||||
define('DB_HOST', $_cfg['database']['host'] ?? 'localhost');
|
||||
define('DB_NAME', $_cfg['database']['name'] ?? 'novacpx');
|
||||
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('WEB_SERVER', $_cfg['web']['server'] ?? 'apache');
|
||||
define('PHP_DEFAULT',$_cfg['web']['php_default'] ?? '8.3');
|
||||
|
||||
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,
|
||||
$ctx ? json_encode($ctx) : ''
|
||||
);
|
||||
file_put_contents('/var/log/novacpx/panel.log', $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
function audit(string $action, string $resource = '', array $detail = []): void {
|
||||
try {
|
||||
$db = DB::getInstance();
|
||||
$auth = Auth::getInstance();
|
||||
$user = $auth->user();
|
||||
$db->execute(
|
||||
"INSERT INTO audit_log (user_id, username, action, resource, detail, ip_address, user_agent)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
$user['id'] ?? null,
|
||||
$user['username'] ?? 'system',
|
||||
$action,
|
||||
$resource,
|
||||
json_encode($detail),
|
||||
$_SERVER['REMOTE_ADDR'] ?? '',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
novacpx_log('error', 'audit failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
class DB {
|
||||
private static ?DB $instance = null;
|
||||
private PDO $pdo;
|
||||
|
||||
private function __construct() {
|
||||
$this->pdo = new PDO(
|
||||
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||
DB_USER, DB_PASS,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function getInstance(): self {
|
||||
if (!self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function execute(string $sql, array $params = []): PDOStatement {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
public function fetchOne(string $sql, array $params = []): ?array {
|
||||
return $this->execute($sql, $params)->fetch() ?: null;
|
||||
}
|
||||
|
||||
public function fetchAll(string $sql, array $params = []): array {
|
||||
return $this->execute($sql, $params)->fetchAll();
|
||||
}
|
||||
|
||||
public function insert(string $sql, array $params = []): string {
|
||||
$this->execute($sql, $params);
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function pdo(): PDO { return $this->pdo; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
class Response {
|
||||
public static function json(array $data, int $code = 200): never {
|
||||
http_response_code($code);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function success(mixed $data = null, string $message = 'OK'): never {
|
||||
self::json(['success' => true, 'message' => $message, 'data' => $data]);
|
||||
}
|
||||
|
||||
public static function error(string $message, int $code = 400, array $errors = []): never {
|
||||
self::json(['success' => false, 'message' => $message, 'errors' => $errors], $code);
|
||||
}
|
||||
|
||||
public static function paginate(array $items, int $total, int $page, int $perPage): never {
|
||||
self::json([
|
||||
'success' => true,
|
||||
'data' => $items,
|
||||
'meta' => [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'pages' => (int) ceil($total / $perPage),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user