mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
88e98b4727
#26 Mobile responsive: - Hamburger button (SVG) in topbar for all three panels (admin/user/reseller) - Sidebar overlay div for click-outside-to-close on mobile - nova.js: DOMContentLoaded toggle handler with overlay and auto-close on nav click - nova.css: sidebar-overlay, page-header, panel/panel-header, table, btn-success/warning/danger/secondary/xs, badge-muted; mobile media query shows toggle, fixes stats-grid/modal/panel-header layout #27 Custom error pages: - /errors/404.php and /errors/500.php with NovaCPX dark theme matching panel design - Apache ErrorDocument 400/401/403/404/500/503 for ports 8880/8881/8882 with Alias /errors #28 API rate limiting: - api_rate_limits table (migration 004) with per-IP per-bucket counters - api/index.php: 10 req/min for auth endpoint, 120 req/min for all others - Returns X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers - Returns 429 Too Many Requests when exceeded; rate limit failure is non-fatal #29 Session Manager: - sessions.php endpoint: list/revoke/revoke-user/revoke-all - Admin panel Sessions page: table of active sessions with user, role, IP, browser, timestamps - Revoke single session, revoke all for user, revoke all sessions (self-evicts)
94 lines
3.4 KiB
PHP
94 lines
3.4 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');
|
|
$_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)
|
|
$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);
|
|
}
|
|
|
|
|
|
|
|
// #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());
|
|
}
|
|
})();
|
|
|
|
require $endpointFile;
|