Upload folder via GUI - src
This commit is contained in:
332
src/main/java/de/ticketsystem/web/handlers/ApiHandler.java
Normal file
332
src/main/java/de/ticketsystem/web/handlers/ApiHandler.java
Normal file
@@ -0,0 +1,332 @@
|
||||
package de.ticketsystem.web.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import de.ticketsystem.database.DatabaseManager;
|
||||
import de.ticketsystem.model.*;
|
||||
import de.ticketsystem.web.SessionManager;
|
||||
import de.ticketsystem.web.WebSession;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* /api/* – JSON-API für AJAX-Aktionen aus dem Web-Panel.
|
||||
*
|
||||
* Alle Responses:
|
||||
* Erfolg: {"ok":true}
|
||||
* Fehler: {"ok":false,"error":"Beschreibung"}
|
||||
*
|
||||
* Endpunkte:
|
||||
* POST /api/ticket/{id}/claim
|
||||
* POST /api/ticket/{id}/close body: {comment?}
|
||||
* POST /api/ticket/{id}/priority body: {priority}
|
||||
* POST /api/ticket/{id}/comment body: {message}
|
||||
* POST /api/ticket/{id}/forward body: {target} (admin only)
|
||||
* GET /api/ticket/{id}/comments
|
||||
*
|
||||
* GET /api/faq
|
||||
* POST /api/faq body: {question, answer, category?} (admin only)
|
||||
* PUT /api/faq/{id} body: {question, answer, category?} (admin only)
|
||||
* DELETE /api/faq/{id} (admin only)
|
||||
*/
|
||||
public class ApiHandler extends BaseHandler implements HttpHandler {
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
private final SessionManager sessionManager;
|
||||
|
||||
public ApiHandler(TicketPlugin plugin, SessionManager sessionManager) {
|
||||
this.plugin = plugin;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange ex) throws IOException {
|
||||
WebSession session = requireSession(ex, sessionManager);
|
||||
if (session == null) return;
|
||||
|
||||
String path = ex.getRequestURI().getPath(); // z.B. /api/ticket/5/claim
|
||||
String method = ex.getRequestMethod().toUpperCase();
|
||||
|
||||
try {
|
||||
// ── Ticket-Aktionen ──────────────────────────────────────────
|
||||
if (path.startsWith("/api/ticket/")) {
|
||||
handleTicketApi(ex, session, path, method);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── FAQ-Aktionen ─────────────────────────────────────────────
|
||||
if (path.startsWith("/api/faq")) {
|
||||
handleFaqApi(ex, session, path, method);
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(ex, 404, "{\"ok\":false,\"error\":\"Unbekannter Endpunkt\"}");
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[WebPanel/API] Fehler: " + e.getMessage());
|
||||
if (plugin.isDebug()) e.printStackTrace();
|
||||
sendJson(ex, 500, "{\"ok\":false,\"error\":\"Interner Fehler\"}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── Ticket-API ────────────────────────────────
|
||||
|
||||
private void handleTicketApi(HttpExchange ex, WebSession session, String path, String method) throws IOException {
|
||||
// /api/ticket/{id}/{action}
|
||||
String[] parts = path.split("/"); // ["", "api", "ticket", "5", "claim"]
|
||||
if (parts.length < 5) { sendJson(ex, 400, err("Ungültiger Pfad")); return; }
|
||||
|
||||
int ticketId;
|
||||
try { ticketId = Integer.parseInt(parts[3]); }
|
||||
catch (NumberFormatException e) { sendJson(ex, 400, err("Ungültige Ticket-ID")); return; }
|
||||
|
||||
String action = parts[4]; // claim | close | priority | comment | forward | comments
|
||||
DatabaseManager db = plugin.getDatabaseManager();
|
||||
Ticket ticket = db.getTicketById(ticketId);
|
||||
|
||||
if (ticket == null) { sendJson(ex, 404, err("Ticket nicht gefunden")); return; }
|
||||
|
||||
switch (action) {
|
||||
|
||||
case "claim" -> {
|
||||
if (!method.equals("POST")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||
if (ticket.getStatus() != TicketStatus.OPEN) {
|
||||
sendJson(ex, 400, err("Ticket ist nicht offen")); return;
|
||||
}
|
||||
// UUID des Web-Users → wir nutzen einen Pseudo-UUID aus dem Benutzernamen
|
||||
UUID webUUID = webUserUUID(session.getUsername());
|
||||
boolean ok = db.claimTicket(ticketId, webUUID, "[Web] " + session.getUsername());
|
||||
if (ok) {
|
||||
ticket.setClaimerName("[Web] " + session.getUsername());
|
||||
plugin.getTicketManager().notifyCreatorClaimed(ticket);
|
||||
plugin.getTicketCache().invalidate(ticketId);
|
||||
sendJson(ex, 200, "{\"ok\":true}");
|
||||
} else {
|
||||
sendJson(ex, 500, err("Claim fehlgeschlagen"));
|
||||
}
|
||||
}
|
||||
|
||||
case "close" -> {
|
||||
if (!method.equals("POST")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||
Map<String, String> body = parseJsonBody(ex);
|
||||
String comment = body.getOrDefault("comment", "");
|
||||
boolean ok = db.closeTicket(ticketId, comment.isEmpty() ? null : comment);
|
||||
if (ok) {
|
||||
ticket.setStatus(TicketStatus.CLOSED);
|
||||
ticket.setCloseComment(comment);
|
||||
plugin.getTicketManager().notifyCreatorClosed(ticket, "[Web] " + session.getUsername());
|
||||
plugin.getTicketCache().invalidate(ticketId);
|
||||
sendJson(ex, 200, "{\"ok\":true}");
|
||||
} else {
|
||||
sendJson(ex, 500, err("Schließen fehlgeschlagen"));
|
||||
}
|
||||
}
|
||||
|
||||
case "priority" -> {
|
||||
if (!method.equals("POST")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||
Map<String, String> body = parseJsonBody(ex);
|
||||
String prioStr = body.getOrDefault("priority", "NORMAL");
|
||||
TicketPriority prio = TicketPriority.fromString(prioStr);
|
||||
boolean ok = db.setTicketPriority(ticketId, prio);
|
||||
if (ok) {
|
||||
plugin.getTicketCache().invalidate(ticketId);
|
||||
sendJson(ex, 200, "{\"ok\":true}");
|
||||
} else {
|
||||
sendJson(ex, 500, err("Priorität setzen fehlgeschlagen"));
|
||||
}
|
||||
}
|
||||
|
||||
case "comment" -> {
|
||||
if (!method.equals("POST")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||
Map<String, String> body = parseJsonBody(ex);
|
||||
String msg = body.getOrDefault("message", "").trim();
|
||||
if (msg.isEmpty()) { sendJson(ex, 400, err("Nachricht leer")); return; }
|
||||
|
||||
UUID webUUID = webUserUUID(session.getUsername());
|
||||
String authorDisplay = "[Web] " + session.getUsername();
|
||||
TicketComment comment = new TicketComment(ticketId, webUUID, authorDisplay, msg);
|
||||
boolean ok = db.addComment(comment);
|
||||
if (ok) {
|
||||
// Ersteller benachrichtigen (online oder Pending)
|
||||
String notifyMsg = plugin.lang().format("comment.notify-online",
|
||||
"{id}", String.valueOf(ticketId),
|
||||
"{player}", authorDisplay,
|
||||
"{message}", msg);
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
org.bukkit.entity.Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
if (creator != null && creator.isOnline()) {
|
||||
creator.sendMessage(notifyMsg);
|
||||
} else {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
db.addPendingNotification(ticket.getCreatorUUID(), notifyMsg));
|
||||
}
|
||||
});
|
||||
sendJson(ex, 200, "{\"ok\":true}");
|
||||
} else {
|
||||
sendJson(ex, 500, err("Kommentar speichern fehlgeschlagen"));
|
||||
}
|
||||
}
|
||||
|
||||
case "forward" -> {
|
||||
if (!method.equals("POST")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||
if (!session.isAdmin()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||
Map<String, String> body = parseJsonBody(ex);
|
||||
String targetName = body.getOrDefault("target", "").trim();
|
||||
if (targetName.isEmpty()) { sendJson(ex, 400, err("Ziel fehlt")); return; }
|
||||
|
||||
// Spieler-UUID ermitteln: erst Online-Spieler, dann Offline-Cache
|
||||
OfflinePlayer target = null;
|
||||
for (var p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.getName().equalsIgnoreCase(targetName)) { target = p; break; }
|
||||
}
|
||||
if (target == null) {
|
||||
// getOfflinePlayer lädt ggf. aus dem Usercache – nie null, aber
|
||||
// hasPlayedBefore() == false bedeutet: unbekannter Spieler
|
||||
OfflinePlayer op = Bukkit.getOfflinePlayer(targetName);
|
||||
if (op.hasPlayedBefore()) target = op;
|
||||
}
|
||||
if (target == null) { sendJson(ex, 404, err("Spieler nicht gefunden: " + targetName)); return; }
|
||||
|
||||
boolean ok = db.forwardTicket(ticketId, target.getUniqueId(), target.getName());
|
||||
if (ok) {
|
||||
ticket.setForwardedToUUID(target.getUniqueId());
|
||||
ticket.setForwardedToName(target.getName());
|
||||
plugin.getTicketManager().notifyForwardedTo(ticket, "[Web] " + session.getUsername());
|
||||
plugin.getTicketManager().notifyCreatorForwarded(ticket);
|
||||
plugin.getTicketCache().invalidate(ticketId);
|
||||
sendJson(ex, 200, "{\"ok\":true}");
|
||||
} else {
|
||||
sendJson(ex, 500, err("Weiterleiten fehlgeschlagen"));
|
||||
}
|
||||
}
|
||||
|
||||
case "comments" -> {
|
||||
if (!method.equals("GET")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||
List<TicketComment> comments = db.getComments(ticketId);
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
for (int i = 0; i < comments.size(); i++) {
|
||||
TicketComment c = comments.get(i);
|
||||
if (i > 0) json.append(",");
|
||||
json.append("{\"author\":\"").append(jsonEsc(c.getAuthorName()))
|
||||
.append("\",\"message\":\"").append(jsonEsc(c.getMessage()))
|
||||
.append("\",\"time\":\"").append(c.getCreatedAt() != null ? c.getCreatedAt().getTime() : 0)
|
||||
.append("\"}");
|
||||
}
|
||||
json.append("]");
|
||||
sendJson(ex, 200, json.toString());
|
||||
}
|
||||
|
||||
default -> sendJson(ex, 404, err("Unbekannte Aktion: " + action));
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── FAQ-API ───────────────────────────────────
|
||||
|
||||
private void handleFaqApi(HttpExchange ex, WebSession session, String path, String method) throws IOException {
|
||||
// GET /api/faq – Liste
|
||||
if (path.equals("/api/faq") && method.equals("GET")) {
|
||||
List<de.ticketsystem.model.FaqEntry> entries = plugin.getFaqManager().getAll();
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
var e = entries.get(i);
|
||||
if (i > 0) json.append(",");
|
||||
json.append("{\"id\":").append(e.getId())
|
||||
.append(",\"question\":\"").append(jsonEsc(e.getQuestion()))
|
||||
.append("\",\"answer\":\"").append(jsonEsc(e.getAnswer()))
|
||||
.append("\",\"category\":\"").append(jsonEsc(e.getCategoryKey()))
|
||||
.append("\"}");
|
||||
}
|
||||
json.append("]");
|
||||
sendJson(ex, 200, json.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// POST /api/faq – Hinzufügen (admin only)
|
||||
if (path.equals("/api/faq") && method.equals("POST")) {
|
||||
if (!session.isAdmin()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||
Map<String, String> body = parseJsonBody(ex);
|
||||
String q = body.getOrDefault("question", "").trim();
|
||||
String a = body.getOrDefault("answer", "").trim();
|
||||
String cat = body.getOrDefault("category", "");
|
||||
if (q.isEmpty() || a.isEmpty()) { sendJson(ex, 400, err("Frage und Antwort erforderlich")); return; }
|
||||
plugin.getFaqManager().add(q, a, cat.isEmpty() ? null : cat);
|
||||
sendJson(ex, 200, "{\"ok\":true}");
|
||||
return;
|
||||
}
|
||||
|
||||
// PUT /api/faq/{id} – Bearbeiten (admin only)
|
||||
if (path.matches("/api/faq/\\d+") && method.equals("PUT")) {
|
||||
if (!session.isAdmin()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||
int id = Integer.parseInt(path.substring("/api/faq/".length()));
|
||||
Map<String, String> body = parseJsonBody(ex);
|
||||
String q = body.getOrDefault("question", "").trim();
|
||||
String a = body.getOrDefault("answer", "").trim();
|
||||
String cat = body.getOrDefault("category", "");
|
||||
if (q.isEmpty() || a.isEmpty()) { sendJson(ex, 400, err("Frage und Antwort erforderlich")); return; }
|
||||
boolean ok = plugin.getFaqManager().edit(id, q, a);
|
||||
if (!cat.isEmpty()) plugin.getFaqManager().setCategory(id, cat);
|
||||
sendJson(ex, ok ? 200 : 404, ok ? "{\"ok\":true}" : err("FAQ nicht gefunden"));
|
||||
return;
|
||||
}
|
||||
|
||||
// DELETE /api/faq/{id} – Löschen (admin only)
|
||||
if (path.matches("/api/faq/\\d+") && method.equals("DELETE")) {
|
||||
if (!session.isAdmin()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||
int id = Integer.parseInt(path.substring("/api/faq/".length()));
|
||||
boolean ok = plugin.getFaqManager().delete(id);
|
||||
sendJson(ex, ok ? 200 : 404, ok ? "{\"ok\":true}" : err("FAQ nicht gefunden"));
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(ex, 404, err("Unbekannter FAQ-Endpunkt"));
|
||||
}
|
||||
|
||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Liest einen JSON-Body und parst ihn als flaches Key-Value-Objekt.
|
||||
* Kein externer JSON-Parser – minimale Eigenimplementierung.
|
||||
*/
|
||||
private Map<String, String> parseJsonBody(HttpExchange ex) throws IOException {
|
||||
java.io.InputStream is = ex.getRequestBody();
|
||||
String body = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).trim();
|
||||
Map<String, String> result = new java.util.HashMap<>();
|
||||
if (body.startsWith("{") && body.endsWith("}")) {
|
||||
body = body.substring(1, body.length() - 1);
|
||||
// Einfaches Parsing: "key":"value", "key2":"value2"
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern
|
||||
.compile("\"([^\"]+)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"")
|
||||
.matcher(body);
|
||||
while (m.find()) {
|
||||
result.put(m.group(1), m.group(2)
|
||||
.replace("\\\"", "\"")
|
||||
.replace("\\\\", "\\")
|
||||
.replace("\\n", "\n"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine deterministische UUID aus einem Web-Benutzernamen.
|
||||
* Damit kann der Web-User als "Autor" in DB-Einträgen gespeichert werden.
|
||||
*/
|
||||
private UUID webUserUUID(String username) {
|
||||
return UUID.nameUUIDFromBytes(("webpanel:" + username).getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String err(String msg) {
|
||||
return "{\"ok\":false,\"error\":\"" + jsonEsc(msg) + "\"}";
|
||||
}
|
||||
|
||||
private String jsonEsc(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user