repo = new Repository($db); $this->auth = new AuthMiddleware($this->repo); $this->engine = new Engine($this->repo); } public function handle(): void { $method = $_SERVER['REQUEST_METHOD']; $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = rtrim($path, '/'); $path = $path ?: '/'; header('Content-Type: application/json'); try { $publicPaths = ['/health', '/oauth', '/auth/me', '/auth/logout', '/ingest']; $isPublic = false; foreach ($publicPaths as $pp) { if ($path === $pp || str_starts_with($path, $pp . '/')) { $isPublic = true; break; } } if (!$isPublic) { $user = $this->auth->requireAuth(); if (!$user) { http_response_code(401); echo json_encode(['error' => 'Unauthorized', 'login_url' => $this->loginUrl()]); return; } } $result = match (true) { $path === '/health' && $method === 'GET' => ['status' => 'ok'], $path === '/ingest' && $method === 'POST' => $this->ingest(), $path === '/auth/me' && $method === 'GET' => $this->getMe(), $path === '/auth/logout' && $method === 'POST' => $this->logout(), $path === '/sources' && $method === 'GET' => $this->repo->getSources(), $path === '/sources' && $method === 'POST' => $this->createSource(), preg_match('#^/sources/(\d+)$#', $path, $m) && $method === 'DELETE' => $this->deleteEntity('source', (int) $m[1]), $path === '/rules' && $method === 'GET' => $this->repo->getRules(), $path === '/rules' && $method === 'POST' => $this->createRule(), preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'DELETE' => $this->deleteEntity('rule', (int) $m[1]), $path === '/alerts' && $method === 'GET' => $this->getAlerts(), $path === '/alerts/search' && $method === 'GET' => $this->searchAlerts(), preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST' => $this->ackAlert((int) $m[1]), preg_match('#^/alerts/counts$#', $path) && $method === 'GET' => $this->repo->getAlertCounts(), $path === '/logs/search' && $method === 'GET' => $this->searchLogs(), $path === '/config/allowed_tokens' && $method === 'GET' => ['tokens' => $this->repo->getAllowedUserTokens()], $path === '/config/allowed_tokens' && $method === 'PUT' => $this->updateAllowedTokens(), default => throw new \RuntimeException('Not found', 404), }; $this->respond(200, $result); } catch (\RuntimeException $e) { $this->respond($e->getCode() ?: 500, ['error' => $e->getMessage()]); } } private function respond(int $code, mixed $result): void { http_response_code($code); if (is_array($result)) { $hasObjects = false; $isList = array_is_list($result); foreach ($result as $key => $val) { if (is_object($val) && method_exists($val, 'toArray')) { $result[$key] = $val->toArray(); $hasObjects = true; } } if ($hasObjects || ($isList && (empty($result) || !isset($result['data'])))) { if ($isList) { $result = ['data' => $result]; } } } elseif (is_object($result) && method_exists($result, 'toArray')) { $result = ['data' => $result->toArray()]; } echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } private function loginUrl(): string { $redirect = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/oauth.php'; return 'https://auth.jakach.ch/?send_to=' . urlencode($redirect); } private function getMe(): array { $user = $this->auth->requireAuth(); if (!$user) { http_response_code(401); return ['error' => 'Not logged in', 'login_url' => $this->loginUrl()]; } return ['user' => $user]; } private function logout(): array { if (session_status() === PHP_SESSION_NONE) { session_start(); } $_SESSION = []; if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } session_destroy(); return ['status' => 'logged_out']; } private function createSource(): mixed { $body = json_decode(file_get_contents('php://input'), true); $type = LogSourceType::from($body['type'] ?? ''); return $this->repo->createSource( name: $body['name'], type: $type, address: $body['address'], labels: $body['labels'] ?? [], ); } private function createRule(): mixed { $body = json_decode(file_get_contents('php://input'), true); return $this->repo->createRule( name: $body['name'], pattern: $body['pattern'], severity: $body['severity'] ?? 'warning', rateLimitSeconds: $body['rate_limit_seconds'] ?? null, ); } private function deleteEntity(string $type, int $id): array { if ($type === 'source') { $this->repo->deleteSource($id); } else { $this->repo->deleteRule($id); } return ['status' => 'deleted', 'id' => $id]; } private function getAlerts(): mixed { $limit = (int) ($_GET['limit'] ?? 100); $offset = (int) ($_GET['offset'] ?? 0); $status = $_GET['status'] ?? null; $severity = $_GET['severity'] ?? null; return $this->repo->getAlerts($limit, $offset, $status, $severity); } private function ackAlert(int $id): array { $this->repo->updateAlertStatus($id, AlertStatus::Acknowledged); return ['status' => 'acknowledged', 'id' => $id]; } private function searchAlerts(): array { $query = $_GET['q'] ?? ''; if (empty($query)) { return []; } $limit = (int) ($_GET['limit'] ?? 100); return $this->repo->searchAlerts($query, $limit); } private function searchLogs(): array { $query = $_GET['q'] ?? ''; if (empty($query)) { return ['data' => []]; } $limit = (int) ($_GET['limit'] ?? 200); $offset = (int) ($_GET['offset'] ?? 0); return ['data' => $this->repo->searchLogEntries($query, $limit, $offset)]; } private function updateAllowedTokens(): array { $body = json_decode(file_get_contents('php://input'), true); $tokens = $body['tokens'] ?? []; $this->repo->setAllowedUserTokens($tokens); return ['status' => 'saved', 'tokens' => $this->repo->getAllowedUserTokens()]; } private function ingest(): array { $body = json_decode(file_get_contents('php://input'), true); $line = $body['line'] ?? ''; $source = $body['source'] ?? 'http'; if (empty($line)) { http_response_code(400); return ['error' => 'Missing "line" field']; } $this->engine->evaluate($line, null); return ['status' => 'ingested', 'line' => substr($line, 0, 100)]; } }