This command requires the permission "mychest.admin.setlimit".
+ */ +public class AdminSetLimitCommand implements CommandExecutor { + + private static final String PERMISSION = "mychest.admin.setlimit"; + + private final ChestLimitService limitService; + private final AuditLoggerService auditService; + + /** + * Creates a new AdminSetLimitCommand. + * + * @param limitService the limit service + * @param auditService the audit service for logging + */ + public AdminSetLimitCommand(ChestLimitService limitService, AuditLoggerService auditService) { + this.limitService = limitService; + this.auditService = auditService; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Check permission + if (!sender.hasPermission(PERMISSION)) { + sender.sendMessage(ChatColor.RED + "You don't have permission to use this command."); + return true; + } + + // Validate arguments + if (args.length < 2) { + sender.sendMessage(ChatColor.RED + "Usage: /" + label + "This command requires the permission "mychest.share".
+ */ +public class UserShareCommand implements CommandExecutor { + + private static final String PERMISSION = "mychest.share"; + + private final ShareService shareService; + private final AuditLoggerService auditService; + + /** + * Creates a new UserShareCommand. + * + * @param shareService the share service + * @param auditService the audit service for logging + */ + public UserShareCommand(ShareService shareService, AuditLoggerService auditService) { + this.shareService = shareService; + this.auditService = auditService; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Only players can share chests + if (!(sender instanceof Player)) { + sender.sendMessage(ChatColor.RED + "This command can only be used by players."); + return true; + } + + Player player = (Player) sender; + + // Check permission + if (!player.hasPermission(PERMISSION)) { + player.sendMessage(ChatColor.RED + "You don't have permission to share chests."); + return true; + } + + if (args.length < 1) { + sendUsage(player, label); + return true; + } + + String subCommand = args[0].toLowerCase(); + + switch (subCommand) { + case "revoke": + return handleRevoke(player, args, label); + case "list": + return handleList(player, args, label); + default: + return handleShare(player, args, label); + } + } + + /** + * Handle the share subcommand. + */ + private boolean handleShare(Player player, String[] args, String label) { + if (args.length < 2) { + sendUsage(player, label); + return true; + } + + int chestId; + try { + chestId = Integer.parseInt(args[0]); + } catch (NumberFormatException e) { + player.sendMessage(ChatColor.RED + "Invalid chest ID: " + args[0]); + return true; + } + + String targetName = args[1]; + Player targetPlayer = Bukkit.getPlayer(targetName); + + if (targetPlayer == null) { + player.sendMessage(ChatColor.RED + "Player not found: " + targetName); + return true; + } + + if (targetPlayer.getUniqueId().equals(player.getUniqueId())) { + player.sendMessage(ChatColor.RED + "You cannot share a chest with yourself."); + return true; + } + + // Determine permission level + String permissions = ShareService.PERMISSION_VIEW; + if (args.length >= 3) { + String permArg = args[2].toUpperCase(); + if (permArg.equals(ShareService.PERMISSION_VIEW) || permArg.equals(ShareService.PERMISSION_EDIT)) { + permissions = permArg; + } else { + player.sendMessage(ChatColor.RED + "Invalid permission. Use VIEW or EDIT."); + return true; + } + } + + // Share the chest + try { + shareService.shareChest(chestId, player.getUniqueId(), targetPlayer.getUniqueId(), permissions); + auditService.logShare(player.getUniqueId(), chestId, targetPlayer.getUniqueId(), permissions); + + player.sendMessage(ChatColor.GREEN + "Shared chest #" + chestId + " with " + targetName + + " (" + permissions + " permission)."); + targetPlayer.sendMessage(ChatColor.YELLOW + player.getName() + " shared their chest #" + + chestId + " with you (" + permissions + " permission)."); + } catch (IllegalArgumentException e) { + player.sendMessage(ChatColor.RED + e.getMessage()); + } + + return true; + } + + /** + * Handle the revoke subcommand. + */ + private boolean handleRevoke(Player player, String[] args, String label) { + if (args.length < 3) { + player.sendMessage(ChatColor.RED + "Usage: /" + label + " revoke+ * mysql: + * host: localhost + * port: 3306 + * database: mychest + * user: root + * password: password + * poolSize: 10 + * useSSL: false + *+ */ + public void initialize() { + FileConfiguration config = plugin.getConfig(); + + String host = config.getString("mysql.host", "localhost"); + int port = config.getInt("mysql.port", 3306); + String database = config.getString("mysql.database", "mychest"); + String user = config.getString("mysql.user", config.getString("mysql.username", "root")); + String password = config.getString("mysql.password", ""); + int poolSize = config.getInt("mysql.poolSize", 10); + boolean useSSL = config.getBoolean("mysql.useSSL", false); + + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database); + hikariConfig.setUsername(user); + hikariConfig.setPassword(password); + hikariConfig.setMaximumPoolSize(poolSize); + hikariConfig.addDataSourceProperty("useSSL", String.valueOf(useSSL)); + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + hikariConfig.addDataSourceProperty("useServerPrepStmts", "true"); + hikariConfig.addDataSourceProperty("useLocalSessionState", "true"); + hikariConfig.addDataSourceProperty("rewriteBatchedStatements", "true"); + hikariConfig.addDataSourceProperty("cacheResultSetMetadata", "true"); + hikariConfig.addDataSourceProperty("cacheServerConfiguration", "true"); + hikariConfig.addDataSourceProperty("elideSetAutoCommits", "true"); + hikariConfig.addDataSourceProperty("maintainTimeStats", "false"); + hikariConfig.setPoolName("MyChest-HikariPool"); + + try { + dataSource = new HikariDataSource(hikariConfig); + logger.info("HikariCP connection pool initialized successfully."); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to initialize HikariCP connection pool", e); + throw new RuntimeException("Failed to initialize database connection pool", e); + } + } + + /** + * Get the DataSource for database operations. + * + * @return the HikariCP DataSource + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Get a connection from the pool. + * + * @return a database connection + * @throws SQLException if a connection cannot be obtained + */ + public Connection getConnection() throws SQLException { + if (dataSource == null) { + throw new IllegalStateException("DatabaseManager not initialized. Call initialize() first."); + } + return dataSource.getConnection(); + } + + /** + * Executes the embedded schema SQL file if present in resources. + * This method reads sql/schema.sql from the plugin resources and executes + * each statement separated by semicolons. + * + *
Note: This method should be called explicitly in onEnable() + * if you want to auto-create tables. It does NOT run automatically.
+ */ + public void runEmbeddedSchemaIfPresent() { + try (InputStream is = plugin.getResource("sql/schema.sql")) { + if (is == null) { + logger.info("No embedded schema found at sql/schema.sql - skipping schema execution."); + return; + } + + String schema = readInputStream(is); + executeSchema(schema); + logger.info("Embedded schema executed successfully."); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to read embedded schema", e); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to execute embedded schema", e); + } + } + + /** + * Execute a schema string containing multiple SQL statements separated by semicolons. + * + *Note: This method splits statements by semicolons, which is a simple + * approach that works for standard CREATE TABLE statements. For complex schemas with + * stored procedures or strings containing semicolons, consider running the schema manually.
+ * + * @param schema the SQL schema string + * @throws SQLException if execution fails + */ + private void executeSchema(String schema) throws SQLException { + // Remove SQL comments (lines starting with --) + StringBuilder cleanedSchema = new StringBuilder(); + for (String line : schema.split("\n")) { + String trimmedLine = line.trim(); + if (!trimmedLine.startsWith("--") && !trimmedLine.isEmpty()) { + cleanedSchema.append(line).append("\n"); + } + } + + String[] statements = cleanedSchema.toString().split(";"); + + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + + for (String sql : statements) { + String trimmed = sql.trim(); + if (!trimmed.isEmpty()) { + stmt.execute(trimmed); + } + } + } + } + + /** + * Read an InputStream to a String. + * + * @param is the input stream + * @return the content as a string + * @throws IOException if reading fails + */ + private String readInputStream(InputStream is) throws IOException { + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + } + return sb.toString(); + } + + /** + * Close the connection pool. + */ + public void close() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + logger.info("HikariCP connection pool closed."); + } + } + + /** + * Check if the connection pool is initialized and active. + * + * @return true if the pool is active, false otherwise + */ + public boolean isActive() { + return dataSource != null && !dataSource.isClosed(); + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/AuditLoggerService.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/AuditLoggerService.java new file mode 100644 index 0000000..13b9756 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/AuditLoggerService.java @@ -0,0 +1,178 @@ +package com.github.yannicklampers.mychest.feature.service; + +import com.github.yannicklampers.mychest.dao.AuditDao; +import com.github.yannicklampers.mychest.dao.AuditDao.AuditEntry; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.List; +import java.util.UUID; +import java.util.logging.Logger; + +/** + * Service for logging and retrieving audit records. + */ +public class AuditLoggerService { + + // Common audit action types + public static final String ACTION_LIMIT_SET = "LIMIT_SET"; + public static final String ACTION_LIMIT_RESET = "LIMIT_RESET"; + public static final String ACTION_SHARE = "SHARE"; + public static final String ACTION_REVOKE = "REVOKE"; + public static final String ACTION_CHEST_CREATE = "CHEST_CREATE"; + public static final String ACTION_CHEST_DELETE = "CHEST_DELETE"; + public static final String ACTION_CHEST_OPEN = "CHEST_OPEN"; + + private final AuditDao auditDao; + private final Logger logger; + private final int retentionDays; + + /** + * Creates a new AuditLoggerService. + * + * @param auditDao the audit DAO implementation + * @param logger the plugin logger + * @param retentionDays the number of days to retain logs + */ + public AuditLoggerService(AuditDao auditDao, Logger logger, int retentionDays) { + this.auditDao = auditDao; + this.logger = logger; + this.retentionDays = retentionDays; + } + + /** + * Creates a new AuditLoggerService with retention from config. + * + * @param auditDao the audit DAO implementation + * @param logger the plugin logger + * @param config the plugin configuration + */ + public AuditLoggerService(AuditDao auditDao, Logger logger, FileConfiguration config) { + this(auditDao, logger, config.getInt("settings.audit_retention_days", 30)); + } + + /** + * Log an audit action. + * + * @param playerId the UUID of the player performing the action + * @param action the action type + * @param details additional details + */ + public void log(UUID playerId, String action, String details) { + // Sanitize details to prevent log injection + String sanitizedDetails = sanitizeLogInput(details); + auditDao.logAction(playerId, action, sanitizedDetails); + logger.info("[Audit] Player " + playerId + " performed " + action + ": " + sanitizedDetails); + } + + /** + * Sanitize input to prevent log injection attacks. + * Removes newlines and carriage returns, and limits length. + * + * @param input the input string + * @return sanitized string + */ + private String sanitizeLogInput(String input) { + if (input == null) { + return ""; + } + // Remove newlines and carriage returns to prevent log forging + String sanitized = input.replace("\n", " ").replace("\r", " "); + // Limit length to prevent excessively long log entries + if (sanitized.length() > 500) { + sanitized = sanitized.substring(0, 500) + "..."; + } + return sanitized; + } + + /** + * Log a limit set action. + * + * @param adminId the admin who set the limit + * @param targetId the player whose limit was set + * @param newSlots the new slot limit + */ + public void logLimitSet(UUID adminId, UUID targetId, int newSlots) { + String details = String.format("Set limit for %s to %d slots", targetId.toString(), newSlots); + log(adminId, ACTION_LIMIT_SET, details); + } + + /** + * Log a limit reset action. + * + * @param adminId the admin who reset the limit + * @param targetId the player whose limit was reset + */ + public void logLimitReset(UUID adminId, UUID targetId) { + String details = String.format("Reset limit for %s to default", targetId.toString()); + log(adminId, ACTION_LIMIT_RESET, details); + } + + /** + * Log a chest share action. + * + * @param ownerId the owner of the chest + * @param chestId the chest ID + * @param targetId the player the chest was shared with + * @param permissions the permission level + */ + public void logShare(UUID ownerId, int chestId, UUID targetId, String permissions) { + String details = String.format("Shared chest %d with %s (permission: %s)", + chestId, targetId.toString(), permissions); + log(ownerId, ACTION_SHARE, details); + } + + /** + * Log a chest revoke action. + * + * @param ownerId the owner of the chest + * @param chestId the chest ID + * @param targetId the player whose access was revoked + */ + public void logRevoke(UUID ownerId, int chestId, UUID targetId) { + String details = String.format("Revoked access to chest %d from %s", chestId, targetId.toString()); + log(ownerId, ACTION_REVOKE, details); + } + + /** + * Get audit logs for a specific player. + * + * @param playerId the UUID of the player + * @param limit maximum number of entries + * @return list of audit entries + */ + public List