From 5792c817a5c1cd22a9ac0b6414807da1c3b6eefb Mon Sep 17 00:00:00 2001 From: Christian 'ketura' McCarty Date: Thu, 9 Mar 2023 02:55:45 -0600 Subject: [PATCH] Adding ability for admins to reset the passwords for users. --- .../async/handler/AdminRequestHandler.java | 23 ++- .../async/handler/LoginRequestHandler.java | 6 +- .../main/web/includes/admin/userAdmin.html | 9 + .../src/main/web/js/gemp-022/communication.js | 28 ++- .../src/main/web/js/gemp-022/manage.js | 15 ++ .../src/main/web/src/Login/login.js | 36 +++- .../com/gempukku/lotro/common/DBDefs.java | 13 ++ .../java/com/gempukku/lotro/game/Player.java | 67 ++++++- .../gempukku/lotro/db/CachedPlayerDAO.java | 8 + .../com/gempukku/lotro/db/DbPlayerDAO.java | 179 +++++++++++++++--- .../java/com/gempukku/lotro/db/PlayerDAO.java | 2 + .../gempukku/lotro/service/AdminService.java | 12 ++ 12 files changed, 353 insertions(+), 45 deletions(-) diff --git a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/AdminRequestHandler.java b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/AdminRequestHandler.java index 96e91b0e6..69a8e1035 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/AdminRequestHandler.java +++ b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/AdminRequestHandler.java @@ -100,6 +100,8 @@ public class AdminRequestHandler extends LotroServerRequestHandler implements Ur addItemsToCollection(request, responseWriter); } else if (uri.equals("/banUser") && request.method() == HttpMethod.POST) { banUser(request, responseWriter); + } else if (uri.equals("/resetUserPassword") && request.method() == HttpMethod.POST) { + resetUserPassword(request, responseWriter); } else if (uri.equals("/banMultiple") && request.method() == HttpMethod.POST) { banMultiple(request, responseWriter); } else if (uri.equals("/banUserTemp") && request.method() == HttpMethod.POST) { @@ -161,6 +163,25 @@ public class AdminRequestHandler extends LotroServerRequestHandler implements Ur return "OK"; } + private void resetUserPassword(HttpRequest request, ResponseWriter responseWriter) throws Exception { + validateAdmin(request); + + HttpPostRequestDecoder postDecoder = new HttpPostRequestDecoder(request); + try { + String login = getFormParameterSafely(postDecoder, "login"); + + if (login==null) + throw new HttpProcessingException(400); + + if (!_adminService.resetUserPassword(login)) + throw new HttpProcessingException(404); + + responseWriter.writeHtmlResponse("OK"); + } finally { + postDecoder.destroy(); + } + } + private void banUser(HttpRequest request, ResponseWriter responseWriter) throws Exception { validateAdmin(request); @@ -174,7 +195,7 @@ public class AdminRequestHandler extends LotroServerRequestHandler implements Ur if (!_adminService.banUser(login)) throw new HttpProcessingException(404); - responseWriter.writeHtmlResponse("OK"); + responseWriter.writeHtmlResponse("OK"); } finally { postDecoder.destroy(); } diff --git a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/LoginRequestHandler.java b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/LoginRequestHandler.java index d526906db..f675c78bc 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/LoginRequestHandler.java +++ b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/LoginRequestHandler.java @@ -3,6 +3,7 @@ package com.gempukku.lotro.async.handler; import com.gempukku.lotro.async.HttpProcessingException; import com.gempukku.lotro.async.ResponseWriter; import com.gempukku.lotro.game.Player; +import com.mysql.cj.util.StringUtils; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; @@ -30,7 +31,10 @@ public class LoginRequestHandler extends LotroServerRequestHandler implements Ur Player player = _playerDao.loginUser(login, password); if (player != null) { - if (player.getType().contains("u")) { + if (StringUtils.isNullOrEmpty(player.getPassword())) { + throw new HttpProcessingException(202); + } + if (player.getType().contains(Player.Type.USER.getValue())) { final Date bannedUntil = player.getBannedUntil(); if (bannedUntil != null && bannedUntil.after(new Date())) throw new HttpProcessingException(409); diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/includes/admin/userAdmin.html b/gemp-lotr/gemp-lotr-async/src/main/web/includes/admin/userAdmin.html index 67daeb6e2..5166ff73f 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/includes/admin/userAdmin.html +++ b/gemp-lotr/gemp-lotr-async/src/main/web/includes/admin/userAdmin.html @@ -3,6 +3,15 @@
+

Reset User Password

+
+ Name (case-sensitive):
+ Ready. +
+ +

Ban User

Permanently

diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/communication.js b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/communication.js index 5134b27ce..6949f7cc6 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/communication.js +++ b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/communication.js @@ -52,6 +52,16 @@ var GempLotrCommunication = Class.extend({ callback(xml); }; }, + + deliveryCheckStatus:function (callback) { + var that = this; + return function (xml, status, request) { + var delivery = request.getResponseHeader("Delivery-Service-Package"); + if (delivery == "true" && window.deliveryService != null) + that.getDelivery(window.deliveryService); + callback(xml, request.status); + }; + }, getGameHistory:function (start, count, callback, errorMap) { $.ajax({ @@ -817,6 +827,20 @@ var GempLotrCommunication = Class.extend({ }); }, + resetUserPassword:function (login, callback, errorMap) { + $.ajax({ + type:"POST", + url:this.url + "/admin/resetUserPassword", + cache:false, + data:{ + login:login + }, + success:this.deliveryCheck(callback), + error:this.errorCheck(errorMap), + dataType:"html" + }); + }, + permabanUser:function (login, callback, errorMap) { $.ajax({ type:"POST", @@ -1050,7 +1074,7 @@ var GempLotrCommunication = Class.extend({ login:login, password:password, participantId:getUrlParam("participantId")}, - success:this.deliveryCheck(callback), + success:this.deliveryCheckStatus(callback), error:this.errorCheck(errorMap), dataType:"html" }); @@ -1064,7 +1088,7 @@ var GempLotrCommunication = Class.extend({ login:login, password:password, participantId:getUrlParam("participantId")}, - success:this.deliveryCheck(callback), + success:this.deliveryCheckStatus(callback), error:this.errorCheck(errorMap), dataType:"html" }); diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/manage.js b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/manage.js index cf5a81100..ef2a7b4a1 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/manage.js +++ b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/manage.js @@ -1,6 +1,21 @@ $("banMain").ready( function () { + $("#reset-button").button().click( + function () { + let execute = confirm("Are you sure you want to reset the password for '" + $("#reset-input").val() + "'? This action cannot be undone."); + + if(!execute) + return; + + let resultdiv = $("#reset-result"); + resultdiv.html("Processing..."); + + hall.comm.resetUserPassword($("#reset-input").val(), function (string) { + resultdiv.html(string); + }, banErrorMap(resultdiv)); + }); + $("#permaban-button").button().click( function () { let resultdiv = $("#permaban-result"); diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/src/Login/login.js b/gemp-lotr/gemp-lotr-async/src/main/web/src/Login/login.js index a990903a9..58fad4380 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/src/Login/login.js +++ b/gemp-lotr/gemp-lotr-async/src/main/web/src/Login/login.js @@ -10,14 +10,22 @@ function register() { if (password != password2) { $(".error").html("Password and Password repeated are different! Try again"); } else { - comm.register(login, password, function () { - location.href = "/gemp-lotr/hall.html"; + comm.register(login, password, function (_, status) { + if(status == "202") { + $(".error").html("Your password has successfully been reset! Please refresh the page and log in."); + } + else { + location.href = "/gemp-lotr/hall.html"; + } }, { "0": function () { alert("Unable to connect to server, either server is down or there is a problem" + " with your internet connection"); }, + "202": function () { + $(".error").html("Your password has successfully been reset! Please refresh the page and log in."); + }, "400": function () { $(".error").html("Login is invalid. Login must be between 2-10 characters long, and contain only
" + " english letters, numbers or _ (underscore) and - (dash) characters."); @@ -45,24 +53,38 @@ function registrationScreen() { function login() { var login = $("#login").val(); var password = $("#password").val(); - comm.login(login, password, function () { - location.href = "/gemp-lotr/hall.html"; + comm.login(login, password, function (_, status) { + if(status == "202") { + registrationScreen(); + $("#registerButton").html("Update Password"); + $(".error").html("Your password must be reset. Please enter a new password."); + $("#login").val(login); + } + else { + location.href = "/gemp-lotr/hall.html"; + } }, { "0": function () { alert("Unable to connect to server, either server is down or there is a problem" + " with your internet connection"); }, + "202": function () { + registrationScreen(); + $("#registerButton").html("Update Password"); + $(".error").html("Your password must be reset. Please enter a new password."); + $("#login").val(login); + }, "401": function () { $(".error").html("Invalid username or password. Try again."); loginScreen(); }, "403": function () { - $(".error").html("You have been permanently banned. If you think it was a mistake you can try sending a private message to Merrick_H on TLHH forums."); + $(".error").html("You have been permanently banned. If you think it was a mistake please appeal with dmaz or ketura on TLHH forums."); + $(".error").html("You have been temporarily banned. You can try logging in at a later time. If you think it was a mistake please appeal with dmaz or ketura on Forgot your password? Contact ketura on the PC Discord."); } $(document).ready( diff --git a/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/DBDefs.java b/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/DBDefs.java index b10e8d936..b358cc56a 100644 --- a/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/DBDefs.java +++ b/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/DBDefs.java @@ -64,6 +64,19 @@ public class DBDefs { public static class Player { public int id; public String name; + public String password; + public String type; + public Integer last_login_reward; + public Integer banned_until; + public String create_ip; + public String last_ip; + + public Date GetBannedUntilDate() + { + if(banned_until == null) + return null; + return new Date(banned_until); + } } public static class FormatStats { diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/Player.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/Player.java index 1642c101e..7df803d0b 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/Player.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/game/Player.java @@ -1,8 +1,61 @@ package com.gempukku.lotro.game; +import com.gempukku.lotro.common.DBDefs; + +import java.util.ArrayList; import java.util.Date; +import java.util.List; +import java.util.Objects; public class Player { + + public enum Type { + ADMIN("a"), + //wow this is such a better idea than a blank column + //DEACTIVATED("d"), + LEAGUE_ADMIN("l"), + PLAY_TESTER("p"), + //PLAY_TESTING_ADMIN("t"), + //COMMENTATOR("c"), + //COMMENTATOR_ADMIN("m"), + UNBANNED("n"), + USER("u"); + + private final String _value; + + Type(String value) { + _value = value; + } + + public String getValue() { + return _value; + } + + public String toString() { + return getValue(); + } + + public static List getTypes(String typeString) { + List types = new ArrayList<>(); + for (Type type : values()) { + if (typeString.contains(type.getValue())) { + types.add(type); + } + } + return types; + } + + public static String getTypeString(List types) { + StringBuilder sb = new StringBuilder(); + for (Type type : values()) { + if (types.contains(type)) { + sb.append(type.getValue()); + } + } + return sb.toString(); + } + } + private final int _id; private final String _name; private final String _password; @@ -12,6 +65,10 @@ public class Player { private final String _createIp; private final String _lastIp; + public Player(DBDefs.Player def) { + this(def.id, def.name, def.password, def.type, def.last_login_reward, def.GetBannedUntilDate(), def.create_ip, def.last_ip); + } + public Player(int id, String name, String password, String type, Integer lastLoginReward, Date bannedUntil, String createIp, String lastIp) { _id = id; _name = name; @@ -66,7 +123,7 @@ public class Player { Player player = (Player) o; - if (_name != null ? !_name.equals(player._name) : player._name != null) return false; + if (!Objects.equals(_name, player._name)) return false; return true; } @@ -76,15 +133,15 @@ public class Player { return _name != null ? _name.hashCode() : 0; } - public PlayerDefinition GetUserInfo() { - return new PlayerDefinition(_name, _type); + public PlayerInfo GetUserInfo() { + return new PlayerInfo(_name, _type); } - public class PlayerDefinition { + public class PlayerInfo { public String name; public String type; - public PlayerDefinition(String name, String info) { + public PlayerInfo(String name, String info) { this.name = name; type = info; } diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedPlayerDAO.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedPlayerDAO.java index d497aca06..f3d906f0e 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedPlayerDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/CachedPlayerDAO.java @@ -30,6 +30,14 @@ public class CachedPlayerDAO implements PlayerDAO, Cached { return _playerById.size() + _playerByName.size(); } + @Override + public boolean resetUserPassword(String login) throws SQLException { + final boolean success = _delegate.resetUserPassword(login); + if (success) + clearCache(); + return success; + } + @Override public boolean banPlayerPermanently(String login) throws SQLException { final boolean success = _delegate.banPlayerPermanently(login); diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbPlayerDAO.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbPlayerDAO.java index 5d83c6219..9765aa70d 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbPlayerDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/DbPlayerDAO.java @@ -13,7 +13,19 @@ import java.util.*; public class DbPlayerDAO implements PlayerDAO { private final String validLoginChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; - private final String _selectPlayer = "select id, name, password, type, last_login_reward, banned_until, create_ip, last_ip from player"; + private final String _selectPlayer = """ + SELECT + id, + name, + password, + type, + last_login_reward, + banned_until, + create_ip, + last_ip + FROM player + """; + private final DbAccess _dbAccess; @@ -95,6 +107,29 @@ public class DbPlayerDAO implements PlayerDAO { } } + @Override + public boolean resetUserPassword(String login) throws SQLException { + try { + Sql2o db = new Sql2o(_dbAccess.getDataSource()); + + try (org.sql2o.Connection conn = db.beginTransaction()) { + String sql = """ + UPDATE player + SET password = '' + WHERE name = :login + """; + conn.createQuery(sql) + .addParameter("login", login) + .executeUpdate(); + + conn.commit(); + return true; + } + } catch (Exception ex) { + throw new RuntimeException("Unable to reset password", ex); + } + } + @Override public boolean banPlayerPermanently(String login) throws SQLException { try (Connection conn = _dbAccess.getDataSource().getConnection()) { @@ -152,17 +187,29 @@ public class DbPlayerDAO implements PlayerDAO { @Override public Player loginUser(String login, String password) throws SQLException { - try (Connection conn = _dbAccess.getDataSource().getConnection()) { - try (PreparedStatement statement = conn.prepareStatement(_selectPlayer + " where name=? and password=?")) { - statement.setString(1, login); - statement.setString(2, encodePassword(password)); - try (ResultSet rs = statement.executeQuery()) { - if (rs.next()) { - return getPlayerFromResultSet(rs); - } else - return null; - } + + try { + Sql2o db = new Sql2o(_dbAccess.getDataSource()); + + try (org.sql2o.Connection conn = db.open()) { + String sql = _selectPlayer + + """ + WHERE name = :login + AND (password = :password OR password = '') + """; + List result = conn.createQuery(sql) + .addParameter("login", login) + .addParameter("password", encodePassword(password)) + .executeAndFetch(DBDefs.Player.class); + + var def = result.stream().findFirst().orElse(null); + if(def == null) + return null; + + return new Player(def); } + } catch (Exception ex) { + throw new RuntimeException("Unable to retrieve login entries", ex); } } @@ -217,23 +264,61 @@ public class DbPlayerDAO implements PlayerDAO { @Override public synchronized boolean registerUser(String login, String password, String remoteAddr) throws SQLException, LoginInvalidException { - boolean result = validateLogin(login); - if (!result) + if (!validLoginName(login)) return false; - try (Connection conn = _dbAccess.getDataSource().getConnection()) { - try (PreparedStatement statement = conn.prepareStatement("insert into player (name, password, type, create_ip) values (?, ?, ?, ?)")) { - statement.setString(1, login); - statement.setString(2, encodePassword(password)); - statement.setString(3, "u"); - statement.setString(4, remoteAddr); - statement.execute(); + if(loginExists(login)) { + if(!needsPasswordReset(login)) + return false; + + //Login exists but has a blank/null password, meaning this user is actually performing a password reset + try { + Sql2o db = new Sql2o(_dbAccess.getDataSource()); + + try (org.sql2o.Connection conn = db.beginTransaction()) { + String sql = """ + UPDATE player + SET password = :password + WHERE name = :login + """; + conn.createQuery(sql) + .addParameter("login", login) + .addParameter("password", encodePassword(password)) + .executeUpdate(); + + conn.commit(); + return true; + } + } catch (Exception ex) { + throw new RuntimeException("Unable to update password", ex); + } + } + + + try { + Sql2o db = new Sql2o(_dbAccess.getDataSource()); + + try (org.sql2o.Connection conn = db.beginTransaction()) { + String sql = """ + INSERT INTO player (name, password, type, create_ip) + VALUES (:login, :password, :type, :create_ip) + """; + conn.createQuery(sql) + .addParameter("login", login) + .addParameter("password", encodePassword(password)) + .addParameter("type", Player.Type.USER.toString()) + .addParameter("create_ip", remoteAddr) + .executeUpdate(); + + conn.commit(); return true; } + } catch (Exception ex) { + throw new RuntimeException("Unable to insert new user", ex); } } - private boolean validateLogin(String login) throws SQLException, LoginInvalidException { + private boolean validLoginName(String login) throws LoginInvalidException { if (login.length() < 2 || login.length() > 30) throw new LoginInvalidException(); for (int i = 0; i < login.length(); i++) { @@ -246,16 +331,50 @@ public class DbPlayerDAO implements PlayerDAO { if (lowerCase.startsWith("admin") || lowerCase.startsWith("guest") || lowerCase.startsWith("system") || lowerCase.startsWith("bye")) return false; - try (Connection conn = _dbAccess.getDataSource().getConnection()) { - try (PreparedStatement statement = conn.prepareStatement("select id, name from player where LOWER(name)=?")) { - statement.setString(1, lowerCase); - try (ResultSet rs = statement.executeQuery()) { - if (rs.next()) { - return false; - } else - return true; - } + return true; + } + + private boolean loginExists(String login) throws SQLException { + + try { + Sql2o db = new Sql2o(_dbAccess.getDataSource()); + + try (org.sql2o.Connection conn = db.open()) { + String sql = _selectPlayer + + """ + WHERE LOWER(name) = :login + """; + List result = conn.createQuery(sql) + .addParameter("login", login.toLowerCase()) + .executeAndFetch(DBDefs.Player.class); + + var def = result.stream().findFirst().orElse(null); + return def != null; } + } catch (Exception ex) { + throw new RuntimeException("Unable to retrieve password reset entries", ex); + } + } + + private boolean needsPasswordReset(String login) throws SQLException { + try { + Sql2o db = new Sql2o(_dbAccess.getDataSource()); + + try (org.sql2o.Connection conn = db.open()) { + String sql = _selectPlayer + + """ + WHERE LOWER(name) = :login + AND (password = '' OR password IS NULL) + """; + List result = conn.createQuery(sql) + .addParameter("login", login.toLowerCase()) + .executeAndFetch(DBDefs.Player.class); + + var def = result.stream().findFirst().orElse(null); + return def != null; + } + } catch (Exception ex) { + throw new RuntimeException("Unable to retrieve password reset entries", ex); } } diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/PlayerDAO.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/PlayerDAO.java index 1dfba9b15..a78847551 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/PlayerDAO.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/db/PlayerDAO.java @@ -12,6 +12,8 @@ public interface PlayerDAO { public Player getPlayer(String playerName); + public boolean resetUserPassword(String login) throws SQLException; + public boolean banPlayerPermanently(String login) throws SQLException; public boolean banPlayerTemporarily(String login, long dateTo) throws SQLException; diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/service/AdminService.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/service/AdminService.java index 2567970aa..c05d9a19c 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/service/AdminService.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/service/AdminService.java @@ -18,6 +18,18 @@ public class AdminService { _loggedUserHolder = loggedUserHolder; } + public boolean resetUserPassword(String login) { + try { + final boolean success = _playerDAO.resetUserPassword(login); + if (!success) + return false; + _loggedUserHolder.forceLogoutUser(login); + return true; + } catch (SQLException exp) { + return false; + } + } + public boolean banUser(String login) { try { final boolean success = _playerDAO.banPlayerPermanently(login);