Upload via GUI (364 Dateien)

This commit is contained in:
2026-11-01 11:16:11 +00:00
parent 323d49e13e
commit 25beaea671
6 changed files with 56 additions and 8 deletions
+31 -1
View File
@@ -8,6 +8,9 @@ declare(strict_types=1);
* POST /api/public/v1/tickets.php Ticket lösen (Recht "Tickets anlegen")
* JSON: { "action": "resolve", "number", "email", "message"? } nur für Tickets, die über die API angelegt wurden, mit passender E-Mail-Adresse
* GET /api/public/v1/tickets.php?number=…&email=… Status + öffentlicher Verlauf (Recht "Tickets lesen")
* GET /api/public/v1/tickets.php?topics=1 Öffentliche Hilfethemen (Recht "Tickets anlegen")
* GET /api/public/v1/tickets.php?form=1&helpTopicId=… Kundenformular samt eigener Felder (Recht "Tickets anlegen")
* Beim Anlegen nimmt "fields": { "<Feld-ID>": "Wert", … } die Angaben dieses Formulars entgegen (Pflichtfelder werden geprüft).
*
* Authentifizierung: Header "X-API-Key: dh_…" oder "Authorization: Bearer dh_…".
* Schlüssel verwaltet ein Administrator unter Verwalten → API-Schlüssel.
@@ -34,7 +37,12 @@ $key = trim((string) ($_SERVER['HTTP_X_API_KEY'] ?? ''));
if ($key === '' && preg_match('/^Bearer\s+(\S+)$/i', (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? ''), $m)) $key = $m[1];
$pdo = get_db();
$apiKey = $key !== '' ? api_key_verify($pdo, $key, client_ip()) : null;
if (!$apiKey) public_api_fail(401, 'UNAUTHENTICATED', 'Ein gültiger API-Schlüssel ist erforderlich (Header X-API-Key).');
if (!$apiKey) {
if ($key !== '' && api_key_ip_blocked($pdo, $key, client_ip())) {
public_api_fail(401, 'IP_NOT_ALLOWED', 'Der Schlüssel ist gültig, aber für die IP-Adresse ' . client_ip() . ' nicht freigegeben. Diese Adresse bei „Erlaubte IPs“ des Schlüssels eintragen (oder das Feld leer lassen).');
}
public_api_fail(401, 'UNAUTHENTICATED', 'Ein gültiger API-Schlüssel ist erforderlich (Header X-API-Key).');
}
if ($method === 'POST') {
if (!$apiKey['can_create_tickets']) public_api_fail(403, 'FORBIDDEN', 'Dieser Schlüssel darf keine Tickets anlegen.');
@@ -80,6 +88,28 @@ if ($method === 'POST') {
exit;
}
// GET ?topics=1 (Hilfethemen) und GET ?form=1&helpTopicId=…|departmentId=… (Kundenformular mit eigenen Feldern) für Website-Formulare.
// Das ist dasselbe, was Kunden im Kundenportal sehen; es braucht das Recht "Tickets anlegen".
if (isset($_GET['topics']) || isset($_GET['form'])) {
if (!$apiKey['can_create_tickets']) public_api_fail(403, 'FORBIDDEN', 'Dieser Schlüssel darf keine Tickets anlegen.');
if (isset($_GET['topics'])) {
echo json_encode(['topics' => array_map(fn(array $t) => [
'id' => (int) $t['id'], 'name' => (string) $t['name'], 'path' => (string) $t['path'], 'depth' => (int) $t['depth'],
'parentId' => !empty($t['parent_id']) ? (int) $t['parent_id'] : null, 'departmentId' => (int) $t['department_id'],
], help_topic_options($pdo, true))], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$topicId = am_int_or_null($_GET, 'helpTopicId');
$deptId = am_int_or_null($_GET, 'departmentId');
$def = public_form_definition($pdo, $topicId, $topicId === null ? ($deptId ?: (int) get_setting('default_department_id', '1')) : $deptId);
} catch (AdminModelException $e) {
public_api_fail($e->httpStatus(), $e->errorCode(), $e->getMessage());
}
echo json_encode(['form' => public_form_payload($def)], JSON_UNESCAPED_UNICODE);
exit;
}
// GET: Status eines Tickets (nur mit passender E-Mail-Adresse des Anfragenden)
if (!$apiKey['can_read_tickets']) public_api_fail(403, 'FORBIDDEN', 'Dieser Schlüssel darf keine Tickets lesen.');
$number = trim((string) ($_GET['number'] ?? ''));
+18 -1
View File
@@ -9,7 +9,24 @@ header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('Pragma: no-cache');
const MOBILE_API_STATUSES = ['open', 'pending', 'answered', 'on_hold', 'resolved', 'closed'];
// Browser-Erweiterungen (Chrome/Edge/Brave: chrome-extension://…, Firefox: moz-extension://…) dürfen die API aufrufen. Ohne diese Antwort hängt der
// Zugriff davon ab, ob der Browser die Server-Berechtigung der Erweiterung schon angewendet hat (nach der ersten Erlaubnis oft erst nach einem Neustart).
// Es werden keine Cookies gesendet; wer die API nutzt, braucht weiterhin das Token bzw. Passwort.
$__origin = (string) ($_SERVER['HTTP_ORIGIN'] ?? '');
if ($__origin !== '' && preg_match('#^(chrome|moz)-extension://[a-z0-9-]{8,64}$#', $__origin)) {
header('Access-Control-Allow-Origin: ' . $__origin);
header('Vary: Origin');
header('Access-Control-Allow-Headers: Authorization, X-DeskHub-Authorization, Content-Type, Cache-Control, Accept');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Max-Age: 600');
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
http_response_code(204);
exit;
}
}
unset($__origin);
const MOBILE_API_STATUSES =['open', 'pending', 'answered', 'on_hold', 'resolved', 'closed'];
set_exception_handler(function (Throwable $error): void {
error_log('Mobile Staff API: ' . $error->getMessage());
+1 -1
View File
@@ -118,7 +118,7 @@ if (!$ticket) api_error(404, 'TICKET_NOT_FOUND', 'Chat nicht gefunden oder kein
if ($action === 'typing') {
if ($ticket['status'] !== 'closed' && !live_chat_locked_by_other($ticket, (int) $staff['id'], $pdo)) {
$pdo->prepare('UPDATE tickets SET typing_staff_until = ? WHERE id = ?')
$pdo->prepare('UPDATE tickets SET typing_staff_until = ?, updated_at = updated_at WHERE id = ?')
->execute([date('Y-m-d H:i:s', time() + 6), $ticket['id']]);
}
api_json(['ok' => true]);
+4 -3
View File
@@ -12,9 +12,10 @@ $totpCode = trim((string) ($body['totpCode'] ?? ''));
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') {
api_error(422, 'CREDENTIALS_REQUIRED', 'E-Mail-Adresse und Passwort sind erforderlich.');
}
if (!rate_limit_check('mobile_staff_login_ip', client_ip(), 10, 15)
|| !rate_limit_check('mobile_staff_login_email', strtolower($email), login_guard_max_attempts(), login_guard_lock_minutes())) {
login_guard_locked($email, 'app');
$ipOk = rate_limit_check('mobile_staff_login_ip', client_ip(), 10, 15);
$accountOk = !$ipOk || rate_limit_check('mobile_staff_login_email', strtolower($email), login_guard_max_attempts(), login_guard_lock_minutes());
if (!$ipOk || !$accountOk) {
if (!$accountOk) login_guard_locked($email, 'app'); // das IP-Limit allein sperrt kein Konto und löst keine Mail an den Kontoinhaber aus
api_error(429, 'RATE_LIMITED', 'Zu viele Anmeldeversuche. Bitte versuchen Sie es später erneut.');
}