diff --git a/mychest/README-feature.md b/mychest/README-feature.md new file mode 100644 index 0000000..c27bd40 --- /dev/null +++ b/mychest/README-feature.md @@ -0,0 +1,258 @@ +# MyChest Feature Module + +This document describes how to integrate the MySQL persistence feature module into the MyChest plugin. + +## Overview + +The feature module provides: +- **Chest Limits**: Custom per-player slot limits +- **Chest Sharing**: Share chests with other players (VIEW/EDIT permissions) +- **Audit Logging**: Track all chest operations for security and debugging +- **Database Management**: HikariCP connection pool for optimal performance + +## Dependencies + +Add the following dependencies to your `pom.xml`: + +```xml + + + com.zaxxer + HikariCP + 4.0.3 + + + + + mysql + mysql-connector-java + 8.0.28 + +``` + +## Configuration + +Add the following to your `config.yml`: + +```yaml +# MySQL Configuration +mysql: + host: localhost + port: 3306 + database: mychest + user: root + password: your_password + poolSize: 10 + useSSL: false + +# Feature Module Settings +settings: + # Default number of slots for new players + default_slots: 27 + # Number of days to retain audit logs + audit_retention_days: 30 +``` + +## Integration + +### 1. Initialize in Main Plugin + +In your main plugin class `onEnable()` method: + +```java +import com.github.yannicklampers.mychest.feature.database.DatabaseManager; +import com.github.yannicklampers.mychest.feature.dao.*; +import com.github.yannicklampers.mychest.feature.service.*; +import com.github.yannicklampers.mychest.feature.commands.*; + +public class Main extends JavaPlugin { + + private DatabaseManager databaseManager; + private ChestLimitService limitService; + private ShareService shareService; + private AuditLoggerService auditService; + + @Override + public void onEnable() { + // ... existing initialization code ... + + // Initialize the feature module database manager + databaseManager = new DatabaseManager(this); + databaseManager.initialize(); + + // Optional: Run embedded schema to create tables + // Only call this if you want to auto-create tables on startup + // databaseManager.runEmbeddedSchemaIfPresent(); + + // Create DAOs + LimitDaoMySQL limitDao = new LimitDaoMySQL(databaseManager.getDataSource(), getLogger()); + ShareDaoMySQL shareDao = new ShareDaoMySQL(databaseManager.getDataSource(), getLogger()); + AuditDaoMySQL auditDao = new AuditDaoMySQL(databaseManager.getDataSource(), getLogger()); + + // Create Services + limitService = new ChestLimitService(limitDao, getConfig()); + shareService = new ShareService(shareDao); + auditService = new AuditLoggerService(auditDao, getLogger(), getConfig()); + + // Register Commands + getCommand("setlimit").setExecutor(new AdminSetLimitCommand(limitService, auditService)); + getCommand("sharechest").setExecutor(new UserShareCommand(shareService, auditService)); + + // ... rest of existing code ... + } + + @Override + public void onDisable() { + // ... existing disable code ... + + // Close the database connection pool + if (databaseManager != null) { + databaseManager.close(); + } + } +} +``` + +### 2. Register Commands in plugin.yml + +Add the new commands to your `plugin.yml`: + +```yaml +commands: + setlimit: + description: Set chest slot limit for a player + usage: / + permission: mychest.admin.setlimit + sharechest: + description: Share a chest with another player + usage: / [VIEW|EDIT] + permission: mychest.share + +permissions: + mychest.admin.setlimit: + description: Allows setting chest limits for players + default: op + mychest.share: + description: Allows sharing chests with other players + default: true +``` + +## Database Schema + +The schema file is located at `resources/sql/schema.sql` and creates the following tables: + +| Table | Description | +|-------|-------------| +| `chest_limits` | Stores custom slot limits per player (player_id CHAR(36) PK) | +| `chest_shares` | Stores chest sharing relationships (chest_id, owner_id, target_id) | +| `chest_audit` | Stores audit logs for all operations | +| `chest_backups` | Stores chest backup data as JSON | + +### Running the Schema + +**Option 1: Automatic (Not recommended for production)** +```java +// In onEnable(), after databaseManager.initialize(): +databaseManager.runEmbeddedSchemaIfPresent(); +``` + +**Option 2: Manual (Recommended)** +Execute the SQL file directly on your MySQL server: +```bash +mysql -u root -p mychest < sql/schema.sql +``` + +Or copy the contents of `sql/schema.sql` and execute in your MySQL client. + +## Usage Examples + +### Setting Limits (Admin) +``` +/setlimit PlayerName 54 # Set 54 slots for PlayerName +/setlimit PlayerName reset # Reset to default +``` + +### Sharing Chests (Players) +``` +/sharechest 1 PlayerName # Share chest #1 with VIEW permission +/sharechest 1 PlayerName EDIT # Share chest #1 with EDIT permission +/sharechest revoke 1 PlayerName # Revoke access +/sharechest list 1 # List who has access to chest #1 +``` + +## API Usage + +### Using Services Programmatically + +```java +// Get slot limit for a player +int slots = limitService.getSlotLimit(playerId); + +// Check if player has custom limit +boolean hasCustom = limitService.hasCustomLimit(playerId); + +// Share a chest +shareService.shareChest(chestId, ownerId, targetId, ShareService.PERMISSION_EDIT); + +// Check access +boolean canAccess = shareService.hasAccess(chestId, playerId); +boolean canEdit = shareService.canEdit(chestId, playerId); + +// Log actions +auditService.log(playerId, "CUSTOM_ACTION", "Details here"); + +// Cleanup old audit logs +int deleted = auditService.cleanupOldLogs(); +``` + +## File Structure + +``` +mychest/src/main/java/com/github/yannicklampers/mychest/ +├── dao/ +│ ├── LimitDao.java # Interface for limit operations +│ ├── ShareDao.java # Interface for share operations +│ └── AuditDao.java # Interface for audit operations +└── feature/ + ├── database/ + │ └── DatabaseManager.java # HikariCP pool manager + ├── dao/ + │ ├── LimitDaoMySQL.java # MySQL limit implementation + │ ├── ShareDaoMySQL.java # MySQL share implementation + │ └── AuditDaoMySQL.java # MySQL audit implementation + ├── service/ + │ ├── ChestLimitService.java # Limit business logic + │ ├── ShareService.java # Share business logic + │ └── AuditLoggerService.java # Audit business logic + └── commands/ + ├── AdminSetLimitCommand.java # /setlimit command + └── UserShareCommand.java # /sharechest command + +mychest/src/main/resources/ +└── sql/ + └── schema.sql # Database schema +``` + +## Notes + +- The `DatabaseManager.runEmbeddedSchemaIfPresent()` method does **NOT** run automatically. You must call it explicitly in `onEnable()` if you want automatic table creation. +- UUIDs are stored as `CHAR(36)` strings for compatibility. +- The audit retention cleanup should be scheduled (e.g., daily task) or called manually. +- All SQL operations use prepared statements to prevent SQL injection. + +## Troubleshooting + +### Connection Pool Errors +- Verify MySQL credentials in `config.yml` +- Ensure MySQL server is running and accessible +- Check firewall rules if connecting remotely + +### Schema Errors +- Run the schema manually if automatic execution fails +- Ensure the database exists before running the schema +- Check MySQL version compatibility (tested with MySQL 8.0+) + +### Permission Issues +- Verify permissions are defined in `plugin.yml` +- Check player has the required permission node +- Use a permissions plugin like LuckPerms for fine-grained control diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/dao/AuditDao.java b/mychest/src/main/java/com/github/yannicklampers/mychest/dao/AuditDao.java new file mode 100644 index 0000000..e6d0ab5 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/dao/AuditDao.java @@ -0,0 +1,83 @@ +package com.github.yannicklampers.mychest.dao; + +import java.util.List; +import java.util.UUID; + +/** + * DAO interface for audit logging of chest operations. + */ +public interface AuditDao { + + /** + * Represents an audit log entry. + */ + class AuditEntry { + private final long id; + private final UUID playerId; + private final String action; + private final String details; + private final long timestamp; + + public AuditEntry(long id, UUID playerId, String action, String details, long timestamp) { + this.id = id; + this.playerId = playerId; + this.action = action; + this.details = details; + this.timestamp = timestamp; + } + + public long getId() { + return id; + } + + public UUID getPlayerId() { + return playerId; + } + + public String getAction() { + return action; + } + + public String getDetails() { + return details; + } + + public long getTimestamp() { + return timestamp; + } + } + + /** + * Log an audit event. + * + * @param playerId the UUID of the player performing the action + * @param action the action type (e.g., "LIMIT_SET", "SHARE", "REVOKE") + * @param details additional details about the action + */ + void logAction(UUID playerId, String action, String details); + + /** + * Get audit logs for a specific player. + * + * @param playerId the UUID of the player + * @param limit maximum number of entries to return + * @return a list of audit entries + */ + List getLogsForPlayer(UUID playerId, int limit); + + /** + * Get recent audit logs. + * + * @param limit maximum number of entries to return + * @return a list of recent audit entries + */ + List getRecentLogs(int limit); + + /** + * Delete audit logs older than a certain number of days. + * + * @param days the retention period in days + * @return the number of entries deleted + */ + int deleteOldLogs(int days); +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/dao/LimitDao.java b/mychest/src/main/java/com/github/yannicklampers/mychest/dao/LimitDao.java new file mode 100644 index 0000000..8a2bc79 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/dao/LimitDao.java @@ -0,0 +1,33 @@ +package com.github.yannicklampers.mychest.dao; + +import java.util.Optional; +import java.util.UUID; + +/** + * DAO interface for managing chest limits per player. + */ +public interface LimitDao { + + /** + * Get the maximum number of chest slots allowed for a player. + * + * @param playerId the UUID of the player + * @return an Optional containing the slot limit, or empty if not set + */ + Optional getSlotLimit(UUID playerId); + + /** + * Set the maximum number of chest slots allowed for a player. + * + * @param playerId the UUID of the player + * @param slots the maximum number of slots + */ + void setSlotLimit(UUID playerId, int slots); + + /** + * Delete the slot limit for a player (revert to default). + * + * @param playerId the UUID of the player + */ + void deleteSlotLimit(UUID playerId); +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/dao/ShareDao.java b/mychest/src/main/java/com/github/yannicklampers/mychest/dao/ShareDao.java new file mode 100644 index 0000000..57c7aa0 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/dao/ShareDao.java @@ -0,0 +1,62 @@ +package com.github.yannicklampers.mychest.dao; + +import java.util.List; +import java.util.UUID; + +/** + * DAO interface for managing chest sharing between players. + */ +public interface ShareDao { + + /** + * Share a chest with another player. + * + * @param chestId the ID of the chest to share + * @param ownerId the UUID of the chest owner + * @param targetId the UUID of the player to share with + * @param permissions the permission level (e.g., "VIEW", "EDIT") + */ + void shareChest(int chestId, UUID ownerId, UUID targetId, String permissions); + + /** + * Revoke chest sharing from a player. + * + * @param chestId the ID of the chest + * @param targetId the UUID of the player to revoke access from + */ + void revokeShare(int chestId, UUID targetId); + + /** + * Get all players a chest is shared with. + * + * @param chestId the ID of the chest + * @return a list of UUIDs of players the chest is shared with + */ + List getSharedPlayers(int chestId); + + /** + * Get all chests shared with a specific player. + * + * @param playerId the UUID of the player + * @return a list of chest IDs shared with this player + */ + List getSharedChests(UUID playerId); + + /** + * Check if a player has access to a chest. + * + * @param chestId the ID of the chest + * @param playerId the UUID of the player + * @return true if the player has access, false otherwise + */ + boolean hasAccess(int chestId, UUID playerId); + + /** + * Get the permission level for a player on a chest. + * + * @param chestId the ID of the chest + * @param playerId the UUID of the player + * @return the permission string, or null if no access + */ + String getPermissions(int chestId, UUID playerId); +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/commands/AdminSetLimitCommand.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/commands/AdminSetLimitCommand.java new file mode 100644 index 0000000..b9b942e --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/commands/AdminSetLimitCommand.java @@ -0,0 +1,117 @@ +package com.github.yannicklampers.mychest.feature.commands; + +import com.github.yannicklampers.mychest.feature.service.AuditLoggerService; +import com.github.yannicklampers.mychest.feature.service.ChestLimitService; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.UUID; + +/** + * Example admin command for setting player chest slot limits. + * Usage: /setlimit + * + *

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 + " "); + sender.sendMessage(ChatColor.GRAY + "Use 'reset' as slots to reset to default."); + return true; + } + + String playerName = args[0]; + String slotsArg = args[1]; + + // Find the target player + Player targetPlayer = Bukkit.getPlayer(playerName); + UUID targetId; + + if (targetPlayer != null) { + targetId = targetPlayer.getUniqueId(); + } else { + // Get offline player UUID (generates a random UUID if player never joined) + @SuppressWarnings("deprecation") + targetId = Bukkit.getOfflinePlayer(playerName).getUniqueId(); + } + + // Get admin UUID (console uses null UUID) + UUID adminId = (sender instanceof Player) + ? ((Player) sender).getUniqueId() + : new UUID(0, 0); + + // Handle reset + if (slotsArg.equalsIgnoreCase("reset")) { + limitService.resetSlotLimit(targetId); + auditService.logLimitReset(adminId, targetId); + + sender.sendMessage(ChatColor.GREEN + "Reset slot limit for " + playerName + " to default (" + + limitService.getDefaultSlots() + " slots)."); + + if (targetPlayer != null && targetPlayer.isOnline()) { + targetPlayer.sendMessage(ChatColor.YELLOW + "Your chest slot limit has been reset to default."); + } + return true; + } + + // Parse and validate slot count + int slots; + try { + slots = Integer.parseInt(slotsArg); + } catch (NumberFormatException e) { + sender.sendMessage(ChatColor.RED + "Invalid slot count: " + slotsArg); + return true; + } + + if (slots <= 0) { + sender.sendMessage(ChatColor.RED + "Slot count must be positive."); + return true; + } + + if (slots > 54) { + sender.sendMessage(ChatColor.YELLOW + "Warning: Setting slots above 54 may cause UI issues."); + } + + // Set the limit + limitService.setSlotLimit(targetId, slots); + auditService.logLimitSet(adminId, targetId, slots); + + sender.sendMessage(ChatColor.GREEN + "Set slot limit for " + playerName + " to " + slots + " slots."); + + if (targetPlayer != null && targetPlayer.isOnline()) { + targetPlayer.sendMessage(ChatColor.YELLOW + "Your chest slot limit has been set to " + slots + " slots."); + } + + return true; + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/commands/UserShareCommand.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/commands/UserShareCommand.java new file mode 100644 index 0000000..1d7533b --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/commands/UserShareCommand.java @@ -0,0 +1,217 @@ +package com.github.yannicklampers.mychest.feature.commands; + +import com.github.yannicklampers.mychest.feature.service.AuditLoggerService; +import com.github.yannicklampers.mychest.feature.service.ShareService; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.UUID; + +/** + * Example command for sharing chests with other players. + * Usage: /sharechest [permission] + * /sharechest revoke + * /sharechest list + * + *

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 "); + return true; + } + + int chestId; + try { + chestId = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage(ChatColor.RED + "Invalid chest ID: " + args[1]); + return true; + } + + String targetName = args[2]; + Player targetPlayer = Bukkit.getPlayer(targetName); + UUID targetId; + + if (targetPlayer != null) { + targetId = targetPlayer.getUniqueId(); + } else { + // Get offline player UUID (generates a random UUID if player never joined) + @SuppressWarnings("deprecation") + targetId = Bukkit.getOfflinePlayer(targetName).getUniqueId(); + } + + shareService.revokeShare(chestId, targetId); + auditService.logRevoke(player.getUniqueId(), chestId, targetId); + + player.sendMessage(ChatColor.GREEN + "Revoked access to chest #" + chestId + " from " + targetName + "."); + + if (targetPlayer != null && targetPlayer.isOnline()) { + targetPlayer.sendMessage(ChatColor.YELLOW + player.getName() + " revoked your access to their chest #" + chestId + "."); + } + + return true; + } + + /** + * Handle the list subcommand. + */ + private boolean handleList(Player player, String[] args, String label) { + if (args.length < 2) { + player.sendMessage(ChatColor.RED + "Usage: /" + label + " list "); + return true; + } + + int chestId; + try { + chestId = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage(ChatColor.RED + "Invalid chest ID: " + args[1]); + return true; + } + + List sharedPlayers = shareService.getSharedPlayers(chestId); + + if (sharedPlayers.isEmpty()) { + player.sendMessage(ChatColor.YELLOW + "Chest #" + chestId + " is not shared with anyone."); + return true; + } + + player.sendMessage(ChatColor.GREEN + "Chest #" + chestId + " is shared with:"); + for (UUID uuid : sharedPlayers) { + String name = Bukkit.getOfflinePlayer(uuid).getName(); + String perms = shareService.getPermissions(chestId, uuid); + player.sendMessage(ChatColor.GRAY + " - " + (name != null ? name : uuid.toString()) + + " (" + perms + ")"); + } + + return true; + } + + /** + * Send usage information to the player. + */ + private void sendUsage(Player player, String label) { + player.sendMessage(ChatColor.YELLOW + "Chest Sharing Commands:"); + player.sendMessage(ChatColor.GRAY + " /" + label + " [VIEW|EDIT]" + ChatColor.WHITE + " - Share a chest"); + player.sendMessage(ChatColor.GRAY + " /" + label + " revoke " + ChatColor.WHITE + " - Revoke access"); + player.sendMessage(ChatColor.GRAY + " /" + label + " list " + ChatColor.WHITE + " - List shared players"); + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/AuditDaoMySQL.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/AuditDaoMySQL.java new file mode 100644 index 0000000..9d44f92 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/AuditDaoMySQL.java @@ -0,0 +1,135 @@ +package com.github.yannicklampers.mychest.feature.dao; + +import com.github.yannicklampers.mychest.dao.AuditDao; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * MySQL implementation of the AuditDao interface. + */ +public class AuditDaoMySQL implements AuditDao { + + private static final String INSERT_LOG = + "INSERT INTO chest_audit (player_id, action, details, created_at) VALUES (?, ?, ?, NOW())"; + private static final String SELECT_PLAYER_LOGS = + "SELECT id, player_id, action, details, UNIX_TIMESTAMP(created_at) as timestamp " + + "FROM chest_audit WHERE player_id = ? ORDER BY created_at DESC LIMIT ?"; + private static final String SELECT_RECENT_LOGS = + "SELECT id, player_id, action, details, UNIX_TIMESTAMP(created_at) as timestamp " + + "FROM chest_audit ORDER BY created_at DESC LIMIT ?"; + private static final String DELETE_OLD_LOGS = + "DELETE FROM chest_audit WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)"; + + private final DataSource dataSource; + private final Logger logger; + + /** + * Creates a new AuditDaoMySQL instance. + * + * @param dataSource the data source to use + * @param logger the logger instance + */ + public AuditDaoMySQL(DataSource dataSource, Logger logger) { + this.dataSource = dataSource; + this.logger = logger; + } + + @Override + public void logAction(UUID playerId, String action, String details) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(INSERT_LOG)) { + + stmt.setString(1, playerId.toString()); + stmt.setString(2, action); + stmt.setString(3, details); + stmt.executeUpdate(); + + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to log audit action for player " + playerId, e); + } + } + + @Override + public List getLogsForPlayer(UUID playerId, int limit) { + List entries = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_PLAYER_LOGS)) { + + stmt.setString(1, playerId.toString()); + stmt.setInt(2, limit); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + entries.add(createAuditEntry(rs)); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get audit logs for player " + playerId, e); + } + + return entries; + } + + @Override + public List getRecentLogs(int limit) { + List entries = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_RECENT_LOGS)) { + + stmt.setInt(1, limit); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + entries.add(createAuditEntry(rs)); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get recent audit logs", e); + } + + return entries; + } + + @Override + public int deleteOldLogs(int days) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(DELETE_OLD_LOGS)) { + + stmt.setInt(1, days); + return stmt.executeUpdate(); + + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to delete old audit logs", e); + } + + return 0; + } + + /** + * Create an AuditEntry from a ResultSet row. + * + * @param rs the result set positioned at the current row + * @return the audit entry + * @throws SQLException if reading fails + */ + private AuditEntry createAuditEntry(ResultSet rs) throws SQLException { + return new AuditEntry( + rs.getLong("id"), + UUID.fromString(rs.getString("player_id")), + rs.getString("action"), + rs.getString("details"), + rs.getLong("timestamp") * 1000 // Convert to milliseconds + ); + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/LimitDaoMySQL.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/LimitDaoMySQL.java new file mode 100644 index 0000000..2c5bbf5 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/LimitDaoMySQL.java @@ -0,0 +1,86 @@ +package com.github.yannicklampers.mychest.feature.dao; + +import com.github.yannicklampers.mychest.dao.LimitDao; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * MySQL implementation of the LimitDao interface. + */ +public class LimitDaoMySQL implements LimitDao { + + private static final String SELECT_LIMIT = + "SELECT slots FROM chest_limits WHERE player_id = ?"; + private static final String INSERT_LIMIT = + "INSERT INTO chest_limits (player_id, slots) VALUES (?, ?) " + + "ON DUPLICATE KEY UPDATE slots = VALUES(slots)"; + private static final String DELETE_LIMIT = + "DELETE FROM chest_limits WHERE player_id = ?"; + + private final DataSource dataSource; + private final Logger logger; + + /** + * Creates a new LimitDaoMySQL instance. + * + * @param dataSource the data source to use + * @param logger the logger instance + */ + public LimitDaoMySQL(DataSource dataSource, Logger logger) { + this.dataSource = dataSource; + this.logger = logger; + } + + @Override + public Optional getSlotLimit(UUID playerId) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_LIMIT)) { + + stmt.setString(1, playerId.toString()); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of(rs.getInt("slots")); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get slot limit for player " + playerId, e); + } + return Optional.empty(); + } + + @Override + public void setSlotLimit(UUID playerId, int slots) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(INSERT_LIMIT)) { + + stmt.setString(1, playerId.toString()); + stmt.setInt(2, slots); + stmt.executeUpdate(); + + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to set slot limit for player " + playerId, e); + } + } + + @Override + public void deleteSlotLimit(UUID playerId) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(DELETE_LIMIT)) { + + stmt.setString(1, playerId.toString()); + stmt.executeUpdate(); + + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to delete slot limit for player " + playerId, e); + } + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/ShareDaoMySQL.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/ShareDaoMySQL.java new file mode 100644 index 0000000..49df666 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/dao/ShareDaoMySQL.java @@ -0,0 +1,158 @@ +package com.github.yannicklampers.mychest.feature.dao; + +import com.github.yannicklampers.mychest.dao.ShareDao; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * MySQL implementation of the ShareDao interface. + */ +public class ShareDaoMySQL implements ShareDao { + + private static final String INSERT_SHARE = + "INSERT INTO chest_shares (chest_id, owner_id, target_id, permissions) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE permissions = VALUES(permissions)"; + private static final String DELETE_SHARE = + "DELETE FROM chest_shares WHERE chest_id = ? AND target_id = ?"; + private static final String SELECT_SHARED_PLAYERS = + "SELECT target_id FROM chest_shares WHERE chest_id = ?"; + private static final String SELECT_SHARED_CHESTS = + "SELECT chest_id FROM chest_shares WHERE target_id = ?"; + private static final String SELECT_HAS_ACCESS = + "SELECT 1 FROM chest_shares WHERE chest_id = ? AND target_id = ?"; + private static final String SELECT_PERMISSIONS = + "SELECT permissions FROM chest_shares WHERE chest_id = ? AND target_id = ?"; + + private final DataSource dataSource; + private final Logger logger; + + /** + * Creates a new ShareDaoMySQL instance. + * + * @param dataSource the data source to use + * @param logger the logger instance + */ + public ShareDaoMySQL(DataSource dataSource, Logger logger) { + this.dataSource = dataSource; + this.logger = logger; + } + + @Override + public void shareChest(int chestId, UUID ownerId, UUID targetId, String permissions) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(INSERT_SHARE)) { + + stmt.setInt(1, chestId); + stmt.setString(2, ownerId.toString()); + stmt.setString(3, targetId.toString()); + stmt.setString(4, permissions); + stmt.executeUpdate(); + + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to share chest " + chestId + " with player " + targetId, e); + } + } + + @Override + public void revokeShare(int chestId, UUID targetId) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(DELETE_SHARE)) { + + stmt.setInt(1, chestId); + stmt.setString(2, targetId.toString()); + stmt.executeUpdate(); + + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to revoke share for chest " + chestId + " from player " + targetId, e); + } + } + + @Override + public List getSharedPlayers(int chestId) { + List players = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_SHARED_PLAYERS)) { + + stmt.setInt(1, chestId); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + players.add(UUID.fromString(rs.getString("target_id"))); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get shared players for chest " + chestId, e); + } + + return players; + } + + @Override + public List getSharedChests(UUID playerId) { + List chests = new ArrayList<>(); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_SHARED_CHESTS)) { + + stmt.setString(1, playerId.toString()); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + chests.add(rs.getInt("chest_id")); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get shared chests for player " + playerId, e); + } + + return chests; + } + + @Override + public boolean hasAccess(int chestId, UUID playerId) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_HAS_ACCESS)) { + + stmt.setInt(1, chestId); + stmt.setString(2, playerId.toString()); + + try (ResultSet rs = stmt.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to check access for chest " + chestId + " player " + playerId, e); + } + + return false; + } + + @Override + public String getPermissions(int chestId, UUID playerId) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(SELECT_PERMISSIONS)) { + + stmt.setInt(1, chestId); + stmt.setString(2, playerId.toString()); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getString("permissions"); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get permissions for chest " + chestId + " player " + playerId, e); + } + + return null; + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/database/DatabaseManager.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/database/DatabaseManager.java new file mode 100644 index 0000000..93510d4 --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/database/DatabaseManager.java @@ -0,0 +1,209 @@ +package com.github.yannicklampers.mychest.feature.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import javax.sql.DataSource; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Database manager for the MySQL feature module. + * Initializes HikariCP connection pool from config.yml settings. + */ +public class DatabaseManager { + + private final JavaPlugin plugin; + private final Logger logger; + private HikariDataSource dataSource; + + /** + * Creates a new DatabaseManager instance. + * + * @param plugin the JavaPlugin instance + */ + public DatabaseManager(JavaPlugin plugin) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + } + + /** + * Initialize the connection pool using config.yml settings. + * Expected config structure: + *
+     * 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 getLogsForPlayer(UUID playerId, int limit) { + return auditDao.getLogsForPlayer(playerId, limit); + } + + /** + * Get recent audit logs. + * + * @param limit maximum number of entries + * @return list of audit entries + */ + public List getRecentLogs(int limit) { + return auditDao.getRecentLogs(limit); + } + + /** + * Clean up old audit logs based on retention policy. + * + * @return the number of entries deleted + */ + public int cleanupOldLogs() { + int deleted = auditDao.deleteOldLogs(retentionDays); + if (deleted > 0) { + logger.info("[Audit] Cleaned up " + deleted + " old audit entries (older than " + retentionDays + " days)"); + } + return deleted; + } + + /** + * Get the configured retention period. + * + * @return the number of days logs are retained + */ + public int getRetentionDays() { + return retentionDays; + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/ChestLimitService.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/ChestLimitService.java new file mode 100644 index 0000000..0bebfbe --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/ChestLimitService.java @@ -0,0 +1,89 @@ +package com.github.yannicklampers.mychest.feature.service; + +import com.github.yannicklampers.mychest.dao.LimitDao; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.Optional; +import java.util.UUID; + +/** + * Service for managing player chest slot limits. + */ +public class ChestLimitService { + + private final LimitDao limitDao; + private final int defaultSlots; + + /** + * Creates a new ChestLimitService. + * + * @param limitDao the limit DAO implementation + * @param defaultSlots the default number of slots if none is set + */ + public ChestLimitService(LimitDao limitDao, int defaultSlots) { + this.limitDao = limitDao; + this.defaultSlots = defaultSlots; + } + + /** + * Creates a new ChestLimitService with default from config. + * + * @param limitDao the limit DAO implementation + * @param config the plugin configuration + */ + public ChestLimitService(LimitDao limitDao, FileConfiguration config) { + this(limitDao, config.getInt("settings.default_slots", 27)); + } + + /** + * Get the slot limit for a player. + * Returns the player-specific limit if set, otherwise returns the default. + * + * @param playerId the UUID of the player + * @return the slot limit + */ + public int getSlotLimit(UUID playerId) { + return limitDao.getSlotLimit(playerId).orElse(defaultSlots); + } + + /** + * Set a custom slot limit for a player. + * + * @param playerId the UUID of the player + * @param slots the number of slots + */ + public void setSlotLimit(UUID playerId, int slots) { + if (slots <= 0) { + throw new IllegalArgumentException("Slot limit must be positive"); + } + limitDao.setSlotLimit(playerId, slots); + } + + /** + * Reset the player's slot limit to the default. + * + * @param playerId the UUID of the player + */ + public void resetSlotLimit(UUID playerId) { + limitDao.deleteSlotLimit(playerId); + } + + /** + * Check if a player has a custom slot limit set. + * + * @param playerId the UUID of the player + * @return true if a custom limit is set + */ + public boolean hasCustomLimit(UUID playerId) { + return limitDao.getSlotLimit(playerId).isPresent(); + } + + /** + * Get the default slot limit. + * + * @return the default number of slots + */ + public int getDefaultSlots() { + return defaultSlots; + } +} diff --git a/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/ShareService.java b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/ShareService.java new file mode 100644 index 0000000..af81bbe --- /dev/null +++ b/mychest/src/main/java/com/github/yannicklampers/mychest/feature/service/ShareService.java @@ -0,0 +1,119 @@ +package com.github.yannicklampers.mychest.feature.service; + +import com.github.yannicklampers.mychest.dao.ShareDao; + +import java.util.List; +import java.util.UUID; + +/** + * Service for managing chest sharing between players. + */ +public class ShareService { + + /** Permission level for view-only access. */ + public static final String PERMISSION_VIEW = "VIEW"; + + /** Permission level for full edit access. */ + public static final String PERMISSION_EDIT = "EDIT"; + + private final ShareDao shareDao; + + /** + * Creates a new ShareService. + * + * @param shareDao the share DAO implementation + */ + public ShareService(ShareDao shareDao) { + this.shareDao = shareDao; + } + + /** + * Share a chest with another player with view permission. + * + * @param chestId the ID of the chest + * @param ownerId the UUID of the chest owner + * @param targetId the UUID of the player to share with + */ + public void shareChest(int chestId, UUID ownerId, UUID targetId) { + shareChest(chestId, ownerId, targetId, PERMISSION_VIEW); + } + + /** + * Share a chest with another player with specified permission. + * + * @param chestId the ID of the chest + * @param ownerId the UUID of the chest owner + * @param targetId the UUID of the player to share with + * @param permissions the permission level (VIEW or EDIT) + */ + public void shareChest(int chestId, UUID ownerId, UUID targetId, String permissions) { + if (ownerId.equals(targetId)) { + throw new IllegalArgumentException("Cannot share a chest with yourself"); + } + shareDao.shareChest(chestId, ownerId, targetId, permissions); + } + + /** + * Revoke chest sharing from a player. + * + * @param chestId the ID of the chest + * @param targetId the UUID of the player to revoke access from + */ + public void revokeShare(int chestId, UUID targetId) { + shareDao.revokeShare(chestId, targetId); + } + + /** + * Get all players a chest is shared with. + * + * @param chestId the ID of the chest + * @return a list of UUIDs of players the chest is shared with + */ + public List getSharedPlayers(int chestId) { + return shareDao.getSharedPlayers(chestId); + } + + /** + * Get all chests shared with a specific player. + * + * @param playerId the UUID of the player + * @return a list of chest IDs shared with this player + */ + public List getSharedChests(UUID playerId) { + return shareDao.getSharedChests(playerId); + } + + /** + * Check if a player has access to a chest. + * + * @param chestId the ID of the chest + * @param playerId the UUID of the player + * @return true if the player has access + */ + public boolean hasAccess(int chestId, UUID playerId) { + return shareDao.hasAccess(chestId, playerId); + } + + /** + * Check if a player can edit a chest. + * + * @param chestId the ID of the chest + * @param playerId the UUID of the player + * @return true if the player can edit + */ + public boolean canEdit(int chestId, UUID playerId) { + String perms = shareDao.getPermissions(chestId, playerId); + return PERMISSION_EDIT.equals(perms); + } + + /** + * Get the permission level for a player on a chest. + * + * @param chestId the ID of the chest + * @param playerId the UUID of the player + * @return the permission string, or null if no access + */ + public String getPermissions(int chestId, UUID playerId) { + return shareDao.getPermissions(chestId, playerId); + } +} diff --git a/mychest/src/main/resources/sql/schema.sql b/mychest/src/main/resources/sql/schema.sql new file mode 100644 index 0000000..acc0f39 --- /dev/null +++ b/mychest/src/main/resources/sql/schema.sql @@ -0,0 +1,53 @@ +-- MyChest Feature Module Schema +-- This schema creates the tables required for the limits, shares, audit, and backups features. + +-- Table: chest_limits +-- Stores custom slot limits per player +CREATE TABLE IF NOT EXISTS chest_limits ( + player_id CHAR(36) NOT NULL PRIMARY KEY, + slots INT NOT NULL DEFAULT 27, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Table: chest_shares +-- Stores chest sharing relationships between players +CREATE TABLE IF NOT EXISTS chest_shares ( + id INT AUTO_INCREMENT PRIMARY KEY, + chest_id INT NOT NULL, + owner_id CHAR(36) NOT NULL, + target_id CHAR(36) NOT NULL, + permissions VARCHAR(16) NOT NULL DEFAULT 'VIEW', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_share (chest_id, target_id), + INDEX idx_chest_id (chest_id), + INDEX idx_owner_id (owner_id), + INDEX idx_target_id (target_id) +); + +-- Table: chest_audit +-- Stores audit logs for chest operations +CREATE TABLE IF NOT EXISTS chest_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + player_id CHAR(36) NOT NULL, + action VARCHAR(64) NOT NULL, + details TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_player_id (player_id), + INDEX idx_action (action), + INDEX idx_created_at (created_at) +); + +-- Table: chest_backups +-- Stores chest backup data as JSON for recovery purposes +CREATE TABLE IF NOT EXISTS chest_backups ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + chest_id INT NOT NULL, + player_id CHAR(36) NOT NULL, + backup_data JSON NOT NULL, + backup_type VARCHAR(32) NOT NULL DEFAULT 'AUTO', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_chest_id (chest_id), + INDEX idx_player_id (player_id), + INDEX idx_created_at (created_at) +);