Adding ability for admins to reset the passwords for users.

This commit is contained in:
Christian 'ketura' McCarty
2023-03-09 02:55:45 -06:00
parent 60436e22f8
commit 5792c817a5
12 changed files with 353 additions and 45 deletions

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -3,6 +3,15 @@
<div id="banMain" class="article">
<h1>Reset User Password</h1>
<div>
Name (case-sensitive): <input id="reset-input" type="text" ><br/>
<button id="reset-button" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" role="button" aria-disabled="false" style="padding:4px;">
Reset Password
</button> <span id="reset-result" style="display:inline-block;">Ready.</span>
</div>
<h1>Ban User</h1>
<h2>Permanently</h2>
<div>

View File

@@ -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"
});

View File

@@ -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");

View File

@@ -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<br/>" +
" 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 <a href='http://lotrtcgwiki.com/forums/'>TLHH forums</a>.");
$(".error").html("You have been permanently banned. If you think it was a mistake please appeal with dmaz or ketura on <a href='https://lotrtcgpc.net/discord>the PC Discord</a>.");
$(".interaction").html("");
},
"409": function () {
$(".error").html("You have been temporarily banned. You can try logging in at a later time. If you think it was a mistake you can try sending a private message to Merrick_H on <a href='http://lotrtcgwiki.com/forums/'>TLHH forums</a>.");
$(".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 <a href='https://lotrtcgpc.net/discord>the PC Discord</a>.");
$(".interaction").html("");
},
"503": function () {
@@ -92,6 +114,8 @@ function loginScreen() {
});
$(".interaction").append(loginButton);
$(".interaction").append("<br/><a href='https://lotrtcgpc.net/discord'>Forgot your password? Contact <span style='color:orange'>ketura</span> on the PC Discord.</a>");
}
$(document).ready(

View File

@@ -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 {

View File

@@ -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<Type> getTypes(String typeString) {
List<Type> types = new ArrayList<>();
for (Type type : values()) {
if (typeString.contains(type.getValue())) {
types.add(type);
}
}
return types;
}
public static String getTypeString(List<Type> 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;
}

View File

@@ -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);

View File

@@ -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<DBDefs.Player> 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<DBDefs.Player> 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<DBDefs.Player> 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);
}
}

View File

@@ -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;

View File

@@ -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);