fix(2.7.1): Auskuenfte, die stimmen - Fassung, Health, doctor, Ansicht

Der erste Durchlauf der Testinstanz hat neun Bugs zutage gefoerdert, und fast
alle waren dieselbe Sorte Fehler: eine Auskunft, die nicht stimmte.

Am folgenreichsten die Fassung. config/config.php fuehrte app.version als feste
Zeichenkette und stand seit 2.0.0 darauf, waehrend 2.7.0 lief - unbemerkt, weil
die Datei gitignored ist und in keinem Diff auftaucht. Zwei gemeldete Bugs sind
daran fehldiagnostiziert worden ("der Server ist zu alt"). src/bootstrap.php
leitet die Fassung jetzt aus "current" in public/docs/changelog.json ab.

Im Einzelnen:

- #346 Fassung aus dem Changelog statt aus der Konfiguration
- #60  /api/health ueberlebt eine leere Datenbank: eigener Check "schema",
       healthy=false mit Begruendung statt HTTP 500
- #54  dc-testkit doctor prueft Mindestfassung (semantisch) und Rechte, statt
       sie nur anzuzeigen - dafuer neu: GET /api/tokens/v1/whoami
- #53  dc-testkit.sh unter /testkit/ erreichbar (Route fehlte, der
       dokumentierte Bezugsweg hat nie funktioniert) und mit LF ausgeliefert
       (deploy.py/deploy_dev.py entfernen CR bei .sh/.bash beim Upload)
- #288 Filter und Formulare werfen nicht mehr aufs Dashboard zurueck: die
       offene Ansicht wandert bei GET als "view" in den Query-String, und die
       Filter loesen ueber requestSubmit() aus - form.submit() erzeugt kein
       submit-Ereignis, weshalb der zentrale Zuhoerer dort nie lief
- #347 /api/bugtracker/v1/roadmap auch ohne .php erreichbar
- #55  Fassungshinweis in TESTUMGEBUNG.md 4.6 (serverseitig war das mit 2.6.0
       bereits behoben)

Nicht geprueft: php -l. Auf dem Arbeitsplatz ist kein PHP installiert; die
Abnahme laeuft ueber die Testinstanz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-30 21:06:33 +02:00
co-authored by Claude Opus 5
parent afcbb3c091
commit 3b56839a1e
13 changed files with 566 additions and 50 deletions
+48 -1
View File
@@ -47,6 +47,34 @@ try {
$logDir = DC_VAR . '/log';
$checks['log_writable'] = ['ok' => is_dir($logDir) ? is_writable($logDir) : is_writable(DC_ROOT)];
// --- Schema vorhanden? ---
// Dass die Verbindung steht, heisst noch nicht, dass die Migrationen gelaufen
// sind: SELECT 1 gelingt auch auf einer leeren Datenbank. Vor der ersten
// Migration gibt es dc_tokens nicht - und daran starb die Rechtepruefung
// weiter unten mit einer PDOException. Ausgerechnet der Endpunkt, der ueber
// kaputte Zustaende Auskunft geben soll, antwortete dann mit HTTP 500 statt
// mit healthy=false und einer Begruendung. Betroffen war jede Neuinstallation
// zwischen Deployment und erster Migration.
$schemaReady = false;
if ($db !== null) {
try {
$schemaReady = $db->query("SHOW TABLES LIKE 'dc_tokens'")->fetchColumn() !== false;
} catch (Throwable $e) {
Logger::error('Health-Check: Schemapruefung fehlgeschlagen', ['error' => $e->getMessage()]);
}
$checks['schema'] = $schemaReady
? ['ok' => true]
: [
'ok' => false,
'message' => 'Datenbankschema fehlt - die Migrationen wurden noch nicht ausgefuehrt (install_db.php aufrufen).',
];
if (!$schemaReady) {
$healthy = false;
}
}
$response = [
'healthy' => $healthy,
'app' => Config::get('app.name', 'Deploymentcenter'),
@@ -56,7 +84,26 @@ $response = [
];
// --- Detailinformationen nur fuer Authentifizierte ---
if ($db !== null && ApiAuth::resolve($db, 'bugtracker:read') !== null) {
// Die Rechtepruefung selbst bleibt zusaetzlich abgesichert: ein halb
// migriertes Schema besteht die Pruefung oben, kann hier aber trotzdem ueber
// eine fehlende Spalte stolpern. Ein Fehlschlag wird wie "nicht
// authentifiziert" behandelt - dann kommt die Kurzfassung heraus, und die
// Antwort bleibt auswertbar.
$authenticated = false;
if ($db !== null && $schemaReady) {
try {
$authenticated = ApiAuth::resolve($db, 'bugtracker:read') !== null;
} catch (Throwable $e) {
Logger::warning('Health-Check: Rechtepruefung nicht moeglich', ['error' => $e->getMessage()]);
$response['checks']['schema'] = [
'ok' => false,
'message' => 'Datenbankschema unvollstaendig - die Rechtepruefung war nicht moeglich.',
];
$response['healthy'] = false;
}
}
if ($authenticated) {
try {
$status = Migrator::status($db);
$response['schema'] = [
+42 -1
View File
@@ -406,7 +406,7 @@ $spec = [
],
],
'/api/bugtracker/v1/roadmap.php' => [
'/api/bugtracker/v1/roadmap' => [
'get' => [
'tags' => ['Bugtracker'],
'summary' => 'Roadmap lesen',
@@ -1229,6 +1229,47 @@ $spec = [
'responses' => ['201' => ['description' => 'Sub-Token erstellt'], '403' => $errorResponse],
],
],
'/api/tokens/v1/whoami' => [
'get' => [
'tags' => ['Tokens'],
'summary' => 'Auskunft ueber das eigene Token',
'description' =>
"Meldet Besitzerart, Projektbindung, Rechte, Umgebung und Frist des uebergebenen Tokens.\n\n"
. "Gedacht fuer Werkzeuge, die ihre Voraussetzungen pruefen wollen, BEVOR sie etwas tun. "
. "Ohne diesen Endpunkt liesse sich ein Recht nur durch Ausprobieren feststellen - und das "
. "hat Nebenwirkungen: Ein Test auf `bugtracker:manage` uebernaehme ein Item, einer auf "
. "`license:dev` stellte einen Schluessel aus.\n\n"
. "`is_workbench` ist der Wert, der ueber Fremdmeldungen entscheidet: nur ein Token der "
. "Besitzerart `dev_agent` darf in ein fremdes Projekt melden.\n\n"
. "Der Klartextwert des Tokens wird nicht ausgeliefert. Ein widerrufenes oder abgelaufenes "
. "Token bekommt `401`.",
'responses' => [
'200' => [
'description' => 'Angaben zum Token',
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'properties' => [
'token_id' => ['type' => 'string'],
'name' => ['type' => 'string'],
'type' => ['type' => 'string', 'enum' => ['master', 'sub']],
'owner_type' => ['type' => 'string', 'enum' => TokenManager::OWNER_TYPES],
'owner_identity' => ['type' => 'string', 'nullable' => true],
'is_workbench' => ['type' => 'boolean', 'description' => 'owner_type = dev_agent'],
'project_slug' => ['type' => 'string', 'nullable' => true],
'environment' => ['type' => 'string', 'enum' => TokenManager::ENVIRONMENTS],
'scopes' => ['type' => 'array', 'items' => ['type' => 'string']],
'created_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
'last_used_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'Stand VOR diesem Aufruf.'],
'expires_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
'parent_token_id' => ['type' => 'string', 'nullable' => true],
],
]]],
],
'401' => $errorResponse,
],
],
],
],
];
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* GET /api/tokens/v1/whoami
*
* Auskunft ueber das eigene Token: Besitzerart, Projektbindung, Rechte,
* Umgebung, Frist.
*
* Warum es das gibt: Bis hierher konnte ein Aufrufer nur ausprobieren, was
* sein Token darf - und das geht nicht ohne Nebenwirkung. "Darf ich
* bugtracker:manage?" haette geheissen, ein Item zu uebernehmen; "habe ich
* license:dev?" haette einen Schluessel ausgestellt. Also meldete
* dc-testkit doctor nur "gueltig - der Bugtracker antwortet" und lief mit
* einem Laufzeit-Token glatt durch, dem beides fehlte. Der Lauf scheiterte
* dann beim ersten Schritt, der die Rechte wirklich brauchte.
*
* Ausgeliefert wird ausschliesslich, was der Aufrufer ohnehin besitzt. Der
* Klartextwert des Tokens ist nicht dabei.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../../../src/bootstrap.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Http;
use Deploymentcenter\Core\TokenManager;
Http::beginJson(['GET', 'OPTIONS'], true);
$presented = Http::bearerToken();
if ($presented === null) {
Http::fail(401, 'unauthorized', 'Kein Token uebergeben. Erwartet als Authorization: Bearer <token> oder X-Agent-Token.');
}
$db = Db::init();
$manager = new TokenManager($db);
// Ohne geforderten Scope: die Frage lautet "wer bin ich", nicht "darf ich
// etwas". Widerruf, kaskadierender Widerruf und Ablauf werden trotzdem
// geprueft - ein entwertetes Token bekommt hier keine Auskunft.
$token = $manager->validateToken($presented, null);
if ($token === null) {
Http::fail(401, 'invalid_token', 'Token unbekannt, widerrufen oder abgelaufen.');
}
$scopes = TokenManager::decodeScopes($token['scopes']);
$isWorkbench = ($token['owner_type'] ?? null) === 'dev_agent';
Http::ok([
'token_id' => (string)$token['token_id'],
'name' => (string)$token['name'],
'type' => (string)$token['type'],
'owner_type' => (string)$token['owner_type'],
'owner_identity' => $token['owner_identity'] !== null ? (string)$token['owner_identity'] : null,
// Werkbank-Token duerfen in fremde Projekte melden, Laufzeit-Token nicht.
// Der Unterschied entscheidet ueber Fremdmeldungen und ist deshalb der
// erste Wert, den ein Werkzeug pruefen sollte.
'is_workbench' => $isWorkbench,
'project_slug' => $token['project_slug'] !== null ? (string)$token['project_slug'] : null,
'environment' => (string)$token['environment'],
'scopes' => $scopes,
'created_at' => $token['created_at'] !== null ? (string)$token['created_at'] : null,
'last_used_at' => $token['last_used_at'] !== null ? (string)$token['last_used_at'] : null,
'expires_at' => $token['expires_at'] !== null ? (string)$token['expires_at'] : null,
'parent_token_id' => $token['parent_token_id'] !== null ? (string)$token['parent_token_id'] : null,
]);