API-Reife: Scopes (read/write), Rate-Limit pro Key, OpenAPI-Spec
- ApiKey::hasScope + checkRateLimit (Fixed-Window/min via api_key_hits) - bootstrap erzwingt Rate-Limit (429) und api_require_scope() in Endpunkten - admin/api_keys.php: Scope-Auswahl + Limit beim Erstellen, Anzeige in Tabelle - api/openapi.php: OpenAPI-3.0-Spec (Import in Postman/Swagger) - Schema 0003 (api_keys.scope, api_keys.rate_limit, Tabelle api_key_hits)
This commit is contained in:
parent
ba121d60e6
commit
c7f9b7d39c
6 changed files with 146 additions and 4 deletions
|
|
@ -37,3 +37,17 @@ $apiKeyRow = ApiKey::verify(ApiKey::fromRequest(), $db);
|
|||
if (!$apiKeyRow) {
|
||||
api_json(['error' => 'unauthorized', 'message' => 'Gültiger API-Schlüssel erforderlich (Authorization: Bearer …)'], 401);
|
||||
}
|
||||
|
||||
// Rate-Limit pro Schlüssel
|
||||
if (!ApiKey::checkRateLimit($apiKeyRow, $db)) {
|
||||
header('Retry-After: 60');
|
||||
api_json(['error' => 'rate_limited', 'message' => 'Rate-Limit überschritten. Bitte später erneut versuchen.'], 429);
|
||||
}
|
||||
|
||||
/** Erzwingt einen Scope für den aktuellen Schlüssel. */
|
||||
function api_require_scope($needed) {
|
||||
global $apiKeyRow;
|
||||
if (!ApiKey::hasScope($apiKeyRow, $needed)) {
|
||||
api_json(['error' => 'forbidden', 'message' => "Schlüssel hat keinen '$needed'-Scope"], 403);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
api/openapi.php
Normal file
77
api/openapi.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/**
|
||||
* OpenAPI 3.0 Spezifikation der REST-API (zum Import in Postman/Swagger).
|
||||
* GET /api/openapi.php
|
||||
*/
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$base = $scheme . '://' . $host . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
|
||||
|
||||
$spec = [
|
||||
'openapi' => '3.0.3',
|
||||
'info' => [
|
||||
'title' => 'UniFi Voucher Tool API',
|
||||
'version' => '1.0.0',
|
||||
'description' => 'REST-API zum Erstellen und Abrufen von WLAN-Vouchers. Authentifizierung per API-Schlüssel (Authorization: Bearer … oder X-API-Key).',
|
||||
],
|
||||
'servers' => [['url' => $base]],
|
||||
'components' => [
|
||||
'securitySchemes' => [
|
||||
'bearerAuth' => ['type' => 'http', 'scheme' => 'bearer'],
|
||||
'apiKeyAuth' => ['type' => 'apiKey', 'in' => 'header', 'name' => 'X-API-Key'],
|
||||
],
|
||||
],
|
||||
'security' => [['bearerAuth' => []], ['apiKeyAuth' => []]],
|
||||
'paths' => [
|
||||
'/sites.php' => [
|
||||
'get' => [
|
||||
'summary' => 'Aktive Sites auflisten',
|
||||
'description' => 'Erfordert Scope read.',
|
||||
'responses' => ['200' => ['description' => 'Liste der Sites']],
|
||||
],
|
||||
],
|
||||
'/vouchers.php' => [
|
||||
'get' => [
|
||||
'summary' => 'Voucher einer Site auflisten',
|
||||
'description' => 'Erfordert Scope read.',
|
||||
'parameters' => [[
|
||||
'name' => 'site_id', 'in' => 'query', 'required' => true,
|
||||
'schema' => ['type' => 'integer'],
|
||||
]],
|
||||
'responses' => ['200' => ['description' => 'Liste der Voucher']],
|
||||
],
|
||||
'post' => [
|
||||
'summary' => 'Voucher erstellen',
|
||||
'description' => 'Erfordert Scope write.',
|
||||
'requestBody' => [
|
||||
'required' => true,
|
||||
'content' => ['application/json' => ['schema' => [
|
||||
'type' => 'object',
|
||||
'required' => ['site_id', 'name'],
|
||||
'properties' => [
|
||||
'site_id' => ['type' => 'integer'],
|
||||
'name' => ['type' => 'string'],
|
||||
'max_uses' => ['type' => 'integer', 'default' => 1],
|
||||
'expire_minutes' => ['type' => 'integer', 'default' => 480],
|
||||
'qos' => ['type' => 'object', 'properties' => [
|
||||
'down' => ['type' => 'integer', 'description' => 'Download kbit/s'],
|
||||
'up' => ['type' => 'integer', 'description' => 'Upload kbit/s'],
|
||||
'quota_mb' => ['type' => 'integer', 'description' => 'Datenkontingent MB'],
|
||||
]],
|
||||
],
|
||||
]]],
|
||||
],
|
||||
'responses' => [
|
||||
'201' => ['description' => 'Voucher erstellt'],
|
||||
'401' => ['description' => 'Nicht authentifiziert'],
|
||||
'403' => ['description' => 'Fehlender Scope'],
|
||||
'429' => ['description' => 'Rate-Limit überschritten'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
echo json_encode($spec, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
|
|
@ -7,6 +7,7 @@ require_once __DIR__ . '/bootstrap.php';
|
|||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
api_json(['error' => 'method_not_allowed'], 405);
|
||||
}
|
||||
api_require_scope('read');
|
||||
|
||||
$sites = $db->fetchAll("SELECT id, name, site_id FROM sites WHERE is_active = 1 ORDER BY name");
|
||||
api_json(['sites' => array_map(function ($s) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ require_once __DIR__ . '/../includes/Notifier.php';
|
|||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if ($method === 'GET') {
|
||||
api_require_scope('read');
|
||||
$siteId = (int)($_GET['site_id'] ?? 0);
|
||||
if ($siteId <= 0) {
|
||||
api_json(['error' => 'invalid_request', 'message' => 'site_id erforderlich'], 400);
|
||||
|
|
@ -29,6 +30,7 @@ if ($method === 'GET') {
|
|||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
api_require_scope('write');
|
||||
$body = api_body();
|
||||
$siteId = (int)($body['site_id'] ?? 0);
|
||||
$name = trim((string)($body['name'] ?? ''));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue