Wertungsportal // REST-Schnittstelle

Die REST-Schnittstelle in den Bereichen Spieler, Vereine und Verbände ist öffentlich zugänglich. Wichtig: Die Daten im Livesystem sind echt, die Daten im Demosystem sind "Spieldaten" und nicht zur Veröffentlichung gedacht. Das Demosystem wird auch nach der Projektphase als "Spielwiese" weiter existieren.

Beispielaufrufe, mit cURL

# ==============================================
# Livesystem
# ==============================================
# ZIP-Datei für Nordrhein-Westfalen
curl 'https://schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/download/LV-6-dwzliste.zip --output LV-6-dwzliste.zip';

# Die ersten zehn Verbände
curl 'https://schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/federations?limit=10&offset=0';

# Der aktuelle DWZ-Wert für Andre Martin
curl 'https://schachde-appso.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons?lastname=Martin&firstname=Andre';

# Oder direkt über die nuLigaPersonId
curl 'https://schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons/NU4211295';

# ==============================================
# Demosystem
# ==============================================
# ZIP-Datei für Nordrhein-Westfalen
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/download/LV-6-dwzliste.zip --output LV-6-dwzliste.zip';

# Die ersten zehn Verbände
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/federations?limit=10&offset=0';

# Der aktuelle DWZ-Wert für Andre Martin
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons?lastname=Martin&firstname=Andre';

# Oder direkt über die nuLigaPersonId
curl 'https://schachde-appsdemo.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/persons/NU4211295';

Da die DWZ-Listen-API nun ohne Authentifizierung erreichbar ist, könnet ihr die URL's auch mit jedem Webbrowser direkt ausprobieren. Für den ZIP-Download: schachde-apps.liga.nu/dsbwertungsportal/rs/dwz/dwzliste/download/LV-0-dwzliste.zip

PHP-Beispiel für den Zugriff auf die Schnittstelle

Nachfolgend finden Sie ein PHP-Beispielskript für den Zugriff auf die REST-Schnittstelle des Wertungsportals. Im Konstruktur (Zeile 24-29) müssen Sie die Ihnen zugteilten URL's und Schlüssel eintragen. In Zeile 273 muß evtl. der Funktionsaufruf ("/dwz/dwzliste/clubs") ausgetauscht werden.

Das PHP-Skript wurde zu 100% mit Claude.ai erstellt und umfasst alle nötigen Funktionen wie Anforderung des Access Tokens bzw. des Refresh Tokens, sowie das Caching der Tokens.

Das Skript wurde mit den uns zugeteilten Parametern (Zeile 24-29) erfolgreich getestet.

PHP-Code (Variante für den geschützten Zugriff)

<?php

/**
 * OAuth2 Client Credentials Flow mit Refresh-Token-Unterstützung
 * Spec: https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/
 */

class OAuth2Client
{
    // ─────────────────────────────────────────────
    //  Konfiguration (öffentliche Eigenschaften)
    // ─────────────────────────────────────────────
    public string $apiBaseUrl;
    public string $clientId;
    public string $clientSecret;
    public string $tokenEndpoint;
    public string $scope;
    public string $cacheFile;

    // ─────────────────────────────────────────────
    //  Konstruktor – initialisiert alle Konfigurationswerte
    // ─────────────────────────────────────────────
    public function __construct(
        string $apiBaseUrl    = '',
        string $clientId      = '',
        string $clientSecret  = '',
        string $tokenEndpoint = '',
        string $scope         = '',
        string $cacheFile     = ''
    ) {
        $this->apiBaseUrl    = $apiBaseUrl;
        $this->clientId      = $clientId;
        $this->clientSecret  = $clientSecret;
        $this->tokenEndpoint = $tokenEndpoint;
        $this->scope         = $scope;
        $this->cacheFile     = $cacheFile !== '' ? $cacheFile : sys_get_temp_dir() . '/oauth2_token_cache.json';
    }

    // ─────────────────────────────────────────────
    //  Cache lesen / schreiben / löschen
    // ─────────────────────────────────────────────
    public function readCache(): array
    {
        if (!file_exists($this->cacheFile)) {
            return [];
        }
        return json_decode(file_get_contents($this->cacheFile), true) ?? [];
    }

    public function writeCache(array $data): void
    {
        file_put_contents($this->cacheFile, json_encode($data));
    }

    public function clearCache(): void
    {
        if (file_exists($this->cacheFile)) {
            unlink($this->cacheFile);
            echo "🗑️  Token-Cache gelöscht.\n";
        }
    }

    // ─────────────────────────────────────────────
    //  Token-Request (flexibel für beide Grant-Typen)
    // ─────────────────────────────────────────────
    public function requestToken(array $postFields): array
    {
        $ch = curl_init($this->tokenEndpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => http_build_query($postFields),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/x-www-form-urlencoded',
                'Accept: application/json',
            ],
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 5,
            CURLOPT_POSTREDIR      => 3,
        ]);

        $response     = curl_exec($ch);
        $httpCode     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        $curlError    = curl_error($ch);
        curl_close($ch);

        if ($effectiveUrl !== $this->tokenEndpoint) {
            echo "ℹ️  Weitergeleitet zu: $effectiveUrl\n";
        }

        if ($curlError) {
            throw new RuntimeException("cURL-Fehler: $curlError");
        }

        $data = json_decode($response, true);

        if ($httpCode !== 200 || empty($data['access_token'])) {
            $errorMsg = $data['error_description'] ?? $data['error'] ?? 'Unbekannter Fehler';
            throw new RuntimeException("Token-Anfrage fehlgeschlagen (HTTP $httpCode): $errorMsg");
        }

        return $data;
    }

    // ─────────────────────────────────────────────
    //  Neuen Token via Client Credentials holen
    // ─────────────────────────────────────────────
    public function fetchNewToken(): array
    {
        echo "🔑 Hole neuen Access Token (client_credentials) ...\n";

        $tokenData = $this->requestToken([
            'grant_type'    => 'client_credentials',
            'client_id'     => $this->clientId,
            'client_secret' => $this->clientSecret,
            'scope'         => $this->scope,
        ]);

        $this->saveTokenToCache($tokenData);
        echo "✅ Neuer Token erhalten (gültig für {$tokenData['expires_in']} Sekunden).\n";
        return $tokenData;
    }

    // ─────────────────────────────────────────────
    //  Bestehenden Token via Refresh-Token erneuern
    // ─────────────────────────────────────────────
    public function refreshToken(string $refreshToken): array
    {
        echo "🔄 Erneuere Access Token via Refresh-Token ...\n";

        $tokenData = $this->requestToken([
            'grant_type'    => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id'     => $this->clientId,
            'client_secret' => $this->clientSecret,
        ]);

        $this->saveTokenToCache($tokenData);
        echo "✅ Token erneuert (gültig für {$tokenData['expires_in']} Sekunden).\n";
        return $tokenData;
    }

    // ─────────────────────────────────────────────
    //  Token-Daten im Cache speichern
    // ─────────────────────────────────────────────
    public function saveTokenToCache(array $tokenData): void
    {
        $expiresIn = $tokenData['expires_in'] ?? 3600;
        $this->writeCache([
            'access_token'  => $tokenData['access_token'],
            'refresh_token' => $tokenData['refresh_token'] ?? null,
            'expires_at'    => time() + $expiresIn,
        ]);
    }

    // ─────────────────────────────────────────────
    //  Gültigen Access Token liefern
    //  Reihenfolge:
    //    1. Gecachter Token noch gültig   → direkt verwenden
    //    2. Refresh-Token vorhanden       → Token erneuern
    //    3. Kein Cache                    → neuen Token holen
    // ─────────────────────────────────────────────
    public function getValidToken(): string
    {
        $cache = $this->readCache();

        // 1. Access Token noch gültig? (30 Sekunden Puffer)
        if (!empty($cache['access_token']) && isset($cache['expires_at'])
            && time() < ($cache['expires_at'] - 30)) {
            echo "ℹ️  Verwende gecachten Access Token.\n";
            return $cache['access_token'];
        }

        // 2. Refresh-Token vorhanden → erneuern statt neu anfordern
        if (!empty($cache['refresh_token'])) {
            try {
                $tokenData = $this->refreshToken($cache['refresh_token']);
                return $tokenData['access_token'];
            } catch (RuntimeException $e) {
                // Refresh-Token ungültig → Cache leeren und neu starten
                echo "⚠️  Refresh fehlgeschlagen ({$e->getMessage()}), hole neuen Token ...\n";
                $this->clearCache();
            }
        }

        // 3. Komplett neuen Token holen
        $tokenData = $this->fetchNewToken();
        return $tokenData['access_token'];
    }

    // ─────────────────────────────────────────────
    //  API-Aufruf mit Bearer Token
    // ─────────────────────────────────────────────
    public function callApi(string $accessToken, string $apiUrl, string $method = 'GET', ?array $body = null): array
    {
        $ch = curl_init($apiUrl);

        $headers = [
            'Authorization: Bearer ' . $accessToken,
            'Accept: application/json',
        ];

        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_SSL_VERIFYPEER => true,
        ];

        if (strtoupper($method) === 'POST') {
            $options[CURLOPT_POST]       = true;
            $options[CURLOPT_POSTFIELDS] = json_encode($body ?? []);
            $headers[]                   = 'Content-Type: application/json';
            $options[CURLOPT_HTTPHEADER] = $headers;
        }

        curl_setopt_array($ch, $options);

        $response  = curl_exec($ch);
        $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            throw new RuntimeException("cURL-Fehler beim API-Aufruf: $curlError");
        }

        return [
            'http_code' => $httpCode,
            'body'      => json_decode($response, true) ?? $response,
        ];
    }

    // ─────────────────────────────────────────────
    //  API-Aufruf mit automatischem Token-Refresh
    //  bei HTTP 401 (Token abgelaufen / ungültig)
    // ─────────────────────────────────────────────
    public function callApiWithRefresh(string $apiUrl, string $method = 'GET', ?array $body = null): array
    {
        $token  = $this->getValidToken();
        $result = $this->callApi($token, $apiUrl, $method, $body);

        // Bei 401: Token per Refresh-Token erneuern und einmal wiederholen
        if ($result['http_code'] === 401) {
            echo "⚠️  HTTP 401 – Token wird erneuert ...\n";
            $cache = $this->readCache();
            $this->clearCache();

            if (!empty($cache['refresh_token'])) {
                $tokenData = $this->refreshToken($cache['refresh_token']);
                $token     = $tokenData['access_token'];
            } else {
                $tokenData = $this->fetchNewToken();
                $token     = $tokenData['access_token'];
            }

            $result = $this->callApi($token, $apiUrl, $method, $body);
        }

        return $result;
    }
}

// ─────────────────────────────────────────────
//  Hauptprogramm
// ─────────────────────────────────────────────
try {
    $client = new OAuth2Client();

    $result = $client->callApiWithRefresh($client->apiBaseUrl . '/dwz/dwzliste/clubs');

    echo "\n📦 API-Antwort (HTTP {$result['http_code']}):\n";
    echo json_encode($result['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

} catch (RuntimeException $e) {
    echo "\n❌ Fehler: " . $e->getMessage() . "\n";
    exit(1);
}

Ideen zur Weiterentwicklung

Die Anpassungen sind mit Claude leicht umzusetzen und man vermeidet dadurch auch Fehler die durch eigene Arbeit am Code entstehen können.

  • application/zip zusätzlich zu application/json bei erlaubter Antwort
  • Auskommentierung/Entfernung der echo-Ausgaben (in der Klasse für DSB-Website umgesetzt)
  • Rückgabe von Fehlern an das aufrufende Skript statt einer Exception (in der Klasse für DSB-Website umgesetzt)