check($ip)) { Http::fail(429, 'rate_limited', 'Zu viele Anfragen.'); } $service = new LicenseService($db); $action = resolveLicenseAction(); switch ($action) { // WICHTIG: Die Lizenz-Endpunkte antworten bewusst OHNE den // status/error-Umschlag der uebrigen API. Ihr Format ist ein bereits // ausgerollter Vertrag: das Feld "status" auf oberster Ebene traegt den // Lizenzzustand (valid, revoked, expired, not_found, activation_limit). // Ein Umschlag mit status=success wuerde von jedem bestehenden Client als // "nicht valid" interpretiert - saemtliche Lizenzen gaelten als ungueltig. case 'validate': if (Http::method() !== 'POST') { Http::fail(405, 'method_not_allowed', 'Diese Aktion erwartet POST.'); } // Bewusst oeffentlich: Client-Anwendungen pruefen hier ihre Lizenz. // Die Antwort verraet nichts ueber fremde Lizenzen. Http::raw($service->validate(Http::body(), $ip)); case 'deactivate': if (Http::method() !== 'POST') { Http::fail(405, 'method_not_allowed', 'Diese Aktion erwartet POST.'); } ApiAuth::requireScope($db, 'license:deactivate'); Http::raw($service->deactivate(Http::body(), $ip)); case 'status': Http::raw(['status' => 'ok', 'module' => 'license', 'version' => '2.0']); default: Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [ 'available' => ['validate', 'deactivate', 'status'], ]); } function resolveLicenseAction(): string { $explicit = Http::str('action'); if ($explicit !== null) { return strtolower($explicit); } $segments = array_values(array_filter( explode('/', trim(Http::path(), '/')), static fn(string $s): bool => $s !== '' )); $last = strtolower((string)end($segments)); return match ($last) { 'validate', 'deactivate', 'status' => $last, default => 'status', }; }