mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
39942929a7
- set_exception_handler in api/index.php prevents uncaught exceptions from crashing PHP-FPM - AccountManager::create() wrapped in DB transaction with rollback + Linux user cleanup on failure - CORS origin regex updated to allow requests from port 443 (NPM reverse proxy) - index.html written via sudo tee instead of file_put_contents (www-data permission fix) - chpasswd now called with sudo prefix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
90 lines
3.1 KiB
PHP
90 lines
3.1 KiB
PHP
<?php
|
|
/**
|
|
* NovaCPX API Router
|
|
* All requests: /api/{endpoint}/{action}
|
|
*/
|
|
|
|
define('NOVACPX_ROOT', dirname(__DIR__));
|
|
define('NOVACPX_API', __DIR__);
|
|
define('NOVACPX_LIB', NOVACPX_ROOT . '/lib');
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
// Global exception handler — prevents uncaught exceptions from crashing PHP-FPM (502)
|
|
set_exception_handler(function (Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['success' => false, 'message' => $e->getMessage(), 'errors' => []]);
|
|
exit;
|
|
});
|
|
|
|
$_ver = file_get_contents(NOVACPX_ROOT . '/VERSION')
|
|
?: file_get_contents('/opt/novacpx-src/VERSION')
|
|
?: '1.0.0';
|
|
header('X-NovaCPX-Version: ' . trim($_ver));
|
|
|
|
// CORS for same-origin panel requests (ports 8880/8881/8882/8883 and HTTPS via reverse proxy on 443)
|
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
if (preg_match('#^https?://[^/]+(:(888[0-3]))?$#', $origin)) {
|
|
header("Access-Control-Allow-Origin: $origin");
|
|
header('Access-Control-Allow-Credentials: true');
|
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
|
}
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
|
|
|
require_once NOVACPX_LIB . '/Core.php';
|
|
require_once NOVACPX_LIB . '/Auth.php';
|
|
require_once NOVACPX_LIB . '/DB.php';
|
|
require_once NOVACPX_LIB . '/Response.php';
|
|
|
|
// Parse route: /api/endpoint/action
|
|
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
$parts = array_values(array_filter(explode('/', $uri)));
|
|
$apiIdx = array_search('api', $parts);
|
|
$endpoint = $parts[$apiIdx + 1] ?? null;
|
|
$action = $parts[$apiIdx + 2] ?? null;
|
|
|
|
if (!$endpoint) {
|
|
Response::json(['status' => 'ok', 'panel' => 'NovaCPX', 'version' => NOVACPX_VERSION]);
|
|
}
|
|
|
|
// Public endpoints (no auth required)
|
|
$public = ['auth'];
|
|
if (!in_array($endpoint, $public)) {
|
|
$auth = Auth::getInstance();
|
|
if (!$auth->check()) {
|
|
Response::error('Unauthorized', 401);
|
|
}
|
|
$currentUser = $auth->user();
|
|
}
|
|
|
|
// Route to endpoint handler
|
|
$endpointFile = NOVACPX_API . "/endpoints/{$endpoint}.php";
|
|
if (!file_exists($endpointFile)) {
|
|
Response::error("Unknown endpoint: $endpoint", 404);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Verify the current user can access a given account_id.
|
|
* Returns the account row or sends a 404 error response.
|
|
* Resellers may only access their own customers; users may only access their own account.
|
|
*/
|
|
function assert_account_access(int $accountId): array {
|
|
global $currentUser;
|
|
$db = DB::getInstance();
|
|
$acct = $db->fetchOne("SELECT a.*, u.reseller_id FROM accounts a JOIN users u ON u.id = a.user_id WHERE a.id = ?", [$accountId]);
|
|
if (!$acct) Response::error("Account not found", 404);
|
|
if ($currentUser['role'] === 'reseller' && (int)$acct['reseller_id'] !== $currentUser['uid']) {
|
|
Response::error("Account not found", 404);
|
|
}
|
|
if ($currentUser['role'] === 'user') {
|
|
$own = $db->fetchOne("SELECT id FROM accounts WHERE id = ? AND user_id = ?", [$accountId, $currentUser['uid']]);
|
|
if (!$own) Response::error("Account not found", 404);
|
|
}
|
|
return $acct;
|
|
}
|
|
|
|
require $endpointFile;
|