view = App::getInstance()->getView(); } private function authenticateRequest(): ?array { $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; $token = str_replace('Bearer ', '', $token); if (empty($token) && isset($_GET['api_token'])) { $token = $_GET['api_token']; } if (empty($token)) { $this->view->json(['error' => 'API token is required'], 401); } $userModel = new User(); $user = $userModel->findByApiToken($token); if (!$user) { $this->view->json(['error' => 'Invalid API token'], 401); } return $user; } public function status(): void { $user = $this->authenticateRequest(); $serverModel = new Server(); $accessibleIds = $serverModel->getAccessibleServerIds((int) $user['id']); $monitoringService = new MonitoringService(); $stats = $monitoringService->getDashboardStats($accessibleIds); $this->view->json([ 'success' => true, 'data' => [ 'application' => 'ServerManager', 'version' => '1.0.0', 'stats' => $stats, 'timestamp' => date('c'), ], ]); } public function servers(): void { $user = $this->authenticateRequest(); $serverModel = new Server(); $page = (int) ($_GET['page'] ?? 1); $perPage = (int) ($_GET['per_page'] ?? 20); $search = $_GET['search'] ?? ''; $filters = []; if ($search) $filters['search'] = $search; $accessibleIds = $serverModel->getAccessibleServerIds((int) $user['id']); $filters['accessible_ids'] = !empty($accessibleIds) ? $accessibleIds : [-1]; $servers = $serverModel->getAll($page, $perPage, $filters); $this->view->json([ 'success' => true, 'data' => $servers['data'], 'pagination' => [ 'page' => $servers['page'], 'per_page' => $servers['per_page'], 'total' => $servers['total'], 'total_pages' => $servers['total_pages'], ], ]); } public function serverDetail(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } if (!$serverModel->canView($id, (int) $user['id'])) { $this->view->json(['error' => 'Server not found'], 404); } $monitoringService = new MonitoringService(); $metrics = $monitoringService->getLatestMetrics($id); unset($server['agent_key'], $server['ssh_password'], $server['ssh_key']); $this->view->json([ 'success' => true, 'data' => [ 'server' => $server, 'metrics' => $metrics, ], ]); } public function serverAdd(): void { $user = $this->authenticateRequest(); $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $validator = new Validator(); $rules = [ 'name' => 'required|min:2|max:100', 'ip_address' => 'required', 'ssh_port' => 'required|numeric|port', 'ssh_user' => 'required|min:2|max:50', ]; if (!$validator->validate($input, $rules)) { $this->view->json(['error' => $validator->getFirstError()], 400); } $permissions = $this->extractPermissionsFromInput($input); if (!empty($permissions)) { $serverConfig = [ 'ip_address' => $input['ip_address'], 'ssh_port' => (int) $input['ssh_port'], 'ssh_user' => $input['ssh_user'], 'ssh_password' => $input['ssh_password'] ?? null, 'ssh_key' => $input['ssh_key'] ?? null, ]; $permValidator = new PermissionValidator(); $permResult = $permValidator->validate($serverConfig, $permissions); if (!$permResult['passed']) { $failedPermissions = array_filter($permResult['results'], fn($r) => !$r['passed']); $errors = []; foreach ($failedPermissions as $fp) { $label = $fp['description'] ?: ($fp['type'] . ': ' . $fp['value']); $errors[] = $label . ' — ' . $fp['message']; } $this->view->json([ 'error' => 'Permission validation failed', 'permission_errors' => $errors, ], 400); } } $serverModel = new Server(); $id = $serverModel->create($input); if (!empty($permissions)) { try { $permModel = new ServerPermission(); $permModel->save($id, $permissions); } catch (\Throwable $e) { // Table may not exist yet } } $this->view->json([ 'success' => true, 'message' => 'Server created successfully.', 'server_id' => $id, ], 201); } public function serverUpdate(int $id): void { $user = $this->authenticateRequest(); $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $serverModel = new Server(); $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } if (!$serverModel->canManage($id, (int) $user['id'])) { $this->view->json(['error' => 'Forbidden'], 403); } $permissions = $this->extractPermissionsFromInput($input); if (!empty($permissions)) { $serverConfig = [ 'ip_address' => $input['ip_address'] ?? $server['ip_address'], 'ssh_port' => (int) ($input['ssh_port'] ?? $server['ssh_port']), 'ssh_user' => $input['ssh_user'] ?? $server['ssh_user'], 'ssh_password' => $input['ssh_password'] ?? $server['ssh_password'], 'ssh_key' => $input['ssh_key'] ?? $server['ssh_key'], ]; $permValidator = new PermissionValidator(); $permResult = $permValidator->validate($serverConfig, $permissions); if (!$permResult['passed']) { $failedPermissions = array_filter($permResult['results'], fn($r) => !$r['passed']); $errors = []; foreach ($failedPermissions as $fp) { $label = $fp['description'] ?: ($fp['type'] . ': ' . $fp['value']); $errors[] = $label . ' — ' . $fp['message']; } $this->view->json([ 'error' => 'Permission validation failed', 'permission_errors' => $errors, ], 400); } } $serverModel->update($id, $input); if (!empty($permissions)) { try { $permModel = new ServerPermission(); $permModel->save($id, $permissions); } catch (\Throwable $e) { // Table may not exist yet } } $this->view->json([ 'success' => true, 'message' => 'Server updated successfully.', ]); } private function extractPermissionsFromInput(array $input): array { $permissions = $input['permissions'] ?? []; if (!is_array($permissions)) { return []; } $filtered = []; foreach ($permissions as $perm) { if (!empty($perm['type']) && isset($perm['value']) && $perm['value'] !== '') { $filtered[] = [ 'type' => $perm['type'], 'value' => trim($perm['value']), 'description' => trim($perm['description'] ?? ''), ]; } } return $filtered; } public function serverDelete(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } if (!$serverModel->canManage($id, (int) $user['id'])) { $this->view->json(['error' => 'Forbidden'], 403); } $serverModel->delete($id); $this->view->json([ 'success' => true, 'message' => 'Server deleted successfully.', ]); } public function serverMetrics(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); if (!$serverModel->canView($id, (int) $user['id'])) { $this->view->json(['error' => 'Server not found'], 404); } $monitoringService = new MonitoringService(); $hours = (int) ($_GET['hours'] ?? 24); if ($hours > 168) $hours = 168; $metrics = $monitoringService->getHistoricalMetrics($id, $hours); $this->view->json([ 'success' => true, 'data' => $metrics, ]); } public function executeCommand(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); if (!$serverModel->canView($id, (int) $user['id'])) { $this->view->json(['error' => 'Server not found'], 404); } $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $command = $input['command'] ?? ''; if (empty($command)) { $this->view->json(['error' => 'Command is required'], 400); } $serverModel = new Server(); $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } try { $ssh = new SSHService(); if (!$ssh->connect($server)) { throw new \RuntimeException('Could not establish SSH connection'); } $result = $ssh->exec($command); $ssh->disconnect(); $commandHistory = new CommandHistory(); $commandHistory->log( $id, $user['id'], $command, $result['output'], $result['exit_status'] ?? 0 ); $auditService = new AuditService(); $auditService->logServerAction($id, 'api_command_executed', $command); $this->view->json([ 'success' => true, 'data' => [ 'output' => $result['output'], 'exit_status' => $result['exit_status'], 'stderr' => $result['stderr'], ], ]); } catch (\Throwable $e) { $this->view->json([ 'error' => 'Command execution failed: ' . $e->getMessage(), ], 500); } } public function users(): void { $currentUser = $this->authenticateRequest(); if ($currentUser['role'] !== 'super_admin' && $currentUser['role'] !== 'admin') { $this->view->json(['error' => 'Forbidden'], 403); } $userModel = new User(); $users = $userModel->getAll(1, 100); $this->view->json([ 'success' => true, 'data' => $users['data'], ]); } public function pushMetrics(): void { $rawBody = file_get_contents('php://input'); $input = json_decode($rawBody, true); if (!$input || empty($input['agent_key'])) { $this->view->json(['error' => 'agent_key is required'], 401); } $serverModel = new Server(); $server = $serverModel->findByAgentKey($input['agent_key']); if (!$server || !$server['is_active']) { $this->view->json(['error' => 'Invalid agent key'], 401); } $cpu = isset($input['cpu']) ? max(0, min(100, (float) $input['cpu'])) : 0; $ram = isset($input['ram']) ? max(0, min(100, (float) $input['ram'])) : 0; $disk = isset($input['disk']) ? max(0, min(100, (float) $input['disk'])) : 0; $load = (float) ($input['load'] ?? 0); $uptime = (string) ($input['uptime'] ?? ''); $monitoring = new MonitoringService(); $monitoring->saveMetrics([ 'server_id' => (int) $server['id'], 'status' => 'online', 'cpu_usage' => $cpu, 'ram_usage' => $ram, 'disk_usage' => $disk, 'load_average' => $load, 'uptime' => $uptime, ]); $serverModel->updateAgentLastSeen((int) $server['id']); $checks = [ 'cpu' => ['val' => $cpu, 'warn' => $server['threshold_cpu_warning'], 'crit' => $server['threshold_cpu_critical']], 'ram' => ['val' => $ram, 'warn' => $server['threshold_ram_warning'], 'crit' => $server['threshold_ram_critical']], 'disk' => ['val' => $disk, 'warn' => $server['threshold_disk_warning'], 'crit' => $server['threshold_disk_critical']], ]; $breaches = []; foreach ($checks as $metric => $c) { if ($c['crit'] !== null && $c['val'] >= (float) $c['crit']) { $breaches[] = ['metric' => $metric, 'level' => 'critical', 'current' => $c['val'], 'threshold' => $c['crit']]; } elseif ($c['warn'] !== null && $c['val'] >= (float) $c['warn']) { $breaches[] = ['metric' => $metric, 'level' => 'warning', 'current' => $c['val'], 'threshold' => $c['warn']]; } } if (!empty($breaches)) { $userIds = $serverModel->getAccessibleUserIds((int) $server['id']); $notificationModel = new Notification(); $fcm = new FCMService(); foreach ($breaches as $b) { $title = strtoupper($b['metric']) . " {$b['level']} on {$server['name']}"; $message = "{$server['name']}: {$b['metric']} at {$b['current']}% ({$b['level']} threshold: {$b['threshold']}%)"; foreach ($userIds as $uid) { $noteId = $notificationModel->create([ 'user_id' => (int) $uid, 'title' => $title, 'message' => $message, 'type' => $b['level'], ]); $fcm->sendToUser((int) $uid, $title, $message, $b['level'], $noteId); } } } $this->view->json(['success' => true]); } public function refreshAllMetrics(): void { $user = $this->authenticateRequest(); $monitoringService = new MonitoringService(); $results = $monitoringService->collectAllMetrics(); $this->view->json([ 'success' => true, 'data' => $results, ]); } public function register(): void { $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $validator = new Validator(); $rules = [ 'username' => 'required|min:3|max:50', 'email' => 'required|email', 'password' => 'required|min:8', 'confirm_password' => 'required', ]; if (!$validator->validate($input, $rules)) { $this->view->json([ 'success' => false, 'error' => $validator->getFirstError(), ], 400); return; } if ($input['password'] !== $input['confirm_password']) { $this->view->json([ 'success' => false, 'error' => 'Passwords do not match.', ], 400); return; } $userModel = new User(); $existing = $userModel->findByUsername($input['username']); if ($existing) { $this->view->json([ 'success' => false, 'error' => 'Username is already taken.', ], 409); return; } $existingEmail = $userModel->findByEmail($input['email']); if ($existingEmail) { $this->view->json([ 'success' => false, 'error' => 'Email is already registered.', ], 409); return; } $userId = $userModel->create([ 'username' => $input['username'], 'email' => $input['email'], 'password' => $input['password'], 'role' => 'admin', 'is_active' => 1, ]); $user = $userModel->findById($userId); $auditService = new AuditService(); $auditService->log('user_registered', 'User registered via API', $userId); $this->view->json([ 'success' => true, 'data' => [ 'token' => $user['api_token'], 'user' => [ 'id' => (int) $user['id'], 'username' => $user['username'], 'email' => $user['email'], 'role' => $user['role'], ], ], ], 201); } public function profile(): void { $user = $this->authenticateRequest(); $this->view->json([ 'success' => true, 'data' => [ 'id' => (int) $user['id'], 'username' => $user['username'], 'email' => $user['email'], 'role' => $user['role'], 'created_at' => $user['created_at'], ], ]); } public function updateProfile(): void { $user = $this->authenticateRequest(); $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $validator = new Validator(); $rules = [ 'email' => 'required|email', ]; if (!$validator->validate($input, $rules)) { $this->view->json([ 'success' => false, 'error' => $validator->getFirstError(), ], 400); return; } $updateData = ['email' => $input['email']]; if (!empty($input['new_password'])) { if (strlen($input['new_password']) < 8) { $this->view->json([ 'success' => false, 'error' => 'New password must be at least 8 characters.', ], 400); return; } if (empty($input['current_password'])) { $this->view->json([ 'success' => false, 'error' => 'Current password is required to set a new password.', ], 400); return; } if ($input['new_password'] !== ($input['confirm_password'] ?? '')) { $this->view->json([ 'success' => false, 'error' => 'New passwords do not match.', ], 400); return; } $userModel = new User(); $fullUser = $userModel->findById((int) $user['id']); if (!$fullUser || !(new \ServerManager\Core\Security())->verifyPassword($input['current_password'], $fullUser['password_hash'])) { $this->view->json([ 'success' => false, 'error' => 'Current password is incorrect.', ], 403); return; } $updateData['password'] = $input['new_password']; } $userModel = new User(); $userModel->update((int) $user['id'], $updateData); $auditService = new AuditService(); $auditService->log('profile_updated', 'Profile updated via API', (int) $user['id']); $this->view->json([ 'success' => true, 'message' => 'Profile updated successfully.', ]); } public function appVersion(): void { $db = \ServerManager\Core\Database::getInstance(); try { $configs = $db->fetchAll('SELECT * FROM app_config'); } catch (\Throwable $e) { $this->view->json([ 'success' => false, 'error' => 'Database error', ], 500); return; } $config = []; foreach ($configs as $row) { $config[$row['key']] = $row['value']; } $this->view->json([ 'success' => true, 'data' => [ 'version' => $config['app_version'] ?? '1.0.0', 'apk_url' => $config['apk_url'] ?? '/sysadmin.apk', 'force_update' => ($config['force_update'] ?? '0') === '1', 'release_notes' => $config['release_notes'] ?? '', ], ]); } public function unreadCount(): void { $user = $this->authenticateRequest(); $notificationModel = new \ServerManager\Models\Notification(); $count = $notificationModel->getUnreadCount((int) $user['id']); $this->view->json([ 'success' => true, 'unread_count' => $count, ]); } public function notifications(): void { $user = $this->authenticateRequest(); $notificationModel = new \ServerManager\Models\Notification(); $page = (int) ($_GET['page'] ?? 1); $perPage = (int) ($_GET['per_page'] ?? 20); $result = $notificationModel->getForUser((int) $user['id'], $page, $perPage); $unreadCount = $notificationModel->getUnreadCount((int) $user['id']); $this->view->json([ 'success' => true, 'data' => $result['data'], 'pagination' => [ 'page' => $result['page'], 'per_page' => $result['per_page'], 'total' => $result['total'], 'total_pages' => $result['total_pages'], ], 'unread_count' => $unreadCount, ]); } public function markNotificationRead(int $id): void { $user = $this->authenticateRequest(); $notificationModel = new \ServerManager\Models\Notification(); $notificationModel->markAsRead($id, (int) $user['id']); $this->view->json(['success' => true]); } public function markAllNotificationsRead(): void { $user = $this->authenticateRequest(); $notificationModel = new \ServerManager\Models\Notification(); $notificationModel->markAllAsRead((int) $user['id']); $this->view->json(['success' => true]); } public function registerFcmToken(): void { $user = $this->authenticateRequest(); $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $token = $input['token'] ?? ''; if (empty($token)) { $this->view->json(['error' => 'Token is required'], 400); } $db = \ServerManager\Core\Database::getInstance(); $existing = $db->fetch( 'SELECT id FROM user_fcm_tokens WHERE user_id = ? AND token = ?', [(int) $user['id'], $token] ); if (!$existing) { $db->insert('user_fcm_tokens', [ 'user_id' => (int) $user['id'], 'token' => $token, 'created_at' => date('Y-m-d H:i:s'), ]); } $this->view->json(['success' => true]); } public function serverServices(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); if (!$serverModel->canView($id, (int) $user['id'])) { $this->view->json(['error' => 'Server not found'], 404); } $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } try { $ssh = new SSHService(); if (!$ssh->connect($server)) { throw new \RuntimeException('SSH connection failed'); } $output = $ssh->exec('systemctl list-units --type=service --all --no-pager --no-legend 2>/dev/null'); $ssh->disconnect(); $services = []; if ($output['exit_status'] === 0) { foreach (explode("\n", trim($output['output'])) as $line) { if (preg_match('/^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/', $line, $m)) { $services[] = [ 'name' => trim($m[1]), 'load' => trim($m[2]), 'status' => trim($m[3]), 'description' => trim($m[4] ?? ''), ]; } } } $this->view->json([ 'success' => true, 'data' => $services, ]); } catch (\Throwable $e) { $this->view->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } public function serverReboot(int $id): void { $this->executeServerAction($id, 'reboot', '/sbin/reboot'); } public function serverShutdown(int $id): void { $this->executeServerAction($id, 'shutdown', '/sbin/shutdown -h now'); } public function serverTestConnection(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); if (!$serverModel->canView($id, (int) $user['id'])) { $this->view->json(['error' => 'Server not found'], 404); } $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } try { $ssh = new SSHService(); $connected = $ssh->connect($server); if ($connected) { $ssh->disconnect(); } $this->view->json([ 'success' => true, 'data' => ['connected' => $connected], ]); } catch (\Throwable $e) { $this->view->json([ 'success' => true, 'data' => ['connected' => false, 'error' => $e->getMessage()], ]); } } public function serverServiceAction(int $id): void { $user = $this->authenticateRequest(); $serverModel = new Server(); if (!$serverModel->canManage($id, (int) $user['id'])) { $this->view->json(['error' => 'Forbidden'], 403); } $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $service = (string) ($input['service'] ?? ''); $action = (string) ($input['action'] ?? ''); $allowed = ['start', 'stop', 'restart', 'reload']; if ($service === '' || !in_array($action, $allowed, true)) { $this->view->json(['error' => 'Invalid service or action'], 400); } $sanitized = preg_replace('/[^a-zA-Z0-9\-_.]/', '', $service); if ($sanitized === '') { $this->view->json(['error' => 'Invalid service name'], 400); } try { $ssh = new SSHService(); if (!$ssh->connect($server)) { throw new \RuntimeException('SSH connection failed'); } $result = $ssh->exec("sudo systemctl {$action} {$sanitized} 2>&1"); $ssh->disconnect(); $exitCode = isset($result['exit_status']) ? (int) $result['exit_status'] : -1; $auditService = new AuditService(); $auditService->logServerAction($id, "service_{$action}", "{$sanitized} {$action}"); if ($exitCode === 0) { $this->view->json([ 'success' => true, 'data' => [ 'message' => "Service '{$sanitized}' {$action}ed.", 'output' => trim($result['output'] ?? ''), ], ]); } else { $this->view->json([ 'success' => false, 'error' => "Failed to {$action} '{$sanitized}'. Exit code: {$exitCode}", 'data' => ['output' => trim($result['output'] ?? '')], ], 500); } } catch (\Throwable $e) { $this->view->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } private function executeServerAction(int $id, string $action, string $command): void { $user = $this->authenticateRequest(); $serverModel = new Server(); if (!$serverModel->canManage($id, (int) $user['id'])) { $this->view->json(['error' => 'Forbidden'], 403); } $server = $serverModel->findById($id); if (!$server) { $this->view->json(['error' => 'Server not found'], 404); } try { $ssh = new SSHService(); if (!$ssh->connect($server)) { throw new \RuntimeException('SSH connection failed'); } $result = $ssh->exec($command); $ssh->disconnect(); $auditService = new AuditService(); $auditService->log("server_{$action}", 'server', $id, ['name' => $server['name']]); $this->view->json([ 'success' => true, 'message' => ucfirst($action) . ' command executed.', 'data' => [ 'output' => $result['output'], 'exit_status' => $result['exit_status'], ], ]); } catch (\Throwable $e) { $this->view->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } public function login(): void { $input = json_decode(file_get_contents('php://input'), true) ?? $_POST; $validator = new Validator(); $rules = [ 'username' => 'required', 'password' => 'required|min:6', ]; if (!$validator->validate($input, $rules)) { $this->view->json([ 'success' => false, 'error' => $validator->getFirstError(), ], 400); return; } $userModel = new User(); $user = $userModel->authenticate($input['username'], $input['password']); if (!$user) { $auditService = new AuditService(); $auditService->logLogin($input['username'], false, 'Invalid credentials'); $this->view->json([ 'success' => false, 'error' => 'Invalid username or password.', ], 401); return; } $token = $user['api_token']; if (empty($token)) { $security = new \ServerManager\Core\Security(); $token = $security->generateApiToken(); $userModel->update((int) $user['id'], ['api_token' => $token]); } $auditService = new AuditService(); $auditService->logLogin($input['username'], true); $this->view->json([ 'success' => true, 'data' => [ 'token' => $token, 'user' => [ 'id' => (int) $user['id'], 'username' => $user['username'], 'email' => $user['email'], 'role' => $user['role'], ], ], ]); } }