Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-04-15 19:11:02 +02:00
parent bafddee288
commit d66871234c
20 changed files with 3854 additions and 451 deletions

View 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", "");
}
}