Regression aus dem vorigen Commit - /api/license/v1/validate lieferte die Antwort im neuen status/error-Umschlag. Der Vertrag dieses Endpunkts ist aber bereits ausgerollt: das Feld "status" auf oberster Ebene trägt den Lizenzzustand (valid, revoked, expired ...). LicenseClient las dadurch "success" statt "valid" — jeder ausgelieferte Client hätte seine Lizenz für ungültig gehalten. Die Lizenz-Endpunkte antworten jetzt wieder ohne Umschlag (Http::raw). Gefunden durch Ausführen der projekteigenen Test-Suite gegen den Server. Packager - FTP-Zugangsdaten standen als Standardwerte im Quelltext und zusätzlich in packager.config.json und in der Integrationsanleitung. Alle drei Fundstellen bereinigt; die Konfigurationsdatei ist nicht mehr versioniert. Zugangsdaten kommen aus Datei, Umgebungsvariablen oder CLI-Argument, sonst bricht das Programm mit einer klaren Meldung ab. - Das Veröffentlichen sendet jetzt ein Token (updateservice:publish) und nutzt den Endpunkt /api/updateservice/v1/publish. - Fehler wurden von einem leeren catch verschluckt, und ohne Erfolgsfall wurde gar nichts ausgegeben. Das Werkzeug meldete am Ende immer Erfolg und lieferte Rückgabewert 0, selbst wenn FTP-Upload und API-Aufruf fehlgeschlagen waren. Jetzt ehrliche Meldungen und Rückgabewerte 0/1/2. - packager.config.json wurde vom csproj nie ins Ausgabeverzeichnis kopiert, weshalb sie dort nie gefunden wurde und stets die hartkodierten Werte griffen. UpdateClient - IsVersionNewer entfernte die Vorabkennung, aber kein führendes "v". Damit scheiterte Version.TryParse bei "v1.4.2" und es wurde auf einen alphabetischen Vergleich zurückgefallen, in dem "v1.9.0" als neuer gilt als "v1.10.0" — derselbe Fehler wie zuvor serverseitig im SQL. Ersetzt durch einen vollständigen semantischen Vergleich, verifiziert mit 16 Testfällen. - Der Rückfall auf die API lag in einem catch-Block, aber GetAsync wirft bei einem 404 keine Exception. Fehlte die statische latest.json, brach die Prüfung ab, statt die API zu befragen. Dokumentation - BUGTRACKER_INTEGRATION_GUIDE.md beschrieb denselben Workflow ein zweites Mal und war bereits auseinandergelaufen: Aufrufe ohne Token, alte Pfade, weder Claim/Lease noch Idempotenz. Ersetzt durch einen Verweis auf das gepflegte Agenten-Handbuch samt Übersicht der Änderungen. - UPDATESERVICE_INTEGRATION_GUIDE.md um Token, Umgebungsvariablen und Rückgabewerte ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
2.6 KiB
PHP
86 lines
2.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Lizenz-API
|
|
*
|
|
* POST /api/license/v1/validate Lizenz und Hardware pruefen (oeffentlich)
|
|
* POST /api/license/v1/deactivate Aktivierung freigeben (authentifiziert)
|
|
* GET /api/license/v1/status Verfuegbarkeitspruefung
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../../../src/bootstrap.php';
|
|
|
|
use Deploymentcenter\Core\ApiAuth;
|
|
use Deploymentcenter\Core\Db;
|
|
use Deploymentcenter\Core\Http;
|
|
use Deploymentcenter\Modules\License\LicenseService;
|
|
use Deploymentcenter\Modules\License\RateLimiter;
|
|
|
|
Http::beginJson(['GET', 'POST', 'OPTIONS'], true);
|
|
|
|
$db = Db::init();
|
|
|
|
$ip = Http::clientIp();
|
|
$limiter = new RateLimiter($db, 120, 60, 'license');
|
|
if (!$limiter->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',
|
|
};
|
|
}
|