diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..8df9a76 --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + com.adzel.addyauthreborn + AddyAuthReborn + AddyAuth Reborn + 0.1-pre + + + + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + + maven-shade-plugin + 3.5.0 + + + package + + shade + + + true + + + at.favre.lib + com.adzel.addyauthreborn.shaded.bcrypt + + + + + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + sonatype-oss + https://oss.sonatype.org/content/repositories/releases/ + + + + + io.papermc.paper + paper-api + 1.20.4-R0.1-SNAPSHOT + provided + + + + 0.9.0 + 17 + 1.20.4-R0.1-SNAPSHOT + 3.3.3 + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..abd2e3e --- /dev/null +++ b/pom.xml @@ -0,0 +1,103 @@ + + 4.0.0 + + com.adzel.addyauthreborn + AddyAuthReborn + 0.1-pre + AddyAuth Reborn + + + 17 + 1.20.4-R0.1-SNAPSHOT + 0.9.0 + 3.3.3 + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + sonatype-oss + https://oss.sonatype.org/content/repositories/releases/ + + + + + + + io.papermc.paper + paper-api + ${paper.version} + provided + + + + + at.favre.lib + bcrypt + ${bcrypt.version} + + + + + org.mariadb.jdbc + mariadb-java-client + ${mariadb.version} + + + + + net.kyori + adventure-platform-bukkit + 4.3.2 + + + + + com.zaxxer + HikariCP + 5.1.0 + + + + + + + + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + shade + + true + + + + at.favre.lib + com.adzel.addyauthreborn.shaded.bcrypt + + + + + + + + + diff --git a/src/main/java/com/adzel/addyauthreborn/AddyAuthReborn.java b/src/main/java/com/adzel/addyauthreborn/AddyAuthReborn.java new file mode 100644 index 0000000..dcfe16c --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/AddyAuthReborn.java @@ -0,0 +1,117 @@ +package com.adzel.addyauthreborn; + +import com.adzel.addyauthreborn.auth.AuthManager; +import com.adzel.addyauthreborn.commands.LoginCommand; +import com.adzel.addyauthreborn.commands.LockCommand; +import com.adzel.addyauthreborn.commands.RegisterCommand; +import com.adzel.addyauthreborn.commands.AddyAuthCommand; +import com.adzel.addyauthreborn.listeners.LockListeners; +import com.adzel.addyauthreborn.listeners.AdminJoinListener; +import com.adzel.addyauthreborn.listeners.ConnectionListener; +import com.adzel.addyauthreborn.lock.AFKMonitor; +import com.adzel.addyauthreborn.lock.OfflineLockManager; +import com.adzel.addyauthreborn.updates.UpdateChecker; +import com.adzel.addyauthreborn.utils.DatabaseUtil; +import com.zaxxer.hikari.HikariDataSource; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import org.bukkit.plugin.java.JavaPlugin; + +public class AddyAuthReborn extends JavaPlugin { + + private static AddyAuthReborn instance; + private AuthManager authManager; + private AFKMonitor afkMonitor; + private OfflineLockManager offlineLockManager; + private BukkitAudiences adventure; + private boolean updateAvailable = false; + + @Override + public void onEnable() { + instance = this; + + saveDefaultConfig(); + + adventure = BukkitAudiences.create(this); + + if (!DatabaseUtil.initDatabase()) { + getLogger().severe("Failed to initialize database. Disabling plugin."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + authManager = new AuthManager(); + afkMonitor = new AFKMonitor(this); + offlineLockManager = new OfflineLockManager(this); + + // Commands + getCommand("register").setExecutor(new RegisterCommand(this)); + getCommand("login").setExecutor(new LoginCommand(this)); + getCommand("lock").setExecutor(new LockCommand(this)); + getCommand("addyauth").setExecutor(new AddyAuthCommand(this)); + + // Listeners + getServer().getPluginManager().registerEvents(new LockListeners(this), this); + getServer().getPluginManager().registerEvents(new AdminJoinListener(this), this); + getServer().getPluginManager().registerEvents(new ConnectionListener(this), this); // PlayerJoin/Leave for tracking AFK state + + // Update check + new UpdateChecker(this, "https://git.adzeldevelops.site/api/v1/repos/AdzelFirestar/AddyAuthReborn/releases/latest") + .getLatestVersion(latest -> { + String current = getDescription().getVersion(); + if (!current.equalsIgnoreCase(latest)) { + updateAvailable = true; + getLogger().warning("A new version of AddyAuth Reborn is available: " + latest); + } + }); + + getLogger().info("AddyAuth Reborn enabled successfully."); + } + + @Override + public void onDisable() { + if (adventure != null) { + adventure.close(); + } + if (afkMonitor != null) { + afkMonitor.shutdown(); // 🧼 clean up task + } + DatabaseUtil.shutdown(); + getLogger().info("AddyAuth Reborn has been disabled."); + } + + public static AddyAuthReborn getInstance() { + return instance; + } + + public AuthManager getAuthManager() { + return authManager; + } + + public AFKMonitor getAFKMonitor() { + return afkMonitor; + } + + public OfflineLockManager getOfflineLockManager() { + return offlineLockManager; + } + + public HikariDataSource getDataSource() { + return DatabaseUtil.getDataSource(); + } + + public boolean isUpdateAvailable() { + return updateAvailable; + } + + public int getMinPasswordLength() { + return getConfig().getInt("General Settings.Minimum Password Length", 5); + } + + public int getMaxPasswordLength() { + return getConfig().getInt("General Settings.Maximum Password Length", 15); + } + + public BukkitAudiences adventure() { + return adventure; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/auth/AuthManager.java b/src/main/java/com/adzel/addyauthreborn/auth/AuthManager.java new file mode 100644 index 0000000..2c76b9d --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/auth/AuthManager.java @@ -0,0 +1,61 @@ +package com.adzel.addyauthreborn.auth; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import at.favre.lib.crypto.bcrypt.BCrypt; + +public class AuthManager { + + private final AuthStorage storage; + private final Set lockedPlayers = ConcurrentHashMap.newKeySet(); + + public AuthManager() { + this.storage = new AuthStorage(); // will handle DB interactions + } + + public boolean isRegistered(UUID uuid) { + return storage.isRegistered(uuid); + } + + public boolean registerPassword(UUID uuid, String plainPassword) { + if (isRegistered(uuid)) return false; + + String hash = BCrypt.withDefaults().hashToString(12, plainPassword.toCharArray()); + return storage.savePassword(uuid, hash); + } + + public boolean verifyPassword(UUID uuid, String attempt) { + String hash = storage.getPasswordHash(uuid); + if (hash != null) { + BCrypt.Result result = BCrypt.verifyer().verify(attempt.toCharArray(), hash); + if (result.verified) { + unlock(uuid); // unlock on successful login + return true; + } + } + return false; + } + + public void lock(UUID uuid) { + lockedPlayers.add(uuid); + storage.setLastLockTime(uuid, System.currentTimeMillis()); + } + + public void unlock(UUID uuid) { + lockedPlayers.remove(uuid); + } + + public boolean isLocked(UUID uuid) { + return lockedPlayers.contains(uuid); + } + + public long getLastLockTime(UUID uuid) { + return storage.getLastLockTime(uuid); + } + + public AuthStorage getStorage() { + return storage; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/auth/AuthStorage.java b/src/main/java/com/adzel/addyauthreborn/auth/AuthStorage.java new file mode 100644 index 0000000..79154dd --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/auth/AuthStorage.java @@ -0,0 +1,120 @@ +package com.adzel.addyauthreborn.auth; + +import com.adzel.addyauthreborn.AddyAuthReborn; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; + +public class AuthStorage { + + private final AddyAuthReborn plugin = AddyAuthReborn.getInstance(); + + public AuthStorage() { + try (Connection conn = plugin.getDataSource().getConnection()) { + PreparedStatement ps = conn.prepareStatement( + "CREATE TABLE IF NOT EXISTS addyauth_users (" + + "uuid VARCHAR(36) PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "last_locked_at BIGINT DEFAULT 0)" + ); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed to create users table: " + e.getMessage()); + } + } + + public boolean isRegistered(UUID uuid) { + try (Connection conn = plugin.getDataSource().getConnection()) { + PreparedStatement ps = conn.prepareStatement( + "SELECT uuid FROM addyauth_users WHERE uuid = ?" + ); + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + boolean exists = rs.next(); + rs.close(); + ps.close(); + return exists; + } catch (SQLException e) { + plugin.getLogger().warning("Failed to check registration: " + e.getMessage()); + return false; + } + } + + public boolean savePassword(UUID uuid, String hashedPassword) { + try (Connection conn = plugin.getDataSource().getConnection()) { + PreparedStatement ps = conn.prepareStatement( + "INSERT INTO addyauth_users (uuid, password_hash) VALUES (?, ?)" + ); + ps.setString(1, uuid.toString()); + ps.setString(2, hashedPassword); + ps.executeUpdate(); + ps.close(); + return true; + } catch (SQLException e) { + plugin.getLogger().warning("Failed to save password: " + e.getMessage()); + return false; + } + } + + public String getPasswordHash(UUID uuid) { + try (Connection conn = plugin.getDataSource().getConnection()) { + PreparedStatement ps = conn.prepareStatement( + "SELECT password_hash FROM addyauth_users WHERE uuid = ?" + ); + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String hash = rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning("Failed to retrieve password hash: " + e.getMessage()); + } + return null; + } + + public boolean setLastLockTime(UUID uuid, long timestamp) { + try (Connection conn = plugin.getDataSource().getConnection()) { + PreparedStatement ps = conn.prepareStatement( + "UPDATE addyauth_users SET last_locked_at = ? WHERE uuid = ?" + ); + ps.setLong(1, timestamp); + ps.setString(2, uuid.toString()); + int updated = ps.executeUpdate(); + ps.close(); + return updated > 0; + } catch (SQLException e) { + plugin.getLogger().warning("Failed to update last lock time: " + e.getMessage()); + return false; + } + } + + public long getLastLockTime(UUID uuid) { + try (Connection conn = plugin.getDataSource().getConnection()) { + PreparedStatement ps = conn.prepareStatement( + "SELECT last_locked_at FROM addyauth_users WHERE uuid = ?" + ); + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + long timestamp = rs.getLong("last_locked_at"); + rs.close(); + ps.close(); + return timestamp; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning("Failed to fetch last lock time: " + e.getMessage()); + } + return 0; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/commands/AddyAuthCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/AddyAuthCommand.java new file mode 100644 index 0000000..6e55bea --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/commands/AddyAuthCommand.java @@ -0,0 +1,46 @@ +package com.adzel.addyauthreborn.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.adzel.addyauthreborn.utils.MessageUtil; + +public class AddyAuthCommand implements CommandExecutor { + + private final AddyAuthReborn plugin; + + public AddyAuthCommand(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + + if (!(sender instanceof Player player)) { + sender.sendMessage("This command can only be run by a player."); + return true; + } + + if (!player.hasPermission("addyauth.admin")) { + MessageUtil.send(player, "&cYou do not have permission to use this command."); + return true; + } + + if (args.length == 0) { + MessageUtil.send(player, "&eUsage: /addyauth reload"); + return true; + } + + if (args[0].equalsIgnoreCase("reload")) { + plugin.reloadConfig(); + MessageUtil.send(player, "&aAddyAuth Reborn config reloaded."); + return true; + } + + MessageUtil.send(player, "&cUnknown subcommand."); + return true; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/commands/LockCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/LockCommand.java new file mode 100644 index 0000000..3ccf43b --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/commands/LockCommand.java @@ -0,0 +1,34 @@ +package com.adzel.addyauthreborn.commands; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.adzel.addyauthreborn.utils.MessageUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class LockCommand implements CommandExecutor { + + private final AddyAuthReborn plugin; + + public LockCommand(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command."); + return true; + } + + if (!player.hasPermission("addyauth.lock")) { + MessageUtil.send(player, "&cYou do not have permission to lock yourself."); + return true; + } + + plugin.getAuthManager().lock(player.getUniqueId()); + MessageUtil.send(player, "&eYou are now locked. Use &f/login &eto unlock."); + return true; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/commands/LoginCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/LoginCommand.java new file mode 100644 index 0000000..c349c4c --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/commands/LoginCommand.java @@ -0,0 +1,51 @@ +package com.adzel.addyauthreborn.commands; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.adzel.addyauthreborn.utils.MessageUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class LoginCommand implements CommandExecutor { + + private final AddyAuthReborn plugin; + + public LoginCommand(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command."); + return true; + } + + if (!player.hasPermission("addyauth.login")) { + MessageUtil.send(player, "&cYou do not have permission to login."); + return true; + } + + if (args.length != 1) { + MessageUtil.send(player, "&cUsage: /login "); + return true; + } + + if (!plugin.getAuthManager().isRegistered(player.getUniqueId())) { + MessageUtil.send(player, "&eYou are not registered. Use &f/register &e first."); + return true; + } + + String password = args[0]; + boolean valid = plugin.getAuthManager().verifyPassword(player.getUniqueId(), password); + + if (!valid) { + MessageUtil.send(player, "&cIncorrect password."); + return true; + } + + MessageUtil.send(player, "&aYou are now logged in and unlocked."); + return true; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/commands/RegisterCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/RegisterCommand.java new file mode 100644 index 0000000..0b287ae --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/commands/RegisterCommand.java @@ -0,0 +1,58 @@ +package com.adzel.addyauthreborn.commands; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.adzel.addyauthreborn.utils.MessageUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class RegisterCommand implements CommandExecutor { + + private final AddyAuthReborn plugin; + + public RegisterCommand(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command."); + return true; + } + + if (!player.hasPermission("addyauth.register")) { + MessageUtil.send(player, "&cYou do not have permission to register."); + return true; + } + + if (args.length != 1) { + MessageUtil.send(player, "&cUsage: /register "); + return true; + } + + String password = args[0]; + int minLength = plugin.getMinPasswordLength(); + int maxLength = plugin.getMaxPasswordLength(); + + if (password.length() < minLength || password.length() > maxLength) { + MessageUtil.send(player, "&cPassword must be between " + minLength + " and " + maxLength + " characters."); + return true; + } + + if (plugin.getAuthManager().isRegistered(player.getUniqueId())) { + MessageUtil.send(player, "&eYou are already registered."); + return true; + } + + boolean success = plugin.getAuthManager().registerPassword(player.getUniqueId(), password); + if (success) { + MessageUtil.send(player, "&aYour password has been registered successfully."); + } else { + MessageUtil.send(player, "&cSomething went wrong. Please try again."); + } + + return true; + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/listeners/AdminJoinListener.java b/src/main/java/com/adzel/addyauthreborn/listeners/AdminJoinListener.java new file mode 100644 index 0000000..8bffbf5 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/listeners/AdminJoinListener.java @@ -0,0 +1,31 @@ +package com.adzel.addyauthreborn.listeners; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class AdminJoinListener implements Listener { + + private final AddyAuthReborn plugin; + + public AdminJoinListener(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onAdminJoin(PlayerJoinEvent event) { + if (!plugin.isUpdateAvailable()) return; + + if (event.getPlayer().hasPermission("addyauth.admin")) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + event.getPlayer().sendMessage(ChatColor.YELLOW + "[AddyAuth Reborn] " + + ChatColor.RED + "A new version is available!"); + event.getPlayer().sendMessage(ChatColor.GRAY + "Check: " + + ChatColor.BLUE + "https://git.adzeldevelops.site/Adzel/AddyAuth-Reborn/releases/latest"); + }, 40L); // delay a bit so it doesn't clash with login spam + } + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/listeners/ConnectionListener.java b/src/main/java/com/adzel/addyauthreborn/listeners/ConnectionListener.java new file mode 100644 index 0000000..77ff5f5 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/listeners/ConnectionListener.java @@ -0,0 +1,32 @@ +package com.adzel.addyauthreborn.listeners; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.UUID; + +public class ConnectionListener implements Listener { + + private final AddyAuthReborn plugin; + + public ConnectionListener(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + plugin.getOfflineLockManager().evaluateLogin(uuid); + plugin.getAFKMonitor().updateActivity(uuid); // start tracking afk immediately + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + plugin.getOfflineLockManager().recordLogout(uuid); + plugin.getAFKMonitor().removePlayer(uuid); // clean up + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/listeners/LockListeners.java b/src/main/java/com/adzel/addyauthreborn/listeners/LockListeners.java new file mode 100644 index 0000000..b519b97 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/listeners/LockListeners.java @@ -0,0 +1,90 @@ +package com.adzel.addyauthreborn.listeners; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.adzel.addyauthreborn.utils.MessageUtil; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.*; + +public class LockListeners implements Listener { + + private final AddyAuthReborn plugin; + + public LockListeners(AddyAuthReborn plugin) { + this.plugin = plugin; + } + + private boolean isLocked(Player player) { + return plugin.getAuthManager().isLocked(player.getUniqueId()); + } + + private void updateIfNotLocked(Player player) { + if (!isLocked(player)) { + plugin.getAFKMonitor().updateActivity(player.getUniqueId()); + } + } + + @EventHandler + public void onMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + if (isLocked(player) && event.getFrom().distanceSquared(event.getTo()) > 0) { + event.setCancelled(true); + } else if (event.getFrom().distanceSquared(event.getTo()) > 1) { + updateIfNotLocked(player); + } + } + + @EventHandler + public void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + if (isLocked(player)) { + event.setCancelled(true); + MessageUtil.send(player, "&cYou cannot chat while locked."); + } else { + updateIfNotLocked(player); + } + } + + @EventHandler + public void onClick(InventoryClickEvent event) { + if (event.getWhoClicked() instanceof Player player) { + if (isLocked(player)) { + event.setCancelled(true); + } else { + updateIfNotLocked(player); + } + } + } + + @EventHandler + public void onDrop(PlayerDropItemEvent event) { + Player player = event.getPlayer(); + if (isLocked(player)) { + event.setCancelled(true); + } else { + updateIfNotLocked(player); + } + } + + @EventHandler + public void onPickup(PlayerAttemptPickupItemEvent event) { + Player player = event.getPlayer(); + if (isLocked(player)) { + event.setCancelled(true); + } else { + updateIfNotLocked(player); + } + } + + @EventHandler + public void onInteract(PlayerInteractEvent event) { + Player player = event.getPlayer(); + if (isLocked(player)) { + event.setCancelled(true); + } else { + updateIfNotLocked(player); + } + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/lock/AFKMonitor.java b/src/main/java/com/adzel/addyauthreborn/lock/AFKMonitor.java new file mode 100644 index 0000000..ae024f6 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/lock/AFKMonitor.java @@ -0,0 +1,73 @@ +package com.adzel.addyauthreborn.lock; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.adzel.addyauthreborn.utils.MessageUtil; + +public class AFKMonitor { + + private final AddyAuthReborn plugin; + private final Map lastActivityMap = new ConcurrentHashMap<>(); + private final long timeoutMillis; + private final boolean enabled; + private BukkitRunnable monitorTask; + + public AFKMonitor(AddyAuthReborn plugin) { + this.plugin = plugin; + this.enabled = plugin.getConfig().getBoolean("Time Based Settings.AFK Lock.Enabled", true); + this.timeoutMillis = plugin.getConfig().getLong("Time Based Settings.AFK Lock.Auto Lock", 30) * 60 * 1000L; + + if (enabled) { + startMonitor(); + } + } + + public void updateActivity(UUID uuid) { + lastActivityMap.put(uuid, System.currentTimeMillis()); + } + + public void remove(UUID uuid) { + lastActivityMap.remove(uuid); + } + + public void removePlayer(UUID uuid) { + lastActivityMap.remove(uuid); + } + + private void startMonitor() { + monitorTask = new BukkitRunnable() { + @Override + public void run() { + long now = System.currentTimeMillis(); + + for (Player player : Bukkit.getOnlinePlayers()) { + UUID uuid = player.getUniqueId(); + + if (!plugin.getAuthManager().isRegistered(uuid)) continue; + if (plugin.getAuthManager().isLocked(uuid)) continue; + + long lastActive = lastActivityMap.getOrDefault(uuid, now); + if ((now - lastActive) >= timeoutMillis) { + plugin.getAuthManager().lock(uuid); // ✅ Stores timestamp + MessageUtil.send(player, "&cYou have been locked due to AFK timeout."); + } + } + } + }; + monitorTask.runTaskTimer(plugin, 20L * 60, 20L * 60); // every 60 seconds + } + + public void shutdown() { + if (monitorTask != null) { + monitorTask.cancel(); + } + lastActivityMap.clear(); + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/lock/OfflineLockManager.java b/src/main/java/com/adzel/addyauthreborn/lock/OfflineLockManager.java new file mode 100644 index 0000000..f208e2c --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/lock/OfflineLockManager.java @@ -0,0 +1,40 @@ +package com.adzel.addyauthreborn.lock; + +import com.adzel.addyauthreborn.AddyAuthReborn; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class OfflineLockManager { + + private final AddyAuthReborn plugin; + private final Map logoutTimestamps = new ConcurrentHashMap<>(); + private final long offlineThresholdMillis; + + public OfflineLockManager(AddyAuthReborn plugin) { + this.plugin = plugin; + long minutes = plugin.getConfig().getLong("Time Based Settings.Offline Lock.Auto Lock", 120); + this.offlineThresholdMillis = minutes * 60_000; + } + + public void recordLogout(UUID uuid) { + logoutTimestamps.put(uuid, System.currentTimeMillis()); + } + + public void evaluateLogin(UUID uuid) { + if (!plugin.getConfig().getBoolean("Time Based Settings.Offline Lock.Enabled", true)) return; + + Long logoutTime = logoutTimestamps.get(uuid); + if (logoutTime == null) return; + + long elapsed = System.currentTimeMillis() - logoutTime; + if (elapsed >= offlineThresholdMillis) { + plugin.getAuthManager().lock(uuid); // ✅ Stores timestamp + } + } + + public void forget(UUID uuid) { + logoutTimestamps.remove(uuid); + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/updates/UpdateChecker.java b/src/main/java/com/adzel/addyauthreborn/updates/UpdateChecker.java new file mode 100644 index 0000000..6da5884 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/updates/UpdateChecker.java @@ -0,0 +1,67 @@ +package com.adzel.addyauthreborn.updates; + +import com.adzel.addyauthreborn.AddyAuthReborn; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.function.Consumer; + +public class UpdateChecker { + + private final AddyAuthReborn plugin; + private final String apiUrl; + + public UpdateChecker(AddyAuthReborn plugin, String apiUrl) { + this.plugin = plugin; + this.apiUrl = apiUrl; + } + + public void getLatestVersion(Consumer callback) { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + try { + // Gitea's latest release endpoint returns a JSON object + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestProperty("Accept", "application/json"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + + if (connection.getResponseCode() != 200) { + plugin.getLogger().warning("Update check failed: Invalid response from Gitea (" + connection.getResponseCode() + ")"); + return; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder json = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + json.append(line); + } + reader.close(); + + String latest = parseTagName(json.toString()); + if (latest != null) { + callback.accept(latest); + } else { + plugin.getLogger().warning("Update check failed: Could not parse latest tag."); + } + + } catch (Exception e) { + plugin.getLogger().warning("Update check failed: " + e.getMessage()); + } + }); + } + + private String parseTagName(String json) { + // crude but fast — this works with Gitea’s /releases/latest API + String key = "\"tag_name\":\""; + int index = json.indexOf(key); + if (index == -1) return null; + + int start = index + key.length(); + int end = json.indexOf("\"", start); + return end == -1 ? null : json.substring(start, end); + } +} \ No newline at end of file diff --git a/src/main/java/com/adzel/addyauthreborn/utils/DatabaseUtil.java b/src/main/java/com/adzel/addyauthreborn/utils/DatabaseUtil.java new file mode 100644 index 0000000..49b0654 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/utils/DatabaseUtil.java @@ -0,0 +1,78 @@ +package com.adzel.addyauthreborn.utils; + +import com.adzel.addyauthreborn.AddyAuthReborn; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public class DatabaseUtil { + + private static HikariDataSource dataSource; + + public static boolean initDatabase() { + AddyAuthReborn plugin = AddyAuthReborn.getInstance(); + + String type = plugin.getConfig().getString("Database.Type", "sqlite").toLowerCase(); + HikariConfig config = new HikariConfig(); + + try { + switch (type) { + case "mysql": + case "mariadb": + config.setJdbcUrl("jdbc:mariadb://" + + plugin.getConfig().getString("Database.Host", "localhost") + ":" + + plugin.getConfig().getInt("Database.Port", 3306) + "/" + + plugin.getConfig().getString("Database.Name", "addyauth")); + config.setUsername(plugin.getConfig().getString("Database.Username", "root")); + config.setPassword(plugin.getConfig().getString("Database.Password", "")); + config.setDriverClassName("org.mariadb.jdbc.Driver"); + break; + + case "sqlite": + default: + config.setJdbcUrl("jdbc:sqlite:" + plugin.getDataFolder() + "/addyauth.db"); + config.setDriverClassName("org.sqlite.JDBC"); + break; + } + + config.setMaximumPoolSize(5); + config.setPoolName("AddyAuthPool"); + dataSource = new HikariDataSource(config); + + try (Connection conn = dataSource.getConnection()) { + createTables(conn); + } + + return true; + + } catch (Exception e) { + plugin.getLogger().severe("Database initialization failed: " + e.getMessage()); + return false; + } + } + + private static void createTables(Connection conn) throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate(""" + CREATE TABLE IF NOT EXISTS addyauth_users ( + uuid VARCHAR(36) PRIMARY KEY, + password_hash TEXT NOT NULL, + last_locked_at BIGINT DEFAULT 0 + ) + """); + } + } + + public static HikariDataSource getDataSource() { + return dataSource; + } + + public static void shutdown() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + } + } +} diff --git a/src/main/java/com/adzel/addyauthreborn/utils/MessageUtil.java b/src/main/java/com/adzel/addyauthreborn/utils/MessageUtil.java new file mode 100644 index 0000000..dadde88 --- /dev/null +++ b/src/main/java/com/adzel/addyauthreborn/utils/MessageUtil.java @@ -0,0 +1,25 @@ +package com.adzel.addyauthreborn.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +public class MessageUtil { + + private static final MiniMessage mini = MiniMessage.miniMessage(); + private static final LegacyComponentSerializer legacy = LegacyComponentSerializer.builder() + .character('&') + .hexColors() + .build(); + + public static void send(Player player, String message) { + // Convert &x style to MiniMessage + Component component = mini.deserialize(legacy.serialize(legacy.deserialize(message))); + player.sendMessage(component); + } + + public static Component parse(String message) { + return mini.deserialize(legacy.serialize(legacy.deserialize(message))); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..9842291 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,53 @@ +name: AddyAuthReborn +version: 0.1-pre +main: com.adzel.addyauthreborn.AddyAuthReborn +api-version: 1.20 +author: AdzelFirestar +description: > + A secure authentication system for staff accounts, requiring a password before access to movement, interaction, and chat. + +commands: + register: + description: Register your staff password for the first time. + usage: /register + permission: addyauth.register + + login: + description: Login using your staff password to unlock yourself. + usage: /login + permission: addyauth.login + + lock: + description: Lock your own account until you login again. + usage: /lock + permission: addyauth.lock + + addyauth: + description: Admin command for AddyAuth (reload, future diagnostics, etc.) + usage: /addyauth reload + permission: addyauth.admin + +permissions: + addyauth.*: + description: Gives access to all AddyAuth commands. + children: + addyauth.register: true + addyauth.login: true + addyauth.lock: true + addyauth.admin: true + + addyauth.register: + description: Allows staff to register their password. + default: op + + addyauth.login: + description: Allows staff to login to unlock themselves. + default: op + + addyauth.lock: + description: Allows staff to lock their account. + default: op + + addyauth.admin: + description: Grants access to admin commands and update notifications. + default: op diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..9842291 --- /dev/null +++ b/target/classes/plugin.yml @@ -0,0 +1,53 @@ +name: AddyAuthReborn +version: 0.1-pre +main: com.adzel.addyauthreborn.AddyAuthReborn +api-version: 1.20 +author: AdzelFirestar +description: > + A secure authentication system for staff accounts, requiring a password before access to movement, interaction, and chat. + +commands: + register: + description: Register your staff password for the first time. + usage: /register + permission: addyauth.register + + login: + description: Login using your staff password to unlock yourself. + usage: /login + permission: addyauth.login + + lock: + description: Lock your own account until you login again. + usage: /lock + permission: addyauth.lock + + addyauth: + description: Admin command for AddyAuth (reload, future diagnostics, etc.) + usage: /addyauth reload + permission: addyauth.admin + +permissions: + addyauth.*: + description: Gives access to all AddyAuth commands. + children: + addyauth.register: true + addyauth.login: true + addyauth.lock: true + addyauth.admin: true + + addyauth.register: + description: Allows staff to register their password. + default: op + + addyauth.login: + description: Allows staff to login to unlock themselves. + default: op + + addyauth.lock: + description: Allows staff to lock their account. + default: op + + addyauth.admin: + description: Grants access to admin commands and update notifications. + default: op diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..15e8c9b --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=AddyAuthReborn +groupId=com.adzel.addyauthreborn +version=0.1-pre diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..c95763a --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,16 @@ +com\adzel\addyauthreborn\lock\AFKMonitor$1.class +com\adzel\addyauthreborn\commands\LoginCommand.class +com\adzel\addyauthreborn\listeners\ConnectionListener.class +com\adzel\addyauthreborn\utils\DatabaseUtil.class +com\adzel\addyauthreborn\auth\AuthStorage.class +com\adzel\addyauthreborn\commands\LockCommand.class +com\adzel\addyauthreborn\lock\AFKMonitor.class +com\adzel\addyauthreborn\commands\AddyAuthCommand.class +com\adzel\addyauthreborn\commands\RegisterCommand.class +com\adzel\addyauthreborn\auth\AuthManager.class +com\adzel\addyauthreborn\listeners\LockListeners.class +com\adzel\addyauthreborn\AddyAuthReborn.class +com\adzel\addyauthreborn\listeners\AdminJoinListener.class +com\adzel\addyauthreborn\utils\MessageUtil.class +com\adzel\addyauthreborn\updates\UpdateChecker.class +com\adzel\addyauthreborn\lock\OfflineLockManager.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..7ec1f85 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,15 @@ +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\AddyAuthReborn.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\listeners\AdminJoinListener.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\listeners\LockListeners.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\listeners\ConnectionListener.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\LockCommand.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\lock\OfflineLockManager.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\utils\MessageUtil.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\LoginCommand.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\updates\UpdateChecker.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\AddyAuthCommand.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\utils\DatabaseUtil.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\auth\AuthStorage.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\auth\AuthManager.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\RegisterCommand.java +C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\lock\AFKMonitor.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29