Erweiterte Features (2/2): Cleanup/DSGVO, Webhooks, REST-API, Backup, CI, Docker
- Auto-Cleanup/DSGVO: cron_cleanup.php (token-geschützt) löscht abgelaufene Voucher, Audit-Log, Login-Versuche und Reset-Tokens nach konfigurierbaren Fristen - Webhooks: includes/Notifier.php (Slack/Teams/generisch), Auslösung bei Voucher-Erstellung (Web + API) - REST-API: includes/ApiKey.php (SHA-256, Präfix-Lookup), api/bootstrap.php, api/vouchers.php (GET/POST), api/sites.php; admin/api_keys.php zur Verwaltung - Config-Backup/Restore: admin/backup.php (JSON Export/Import, Cron-Token geschützt) - admin/integrations.php: Trusted-Proxy, Webhook, Cleanup-Fristen - CI: .github/workflows/ci.yml (php -l auf 7.4 & 8.2, lang-Validierung) - Docker: Dockerfile, docker-compose.yml (MariaDB), entrypoint (config aus ENV), .dockerignore - Nav + i18n (DE/EN) für alle neuen Admin-Seiten
This commit is contained in:
parent
eec28f77b8
commit
9463486225
18 changed files with 972 additions and 0 deletions
39
api/bootstrap.php
Normal file
39
api/bootstrap.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
/**
|
||||
* Gemeinsamer Bootstrap für die REST-API-Endpunkte.
|
||||
* Lädt die Basis, authentifiziert den API-Schlüssel und stellt JSON-Helfer bereit.
|
||||
*/
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/UniFiController.php';
|
||||
require_once __DIR__ . '/../includes/ApiKey.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function api_json($data, $status = 200) {
|
||||
http_response_code($status);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function api_body() {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === '' || $raw === false) return [];
|
||||
$data = json_decode($raw, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
} catch (Exception $e) {
|
||||
api_json(['error' => 'database_unavailable'], 503);
|
||||
}
|
||||
|
||||
$apiKeyRow = ApiKey::verify(ApiKey::fromRequest(), $db);
|
||||
if (!$apiKeyRow) {
|
||||
api_json(['error' => 'unauthorized', 'message' => 'Gültiger API-Schlüssel erforderlich (Authorization: Bearer …)'], 401);
|
||||
}
|
||||
14
api/sites.php
Normal file
14
api/sites.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
/**
|
||||
* GET /api/sites.php – Liste aktiver Sites (für API-Clients).
|
||||
*/
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
api_json(['error' => 'method_not_allowed'], 405);
|
||||
}
|
||||
|
||||
$sites = $db->fetchAll("SELECT id, name, site_id FROM sites WHERE is_active = 1 ORDER BY name");
|
||||
api_json(['sites' => array_map(function ($s) {
|
||||
return ['id' => (int)$s['id'], 'name' => $s['name'], 'site_id' => $s['site_id']];
|
||||
}, $sites)]);
|
||||
89
api/vouchers.php
Normal file
89
api/vouchers.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* REST-Endpunkt für Voucher.
|
||||
*
|
||||
* GET /api/vouchers.php?site_id=<id> – Voucher einer Site auflisten (aus DB)
|
||||
* POST /api/vouchers.php – Voucher erstellen
|
||||
* Body (JSON): {
|
||||
* "site_id": 1, "name": "API Gast", "max_uses": 1,
|
||||
* "expire_minutes": 480,
|
||||
* "qos": { "down": 10000, "up": 2000, "quota_mb": 500 } (optional)
|
||||
* }
|
||||
*/
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/Notifier.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if ($method === 'GET') {
|
||||
$siteId = (int)($_GET['site_id'] ?? 0);
|
||||
if ($siteId <= 0) {
|
||||
api_json(['error' => 'invalid_request', 'message' => 'site_id erforderlich'], 400);
|
||||
}
|
||||
$rows = $db->fetchAll(
|
||||
"SELECT voucher_code, voucher_name, max_uses, expire_minutes, status, used_count, created_at, expires_at
|
||||
FROM vouchers WHERE site_id = ? ORDER BY created_at DESC LIMIT 200",
|
||||
[$siteId]
|
||||
);
|
||||
api_json(['vouchers' => $rows]);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$body = api_body();
|
||||
$siteId = (int)($body['site_id'] ?? 0);
|
||||
$name = trim((string)($body['name'] ?? ''));
|
||||
$maxUses = (int)($body['max_uses'] ?? 1);
|
||||
$expireMinutes = (int)($body['expire_minutes'] ?? 480);
|
||||
|
||||
if ($siteId <= 0) api_json(['error' => 'invalid_request', 'message' => 'site_id erforderlich'], 400);
|
||||
if ($name === '') api_json(['error' => 'invalid_request', 'message' => 'name erforderlich'], 400);
|
||||
if ($maxUses < 1) $maxUses = 1;
|
||||
if ($expireMinutes < 1) $expireMinutes = 480;
|
||||
|
||||
$site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
|
||||
if (!$site) {
|
||||
api_json(['error' => 'not_found', 'message' => 'Site nicht gefunden'], 404);
|
||||
}
|
||||
|
||||
$qosIn = is_array($body['qos'] ?? null) ? $body['qos'] : [];
|
||||
$qos = [
|
||||
'down' => max(0, (int)($qosIn['down'] ?? 0)),
|
||||
'up' => max(0, (int)($qosIn['up'] ?? 0)),
|
||||
'quota_mb' => max(0, (int)($qosIn['quota_mb'] ?? 0)),
|
||||
];
|
||||
|
||||
try {
|
||||
$fullName = date('Y-m-d') . '_' . $name;
|
||||
$controller = new UniFiController(
|
||||
$site['unifi_controller_url'],
|
||||
$site['unifi_username'],
|
||||
Crypto::decrypt($site['unifi_password']),
|
||||
$site['site_id']
|
||||
);
|
||||
$voucher = $controller->createVoucher($fullName, $maxUses, $expireMinutes, $qos);
|
||||
if (!is_array($voucher) || empty($voucher['formatted_code'])) {
|
||||
api_json(['error' => 'upstream_error', 'message' => 'UniFi lieferte keinen gültigen Voucher'], 502);
|
||||
}
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?)",
|
||||
[$siteId, $voucher['code'], $fullName, $maxUses, $expireMinutes, $voucher['unifi_id'] ?? null]
|
||||
);
|
||||
|
||||
Notifier::voucherCreated(1, $site['name'], 'API: ' . $apiKeyRow['name']);
|
||||
|
||||
api_json([
|
||||
'success' => true,
|
||||
'code' => $voucher['code'],
|
||||
'formatted_code' => $voucher['formatted_code'],
|
||||
'site' => $site['name'],
|
||||
'max_uses' => $maxUses,
|
||||
'expire_minutes' => $expireMinutes,
|
||||
], 201);
|
||||
} catch (Exception $e) {
|
||||
api_json(['error' => 'server_error', 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
api_json(['error' => 'method_not_allowed'], 405);
|
||||
Loading…
Add table
Add a link
Reference in a new issue