Upload folder via GUI - src
This commit is contained in:
@@ -95,7 +95,7 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
|
|
||||||
// Versionsprüfung der config.yml
|
// Versionsprüfung der config.yml
|
||||||
String configVersion = getConfig().getString("version", "");
|
String configVersion = getConfig().getString("version", "");
|
||||||
String expectedVersion = "2.4";
|
String expectedVersion = "2.5";
|
||||||
if (!expectedVersion.equals(configVersion)) {
|
if (!expectedVersion.equals(configVersion)) {
|
||||||
getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion
|
getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion
|
||||||
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
|
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
case "importieren" -> "import";
|
case "importieren" -> "import";
|
||||||
case "statistik" -> "stats";
|
case "statistik" -> "stats";
|
||||||
case "archivieren" -> "archive";
|
case "archivieren" -> "archive";
|
||||||
|
case "sichern" -> "backup";
|
||||||
case "kommentar" -> "comment";
|
case "kommentar" -> "comment";
|
||||||
case "sperrliste" -> "blacklist";
|
case "sperrliste" -> "blacklist";
|
||||||
case "bewerten" -> "rate";
|
case "bewerten" -> "rate";
|
||||||
@@ -90,6 +91,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
case "stats" -> handleStats(player);
|
case "stats" -> handleStats(player);
|
||||||
case "top" -> handleTop(player);
|
case "top" -> handleTop(player);
|
||||||
case "archive" -> handleArchive(player);
|
case "archive" -> handleArchive(player);
|
||||||
|
case "backup" -> handleBackup(player);
|
||||||
case "comment" -> handleComment(player, args);
|
case "comment" -> handleComment(player, args);
|
||||||
case "blacklist" -> handleBlacklist(player, args);
|
case "blacklist" -> handleBlacklist(player, args);
|
||||||
case "rate" -> handleRate(player, args);
|
case "rate" -> handleRate(player, args);
|
||||||
@@ -552,6 +554,26 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
player.sendMessage(plugin.lang().format("reload.bungee-info", "{server}", plugin.getServerName()));
|
player.sendMessage(plugin.lang().format("reload.bungee-info", "{server}", plugin.getServerName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── /ticket backup ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleBackup(Player player) {
|
||||||
|
if (!player.hasPermission("ticket.admin")) {
|
||||||
|
plugin.lang().send(player, "general.no-permission"); return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.lang().get("backup.start"));
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
java.io.File backup = plugin.getDatabaseManager().createBackup();
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (backup != null) {
|
||||||
|
player.sendMessage(plugin.lang().format("backup.success",
|
||||||
|
"{file}", backup.getName()));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.lang().get("backup.fail"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── /ticket archive ───────────────────────────────────────────────────
|
// ── /ticket archive ───────────────────────────────────────────────────
|
||||||
|
|
||||||
private void handleArchive(Player player) {
|
private void handleArchive(Player player) {
|
||||||
@@ -835,6 +857,52 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
"{count}", String.valueOf(plugin.getFaqManager().getAll().size())));
|
"{count}", String.valueOf(plugin.getFaqManager().getAll().size())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "migrate", "migrieren" -> {
|
||||||
|
if (!player.hasPermission("ticket.admin")) {
|
||||||
|
plugin.lang().send(player, "general.no-permission"); return;
|
||||||
|
}
|
||||||
|
// Richtung bestimmen: tofile oder tomysql (Standard)
|
||||||
|
String direction = args.length >= 3 ? args[2].toLowerCase() : "tomysql";
|
||||||
|
|
||||||
|
if (direction.equals("tofile") || direction.equals("zudatei")) {
|
||||||
|
// MySQL → faqs.yml
|
||||||
|
if (!plugin.getFaqManager().isUsingMySQL()) {
|
||||||
|
player.sendMessage(plugin.lang().get("faq.migrate-no-mysql")); return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.lang().get("faq.migrate-tofile-start"));
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
int[] result = plugin.getFaqManager().migrateFaqToFile();
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result != null) {
|
||||||
|
player.sendMessage(plugin.lang().format("faq.migrate-tofile-success",
|
||||||
|
"{cats}", String.valueOf(result[0]),
|
||||||
|
"{entries}", String.valueOf(result[1])));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.lang().get("faq.migrate-fail"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// faqs.yml → MySQL
|
||||||
|
if (!plugin.getFaqManager().isUsingMySQL()) {
|
||||||
|
player.sendMessage(plugin.lang().get("faq.migrate-no-mysql")); return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.lang().get("faq.migrate-start"));
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
int[] result = plugin.getFaqManager().migrateFaqToMySQL();
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result != null) {
|
||||||
|
player.sendMessage(plugin.lang().format("faq.migrate-success",
|
||||||
|
"{cats}", String.valueOf(result[0]),
|
||||||
|
"{entries}", String.valueOf(result[1])));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.lang().get("faq.migrate-fail"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "list", "liste" -> {
|
case "list", "liste" -> {
|
||||||
List<FaqEntry> all = plugin.getFaqManager().getAll();
|
List<FaqEntry> all = plugin.getFaqManager().getAll();
|
||||||
player.sendMessage(plugin.lang().get("general.separator"));
|
player.sendMessage(plugin.lang().get("general.separator"));
|
||||||
@@ -1045,8 +1113,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
if (useEn) faqSubs.add("list");
|
if (useEn) faqSubs.add("list");
|
||||||
if (useDe) faqSubs.add("liste");
|
if (useDe) faqSubs.add("liste");
|
||||||
if (player.hasPermission("ticket.admin")) {
|
if (player.hasPermission("ticket.admin")) {
|
||||||
if (useEn) faqSubs.addAll(List.of("add", "edit", "delete", "reload"));
|
if (useEn) faqSubs.addAll(List.of("add", "edit", "delete", "reload", "migrate"));
|
||||||
if (useDe) faqSubs.addAll(List.of("hinzufügen", "bearbeiten", "löschen", "neuladen"));
|
if (useDe) faqSubs.addAll(List.of("hinzufügen", "bearbeiten", "löschen", "neuladen", "migrieren"));
|
||||||
}
|
}
|
||||||
for (String s : faqSubs)
|
for (String s : faqSubs)
|
||||||
if (s.startsWith(args[1].toLowerCase())) completions.add(s);
|
if (s.startsWith(args[1].toLowerCase())) completions.add(s);
|
||||||
|
|||||||
@@ -163,7 +163,59 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Connection getConnection() throws SQLException { return dataSource.getConnection(); }
|
private Connection getConnection() throws SQLException {
|
||||||
|
if (dataSource == null || dataSource.isClosed()) {
|
||||||
|
if (plugin != null)
|
||||||
|
plugin.getLogger().warning("[MySQL] DataSource nicht verfügbar – versuche Reconnect...");
|
||||||
|
if (!reconnectMySQL()) {
|
||||||
|
throw new SQLException("MySQL-Verbindung nicht verfügbar und Reconnect fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versucht die MySQL-Verbindung neu aufzubauen.
|
||||||
|
* Wird automatisch aufgerufen wenn getConnection() fehlschlägt.
|
||||||
|
*
|
||||||
|
* @return true wenn Reconnect erfolgreich
|
||||||
|
*/
|
||||||
|
public boolean reconnectMySQL() {
|
||||||
|
if (!useMySQL || plugin == null) return false;
|
||||||
|
try {
|
||||||
|
if (dataSource != null && !dataSource.isClosed()) dataSource.close();
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
config.setJdbcUrl(String.format(
|
||||||
|
"jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8",
|
||||||
|
plugin.getConfig().getString("mysql.host"),
|
||||||
|
plugin.getConfig().getInt("mysql.port"),
|
||||||
|
plugin.getConfig().getString("mysql.database")));
|
||||||
|
config.setUsername(plugin.getConfig().getString("mysql.username"));
|
||||||
|
config.setPassword(plugin.getConfig().getString("mysql.password"));
|
||||||
|
config.setMaximumPoolSize(plugin.getConfig().getInt("mysql.pool-size", 10));
|
||||||
|
config.setConnectionTimeout(plugin.getConfig().getLong("mysql.connection-timeout", 30000));
|
||||||
|
config.setPoolName("TicketSystem-Reconnect");
|
||||||
|
config.addDataSourceProperty("cachePrepStmts", "true");
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSize", "250");
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
||||||
|
dataSource = new HikariDataSource(config);
|
||||||
|
plugin.getLogger().info("[MySQL] Reconnect erfolgreich.");
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("[MySQL] Reconnect fehlgeschlagen: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt true zurück wenn die MySQL-Verbindung aktuell verfügbar ist. */
|
||||||
|
public boolean isConnected() {
|
||||||
|
if (!useMySQL || dataSource == null || dataSource.isClosed()) return false;
|
||||||
|
try (Connection c = dataSource.getConnection()) {
|
||||||
|
return c.isValid(2);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Tabellen erstellen ────────────────────────
|
// ─────────────────────────── Tabellen erstellen ────────────────────────
|
||||||
|
|
||||||
@@ -257,6 +309,27 @@ public class DatabaseManager {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
// FAQ-Einträge (optional, nur wenn faq-storage: mysql)
|
||||||
|
String faqEntriesSql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS faq_entries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
question VARCHAR(512) NOT NULL,
|
||||||
|
answer TEXT NOT NULL,
|
||||||
|
category_key VARCHAR(64) NOT NULL DEFAULT '__none__',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
""";
|
||||||
|
|
||||||
|
// FAQ-Kategorien (optional, nur wenn faq-storage: mysql)
|
||||||
|
String faqCategoriesSql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS faq_categories (
|
||||||
|
cat_key VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
color VARCHAR(8) NOT NULL DEFAULT '&7',
|
||||||
|
description VARCHAR(255) NOT NULL DEFAULT ''
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
""";
|
||||||
|
|
||||||
// Ausstehende BungeeCord-Teleport-Aufträge.
|
// Ausstehende BungeeCord-Teleport-Aufträge.
|
||||||
// Wird gesetzt wenn ein Admin via GUI/Command auf einen anderen Server teleportiert.
|
// Wird gesetzt wenn ein Admin via GUI/Command auf einen anderen Server teleportiert.
|
||||||
// PlayerJoinListener liest den Eintrag beim Ankommen, teleportiert, löscht ihn dann.
|
// PlayerJoinListener liest den Eintrag beim Ankommen, teleportiert, löscht ihn dann.
|
||||||
@@ -295,6 +368,8 @@ public class DatabaseManager {
|
|||||||
stmt.execute(statsSql);
|
stmt.execute(statsSql);
|
||||||
stmt.execute(pendingTeleportSql);
|
stmt.execute(pendingTeleportSql);
|
||||||
stmt.execute(creatorStatsSql);
|
stmt.execute(creatorStatsSql);
|
||||||
|
stmt.execute(faqEntriesSql);
|
||||||
|
stmt.execute(faqCategoriesSql);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e);
|
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@@ -1287,6 +1362,296 @@ public class DatabaseManager {
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── FAQ (MySQL) ───────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle FAQ-Einträge aus der Datenbank zurück.
|
||||||
|
* Gibt eine leere Liste zurück wenn MySQL nicht aktiv ist.
|
||||||
|
*/
|
||||||
|
public List<de.ticketsystem.model.FaqEntry> getFaqEntries() {
|
||||||
|
List<de.ticketsystem.model.FaqEntry> list = new ArrayList<>();
|
||||||
|
if (!useMySQL) return list;
|
||||||
|
String sql = "SELECT * FROM faq_entries ORDER BY sort_order ASC, id ASC";
|
||||||
|
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
|
ResultSet rs = stmt.executeQuery(sql);
|
||||||
|
while (rs.next()) {
|
||||||
|
de.ticketsystem.model.FaqEntry e = new de.ticketsystem.model.FaqEntry(
|
||||||
|
rs.getInt("id"),
|
||||||
|
rs.getString("question"),
|
||||||
|
rs.getString("answer"));
|
||||||
|
e.setCategoryKey(rs.getString("category_key"));
|
||||||
|
list.add(e);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Laden der FAQ-Einträge: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen neuen FAQ-Eintrag und gibt die generierte ID zurück (-1 bei Fehler).
|
||||||
|
*/
|
||||||
|
public int createFaqEntry(String question, String answer, String categoryKey) {
|
||||||
|
if (!useMySQL) return -1;
|
||||||
|
String sql = "INSERT INTO faq_entries (question, answer, category_key) VALUES (?, ?, ?)";
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
ps.setString(1, question);
|
||||||
|
ps.setString(2, answer);
|
||||||
|
ps.setString(3, categoryKey != null ? categoryKey : de.ticketsystem.manager.FaqManager.UNCATEGORIZED_KEY);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ResultSet rs = ps.getGeneratedKeys();
|
||||||
|
if (rs.next()) return rs.getInt(1);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Erstellen des FAQ-Eintrags: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert Frage und Antwort eines FAQ-Eintrags.
|
||||||
|
*/
|
||||||
|
public boolean updateFaqEntry(int id, String question, String answer) {
|
||||||
|
if (!useMySQL) return false;
|
||||||
|
String sql = "UPDATE faq_entries SET question = ?, answer = ? WHERE id = ?";
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, question);
|
||||||
|
ps.setString(2, answer);
|
||||||
|
ps.setInt(3, id);
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Aktualisieren des FAQ-Eintrags: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Kategorie eines FAQ-Eintrags.
|
||||||
|
*/
|
||||||
|
public boolean updateFaqEntryCategory(int id, String categoryKey) {
|
||||||
|
if (!useMySQL) return false;
|
||||||
|
String sql = "UPDATE faq_entries SET category_key = ? WHERE id = ?";
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, categoryKey != null ? categoryKey : de.ticketsystem.manager.FaqManager.UNCATEGORIZED_KEY);
|
||||||
|
ps.setInt(2, id);
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Setzen der FAQ-Kategorie: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht einen FAQ-Eintrag.
|
||||||
|
*/
|
||||||
|
public boolean deleteFaqEntry(int id) {
|
||||||
|
if (!useMySQL) return false;
|
||||||
|
String sql = "DELETE FROM faq_entries WHERE id = ?";
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, id);
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Löschen des FAQ-Eintrags: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle FAQ-Kategorien aus der Datenbank zurück.
|
||||||
|
*/
|
||||||
|
public List<de.ticketsystem.model.FaqCategory> getFaqCategories() {
|
||||||
|
List<de.ticketsystem.model.FaqCategory> list = new ArrayList<>();
|
||||||
|
if (!useMySQL) return list;
|
||||||
|
String sql = "SELECT * FROM faq_categories ORDER BY cat_key ASC";
|
||||||
|
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
|
ResultSet rs = stmt.executeQuery(sql);
|
||||||
|
while (rs.next()) {
|
||||||
|
list.add(new de.ticketsystem.model.FaqCategory(
|
||||||
|
rs.getString("cat_key"),
|
||||||
|
rs.getString("name"),
|
||||||
|
rs.getString("color"),
|
||||||
|
rs.getString("description")));
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Laden der FAQ-Kategorien: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt eine FAQ-Kategorie. Gibt false zurück wenn der Key bereits existiert.
|
||||||
|
*/
|
||||||
|
public boolean createFaqCategory(String key, String name, String color, String description) {
|
||||||
|
if (!useMySQL) return false;
|
||||||
|
String sql = "INSERT IGNORE INTO faq_categories (cat_key, name, color, description) VALUES (?, ?, ?, ?)";
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, key.toLowerCase());
|
||||||
|
ps.setString(2, name);
|
||||||
|
ps.setString(3, color != null ? color : "&7");
|
||||||
|
ps.setString(4, description != null ? description : "");
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Erstellen der FAQ-Kategorie: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert eine FAQ-Kategorie.
|
||||||
|
*/
|
||||||
|
public boolean updateFaqCategory(String key, String name, String color, String description) {
|
||||||
|
if (!useMySQL) return false;
|
||||||
|
String sql = "UPDATE faq_categories SET name = ?, color = ?, description = ? WHERE cat_key = ?";
|
||||||
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, name);
|
||||||
|
ps.setString(2, color != null ? color : "&7");
|
||||||
|
ps.setString(3, description != null ? description : "");
|
||||||
|
ps.setString(4, key.toLowerCase());
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Aktualisieren der FAQ-Kategorie: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht eine FAQ-Kategorie und setzt alle zugehörigen Einträge auf UNCATEGORIZED.
|
||||||
|
*/
|
||||||
|
public boolean deleteFaqCategory(String key) {
|
||||||
|
if (!useMySQL) return false;
|
||||||
|
String lowerKey = key.toLowerCase();
|
||||||
|
try (Connection conn = getConnection()) {
|
||||||
|
// Einträge dieser Kategorie auf __none__ setzen
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"UPDATE faq_entries SET category_key = ? WHERE category_key = ?")) {
|
||||||
|
ps.setString(1, de.ticketsystem.manager.FaqManager.UNCATEGORIZED_KEY);
|
||||||
|
ps.setString(2, lowerKey);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
// Kategorie löschen
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"DELETE FROM faq_categories WHERE cat_key = ?")) {
|
||||||
|
ps.setString(1, lowerKey);
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[FAQ] Fehler beim Löschen der FAQ-Kategorie: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest alle archivierten Tickets aus der Archiv-Datei (archive.json / archive.yml).
|
||||||
|
* Funktioniert sowohl im MySQL- als auch im Datei-Modus, da das Archiv
|
||||||
|
* immer als JSON-Datei im Plugin-Ordner gespeichert wird.
|
||||||
|
*
|
||||||
|
* @return Liste aller archivierten Tickets (leer wenn kein Archiv vorhanden)
|
||||||
|
*/
|
||||||
|
public List<Ticket> getArchivedTickets() {
|
||||||
|
List<Ticket> list = new ArrayList<>();
|
||||||
|
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
|
||||||
|
if (!archiveFile.exists()) return list;
|
||||||
|
|
||||||
|
try (FileReader fr = new FileReader(archiveFile)) {
|
||||||
|
Object parsed = new JSONParser().parse(fr);
|
||||||
|
if (parsed instanceof JSONArray arr) {
|
||||||
|
for (Object o : arr) {
|
||||||
|
if (o instanceof JSONObject obj) {
|
||||||
|
Ticket t = ticketFromJson(obj);
|
||||||
|
if (t != null) list.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[TicketSystem] Fehler beim Laden des Archivs: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht ein Ticket permanent aus dem Archiv.
|
||||||
|
*
|
||||||
|
* @param ticketId ID des zu löschenden Tickets
|
||||||
|
* @return true wenn gefunden und gelöscht
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public boolean deleteArchivedTicket(int ticketId) {
|
||||||
|
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
|
||||||
|
if (!archiveFile.exists()) return false;
|
||||||
|
|
||||||
|
try (FileReader fr = new FileReader(archiveFile)) {
|
||||||
|
Object parsed = new JSONParser().parse(fr);
|
||||||
|
if (!(parsed instanceof JSONArray arr)) return false;
|
||||||
|
|
||||||
|
JSONArray updated = new JSONArray();
|
||||||
|
boolean removed = false;
|
||||||
|
for (Object o : arr) {
|
||||||
|
if (o instanceof JSONObject obj) {
|
||||||
|
long id = obj.get("id") instanceof Long l ? l : ((Number) obj.get("id")).longValue();
|
||||||
|
if (id == ticketId) { removed = true; continue; }
|
||||||
|
updated.add(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!removed) return false;
|
||||||
|
|
||||||
|
try (FileWriter fw = new FileWriter(archiveFile)) {
|
||||||
|
fw.write(updated.toJSONString());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[Archiv] Fehler beim Löschen: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt ein archiviertes Ticket wieder her (zurück in die aktive Datenbank).
|
||||||
|
* Das Ticket wird aus dem Archiv entfernt und neu als CLOSED-Ticket angelegt.
|
||||||
|
*
|
||||||
|
* @param ticketId ID des wiederherzustellenden Tickets
|
||||||
|
* @return true wenn erfolgreich wiederhergestellt
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public boolean restoreArchivedTicket(int ticketId) {
|
||||||
|
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
|
||||||
|
if (!archiveFile.exists()) return false;
|
||||||
|
|
||||||
|
try (FileReader fr = new FileReader(archiveFile)) {
|
||||||
|
Object parsed = new JSONParser().parse(fr);
|
||||||
|
if (!(parsed instanceof JSONArray arr)) return false;
|
||||||
|
|
||||||
|
JSONObject found = null;
|
||||||
|
JSONArray updated = new JSONArray();
|
||||||
|
for (Object o : arr) {
|
||||||
|
if (o instanceof JSONObject obj) {
|
||||||
|
long id = obj.get("id") instanceof Long l ? l : ((Number) obj.get("id")).longValue();
|
||||||
|
if (id == ticketId) { found = obj; }
|
||||||
|
else updated.add(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found == null) return false;
|
||||||
|
|
||||||
|
Ticket t = ticketFromJson(found);
|
||||||
|
if (t == null) return false;
|
||||||
|
|
||||||
|
// Ticket in aktive DB zurückschreiben
|
||||||
|
int newId = createTicket(t);
|
||||||
|
if (newId == -1) return false;
|
||||||
|
|
||||||
|
// Aus Archiv entfernen
|
||||||
|
try (FileWriter fw = new FileWriter(archiveFile)) {
|
||||||
|
fw.write(updated.toJSONString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin != null)
|
||||||
|
plugin.getLogger().info("[Archiv] Ticket #" + ticketId + " wiederhergestellt als #" + newId);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[Archiv] Fehler beim Wiederherstellen: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Statistiken ───────────────────────────────
|
// ─────────────────────────── Statistiken ───────────────────────────────
|
||||||
|
|
||||||
public TicketStats getTicketStats() {
|
public TicketStats getTicketStats() {
|
||||||
@@ -1546,7 +1911,73 @@ public class DatabaseManager {
|
|||||||
catch (IOException e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Speichern von " + dataFileName + ": " + e.getMessage()); }
|
catch (IOException e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Speichern von " + dataFileName + ": " + e.getMessage()); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Backup (Platzhalter) ──────────────────────
|
// ─────────────────────────── Backup ────────────────────────────────────
|
||||||
private void backupMySQL() {}
|
|
||||||
private void backupDataFile() {}
|
/**
|
||||||
|
* Erstellt ein Backup der MySQL-Datenbank als JSON-Datei im Plugin-Ordner.
|
||||||
|
* Dateiname: backup_tickets_YYYY-MM-DD_HH-mm-ss.json
|
||||||
|
* Exportiert alle Tickets aus der aktiven tickets-Tabelle.
|
||||||
|
*
|
||||||
|
* @return Pfad zur Backup-Datei oder null bei Fehler
|
||||||
|
*/
|
||||||
|
public File backupMySQL() {
|
||||||
|
if (!useMySQL) return null;
|
||||||
|
String timestamp = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss")
|
||||||
|
.format(new java.util.Date());
|
||||||
|
File backupDir = new File(plugin.getDataFolder(), "backups");
|
||||||
|
backupDir.mkdirs();
|
||||||
|
File backupFile = new File(backupDir, "backup_tickets_" + timestamp + ".json");
|
||||||
|
|
||||||
|
List<Ticket> tickets = getAllTickets();
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (Ticket t : tickets) arr.add(ticketToJson(t));
|
||||||
|
|
||||||
|
try (FileWriter fw = new FileWriter(backupFile)) {
|
||||||
|
fw.write(arr.toJSONString());
|
||||||
|
if (plugin != null)
|
||||||
|
plugin.getLogger().info("[Backup] MySQL-Backup erstellt: " + backupFile.getName()
|
||||||
|
+ " (" + tickets.size() + " Tickets)");
|
||||||
|
return backupFile;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (plugin != null)
|
||||||
|
plugin.getLogger().severe("[Backup] Fehler beim MySQL-Backup: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein Backup der aktuellen data.yml / data.json im Plugin-Ordner.
|
||||||
|
* Dateiname: backup_tickets_YYYY-MM-DD_HH-mm-ss.yml (bzw. .json)
|
||||||
|
*
|
||||||
|
* @return Pfad zur Backup-Datei oder null bei Fehler
|
||||||
|
*/
|
||||||
|
public File backupDataFile() {
|
||||||
|
if (useMySQL || dataFile == null || !dataFile.exists()) return null;
|
||||||
|
String timestamp = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss")
|
||||||
|
.format(new java.util.Date());
|
||||||
|
String ext = dataFileName.endsWith(".json") ? ".json" : ".yml";
|
||||||
|
File backupDir = new File(plugin.getDataFolder(), "backups");
|
||||||
|
backupDir.mkdirs();
|
||||||
|
File backupFile = new File(backupDir, "backup_tickets_" + timestamp + ext);
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.nio.file.Files.copy(dataFile.toPath(), backupFile.toPath(),
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
if (plugin != null)
|
||||||
|
plugin.getLogger().info("[Backup] Datei-Backup erstellt: " + backupFile.getName());
|
||||||
|
return backupFile;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (plugin != null)
|
||||||
|
plugin.getLogger().severe("[Backup] Fehler beim Datei-Backup: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt automatisch ein Backup (MySQL oder Datei) und gibt den Dateipfad zurück.
|
||||||
|
* Wird z.B. vor Migrationen aufgerufen.
|
||||||
|
*/
|
||||||
|
public File createBackup() {
|
||||||
|
return useMySQL ? backupMySQL() : backupDataFile();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.ticketsystem.manager;
|
package de.ticketsystem.manager;
|
||||||
|
|
||||||
import de.ticketsystem.TicketPlugin;
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.database.DatabaseManager;
|
||||||
import de.ticketsystem.model.FaqCategory;
|
import de.ticketsystem.model.FaqCategory;
|
||||||
import de.ticketsystem.model.FaqEntry;
|
import de.ticketsystem.model.FaqEntry;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
@@ -11,54 +12,93 @@ import java.io.IOException;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages FAQ entries and FAQ categories stored in faqs.yml.
|
* Verwaltet FAQ-Einträge und FAQ-Kategorien.
|
||||||
*
|
*
|
||||||
* faqs.yml wird beim ersten Start automatisch mit Beispiel-Kategorien und -FAQs generiert.
|
* Speichermodus wird in config.yml festgelegt:
|
||||||
* Admins können Kategorien und FAQs direkt in-game verwalten (GUI + Befehle).
|
|
||||||
*
|
*
|
||||||
* faqs.yml Layout:
|
* faq-storage: file → faqs.yml (Standard, Einzelserver)
|
||||||
|
* faq-storage: mysql → MySQL-Datenbank (BungeeCord / Multi-Server, use-mysql muss true sein)
|
||||||
*
|
*
|
||||||
* categories:
|
* Im MySQL-Modus werden Tabellen faq_entries und faq_categories verwendet.
|
||||||
* tickets:
|
* Im Datei-Modus verhält sich der Manager exakt wie bisher (faqs.yml).
|
||||||
* name: "Tickets"
|
|
||||||
* color: "&b"
|
|
||||||
* description: "Fragen zum Ticket-System"
|
|
||||||
*
|
*
|
||||||
* faqs:
|
* Die öffentliche API (add, edit, delete, getAll …) ist in beiden Modi identisch –
|
||||||
* 1:
|
* FaqGUI, FaqHandler und ApiHandler müssen nicht angepasst werden.
|
||||||
* question: "Wie erstelle ich ein Ticket?"
|
|
||||||
* answer: "Nutze /ticket create ..."
|
|
||||||
* category: "tickets"
|
|
||||||
*
|
|
||||||
* Material und Textur der Kategorie-Items werden NICHT hier gespeichert –
|
|
||||||
* sie werden zentral in config.yml unter gui-settings.faq.category-head-item gesteuert.
|
|
||||||
*/
|
*/
|
||||||
public class FaqManager {
|
public class FaqManager {
|
||||||
|
|
||||||
public static final String UNCATEGORIZED_KEY = "__none__";
|
public static final String UNCATEGORIZED_KEY = "__none__";
|
||||||
|
|
||||||
private final TicketPlugin plugin;
|
private final TicketPlugin plugin;
|
||||||
|
private final boolean useMysql;
|
||||||
|
|
||||||
|
// ── YAML-Modus ────────────────────────────────────────────────────────
|
||||||
private final File faqFile;
|
private final File faqFile;
|
||||||
private YamlConfiguration faqConfig;
|
private YamlConfiguration faqConfig;
|
||||||
|
|
||||||
|
// ── In-Memory-State (beide Modi) ──────────────────────────────────────
|
||||||
private final List<FaqEntry> entries = new ArrayList<>();
|
private final List<FaqEntry> entries = new ArrayList<>();
|
||||||
private final LinkedHashMap<String, FaqCategory> categories = new LinkedHashMap<>();
|
private final LinkedHashMap<String, FaqCategory> categories = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/** Nur im YAML-Modus relevant – im MySQL-Modus liefert AUTO_INCREMENT die ID */
|
||||||
private int nextId = 1;
|
private int nextId = 1;
|
||||||
|
|
||||||
public FaqManager(TicketPlugin plugin) {
|
public FaqManager(TicketPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.faqFile = new File(plugin.getDataFolder(), "faqs.yml");
|
this.faqFile = new File(plugin.getDataFolder(), "faqs.yml");
|
||||||
|
|
||||||
|
// Speichermodus direkt aus use-mysql lesen – kein separater faq-storage Key nötig
|
||||||
|
this.useMysql = plugin.getConfig().getBoolean("use-mysql", false);
|
||||||
|
|
||||||
|
if (useMysql) {
|
||||||
|
plugin.getLogger().info("[FaqManager] Speichermodus: MySQL (faq_entries / faq_categories)");
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().info("[FaqManager] Speichermodus: Datei (faqs.yml)");
|
||||||
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Loading & Saving ───────────────────────────
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// LADEN & SPEICHERN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
private void load() {
|
private void load() {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
categories.clear();
|
categories.clear();
|
||||||
nextId = 1;
|
nextId = 1;
|
||||||
|
|
||||||
|
if (useMysql) {
|
||||||
|
loadFromMySQL();
|
||||||
|
} else {
|
||||||
|
loadFromFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MySQL-Modus ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadFromMySQL() {
|
||||||
|
DatabaseManager db = plugin.getDatabaseManager();
|
||||||
|
|
||||||
|
for (FaqCategory cat : db.getFaqCategories()) {
|
||||||
|
categories.put(cat.getKey(), cat);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (FaqEntry e : db.getFaqEntries()) {
|
||||||
|
e.setCategoryKey(normalizeCategoryKey(e.getCategoryKey()));
|
||||||
|
entries.add(e);
|
||||||
|
if (e.getId() >= nextId) nextId = e.getId() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[FaqManager] MySQL: " + categories.size()
|
||||||
|
+ " Kategorie(n), " + entries.size() + " FAQ(s) geladen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Datei-Modus ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadFromFile() {
|
||||||
if (!faqFile.exists()) {
|
if (!faqFile.exists()) {
|
||||||
try {
|
try {
|
||||||
faqFile.getParentFile().mkdirs();
|
faqFile.getParentFile().mkdirs();
|
||||||
@@ -68,13 +108,12 @@ public class FaqManager {
|
|||||||
}
|
}
|
||||||
faqConfig = new YamlConfiguration();
|
faqConfig = new YamlConfiguration();
|
||||||
loadDefaults();
|
loadDefaults();
|
||||||
save();
|
saveToFile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
faqConfig = YamlConfiguration.loadConfiguration(faqFile);
|
faqConfig = YamlConfiguration.loadConfiguration(faqFile);
|
||||||
|
|
||||||
// ── 1. Kategorien laden ───────────────────────────────────────────
|
|
||||||
ConfigurationSection catSection = faqConfig.getConfigurationSection("categories");
|
ConfigurationSection catSection = faqConfig.getConfigurationSection("categories");
|
||||||
if (catSection != null && !catSection.getKeys(false).isEmpty()) {
|
if (catSection != null && !catSection.getKeys(false).isEmpty()) {
|
||||||
for (String key : catSection.getKeys(false)) {
|
for (String key : catSection.getKeys(false)) {
|
||||||
@@ -85,13 +124,8 @@ public class FaqManager {
|
|||||||
String desc = cat.getString("description", "");
|
String desc = cat.getString("description", "");
|
||||||
categories.put(key.toLowerCase(), new FaqCategory(key, name, color, desc));
|
categories.put(key.toLowerCase(), new FaqCategory(key, name, color, desc));
|
||||||
}
|
}
|
||||||
if (plugin.isDebug()) {
|
|
||||||
plugin.getLogger().info("[FaqManager] " + categories.size()
|
|
||||||
+ " FAQ-Kategorie(n) geladen: " + String.join(", ", categories.keySet()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. FAQ-Einträge laden ─────────────────────────────────────────
|
|
||||||
ConfigurationSection faqSection = faqConfig.getConfigurationSection("faqs");
|
ConfigurationSection faqSection = faqConfig.getConfigurationSection("faqs");
|
||||||
if (faqSection != null) {
|
if (faqSection != null) {
|
||||||
for (String key : faqSection.getKeys(false)) {
|
for (String key : faqSection.getKeys(false)) {
|
||||||
@@ -113,7 +147,8 @@ public class FaqManager {
|
|||||||
entries.sort(Comparator.comparingInt(FaqEntry::getId));
|
entries.sort(Comparator.comparingInt(FaqEntry::getId));
|
||||||
|
|
||||||
if (plugin.isDebug()) {
|
if (plugin.isDebug()) {
|
||||||
plugin.getLogger().info("[FaqManager] " + entries.size() + " FAQ(s) geladen.");
|
plugin.getLogger().info("[FaqManager] Datei: " + categories.size()
|
||||||
|
+ " Kategorie(n), " + entries.size() + " FAQ(s) geladen.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,23 +159,23 @@ public class FaqManager {
|
|||||||
"Material/Textur der Kategorie-Items: config.yml → gui-settings.faq.category-head-item"
|
"Material/Textur der Kategorie-Items: config.yml → gui-settings.faq.category-head-item"
|
||||||
);
|
);
|
||||||
|
|
||||||
writeCategory("general", "Allgemein", "&e", "Allgemeine Fragen zum Server");
|
writeFileCategory("general", "Allgemein", "&e", "Allgemeine Fragen zum Server");
|
||||||
writeCategory("rules", "Regeln", "&c", "Fragen zu den Server-Regeln");
|
writeFileCategory("rules", "Regeln", "&c", "Fragen zu den Server-Regeln");
|
||||||
writeCategory("gameplay", "Gameplay", "&a", "Fragen zum Spielgeschehen");
|
writeFileCategory("gameplay", "Gameplay", "&a", "Fragen zum Spielgeschehen");
|
||||||
writeCategory("tickets", "Tickets", "&b", "Fragen zum Ticket-System");
|
writeFileCategory("tickets", "Tickets", "&b", "Fragen zum Ticket-System");
|
||||||
|
|
||||||
categories.put("general", new FaqCategory("general", "Allgemein", "&e", "Allgemeine Fragen zum Server"));
|
categories.put("general", new FaqCategory("general", "Allgemein", "&e", "Allgemeine Fragen zum Server"));
|
||||||
categories.put("rules", new FaqCategory("rules", "Regeln", "&c", "Fragen zu den Server-Regeln"));
|
categories.put("rules", new FaqCategory("rules", "Regeln", "&c", "Fragen zu den Server-Regeln"));
|
||||||
categories.put("gameplay", new FaqCategory("gameplay", "Gameplay", "&a", "Fragen zum Spielgeschehen"));
|
categories.put("gameplay", new FaqCategory("gameplay", "Gameplay", "&a", "Fragen zum Spielgeschehen"));
|
||||||
categories.put("tickets", new FaqCategory("tickets", "Tickets", "&b", "Fragen zum Ticket-System"));
|
categories.put("tickets", new FaqCategory("tickets", "Tickets", "&b", "Fragen zum Ticket-System"));
|
||||||
|
|
||||||
writeEntry(1, "Wie erstelle ich ein Ticket?",
|
writeFileEntry(1, "Wie erstelle ich ein Ticket?",
|
||||||
"Nutze den Befehl /ticket create [Kategorie] [Prio] <Beschreibung>.", "tickets");
|
"Nutze den Befehl /ticket create [Kategorie] [Prio] <Beschreibung>.", "tickets");
|
||||||
writeEntry(2, "Wie lange dauert die Bearbeitung?",
|
writeFileEntry(2, "Wie lange dauert die Bearbeitung?",
|
||||||
"Unser Support-Team bearbeitet Tickets so schnell wie möglich.", "tickets");
|
"Unser Support-Team bearbeitet Tickets so schnell wie möglich.", "tickets");
|
||||||
writeEntry(3, "Kann ich mein Ticket löschen?",
|
writeFileEntry(3, "Kann ich mein Ticket löschen?",
|
||||||
"Ja! Öffne /ticket list und klicke auf dein Ticket.", "tickets");
|
"Ja! Öffne /ticket list und klicke auf dein Ticket.", "tickets");
|
||||||
writeEntry(4, "Wie kann ich meinen Support bewerten?",
|
writeFileEntry(4, "Wie kann ich meinen Support bewerten?",
|
||||||
"Mit /ticket rate <ID> good/bad nach dem Schließen.", "tickets");
|
"Mit /ticket rate <ID> good/bad nach dem Schließen.", "tickets");
|
||||||
nextId = 5;
|
nextId = 5;
|
||||||
|
|
||||||
@@ -155,13 +190,13 @@ public class FaqManager {
|
|||||||
|
|
||||||
// ── YAML-Hilfsmethoden ─────────────────────────────────────────────────
|
// ── YAML-Hilfsmethoden ─────────────────────────────────────────────────
|
||||||
|
|
||||||
private void writeCategory(String key, String name, String color, String description) {
|
private void writeFileCategory(String key, String name, String color, String description) {
|
||||||
faqConfig.set("categories." + key + ".name", name);
|
faqConfig.set("categories." + key + ".name", name);
|
||||||
faqConfig.set("categories." + key + ".color", color);
|
faqConfig.set("categories." + key + ".color", color);
|
||||||
faqConfig.set("categories." + key + ".description", description);
|
faqConfig.set("categories." + key + ".description", description);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeEntry(int id, String question, String answer, String categoryKey) {
|
private void writeFileEntry(int id, String question, String answer, String categoryKey) {
|
||||||
faqConfig.set("faqs." + id + ".question", question);
|
faqConfig.set("faqs." + id + ".question", question);
|
||||||
faqConfig.set("faqs." + id + ".answer", answer);
|
faqConfig.set("faqs." + id + ".answer", answer);
|
||||||
if (categoryKey != null && !categoryKey.equals(UNCATEGORIZED_KEY)) {
|
if (categoryKey != null && !categoryKey.equals(UNCATEGORIZED_KEY)) {
|
||||||
@@ -169,7 +204,8 @@ public class FaqManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void save() {
|
private void saveToFile() {
|
||||||
|
if (faqConfig == null) return;
|
||||||
try {
|
try {
|
||||||
faqConfig.save(faqFile);
|
faqConfig.save(faqFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -177,7 +213,9 @@ public class FaqManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Public API – Kategorien ───────────────────
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// PUBLIC API – KATEGORIEN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
public boolean hasCategoriesEnabled() { return !categories.isEmpty(); }
|
public boolean hasCategoriesEnabled() { return !categories.isEmpty(); }
|
||||||
|
|
||||||
@@ -190,56 +228,62 @@ public class FaqManager {
|
|||||||
return categories.get(key.toLowerCase());
|
return categories.get(key.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fügt eine neue Kategorie hinzu und speichert sofort.
|
|
||||||
*
|
|
||||||
* @return null wenn der Schlüssel bereits existiert, sonst die neue FaqCategory.
|
|
||||||
*/
|
|
||||||
public FaqCategory addCategory(String key, String name, String color, String description) {
|
public FaqCategory addCategory(String key, String name, String color, String description) {
|
||||||
String lowerKey = key.toLowerCase().replaceAll("\\s+", "_");
|
String lowerKey = key.toLowerCase().replaceAll("\\s+", "_");
|
||||||
if (categories.containsKey(lowerKey)) return null;
|
if (categories.containsKey(lowerKey)) return null;
|
||||||
|
|
||||||
FaqCategory cat = new FaqCategory(lowerKey, name, color, description);
|
FaqCategory cat = new FaqCategory(lowerKey, name, color, description);
|
||||||
categories.put(lowerKey, cat);
|
categories.put(lowerKey, cat);
|
||||||
writeCategory(lowerKey, name, color, description);
|
|
||||||
save();
|
if (useMysql) {
|
||||||
|
plugin.getDatabaseManager().createFaqCategory(lowerKey, name, color, description);
|
||||||
|
} else {
|
||||||
|
writeFileCategory(lowerKey, name, color, description);
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
return cat;
|
return cat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Bearbeitet eine bestehende Kategorie und speichert sofort.
|
|
||||||
*
|
|
||||||
* @return true wenn gefunden und aktualisiert.
|
|
||||||
*/
|
|
||||||
public boolean editCategory(String key, String name, String color, String description) {
|
public boolean editCategory(String key, String name, String color, String description) {
|
||||||
String lowerKey = key.toLowerCase();
|
String lowerKey = key.toLowerCase();
|
||||||
if (!categories.containsKey(lowerKey)) return false;
|
if (!categories.containsKey(lowerKey)) return false;
|
||||||
FaqCategory updated = new FaqCategory(lowerKey, name, color, description);
|
|
||||||
categories.put(lowerKey, updated);
|
categories.put(lowerKey, new FaqCategory(lowerKey, name, color, description));
|
||||||
writeCategory(lowerKey, name, color, description);
|
|
||||||
save();
|
if (useMysql) {
|
||||||
|
return plugin.getDatabaseManager().updateFaqCategory(lowerKey, name, color, description);
|
||||||
|
} else {
|
||||||
|
writeFileCategory(lowerKey, name, color, description);
|
||||||
|
saveToFile();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Löscht eine Kategorie. FAQs dieser Kategorie werden auf UNCATEGORIZED_KEY gesetzt.
|
|
||||||
*
|
|
||||||
* @return true wenn gefunden und gelöscht.
|
|
||||||
*/
|
|
||||||
public boolean deleteCategory(String key) {
|
public boolean deleteCategory(String key) {
|
||||||
String lowerKey = key.toLowerCase();
|
String lowerKey = key.toLowerCase();
|
||||||
if (!categories.containsKey(lowerKey)) return false;
|
if (!categories.containsKey(lowerKey)) return false;
|
||||||
|
|
||||||
categories.remove(lowerKey);
|
categories.remove(lowerKey);
|
||||||
|
|
||||||
|
if (useMysql) {
|
||||||
|
boolean ok = plugin.getDatabaseManager().deleteFaqCategory(lowerKey);
|
||||||
|
// In-Memory synchronisieren
|
||||||
|
entries.stream()
|
||||||
|
.filter(e -> lowerKey.equals(e.getCategoryKey()))
|
||||||
|
.forEach(e -> e.setCategoryKey(UNCATEGORIZED_KEY));
|
||||||
|
return ok;
|
||||||
|
} else {
|
||||||
faqConfig.set("categories." + lowerKey, null);
|
faqConfig.set("categories." + lowerKey, null);
|
||||||
// FAQs dieser Kategorie auf "keine Kategorie" setzen
|
|
||||||
for (FaqEntry entry : entries) {
|
for (FaqEntry entry : entries) {
|
||||||
if (lowerKey.equals(entry.getCategoryKey())) {
|
if (lowerKey.equals(entry.getCategoryKey())) {
|
||||||
entry.setCategoryKey(UNCATEGORIZED_KEY);
|
entry.setCategoryKey(UNCATEGORIZED_KEY);
|
||||||
faqConfig.set("faqs." + entry.getId() + ".category", null);
|
faqConfig.set("faqs." + entry.getId() + ".category", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
save();
|
saveToFile();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<FaqEntry> getByCategory(String categoryKey) {
|
public List<FaqEntry> getByCategory(String categoryKey) {
|
||||||
String normalizedKey = normalizeCategoryKey(categoryKey);
|
String normalizedKey = normalizeCategoryKey(categoryKey);
|
||||||
@@ -254,7 +298,9 @@ public class FaqManager {
|
|||||||
return getByCategory(categoryKey).size();
|
return getByCategory(categoryKey).size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Public API – Einträge ─────────────────────
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// PUBLIC API – EINTRÄGE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
public List<FaqEntry> getAll() { return Collections.unmodifiableList(entries); }
|
public List<FaqEntry> getAll() { return Collections.unmodifiableList(entries); }
|
||||||
|
|
||||||
@@ -263,14 +309,24 @@ public class FaqManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public FaqEntry add(String question, String answer, String categoryKey) {
|
public FaqEntry add(String question, String answer, String categoryKey) {
|
||||||
int id = nextId++;
|
|
||||||
String normalizedKey = normalizeCategoryKey(categoryKey);
|
String normalizedKey = normalizeCategoryKey(categoryKey);
|
||||||
|
|
||||||
|
if (useMysql) {
|
||||||
|
int id = plugin.getDatabaseManager().createFaqEntry(question, answer, normalizedKey);
|
||||||
|
if (id == -1) return null;
|
||||||
FaqEntry entry = new FaqEntry(id, question, answer);
|
FaqEntry entry = new FaqEntry(id, question, answer);
|
||||||
entry.setCategoryKey(normalizedKey);
|
entry.setCategoryKey(normalizedKey);
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
writeEntry(id, question, answer, normalizedKey);
|
|
||||||
save();
|
|
||||||
return entry;
|
return entry;
|
||||||
|
} else {
|
||||||
|
int id = nextId++;
|
||||||
|
FaqEntry entry = new FaqEntry(id, question, answer);
|
||||||
|
entry.setCategoryKey(normalizedKey);
|
||||||
|
entries.add(entry);
|
||||||
|
writeFileEntry(id, question, answer, normalizedKey);
|
||||||
|
saveToFile();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public FaqEntry add(String question, String answer) {
|
public FaqEntry add(String question, String answer) {
|
||||||
@@ -280,39 +336,193 @@ public class FaqManager {
|
|||||||
public boolean edit(int id, String question, String answer) {
|
public boolean edit(int id, String question, String answer) {
|
||||||
FaqEntry entry = getById(id);
|
FaqEntry entry = getById(id);
|
||||||
if (entry == null) return false;
|
if (entry == null) return false;
|
||||||
|
|
||||||
entry.setQuestion(question);
|
entry.setQuestion(question);
|
||||||
entry.setAnswer(answer);
|
entry.setAnswer(answer);
|
||||||
writeEntry(id, question, answer, entry.getCategoryKey());
|
|
||||||
save();
|
if (useMysql) {
|
||||||
|
return plugin.getDatabaseManager().updateFaqEntry(id, question, answer);
|
||||||
|
} else {
|
||||||
|
writeFileEntry(id, question, answer, entry.getCategoryKey());
|
||||||
|
saveToFile();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean setCategory(int id, String categoryKey) {
|
public boolean setCategory(int id, String categoryKey) {
|
||||||
FaqEntry entry = getById(id);
|
FaqEntry entry = getById(id);
|
||||||
if (entry == null) return false;
|
if (entry == null) return false;
|
||||||
|
|
||||||
String normalizedKey = normalizeCategoryKey(categoryKey);
|
String normalizedKey = normalizeCategoryKey(categoryKey);
|
||||||
entry.setCategoryKey(normalizedKey);
|
entry.setCategoryKey(normalizedKey);
|
||||||
|
|
||||||
|
if (useMysql) {
|
||||||
|
return plugin.getDatabaseManager().updateFaqEntryCategory(id, normalizedKey);
|
||||||
|
} else {
|
||||||
if (normalizedKey.equals(UNCATEGORIZED_KEY)) {
|
if (normalizedKey.equals(UNCATEGORIZED_KEY)) {
|
||||||
faqConfig.set("faqs." + id + ".category", null);
|
faqConfig.set("faqs." + id + ".category", null);
|
||||||
} else {
|
} else {
|
||||||
faqConfig.set("faqs." + id + ".category", normalizedKey);
|
faqConfig.set("faqs." + id + ".category", normalizedKey);
|
||||||
}
|
}
|
||||||
save();
|
saveToFile();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean delete(int id) {
|
public boolean delete(int id) {
|
||||||
FaqEntry entry = getById(id);
|
FaqEntry entry = getById(id);
|
||||||
if (entry == null) return false;
|
if (entry == null) return false;
|
||||||
|
|
||||||
entries.remove(entry);
|
entries.remove(entry);
|
||||||
|
|
||||||
|
if (useMysql) {
|
||||||
|
return plugin.getDatabaseManager().deleteFaqEntry(id);
|
||||||
|
} else {
|
||||||
faqConfig.set("faqs." + id, null);
|
faqConfig.set("faqs." + id, null);
|
||||||
save();
|
saveToFile();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void reload() { load(); }
|
public void reload() { load(); }
|
||||||
|
|
||||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
/** Gibt zurück ob MySQL als FAQ-Speicher aktiv ist. */
|
||||||
|
public boolean isUsingMySQL() { return useMysql; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migriert FAQ-Daten aus faqs.yml in die MySQL-Datenbank.
|
||||||
|
*
|
||||||
|
* Nur ausführbar wenn use-mysql: true aktiv ist.
|
||||||
|
* Nach erfolgreichem Import wird der In-Memory-Cache neu geladen.
|
||||||
|
*
|
||||||
|
* @return int[2] { importierte Kategorien, importierte Einträge } oder null bei Fehler
|
||||||
|
*/
|
||||||
|
public int[] migrateFaqToMySQL() {
|
||||||
|
if (!useMysql) {
|
||||||
|
plugin.getLogger().warning("[FaqManager] Migration nur im MySQL-Modus möglich (use-mysql: true).");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!faqFile.exists()) {
|
||||||
|
plugin.getLogger().warning("[FaqManager] faqs.yml nicht gefunden – Migration abgebrochen.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
YamlConfiguration yml = YamlConfiguration.loadConfiguration(faqFile);
|
||||||
|
DatabaseManager db = plugin.getDatabaseManager();
|
||||||
|
|
||||||
|
// Bestehende DB-Einträge als Duplikat-Schutz laden
|
||||||
|
java.util.Set<String> existingKeys = new java.util.HashSet<>();
|
||||||
|
for (FaqEntry e : db.getFaqEntries()) {
|
||||||
|
existingKeys.add(e.getQuestion().trim().toLowerCase()
|
||||||
|
+ "\u0000" + e.getAnswer().trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kategorien importieren
|
||||||
|
int importedCats = 0;
|
||||||
|
ConfigurationSection catSec = yml.getConfigurationSection("categories");
|
||||||
|
if (catSec != null) {
|
||||||
|
for (String key : catSec.getKeys(false)) {
|
||||||
|
String name = yml.getString("categories." + key + ".name", key);
|
||||||
|
String color = yml.getString("categories." + key + ".color", "&7");
|
||||||
|
String desc = yml.getString("categories." + key + ".description", "");
|
||||||
|
if (db.createFaqCategory(key.toLowerCase(), name, color, desc)) importedCats++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einträge importieren — Duplikat-Prüfung per ID (nicht nur Inhalt)
|
||||||
|
int importedEntries = 0;
|
||||||
|
java.util.Set<Integer> existingIds = new java.util.HashSet<>();
|
||||||
|
for (FaqEntry e : db.getFaqEntries()) existingIds.add(e.getId());
|
||||||
|
|
||||||
|
ConfigurationSection faqSec = yml.getConfigurationSection("faqs");
|
||||||
|
if (faqSec != null) {
|
||||||
|
for (String key : faqSec.getKeys(false)) {
|
||||||
|
String question = yml.getString("faqs." + key + ".question", "");
|
||||||
|
String answer = yml.getString("faqs." + key + ".answer", "");
|
||||||
|
String category = yml.getString("faqs." + key + ".category", null);
|
||||||
|
if (question.isBlank() || answer.isBlank()) continue;
|
||||||
|
// Duplikat-Check: gleiche Frage UND Antwort (normalisiert)
|
||||||
|
String dupKey = question.trim().toLowerCase() + "\u0000" + answer.trim().toLowerCase();
|
||||||
|
if (existingKeys.contains(dupKey)) continue;
|
||||||
|
int id = db.createFaqEntry(question, answer,
|
||||||
|
category != null ? category : UNCATEGORIZED_KEY);
|
||||||
|
if (id != -1) { importedEntries++; existingKeys.add(dupKey); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().info("[FaqManager] Migration: " + importedCats
|
||||||
|
+ " Kategorie(n), " + importedEntries + " Eintrag/Einträge importiert.");
|
||||||
|
|
||||||
|
load();
|
||||||
|
return new int[]{importedCats, importedEntries};
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("[FaqManager] Fehler bei Migration: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exportiert alle FAQ-Daten aus MySQL zurück in die faqs.yml.
|
||||||
|
* Nützlich beim Wechsel von MySQL zurück auf Datei-Modus.
|
||||||
|
* Überschreibt eine bestehende faqs.yml nach Sicherheits-Backup.
|
||||||
|
*
|
||||||
|
* @return int[2] { exportierte Kategorien, exportierte Einträge } oder null bei Fehler
|
||||||
|
*/
|
||||||
|
public int[] migrateFaqToFile() {
|
||||||
|
if (!useMysql) {
|
||||||
|
plugin.getLogger().warning("[FaqManager] Rückmigration nur im MySQL-Modus möglich.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DatabaseManager db = plugin.getDatabaseManager();
|
||||||
|
|
||||||
|
// Backup der bestehenden faqs.yml
|
||||||
|
if (faqFile.exists()) {
|
||||||
|
String ts = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss")
|
||||||
|
.format(new java.util.Date());
|
||||||
|
java.io.File backup = new java.io.File(faqFile.getParent(), "faqs_backup_" + ts + ".yml");
|
||||||
|
java.nio.file.Files.copy(faqFile.toPath(), backup.toPath(),
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
plugin.getLogger().info("[FaqManager] Backup erstellt: " + backup.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
YamlConfiguration out = new YamlConfiguration();
|
||||||
|
|
||||||
|
// Kategorien schreiben
|
||||||
|
int exportedCats = 0;
|
||||||
|
for (FaqCategory cat : db.getFaqCategories()) {
|
||||||
|
out.set("categories." + cat.getKey() + ".name", cat.getName());
|
||||||
|
out.set("categories." + cat.getKey() + ".color", cat.getColor());
|
||||||
|
out.set("categories." + cat.getKey() + ".description", cat.getDescription());
|
||||||
|
exportedCats++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einträge schreiben
|
||||||
|
int exportedEntries = 0;
|
||||||
|
for (FaqEntry e : db.getFaqEntries()) {
|
||||||
|
String base = "faqs." + e.getId();
|
||||||
|
out.set(base + ".question", e.getQuestion());
|
||||||
|
out.set(base + ".answer", e.getAnswer());
|
||||||
|
if (e.hasCategory()) out.set(base + ".category", e.getCategoryKey());
|
||||||
|
exportedEntries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.save(faqFile);
|
||||||
|
plugin.getLogger().info("[FaqManager] Rückmigration: " + exportedCats
|
||||||
|
+ " Kategorie(n), " + exportedEntries + " Eintrag/Einträge in faqs.yml gespeichert.");
|
||||||
|
return new int[]{exportedCats, exportedEntries};
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("[FaqManager] Fehler bei Rückmigration: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// HILFSMETHODEN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
private String normalizeCategoryKey(String key) {
|
private String normalizeCategoryKey(String key) {
|
||||||
if (key == null || key.isBlank()) return UNCATEGORIZED_KEY;
|
if (key == null || key.isBlank()) return UNCATEGORIZED_KEY;
|
||||||
|
|||||||
@@ -36,6 +36,21 @@ public class SessionManager {
|
|||||||
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
|
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
|
||||||
private final SecureRandom random = new SecureRandom();
|
private final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
// ── Rate-Limiting: fehlgeschlagene Login-Versuche pro IP ──────────────
|
||||||
|
/** Anzahl fehlgeschlagener Versuche pro IP */
|
||||||
|
private final Map<String, Integer> failedAttempts = new ConcurrentHashMap<>();
|
||||||
|
/** Zeitpunkt der letzten fehlgeschlagenen Anfrage pro IP */
|
||||||
|
private final Map<String, Long> lastFailTime = new ConcurrentHashMap<>();
|
||||||
|
/** Zeitpunkt bis zu dem eine IP gesperrt ist (Epoch-ms) */
|
||||||
|
private final Map<String, Long> blockedUntil = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** Maximale Fehlversuche vor Sperre */
|
||||||
|
private static final int MAX_ATTEMPTS = 5;
|
||||||
|
/** Zeitfenster für Fehlversuche in ms (10 Minuten) */
|
||||||
|
private static final long ATTEMPT_WINDOW = 10 * 60 * 1000L;
|
||||||
|
/** Sperrdauer nach zu vielen Fehlversuchen in ms (15 Minuten) */
|
||||||
|
private static final long BLOCK_DURATION = 15 * 60 * 1000L;
|
||||||
|
|
||||||
public SessionManager(TicketPlugin plugin) {
|
public SessionManager(TicketPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
migratePlaintextPasswords();
|
migratePlaintextPasswords();
|
||||||
@@ -45,22 +60,56 @@ public class SessionManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft Benutzername + Passwort und erstellt bei Erfolg eine Session.
|
* Prüft Benutzername + Passwort und erstellt bei Erfolg eine Session.
|
||||||
|
* Blockiert IPs nach zu vielen Fehlversuchen.
|
||||||
*
|
*
|
||||||
* @return Session-Token oder null bei falschem Login
|
* @param username Benutzername
|
||||||
|
* @param password Passwort (Klartext)
|
||||||
|
* @param clientIp IP-Adresse des Clients (für Rate-Limiting)
|
||||||
|
* @return Session-Token oder null bei falschem Login / gesperrter IP
|
||||||
*/
|
*/
|
||||||
public String login(String username, String password) {
|
public String login(String username, String password, String clientIp) {
|
||||||
if (username == null || password == null) return null;
|
if (username == null || password == null) return null;
|
||||||
|
|
||||||
|
// ── IP-Sperre prüfen ──────────────────────────────────────────────
|
||||||
|
if (clientIp != null) {
|
||||||
|
Long blocked = blockedUntil.get(clientIp);
|
||||||
|
if (blocked != null && System.currentTimeMillis() < blocked) {
|
||||||
|
if (plugin.isDebug())
|
||||||
|
plugin.getLogger().info("[WebPanel] Login-Versuch von gesperrter IP: " + clientIp);
|
||||||
|
return null; // IP gesperrt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationSection users = plugin.getConfig().getConfigurationSection("web-panel.users");
|
ConfigurationSection users = plugin.getConfig().getConfigurationSection("web-panel.users");
|
||||||
if (users == null) return null;
|
if (users == null) return null;
|
||||||
|
|
||||||
ConfigurationSection user = users.getConfigurationSection(username.toLowerCase());
|
// Case-insensitiver Lookup
|
||||||
if (user == null) return null;
|
String matchedKey = null;
|
||||||
|
for (String key : users.getKeys(false)) {
|
||||||
|
if (key.equalsIgnoreCase(username)) { matchedKey = key; break; }
|
||||||
|
}
|
||||||
|
if (matchedKey == null) {
|
||||||
|
recordFailedAttempt(clientIp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationSection user = users.getConfigurationSection(matchedKey);
|
||||||
|
if (user == null) { recordFailedAttempt(clientIp); return null; }
|
||||||
|
|
||||||
String storedHash = user.getString("password-hash", "");
|
String storedHash = user.getString("password-hash", "");
|
||||||
String inputHash = sha256(password);
|
String inputHash = sha256(password);
|
||||||
|
|
||||||
if (!storedHash.equalsIgnoreCase(inputHash)) return null;
|
if (!storedHash.equalsIgnoreCase(inputHash)) {
|
||||||
|
recordFailedAttempt(clientIp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login erfolgreich → Fehlversuche zurücksetzen
|
||||||
|
if (clientIp != null) {
|
||||||
|
failedAttempts.remove(clientIp);
|
||||||
|
lastFailTime.remove(clientIp);
|
||||||
|
blockedUntil.remove(clientIp);
|
||||||
|
}
|
||||||
|
|
||||||
String roleStr = user.getString("role", "supporter").toUpperCase();
|
String roleStr = user.getString("role", "supporter").toUpperCase();
|
||||||
WebSession.Role role;
|
WebSession.Role role;
|
||||||
@@ -72,16 +121,81 @@ public class SessionManager {
|
|||||||
|
|
||||||
long timeoutMs = plugin.getConfig().getLong("web-panel.session-timeout-minutes", 60) * 60_000L;
|
long timeoutMs = plugin.getConfig().getLong("web-panel.session-timeout-minutes", 60) * 60_000L;
|
||||||
String token = generateToken();
|
String token = generateToken();
|
||||||
WebSession session = new WebSession(token, username.toLowerCase(), role, timeoutMs);
|
WebSession session = new WebSession(token, matchedKey, role, timeoutMs);
|
||||||
sessions.put(token, session);
|
sessions.put(token, session);
|
||||||
|
|
||||||
if (plugin.isDebug()) {
|
if (plugin.isDebug())
|
||||||
plugin.getLogger().info("[WebPanel] Login: " + username + " (" + role + ")");
|
plugin.getLogger().info("[WebPanel] Login: " + matchedKey + " (" + role + ")"
|
||||||
}
|
+ (clientIp != null ? " von " + clientIp : ""));
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rückwärtskompatible login()-Variante ohne IP (kein Rate-Limiting).
|
||||||
|
*/
|
||||||
|
public String login(String username, String password) {
|
||||||
|
return login(username, password, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rate-Limiting Hilfsmethoden ───────────────────────────────────────
|
||||||
|
|
||||||
|
private void recordFailedAttempt(String ip) {
|
||||||
|
if (ip == null) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
// Zeitfenster zurücksetzen wenn letzte Anfrage zu lange her
|
||||||
|
Long last = lastFailTime.get(ip);
|
||||||
|
if (last != null && now - last > ATTEMPT_WINDOW) {
|
||||||
|
failedAttempts.remove(ip);
|
||||||
|
}
|
||||||
|
lastFailTime.put(ip, now);
|
||||||
|
int attempts = failedAttempts.merge(ip, 1, Integer::sum);
|
||||||
|
|
||||||
|
if (attempts >= MAX_ATTEMPTS) {
|
||||||
|
blockedUntil.put(ip, now + BLOCK_DURATION);
|
||||||
|
failedAttempts.remove(ip);
|
||||||
|
plugin.getLogger().warning("[WebPanel] IP " + ip + " nach " + MAX_ATTEMPTS
|
||||||
|
+ " Fehlversuchen für " + (BLOCK_DURATION / 60000) + " Minuten gesperrt.");
|
||||||
|
} else if (plugin.isDebug()) {
|
||||||
|
plugin.getLogger().info("[WebPanel] Fehlversuch " + attempts + "/" + MAX_ATTEMPTS
|
||||||
|
+ " von IP: " + ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt zurück ob eine IP aktuell gesperrt ist.
|
||||||
|
*/
|
||||||
|
public boolean isBlocked(String ip) {
|
||||||
|
if (ip == null) return false;
|
||||||
|
Long blocked = blockedUntil.get(ip);
|
||||||
|
if (blocked == null) return false;
|
||||||
|
if (System.currentTimeMillis() >= blocked) {
|
||||||
|
blockedUntil.remove(ip);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die verbleibende Sperrdauer in Sekunden zurück (0 wenn nicht gesperrt).
|
||||||
|
*/
|
||||||
|
public long getBlockedSeconds(String ip) {
|
||||||
|
if (ip == null) return 0;
|
||||||
|
Long blocked = blockedUntil.get(ip);
|
||||||
|
if (blocked == null) return 0;
|
||||||
|
long rem = (blocked - System.currentTimeMillis()) / 1000;
|
||||||
|
return Math.max(0, rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entsperrt eine IP manuell.
|
||||||
|
*/
|
||||||
|
public void unblock(String ip) {
|
||||||
|
blockedUntil.remove(ip);
|
||||||
|
failedAttempts.remove(ip);
|
||||||
|
lastFailTime.remove(ip);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt die Session für ein Token zurück, oder null wenn ungültig/abgelaufen.
|
* Gibt die Session für ein Token zurück, oder null wenn ungültig/abgelaufen.
|
||||||
* Erneuert den lastAccess-Zeitstempel bei gültiger Session.
|
* Erneuert den lastAccess-Zeitstempel bei gültiger Session.
|
||||||
@@ -106,13 +220,27 @@ public class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt alle abgelaufenen Sessions (periodisch aufrufen).
|
* Entfernt alle abgelaufenen Sessions und abgelaufenen IP-Sperren (periodisch aufrufen).
|
||||||
*/
|
*/
|
||||||
public void evictExpired() {
|
public void evictExpired() {
|
||||||
Iterator<Map.Entry<String, WebSession>> it = sessions.entrySet().iterator();
|
Iterator<Map.Entry<String, WebSession>> it = sessions.entrySet().iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
if (it.next().getValue().isExpired()) it.remove();
|
if (it.next().getValue().isExpired()) it.remove();
|
||||||
}
|
}
|
||||||
|
// Abgelaufene IP-Sperren bereinigen
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
blockedUntil.entrySet().removeIf(e -> e.getValue() <= now);
|
||||||
|
// Fehlversuche außerhalb des Zeitfensters bereinigen
|
||||||
|
lastFailTime.entrySet().removeIf(e -> now - e.getValue() > ATTEMPT_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meldet alle aktiven Sessions eines Benutzers ab.
|
||||||
|
* Wird aufgerufen wenn ein Passwort geändert oder Benutzer gelöscht wird.
|
||||||
|
*/
|
||||||
|
public void invalidateUser(String username) {
|
||||||
|
sessions.entrySet().removeIf(e ->
|
||||||
|
e.getValue().getUsername().equalsIgnoreCase(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int activeSessionCount() { return sessions.size(); }
|
public int activeSessionCount() { return sessions.size(); }
|
||||||
|
|||||||
@@ -1,21 +1,43 @@
|
|||||||
package de.ticketsystem.web;
|
package de.ticketsystem.web;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import com.sun.net.httpserver.HttpsServer;
|
||||||
|
import com.sun.net.httpserver.HttpsConfigurator;
|
||||||
|
import com.sun.net.httpserver.HttpsParameters;
|
||||||
import de.ticketsystem.TicketPlugin;
|
import de.ticketsystem.TicketPlugin;
|
||||||
import de.ticketsystem.web.handlers.*;
|
import de.ticketsystem.web.handlers.*;
|
||||||
|
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.security.KeyStore;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startet und verwaltet den eingebetteten HTTP-Server für das Web-Panel.
|
* Startet und verwaltet den eingebetteten HTTP/HTTPS-Server für das Web-Panel.
|
||||||
*
|
*
|
||||||
* Konfiguration in config.yml:
|
* Konfiguration in config.yml:
|
||||||
* web-panel:
|
* web-panel:
|
||||||
* enabled: true
|
* enabled: true
|
||||||
* port: 8085
|
* port: 8085
|
||||||
* bind-address: "0.0.0.0" # optional, Standard: alle Interfaces
|
* bind-address: "0.0.0.0"
|
||||||
|
* # HTTPS (optional):
|
||||||
|
* ssl:
|
||||||
|
* enabled: false
|
||||||
|
* keystore-file: "keystore.jks" # im plugins/TicketSystem/-Ordner ablegen
|
||||||
|
* keystore-password: "changeit"
|
||||||
|
* key-password: "changeit" # oft identisch mit keystore-password
|
||||||
|
*
|
||||||
|
* HTTPS-Setup (Self-Signed für Test):
|
||||||
|
* keytool -genkeypair -alias ticketsystem -keyalg RSA -keysize 2048
|
||||||
|
* -validity 365 -keystore keystore.jks -storepass changeit
|
||||||
|
*
|
||||||
|
* Für Produktion: Let's Encrypt Zertifikat per certbot holen, dann:
|
||||||
|
* openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out cert.p12
|
||||||
|
* keytool -importkeystore -srckeystore cert.p12 -srcstoretype PKCS12
|
||||||
|
* -destkeystore keystore.jks -deststoretype JKS
|
||||||
*/
|
*/
|
||||||
public class WebServer {
|
public class WebServer {
|
||||||
|
|
||||||
@@ -31,12 +53,22 @@ public class WebServer {
|
|||||||
public void start() {
|
public void start() {
|
||||||
int port = plugin.getConfig().getInt("web-panel.port", 8085);
|
int port = plugin.getConfig().getInt("web-panel.port", 8085);
|
||||||
String bindStr = plugin.getConfig().getString("web-panel.bind-address", "0.0.0.0");
|
String bindStr = plugin.getConfig().getString("web-panel.bind-address", "0.0.0.0");
|
||||||
|
boolean sslEnabled = plugin.getConfig().getBoolean("web-panel.ssl.enabled", false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InetSocketAddress addr = new InetSocketAddress(bindStr, port);
|
InetSocketAddress addr = new InetSocketAddress(bindStr, port);
|
||||||
server = HttpServer.create(addr, 0);
|
|
||||||
|
|
||||||
// Thread-Pool: max 4 Threads für Web-Requests
|
if (sslEnabled) {
|
||||||
|
server = buildHttpsServer(addr);
|
||||||
|
if (server == null) {
|
||||||
|
plugin.getLogger().warning("[WebPanel] HTTPS-Initialisierung fehlgeschlagen – starte ohne TLS.");
|
||||||
|
server = HttpServer.create(addr, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
server = HttpServer.create(addr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread-Pool
|
||||||
server.setExecutor(Executors.newFixedThreadPool(4));
|
server.setExecutor(Executors.newFixedThreadPool(4));
|
||||||
|
|
||||||
// ── Routes registrieren ──────────────────────────────────────
|
// ── Routes registrieren ──────────────────────────────────────
|
||||||
@@ -59,21 +91,82 @@ public class WebServer {
|
|||||||
|
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
plugin.getLogger().info("[WebPanel] HTTP-Server gestartet auf " + bindStr + ":" + port);
|
String protocol = sslEnabled && server instanceof HttpsServer ? "HTTPS" : "HTTP";
|
||||||
|
plugin.getLogger().info("[WebPanel] " + protocol + "-Server gestartet auf "
|
||||||
|
+ bindStr + ":" + port);
|
||||||
|
if (!sslEnabled) {
|
||||||
|
plugin.getLogger().info("[WebPanel] Tipp: Für HTTPS 'web-panel.ssl.enabled: true' setzen und keystore.jks bereitstellen.");
|
||||||
|
}
|
||||||
|
|
||||||
// Session-Cleanup alle 10 Minuten
|
// Session-Cleanup alle 10 Minuten
|
||||||
plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin,
|
plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin,
|
||||||
() -> sessionManager.evictExpired(), 12000L, 12000L);
|
() -> sessionManager.evictExpired(), 12000L, 12000L);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
plugin.getLogger().severe("[WebPanel] Konnte HTTP-Server nicht starten: " + e.getMessage());
|
plugin.getLogger().severe("[WebPanel] Konnte Server nicht starten: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen HttpsServer mit dem konfigurierten JKS-Keystore.
|
||||||
|
* Gibt null zurück wenn Konfiguration fehlt oder Keystore nicht geladen werden kann.
|
||||||
|
*/
|
||||||
|
private HttpsServer buildHttpsServer(InetSocketAddress addr) {
|
||||||
|
String keystoreFile = plugin.getConfig().getString("web-panel.ssl.keystore-file", "keystore.jks");
|
||||||
|
String keystorePass = plugin.getConfig().getString("web-panel.ssl.keystore-password","changeit");
|
||||||
|
String keyPass = plugin.getConfig().getString("web-panel.ssl.key-password", keystorePass);
|
||||||
|
|
||||||
|
File ksFile = new File(plugin.getDataFolder(), keystoreFile);
|
||||||
|
if (!ksFile.exists()) {
|
||||||
|
plugin.getLogger().severe("[WebPanel/SSL] Keystore nicht gefunden: " + ksFile.getAbsolutePath());
|
||||||
|
plugin.getLogger().info("[WebPanel/SSL] Self-Signed erstellen mit:");
|
||||||
|
plugin.getLogger().info(" keytool -genkeypair -alias ticketsystem -keyalg RSA -keysize 2048 -validity 365 -keystore " + ksFile.getAbsolutePath() + " -storepass changeit");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeyStore ks = KeyStore.getInstance("JKS");
|
||||||
|
try (FileInputStream fis = new FileInputStream(ksFile)) {
|
||||||
|
ks.load(fis, keystorePass.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||||
|
kmf.init(ks, keyPass.toCharArray());
|
||||||
|
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init(ks);
|
||||||
|
|
||||||
|
SSLContext sslCtx = SSLContext.getInstance("TLS");
|
||||||
|
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
||||||
|
|
||||||
|
HttpsServer httpsServer = HttpsServer.create(addr, 0);
|
||||||
|
httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslCtx) {
|
||||||
|
@Override
|
||||||
|
public void configure(HttpsParameters params) {
|
||||||
|
SSLContext ctx = getSSLContext();
|
||||||
|
SSLParameters sslParams = ctx.getDefaultSSLParameters();
|
||||||
|
// Nur TLS 1.2+ zulassen
|
||||||
|
sslParams.setProtocols(new String[]{"TLSv1.2", "TLSv1.3"});
|
||||||
|
params.setSSLParameters(sslParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.getLogger().info("[WebPanel/SSL] TLS-Konfiguration geladen: " + ksFile.getName());
|
||||||
|
return httpsServer;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("[WebPanel/SSL] Fehler beim Laden des Keystores: " + e.getMessage());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
server.stop(1);
|
server.stop(1);
|
||||||
plugin.getLogger().info("[WebPanel] HTTP-Server gestoppt.");
|
plugin.getLogger().info("[WebPanel] Server gestoppt.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() { return server != null; }
|
||||||
|
public boolean isHttps() { return server instanceof HttpsServer; }
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@ public class WebSession {
|
|||||||
|
|
||||||
public enum Role {
|
public enum Role {
|
||||||
ADMIN, // Voller Zugriff: Tickets, FAQ, Stats, Blacklist, Reload
|
ADMIN, // Voller Zugriff: Tickets, FAQ, Stats, Blacklist, Reload
|
||||||
SUPPORTER // Tickets anzeigen, claimen, schließen, kommentieren
|
SUPPORTER, // Tickets anzeigen, claimen, schließen, kommentieren
|
||||||
|
ARCHIVE_VIEWER // Wie SUPPORTER + Zugriff auf das Archiv
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String token;
|
private final String token;
|
||||||
@@ -36,4 +37,10 @@ public class WebSession {
|
|||||||
public String getUsername() { return username; }
|
public String getUsername() { return username; }
|
||||||
public Role getRole() { return role; }
|
public Role getRole() { return role; }
|
||||||
public boolean isAdmin() { return role == Role.ADMIN; }
|
public boolean isAdmin() { return role == Role.ADMIN; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt true zurück wenn der Benutzer das Archiv einsehen darf.
|
||||||
|
* Admins und ARCHIVE_VIEWER haben Zugriff.
|
||||||
|
*/
|
||||||
|
public boolean canViewArchive() { return role == Role.ADMIN || role == Role.ARCHIVE_VIEWER; }
|
||||||
}
|
}
|
||||||
@@ -60,12 +60,36 @@ public class ApiHandler extends BaseHandler implements HttpHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Archiv-Aktionen ──────────────────────────────────────────
|
||||||
|
if (path.startsWith("/api/archive/")) {
|
||||||
|
handleArchiveApi(ex, session, path, method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── FAQ-Aktionen ─────────────────────────────────────────────
|
// ── FAQ-Aktionen ─────────────────────────────────────────────
|
||||||
if (path.startsWith("/api/faq")) {
|
if (path.startsWith("/api/faq")) {
|
||||||
handleFaqApi(ex, session, path, method);
|
handleFaqApi(ex, session, path, method);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Benutzer-Verwaltung ──────────────────────────────────────
|
||||||
|
if (path.startsWith("/api/users")) {
|
||||||
|
handleUsersApi(ex, session, path, method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Backup ───────────────────────────────────────────────────
|
||||||
|
if (path.equals("/api/backup") && method.equals("POST")) {
|
||||||
|
if (!session.isAdmin()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||||
|
java.io.File backup = plugin.getDatabaseManager().createBackup();
|
||||||
|
if (backup != null) {
|
||||||
|
sendJson(ex, 200, "{\"ok\":true,\"file\":\"" + jsonEsc(backup.getName()) + "\"}");
|
||||||
|
} else {
|
||||||
|
sendJson(ex, 500, err("Backup fehlgeschlagen"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sendJson(ex, 404, "{\"ok\":false,\"error\":\"Unbekannter Endpunkt\"}");
|
sendJson(ex, 404, "{\"ok\":false,\"error\":\"Unbekannter Endpunkt\"}");
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -287,6 +311,156 @@ public class ApiHandler extends BaseHandler implements HttpHandler {
|
|||||||
sendJson(ex, 404, err("Unbekannter FAQ-Endpunkt"));
|
sendJson(ex, 404, err("Unbekannter FAQ-Endpunkt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Archiv-API ────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/archive/{id}/delete – Ticket permanent löschen (admin only)
|
||||||
|
* POST /api/archive/{id}/restore – Ticket wiederherstellen (admin only)
|
||||||
|
*/
|
||||||
|
private void handleArchiveApi(HttpExchange ex, WebSession session, String path, String method) throws IOException {
|
||||||
|
if (!session.canViewArchive()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||||
|
if (!method.equals("POST")) { sendJson(ex, 405, err("Method not allowed")); return; }
|
||||||
|
|
||||||
|
// /api/archive/{id}/{action}
|
||||||
|
String[] parts = path.split("/");
|
||||||
|
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 ID")); return; }
|
||||||
|
|
||||||
|
String action = parts[4];
|
||||||
|
var db = plugin.getDatabaseManager();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "delete" -> {
|
||||||
|
if (!session.isAdmin()) { sendJson(ex, 403, err("Nur Admins dürfen löschen")); return; }
|
||||||
|
boolean ok = db.deleteArchivedTicket(ticketId);
|
||||||
|
sendJson(ex, ok ? 200 : 404, ok ? "{\"ok\":true}" : err("Ticket nicht gefunden"));
|
||||||
|
}
|
||||||
|
case "restore" -> {
|
||||||
|
if (!session.isAdmin()) { sendJson(ex, 403, err("Nur Admins dürfen wiederherstellen")); return; }
|
||||||
|
boolean ok = db.restoreArchivedTicket(ticketId);
|
||||||
|
sendJson(ex, ok ? 200 : 500, ok ? "{\"ok\":true}" : err("Wiederherstellen fehlgeschlagen"));
|
||||||
|
}
|
||||||
|
default -> sendJson(ex, 404, err("Unbekannte Archiv-Aktion: " + action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────── Benutzer-API ──────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users – Alle Benutzer auflisten (admin only)
|
||||||
|
* POST /api/users – Neuen Benutzer anlegen (admin only)
|
||||||
|
* body: {username, password, role}
|
||||||
|
* POST /api/users/{name}/password – Passwort ändern (admin only)
|
||||||
|
* body: {password}
|
||||||
|
* DELETE /api/users/{name} – Benutzer löschen (admin only)
|
||||||
|
*/
|
||||||
|
private void handleUsersApi(HttpExchange ex, WebSession session, String path, String method) throws IOException {
|
||||||
|
if (!session.isAdmin()) { sendJson(ex, 403, err("Kein Zugriff")); return; }
|
||||||
|
|
||||||
|
// GET /api/users – Liste
|
||||||
|
if (path.equals("/api/users") && method.equals("GET")) {
|
||||||
|
org.bukkit.configuration.ConfigurationSection users =
|
||||||
|
plugin.getConfig().getConfigurationSection("web-panel.users");
|
||||||
|
StringBuilder json = new StringBuilder("[");
|
||||||
|
if (users != null) {
|
||||||
|
boolean first = true;
|
||||||
|
for (String key : users.getKeys(false)) {
|
||||||
|
if (!first) json.append(",");
|
||||||
|
first = false;
|
||||||
|
String role = plugin.getConfig().getString("web-panel.users." + key + ".role", "supporter");
|
||||||
|
json.append("{\"username\":\"").append(jsonEsc(key))
|
||||||
|
.append("\",\"role\":\"").append(jsonEsc(role)).append("\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.append("]");
|
||||||
|
sendJson(ex, 200, json.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/users – Anlegen
|
||||||
|
if (path.equals("/api/users") && method.equals("POST")) {
|
||||||
|
Map<String, String> body = parseJsonBody(ex);
|
||||||
|
String username = body.getOrDefault("username", "").trim();
|
||||||
|
String password = body.getOrDefault("password", "").trim();
|
||||||
|
String role = body.getOrDefault("role", "supporter").trim().toLowerCase();
|
||||||
|
|
||||||
|
if (username.isEmpty() || password.isEmpty()) {
|
||||||
|
sendJson(ex, 400, err("Benutzername und Passwort erforderlich")); return;
|
||||||
|
}
|
||||||
|
if (!role.equals("admin") && !role.equals("supporter") && !role.equals("archive_viewer")) {
|
||||||
|
sendJson(ex, 400, err("Ungültige Rolle (admin/supporter/archive_viewer)")); return;
|
||||||
|
}
|
||||||
|
// Prüfen ob Benutzer schon existiert
|
||||||
|
org.bukkit.configuration.ConfigurationSection existing =
|
||||||
|
plugin.getConfig().getConfigurationSection("web-panel.users");
|
||||||
|
if (existing != null) {
|
||||||
|
for (String k : existing.getKeys(false)) {
|
||||||
|
if (k.equalsIgnoreCase(username)) {
|
||||||
|
sendJson(ex, 409, err("Benutzer existiert bereits")); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String hash = de.ticketsystem.web.SessionManager.sha256(password);
|
||||||
|
plugin.getConfig().set("web-panel.users." + username + ".password-hash", hash);
|
||||||
|
plugin.getConfig().set("web-panel.users." + username + ".role", role);
|
||||||
|
plugin.saveConfig();
|
||||||
|
sendJson(ex, 200, "{\"ok\":true}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/users/{name}/password – Passwort ändern
|
||||||
|
if (path.matches("/api/users/[^/]+/password") && method.equals("POST")) {
|
||||||
|
String targetUser = path.split("/")[3];
|
||||||
|
Map<String, String> body = parseJsonBody(ex);
|
||||||
|
String newPass = body.getOrDefault("password", "").trim();
|
||||||
|
if (newPass.isEmpty()) { sendJson(ex, 400, err("Passwort darf nicht leer sein")); return; }
|
||||||
|
|
||||||
|
String cfgPath = findUserConfigPath(targetUser);
|
||||||
|
if (cfgPath == null) { sendJson(ex, 404, err("Benutzer nicht gefunden")); return; }
|
||||||
|
|
||||||
|
String hash = de.ticketsystem.web.SessionManager.sha256(newPass);
|
||||||
|
plugin.getConfig().set(cfgPath + ".password-hash", hash);
|
||||||
|
plugin.getConfig().set(cfgPath + ".password", null);
|
||||||
|
plugin.saveConfig();
|
||||||
|
// Alle Sessions dieses Benutzers invalidieren
|
||||||
|
sessionManager.invalidateUser(targetUser);
|
||||||
|
sendJson(ex, 200, "{\"ok\":true}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/users/{name} – Löschen
|
||||||
|
if (path.matches("/api/users/[^/]+") && method.equals("DELETE")) {
|
||||||
|
String targetUser = path.split("/")[3];
|
||||||
|
// Darf sich nicht selbst löschen
|
||||||
|
if (targetUser.equalsIgnoreCase(session.getUsername())) {
|
||||||
|
sendJson(ex, 400, err("Du kannst dich nicht selbst löschen")); return;
|
||||||
|
}
|
||||||
|
String cfgPath = findUserConfigPath(targetUser);
|
||||||
|
if (cfgPath == null) { sendJson(ex, 404, err("Benutzer nicht gefunden")); return; }
|
||||||
|
plugin.getConfig().set(cfgPath, null);
|
||||||
|
plugin.saveConfig();
|
||||||
|
sessionManager.invalidateUser(targetUser);
|
||||||
|
sendJson(ex, 200, "{\"ok\":true}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendJson(ex, 404, err("Unbekannter Benutzer-Endpunkt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sucht den config.yml-Pfad eines Benutzers (case-insensitiv). */
|
||||||
|
private String findUserConfigPath(String username) {
|
||||||
|
org.bukkit.configuration.ConfigurationSection users =
|
||||||
|
plugin.getConfig().getConfigurationSection("web-panel.users");
|
||||||
|
if (users == null) return null;
|
||||||
|
for (String key : users.getKeys(false)) {
|
||||||
|
if (key.equalsIgnoreCase(username)) return "web-panel.users." + key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -322,6 +322,15 @@ public abstract class BaseHandler {
|
|||||||
""".formatted(title, title, message, wl(plugin, "btn-back"));
|
""".formatted(title, title, message, wl(plugin, "btn-back"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getClientIp(HttpExchange ex) {
|
||||||
|
// X-Forwarded-For wenn hinter Reverse-Proxy
|
||||||
|
String forwarded = ex.getRequestHeaders().getFirst("X-Forwarded-For");
|
||||||
|
if (forwarded != null && !forwarded.isBlank()) {
|
||||||
|
return forwarded.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
return ex.getRemoteAddress().getAddress().getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
protected String escHtml(String s) {
|
protected String escHtml(String s) {
|
||||||
if (s == null) return "";
|
if (s == null) return "";
|
||||||
return s.replace("&", "&")
|
return s.replace("&", "&")
|
||||||
|
|||||||
@@ -60,11 +60,26 @@ public class TicketsHandler extends BaseHandler implements HttpHandler {
|
|||||||
int page = parseInt(params.getOrDefault("page", "1"), 1);
|
int page = parseInt(params.getOrDefault("page", "1"), 1);
|
||||||
|
|
||||||
DatabaseManager db = plugin.getDatabaseManager();
|
DatabaseManager db = plugin.getDatabaseManager();
|
||||||
List<Ticket> all = db.getAllTickets();
|
List<Ticket> all;
|
||||||
|
|
||||||
|
// Archiv-Ansicht: nur für berechtigte Benutzer
|
||||||
|
if ("ARCHIVED".equalsIgnoreCase(filterStatus)) {
|
||||||
|
if (!session.canViewArchive()) {
|
||||||
|
sendHtml(ex, 403, errorPage(
|
||||||
|
wl(plugin, "error-403-title"),
|
||||||
|
wl(plugin, "error-403-message"),
|
||||||
|
plugin));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
all = db.getArchivedTickets();
|
||||||
|
} else {
|
||||||
|
all = db.getAllTickets();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Filter ──
|
// ── Filter ──
|
||||||
List<Ticket> filtered = all.stream()
|
List<Ticket> filtered = all.stream()
|
||||||
.filter(t -> filterStatus.equals("all") || t.getStatus().name().equalsIgnoreCase(filterStatus))
|
.filter(t -> filterStatus.equals("all") || filterStatus.equalsIgnoreCase("ARCHIVED")
|
||||||
|
|| t.getStatus().name().equalsIgnoreCase(filterStatus))
|
||||||
.filter(t -> filterCat.equals("all") || t.getCategoryKey().equalsIgnoreCase(filterCat))
|
.filter(t -> filterCat.equals("all") || t.getCategoryKey().equalsIgnoreCase(filterCat))
|
||||||
.filter(t -> filterPrio.equals("all") || t.getPriority().name().equalsIgnoreCase(filterPrio))
|
.filter(t -> filterPrio.equals("all") || t.getPriority().name().equalsIgnoreCase(filterPrio))
|
||||||
.filter(t -> filterSearch.isEmpty()
|
.filter(t -> filterSearch.isEmpty()
|
||||||
@@ -83,27 +98,38 @@ public class TicketsHandler extends BaseHandler implements HttpHandler {
|
|||||||
List<Ticket> pageTickets = filtered.subList(fromIdx, toIdx);
|
List<Ticket> pageTickets = filtered.subList(fromIdx, toIdx);
|
||||||
|
|
||||||
String content = buildList(pageTickets, total, page, totalPages,
|
String content = buildList(pageTickets, total, page, totalPages,
|
||||||
filterStatus, filterCat, filterPrio, filterSearch);
|
filterStatus, filterCat, filterPrio, filterSearch, session);
|
||||||
sendHtml(ex, 200, layout(wl(plugin, "tickets-title"), content, session, plugin));
|
sendHtml(ex, 200, layout(wl(plugin, "tickets-title"), content, session, plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildList(List<Ticket> tickets, int total, int page, int totalPages,
|
private String buildList(List<Ticket> tickets, int total, int page, int totalPages,
|
||||||
String filterStatus, String filterCat, String filterPrio, String filterSearch) {
|
String filterStatus, String filterCat, String filterPrio, String filterSearch,
|
||||||
|
WebSession session) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("<h1 class='page-title'>")
|
sb.append("<div style='display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem'>");
|
||||||
|
sb.append("<h1 class='page-title' style='margin:0'>")
|
||||||
.append(escHtml(wl(plugin, "tickets-title")))
|
.append(escHtml(wl(plugin, "tickets-title")))
|
||||||
.append(" <span>").append(total).append(" ")
|
.append(" <span>").append(total).append(" ")
|
||||||
.append(escHtml(wl(plugin, "tickets-total")))
|
.append(escHtml(wl(plugin, "tickets-total")))
|
||||||
.append("</span></h1>");
|
.append("</span></h1>");
|
||||||
|
if (session.canViewArchive()) {
|
||||||
|
sb.append("<a href='/tickets?status=ARCHIVED' class='btn btn-sm btn-secondary'>")
|
||||||
|
.append("🗃 ").append(escHtml(wl(plugin, "nav-archive"))).append("</a>");
|
||||||
|
}
|
||||||
|
sb.append("</div>");
|
||||||
|
|
||||||
// ── Filter-Bar ──
|
// ── Filter-Bar ──
|
||||||
sb.append("<div class='filter-bar'>");
|
sb.append("<div class='filter-bar'>");
|
||||||
sb.append(selectFilter("status", filterStatus, List.of(
|
List<Map.Entry<String,String>> statusOpts = new ArrayList<>(List.of(
|
||||||
entry("all", wl(plugin, "filter-all-status")),
|
entry("all", wl(plugin, "filter-all-status")),
|
||||||
entry("OPEN", wl(plugin, "filter-open")),
|
entry("OPEN", wl(plugin, "filter-open")),
|
||||||
entry("CLAIMED", wl(plugin, "filter-claimed")),
|
entry("CLAIMED", wl(plugin, "filter-claimed")),
|
||||||
entry("FORWARDED", wl(plugin, "filter-forwarded")),
|
entry("FORWARDED", wl(plugin, "filter-forwarded")),
|
||||||
entry("CLOSED", wl(plugin, "filter-closed")))));
|
entry("CLOSED", wl(plugin, "filter-closed"))));
|
||||||
|
if (session.canViewArchive()) {
|
||||||
|
statusOpts.add(entry("ARCHIVED", wl(plugin, "filter-archived")));
|
||||||
|
}
|
||||||
|
sb.append(selectFilter("status", filterStatus, statusOpts));
|
||||||
|
|
||||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
List<Map.Entry<String,String>> catOpts = new ArrayList<>();
|
List<Map.Entry<String,String>> catOpts = new ArrayList<>();
|
||||||
@@ -194,6 +220,16 @@ public class TicketsHandler extends BaseHandler implements HttpHandler {
|
|||||||
|
|
||||||
private void handleDetail(HttpExchange ex, WebSession session, int ticketId) throws IOException {
|
private void handleDetail(HttpExchange ex, WebSession session, int ticketId) throws IOException {
|
||||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||||
|
boolean fromArchive = false;
|
||||||
|
|
||||||
|
// Nicht in aktiver DB? → im Archiv suchen (nur wenn berechtigt)
|
||||||
|
if (ticket == null && session.canViewArchive()) {
|
||||||
|
ticket = plugin.getDatabaseManager().getArchivedTickets().stream()
|
||||||
|
.filter(t -> t.getId() == ticketId)
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
if (ticket != null) fromArchive = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (ticket == null) {
|
if (ticket == null) {
|
||||||
sendHtml(ex, 404, errorPage(
|
sendHtml(ex, 404, errorPage(
|
||||||
wl(plugin, "detail-not-found"),
|
wl(plugin, "detail-not-found"),
|
||||||
@@ -202,12 +238,18 @@ public class TicketsHandler extends BaseHandler implements HttpHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TicketComment> comments = plugin.getDatabaseManager().getComments(ticketId);
|
List<TicketComment> comments = fromArchive
|
||||||
String content = buildDetail(ticket, comments, session);
|
? List.of()
|
||||||
|
: plugin.getDatabaseManager().getComments(ticketId);
|
||||||
|
String content = buildDetail(ticket, comments, session, fromArchive);
|
||||||
sendHtml(ex, 200, layout("Ticket #" + ticketId, content, session, plugin));
|
sendHtml(ex, 200, layout("Ticket #" + ticketId, content, session, plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildDetail(Ticket t, List<TicketComment> comments, WebSession session) {
|
private String buildDetail(Ticket t, List<TicketComment> comments, WebSession session) {
|
||||||
|
return buildDetail(t, comments, session, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildDetail(Ticket t, List<TicketComment> comments, WebSession session, boolean fromArchive) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
String created = t.getCreatedAt() != null ? SDF.format(t.getCreatedAt()) : "—";
|
String created = t.getCreatedAt() != null ? SDF.format(t.getCreatedAt()) : "—";
|
||||||
String catName = getCategoryName(t);
|
String catName = getCategoryName(t);
|
||||||
@@ -257,7 +299,7 @@ public class TicketsHandler extends BaseHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Aktionen (nur bei aktiven Tickets) ──
|
// ── Aktionen (nur bei aktiven Tickets) ──
|
||||||
if (t.getStatus() != TicketStatus.CLOSED) {
|
if (t.getStatus() != TicketStatus.CLOSED && !fromArchive) {
|
||||||
sb.append("<div class='card'><div class='card-title'>")
|
sb.append("<div class='card'><div class='card-title'>")
|
||||||
.append(escHtml(wl(plugin, "detail-section-actions")))
|
.append(escHtml(wl(plugin, "detail-section-actions")))
|
||||||
.append("</div><div style='display:flex;flex-wrap:wrap;gap:.75rem'>");
|
.append("</div><div style='display:flex;flex-wrap:wrap;gap:.75rem'>");
|
||||||
@@ -302,6 +344,18 @@ public class TicketsHandler extends BaseHandler implements HttpHandler {
|
|||||||
sb.append("</div></div>");
|
sb.append("</div></div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Archiv-Aktionen (nur für archivierte Tickets) ──
|
||||||
|
if (fromArchive && session.isAdmin()) {
|
||||||
|
sb.append("<div class='card'><div class='card-title'>")
|
||||||
|
.append(escHtml(wl(plugin, "detail-section-actions")))
|
||||||
|
.append("</div><div style='display:flex;gap:.75rem'>");
|
||||||
|
sb.append("<button class='btn btn-success' onclick='restoreTicket(").append(t.getId()).append(")'>")
|
||||||
|
.append("♻ ").append(escHtml(wl(plugin, "archive-btn-restore"))).append("</button>");
|
||||||
|
sb.append("<button class='btn btn-danger' onclick='deleteArchivedTicket(").append(t.getId()).append(")'>")
|
||||||
|
.append("🗑 ").append(escHtml(wl(plugin, "archive-btn-delete"))).append("</button>");
|
||||||
|
sb.append("</div></div>");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Kommentare ──
|
// ── Kommentare ──
|
||||||
sb.append("<div class='card'><div class='card-title'>")
|
sb.append("<div class='card'><div class='card-title'>")
|
||||||
.append(escHtml(wl(plugin, "detail-section-comments")))
|
.append(escHtml(wl(plugin, "detail-section-comments")))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
# --- GRUNDLEGEND ---
|
# --- GRUNDLEGEND ---
|
||||||
# Version der Konfigurationsdatei. Nicht ändern!
|
# Version der Konfigurationsdatei. Nicht ändern!
|
||||||
version: "2.4"
|
version: "2.5"
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# SPRACHE / LANGUAGE
|
# SPRACHE / LANGUAGE
|
||||||
@@ -307,6 +307,24 @@ web-panel:
|
|||||||
enabled: false
|
enabled: false
|
||||||
port: 8085
|
port: 8085
|
||||||
|
|
||||||
|
# -- HTTPS / SSL (Optional) -----------------------------------------
|
||||||
|
# Aktiviere HTTPS fuer verschluesselte Verbindungen.
|
||||||
|
# Voraussetzung: Java Keystore (.jks) im Plugin-Ordner.
|
||||||
|
#
|
||||||
|
# Self-Signed Zertifikat erstellen (Test):
|
||||||
|
# keytool -genkeypair -alias ticketsystem -keyalg RSA -keysize 2048
|
||||||
|
# -validity 365 -keystore plugins/TicketSystem/keystore.jks
|
||||||
|
# -storepass changeit
|
||||||
|
#
|
||||||
|
# Produktiv: Lets Encrypt Zertifikat -> PKCS12 -> JKS konvertieren.
|
||||||
|
# Anleitung: WebServer.java Javadoc
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
ssl:
|
||||||
|
enabled: false
|
||||||
|
keystore-file: "keystore.jks" # Datei im plugins/TicketSystem/-Ordner
|
||||||
|
keystore-password: "changeit"
|
||||||
|
key-password: "changeit" # oft identisch mit keystore-password
|
||||||
|
|
||||||
# Bind-Adresse:
|
# Bind-Adresse:
|
||||||
# "0.0.0.0" = alle Interfaces (direkt erreichbar)
|
# "0.0.0.0" = alle Interfaces (direkt erreichbar)
|
||||||
# "127.0.0.1" = nur lokal (Reverse-Proxy empfohlen)
|
# "127.0.0.1" = nur lokal (Reverse-Proxy empfohlen)
|
||||||
@@ -321,8 +339,13 @@ web-panel:
|
|||||||
|
|
||||||
# ── Benutzer ──────────────────────────────────────────────
|
# ── Benutzer ──────────────────────────────────────────────
|
||||||
# Rollen:
|
# Rollen:
|
||||||
# admin → Voller Zugriff: Tickets, FAQ, Weiterleiten
|
# admin → Voller Zugriff: Tickets, FAQ, Weiterleiten, Archiv
|
||||||
# supporter → Tickets anzeigen, claimen, schließen, kommentieren
|
# supporter → Tickets anzeigen, claimen, schließen, kommentieren
|
||||||
|
# archive_viewer → Wie supporter + Zugriff auf das Archiv
|
||||||
|
#
|
||||||
|
# Benutzernamen:
|
||||||
|
# - Groß-/Kleinschreibung wird beim Login ignoriert (case-insensitiv)
|
||||||
|
# - Sonderzeichen erlaubt (z.B. Viper-Admin, support_01, max/muster)
|
||||||
#
|
#
|
||||||
# Passwort setzen:
|
# Passwort setzen:
|
||||||
# Trage "password: deinPasswort" ein.
|
# Trage "password: deinPasswort" ein.
|
||||||
@@ -336,3 +359,7 @@ web-panel:
|
|||||||
supporter:
|
supporter:
|
||||||
password: "aendere_mich"
|
password: "aendere_mich"
|
||||||
role: "supporter"
|
role: "supporter"
|
||||||
|
# Beispiel: Supporter mit Archiv-Zugriff
|
||||||
|
# archiv-viewer:
|
||||||
|
# password: "deinPasswort"
|
||||||
|
# role: "archive_viewer"
|
||||||
@@ -46,6 +46,14 @@ system:
|
|||||||
file-not-found: "&cDatei nicht gefunden: &e{file}"
|
file-not-found: "&cDatei nicht gefunden: &e{file}"
|
||||||
unknown-mode: "&cUnbekannter Modus! Benutze: tomysql oder tofile"
|
unknown-mode: "&cUnbekannter Modus! Benutze: tomysql oder tofile"
|
||||||
validation-warning: "&cEs wurden &e{count} &cungültige Tickets beim Laden gefunden."
|
validation-warning: "&cEs wurden &e{count} &cungültige Tickets beim Laden gefunden."
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BACKUP
|
||||||
|
# ============================================================
|
||||||
|
backup:
|
||||||
|
start: "&eBackup wird erstellt..."
|
||||||
|
success: "&aBackup erfolgreich erstellt: &e{file}"
|
||||||
|
fail: "&cBackup fehlgeschlagen. Prüfe die Konsole."
|
||||||
db-create-error: "&cFehler beim Erstellen des Tickets!"
|
db-create-error: "&cFehler beim Erstellen des Tickets!"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -242,6 +250,12 @@ faq:
|
|||||||
deleted: "&aFAQ &e#{id} &awurde gelöscht."
|
deleted: "&aFAQ &e#{id} &awurde gelöscht."
|
||||||
not-found: "&cFAQ &e#{id} &cwurde nicht gefunden."
|
not-found: "&cFAQ &e#{id} &cwurde nicht gefunden."
|
||||||
reloaded: "&aFAQs wurden neu geladen. ({count} Einträge)"
|
reloaded: "&aFAQs wurden neu geladen. ({count} Einträge)"
|
||||||
|
migrate-no-mysql: "&cFAQ-Migration nur möglich wenn use-mysql: true aktiv ist."
|
||||||
|
migrate-start: "&eFAQ-Migration gestartet... (faqs.yml → MySQL)"
|
||||||
|
migrate-success: "&aFAQ-Migration abgeschlossen! &7{cats} Kategorie(n) und {entries} Eintrag/Einträge importiert."
|
||||||
|
migrate-fail: "&cFAQ-Migration fehlgeschlagen. Prüfe die Konsole für Details."
|
||||||
|
migrate-tofile-start: "&eFAQ-Export gestartet... (MySQL → faqs.yml)"
|
||||||
|
migrate-tofile-success: "&aFAQ-Export abgeschlossen! &7{cats} Kategorie(n) und {entries} Einträge gespeichert."
|
||||||
list-header: "&6Häufige Fragen (FAQ) &7— {count} Einträge"
|
list-header: "&6Häufige Fragen (FAQ) &7— {count} Einträge"
|
||||||
list-empty: "&7Noch keine FAQs vorhanden."
|
list-empty: "&7Noch keine FAQs vorhanden."
|
||||||
list-entry: "&e#{id} &f{question}"
|
list-entry: "&e#{id} &f{question}"
|
||||||
@@ -249,7 +263,7 @@ faq:
|
|||||||
list-admin-hint: "&7Befehle: &e{cmd_faq} add &8| &e{cmd_faq} edit <ID> &8| &e{cmd_faq} delete <ID>"
|
list-admin-hint: "&7Befehle: &e{cmd_faq} add &8| &e{cmd_faq} edit <ID> &8| &e{cmd_faq} delete <ID>"
|
||||||
unknown-sub: "&cUnbekannter FAQ-Befehl."
|
unknown-sub: "&cUnbekannter FAQ-Befehl."
|
||||||
hint-open: "&7Benutze &e{cmd_faq} &7zum Öffnen der GUI."
|
hint-open: "&7Benutze &e{cmd_faq} &7zum Öffnen der GUI."
|
||||||
admin-commands: "&7Admin-Befehle: &e{cmd_faq} add | edit | delete | reload | list"
|
admin-commands: "&7Admin-Befehle: &e{cmd_faq} add | edit | delete | reload | list | migrate"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# FAQ-KATEGORIEN BEFEHL (/ticket kategorie)
|
# FAQ-KATEGORIEN BEFEHL (/ticket kategorie)
|
||||||
@@ -559,6 +573,9 @@ web:
|
|||||||
login-heading: "Willkommen zurück"
|
login-heading: "Willkommen zurück"
|
||||||
login-sub: "Melde dich mit deinem Account an, um fortzufahren."
|
login-sub: "Melde dich mit deinem Account an, um fortzufahren."
|
||||||
login-error: "Benutzername oder Passwort falsch."
|
login-error: "Benutzername oder Passwort falsch."
|
||||||
|
login-blocked: "Zu viele Fehlversuche. Bitte warte {seconds} Sekunden."
|
||||||
|
archive-btn-restore: "Wiederherstellen"
|
||||||
|
archive-btn-delete: "Permanent löschen"
|
||||||
login-label-user: "Benutzername"
|
login-label-user: "Benutzername"
|
||||||
login-label-pass: "Passwort"
|
login-label-pass: "Passwort"
|
||||||
login-btn: "Anmelden"
|
login-btn: "Anmelden"
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ system:
|
|||||||
file-not-found: "&cFile not found: &e{file}"
|
file-not-found: "&cFile not found: &e{file}"
|
||||||
unknown-mode: "&cUnknown mode! Use: tomysql or tofile"
|
unknown-mode: "&cUnknown mode! Use: tomysql or tofile"
|
||||||
validation-warning: "&c&e{count} &cinvalid tickets were found during loading."
|
validation-warning: "&c&e{count} &cinvalid tickets were found during loading."
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BACKUP
|
||||||
|
# ============================================================
|
||||||
|
backup:
|
||||||
|
start: "&eCreating backup..."
|
||||||
|
success: "&aBackup created successfully: &e{file}"
|
||||||
|
fail: "&cBackup failed. Check the console."
|
||||||
db-create-error: "&cFailed to create the ticket!"
|
db-create-error: "&cFailed to create the ticket!"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -242,6 +250,12 @@ faq:
|
|||||||
deleted: "&aFAQ &e#{id} &ahas been deleted."
|
deleted: "&aFAQ &e#{id} &ahas been deleted."
|
||||||
not-found: "&cFAQ &e#{id} &cwas not found."
|
not-found: "&cFAQ &e#{id} &cwas not found."
|
||||||
reloaded: "&aFAQs reloaded. ({count} entries)"
|
reloaded: "&aFAQs reloaded. ({count} entries)"
|
||||||
|
migrate-no-mysql: "&cFAQ migration only possible when use-mysql: true is active."
|
||||||
|
migrate-start: "&eFAQ migration started... (faqs.yml → MySQL)"
|
||||||
|
migrate-success: "&aFAQ migration complete! &7{cats} category/categories and {entries} entry/entries imported."
|
||||||
|
migrate-fail: "&cFAQ migration failed. Check the console for details."
|
||||||
|
migrate-tofile-start: "&eFAQ export started... (MySQL → faqs.yml)"
|
||||||
|
migrate-tofile-success: "&aFAQ export complete! &7{cats} category/categories and {entries} entry/entries saved to faqs.yml."
|
||||||
list-header: "&6Frequently Asked Questions (FAQ) &7— {count} entries"
|
list-header: "&6Frequently Asked Questions (FAQ) &7— {count} entries"
|
||||||
list-empty: "&7No FAQs available yet."
|
list-empty: "&7No FAQs available yet."
|
||||||
list-entry: "&e#{id} &f{question}"
|
list-entry: "&e#{id} &f{question}"
|
||||||
@@ -249,7 +263,7 @@ faq:
|
|||||||
list-admin-hint: "&7Commands: &e{cmd_faq} add &8| &e{cmd_faq} edit <ID> &8| &e{cmd_faq} delete <ID>"
|
list-admin-hint: "&7Commands: &e{cmd_faq} add &8| &e{cmd_faq} edit <ID> &8| &e{cmd_faq} delete <ID>"
|
||||||
unknown-sub: "&cUnknown FAQ subcommand."
|
unknown-sub: "&cUnknown FAQ subcommand."
|
||||||
hint-open: "&7Use &e{cmd_faq} &7to open the GUI."
|
hint-open: "&7Use &e{cmd_faq} &7to open the GUI."
|
||||||
admin-commands: "&7Admin commands: &e{cmd_faq} add | edit | delete | reload | list"
|
admin-commands: "&7Admin commands: &e{cmd_faq} add | edit | delete | reload | list | migrate"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# FAQ CATEGORY COMMAND (/ticket category)
|
# FAQ CATEGORY COMMAND (/ticket category)
|
||||||
@@ -558,7 +572,10 @@ web:
|
|||||||
login-title: "Login – TicketSystem Panel"
|
login-title: "Login – TicketSystem Panel"
|
||||||
login-heading: "Welcome back"
|
login-heading: "Welcome back"
|
||||||
login-sub: "Sign in with your account to continue."
|
login-sub: "Sign in with your account to continue."
|
||||||
|
login-blocked: "Too many failed attempts. Please wait {seconds} seconds."
|
||||||
login-error: "Username or password incorrect."
|
login-error: "Username or password incorrect."
|
||||||
|
archive-btn-restore: "Restore"
|
||||||
|
archive-btn-delete: "Delete permanently"
|
||||||
login-label-user: "Username"
|
login-label-user: "Username"
|
||||||
login-label-pass: "Password"
|
login-label-pass: "Password"
|
||||||
login-btn: "Sign In"
|
login-btn: "Sign In"
|
||||||
|
|||||||
Reference in New Issue
Block a user