You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
🆕 Added Reset password / Email and chenge username in user settings
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.messages.outgoing.users.UserDataComposer;
|
||||
|
||||
public class ChangeNameCommand extends Command {
|
||||
public ChangeNameCommand() {
|
||||
super("cmd_changename", Emulator.getTexts().getValue("commands.keys.cmd_changename").split(";"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
gameClient.getHabbo().getHabboStats().allowNameChange = !gameClient.getHabbo().getHabboStats().allowNameChange;
|
||||
gameClient.sendResponse(new UserDataComposer(gameClient.getHabbo()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,6 @@ public class CommandHandler {
|
||||
addCommand(new BlockAlertCommand());
|
||||
addCommand(new BotsCommand());
|
||||
addCommand(new CalendarCommand());
|
||||
addCommand(new ChangeNameCommand());
|
||||
addCommand(new ChatTypeCommand());
|
||||
addCommand(new CommandsCommand());
|
||||
addCommand(new ControlCommand());
|
||||
|
||||
+540
-2
@@ -38,6 +38,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final String REFRESH_PATH = "/api/auth/refresh";
|
||||
private static final String SERVER_KEY_PATH = "/api/auth/server-key";
|
||||
private static final String SSO_TOKEN_PATH = "/api/auth/sso-token";
|
||||
private static final String CHANGE_PASSWORD_PATH = "/api/auth/change-password";
|
||||
private static final String CHANGE_EMAIL_PATH = "/api/auth/change-email";
|
||||
private static final String CHANGE_USERNAME_PATH = "/api/auth/change-username";
|
||||
private static final String HEALTH_PATH = "/api/health";
|
||||
|
||||
private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
|
||||
@@ -64,6 +67,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
&& !path.equals(REFRESH_PATH)
|
||||
&& !path.equals(SERVER_KEY_PATH)
|
||||
&& !path.equals(SSO_TOKEN_PATH)
|
||||
&& !path.equals(CHANGE_PASSWORD_PATH)
|
||||
&& !path.equals(CHANGE_EMAIL_PATH)
|
||||
&& !path.equals(CHANGE_USERNAME_PATH)
|
||||
&& !path.equals(HEALTH_PATH)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
@@ -180,6 +186,18 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
handleSsoToken(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
if (path.equals(CHANGE_PASSWORD_PATH)) {
|
||||
handleChangePassword(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
if (path.equals(CHANGE_EMAIL_PATH)) {
|
||||
handleChangeEmail(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
if (path.equals(CHANGE_USERNAME_PATH)) {
|
||||
handleChangeUsername(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
|
||||
String turnstileToken = readString(body, "turnstileToken");
|
||||
if (!TurnstileVerifier.verify(turnstileToken, ip)) {
|
||||
@@ -274,6 +292,517 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, res);
|
||||
}
|
||||
|
||||
private void handleChangePassword(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION);
|
||||
String bearer = "";
|
||||
if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
bearer = authHeader.substring(7).trim();
|
||||
}
|
||||
|
||||
int userId = AccessTokenService.verify(bearer);
|
||||
if (userId <= 0) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated."));
|
||||
return;
|
||||
}
|
||||
|
||||
String currentPassword = readString(body, "currentPassword");
|
||||
String newPassword = readString(body, "newPassword");
|
||||
String confirmPassword = readString(body, "confirmPassword");
|
||||
|
||||
if (currentPassword.isEmpty() || newPassword.isEmpty() || confirmPassword.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("All fields are required."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPassword.length() > 256 || newPassword.length() > 256) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Password too long."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPassword.equals(confirmPassword)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("New passwords do not match."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length() < 8) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Password must be at least 8 characters."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.equals(currentPassword)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("New password must be different from the current password."));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
String storedHash = null;
|
||||
String username = null;
|
||||
try (PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT username, password FROM users WHERE id = ? LIMIT 1")) {
|
||||
lookup.setInt(1, userId);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
username = rs.getString("username");
|
||||
storedHash = rs.getString("password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (storedHash == null || storedHash.isEmpty()) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkPassword(currentPassword, storedHash)) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
LOGGER.info("[auth/change-password] current password mismatch for user id={} username='{}'", userId, username);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
|
||||
errorPayload("Current password is incorrect."));
|
||||
return;
|
||||
}
|
||||
|
||||
String hashed = BCrypt.hashpw(newPassword, BCrypt.gensalt(12));
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users SET password = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setString(1, hashed);
|
||||
upd.setInt(2, userId);
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
AuthRateLimiter.recordSuccess(ip);
|
||||
LOGGER.info("[auth/change-password] password updated for user id={} username='{}' ip='{}'", userId, username, ip);
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("message", "Password updated successfully.");
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[auth/change-password] failed for user id=" + userId, e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChangeEmail(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION);
|
||||
String bearer = "";
|
||||
if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
bearer = authHeader.substring(7).trim();
|
||||
}
|
||||
|
||||
int userId = AccessTokenService.verify(bearer);
|
||||
if (userId <= 0) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated."));
|
||||
return;
|
||||
}
|
||||
|
||||
String currentPassword = readString(body, "currentPassword");
|
||||
String newEmail = readString(body, "newEmail").trim();
|
||||
|
||||
if (currentPassword.isEmpty() || newEmail.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("All fields are required."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPassword.length() > 256 || newEmail.length() > 254) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Field too long."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EMAIL_RE.matcher(newEmail).matches()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Invalid email address."));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
String storedHash = null;
|
||||
String username = null;
|
||||
String currentEmail = null;
|
||||
try (PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT username, password, mail FROM users WHERE id = ? LIMIT 1")) {
|
||||
lookup.setInt(1, userId);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
username = rs.getString("username");
|
||||
storedHash = rs.getString("password");
|
||||
currentEmail = rs.getString("mail");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (storedHash == null || storedHash.isEmpty()) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkPassword(currentPassword, storedHash)) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
LOGGER.info("[auth/change-email] password mismatch for user id={} username='{}'", userId, username);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
|
||||
errorPayload("Current password is incorrect."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentEmail != null && currentEmail.equalsIgnoreCase(newEmail)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("New email must be different from the current email."));
|
||||
return;
|
||||
}
|
||||
|
||||
try (PreparedStatement check = conn.prepareStatement(
|
||||
"SELECT id FROM users WHERE mail = ? AND id <> ? LIMIT 1")) {
|
||||
check.setString(1, newEmail);
|
||||
check.setInt(2, userId);
|
||||
try (ResultSet rs = check.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
|
||||
errorPayload("That email address is already in use."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users SET mail = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setString(1, newEmail);
|
||||
upd.setInt(2, userId);
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
if (currentEmail != null && !currentEmail.isEmpty()) AvailabilityCache.invalidateEmail(currentEmail);
|
||||
AvailabilityCache.invalidateEmail(newEmail);
|
||||
|
||||
AuthRateLimiter.recordSuccess(ip);
|
||||
LOGGER.info("[auth/change-email] email updated for user id={} username='{}' ip='{}'", userId, username, ip);
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("message", "Email updated successfully.");
|
||||
ok.addProperty("email", newEmail);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[auth/change-email] failed for user id=" + userId, e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChangeUsername(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION);
|
||||
String bearer = "";
|
||||
if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
bearer = authHeader.substring(7).trim();
|
||||
}
|
||||
|
||||
int userId = AccessTokenService.verify(bearer);
|
||||
if (userId <= 0) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated."));
|
||||
return;
|
||||
}
|
||||
|
||||
String currentPassword = readString(body, "currentPassword");
|
||||
String newUsername = readString(body, "newUsername").trim();
|
||||
|
||||
if (currentPassword.isEmpty() || newUsername.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("All fields are required."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPassword.length() > 256) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Field too long."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newUsername.length() > 25) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Username can be at most 25 characters."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!USERNAME_RE.matcher(newUsername).matches()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("Username must be 3-25 characters (letters, numbers, . _ -)."));
|
||||
return;
|
||||
}
|
||||
|
||||
long cooldownDays = Math.max(0, Emulator.getConfig().getInt("rename.cooldown_days", 30));
|
||||
long cooldownSeconds = cooldownDays * 86400L;
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
String storedHash = null;
|
||||
String currentUsername = null;
|
||||
int lastChange = 0;
|
||||
boolean cooldownColumnExists = true;
|
||||
|
||||
try (PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT username, password, last_username_change FROM users WHERE id = ? LIMIT 1")) {
|
||||
lookup.setInt(1, userId);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
currentUsername = rs.getString("username");
|
||||
storedHash = rs.getString("password");
|
||||
lastChange = rs.getInt("last_username_change");
|
||||
}
|
||||
}
|
||||
} catch (SQLException missingColumn) {
|
||||
cooldownColumnExists = false;
|
||||
LOGGER.warn("[auth/change-username] users.last_username_change column missing — cooldown disabled. Run the migration in config/Database.sql.");
|
||||
try (PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT username, password FROM users WHERE id = ? LIMIT 1")) {
|
||||
lookup.setInt(1, userId);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
currentUsername = rs.getString("username");
|
||||
storedHash = rs.getString("password");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (storedHash == null || storedHash.isEmpty()) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkPassword(currentPassword, storedHash)) {
|
||||
AuthRateLimiter.recordFailure(ip);
|
||||
LOGGER.info("[auth/change-username] password mismatch for user id={} username='{}'", userId, currentUsername);
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
|
||||
errorPayload("Current password is incorrect."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Case-only changes (e.g. "bob" -> "Bob") are allowed; identical strings are not.
|
||||
if (currentUsername != null && currentUsername.equals(newUsername)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
errorPayload("New username must be different from the current username."));
|
||||
return;
|
||||
}
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
if (cooldownColumnExists && cooldownSeconds > 0 && lastChange > 0) {
|
||||
long allowedAt = (long) lastChange + cooldownSeconds;
|
||||
if (now < allowedAt) {
|
||||
long remaining = allowedAt - now;
|
||||
long days = remaining / 86400L;
|
||||
long hours = (remaining % 86400L) / 3600L;
|
||||
String wait = days > 0 ? (days + " day" + (days == 1 ? "" : "s")) : (hours + " hour" + (hours == 1 ? "" : "s"));
|
||||
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
|
||||
errorPayload("You can rename again in " + wait + "."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try (PreparedStatement banned = conn.prepareStatement(
|
||||
"SELECT 1 FROM banned_usernames WHERE LOWER(username) = LOWER(?) LIMIT 1")) {
|
||||
banned.setString(1, newUsername);
|
||||
try (ResultSet rs = banned.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
|
||||
errorPayload("That username is not allowed."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (SQLException bannedTableError) {
|
||||
// Only swallow "table doesn't exist" — banned_usernames is optional in some installs.
|
||||
// Any other DB error (timeout, syntax, permission, connection drop) must fail the
|
||||
// request so a transient outage can't silently bypass the blocklist.
|
||||
if (bannedTableError.getErrorCode() != 1146
|
||||
&& !"42S02".equals(bannedTableError.getSQLState())) {
|
||||
throw bannedTableError;
|
||||
}
|
||||
}
|
||||
|
||||
try (PreparedStatement check = conn.prepareStatement(
|
||||
"SELECT id FROM users WHERE LOWER(username) = LOWER(?) AND id <> ? LIMIT 1")) {
|
||||
check.setString(1, newUsername);
|
||||
check.setInt(2, userId);
|
||||
try (ResultSet rs = check.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
|
||||
errorPayload("That username is already taken."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean previousAutoCommit = conn.getAutoCommit();
|
||||
conn.setAutoCommit(false);
|
||||
|
||||
boolean cooldownRace = false;
|
||||
boolean duplicateName = false;
|
||||
|
||||
try {
|
||||
int rowsUpdated = 0;
|
||||
|
||||
// Folding the cooldown into the WHERE clause makes the rename atomic against
|
||||
// a second concurrent request with the same token: only one of them will see
|
||||
// rowsUpdated == 1, the other gets 0 and we surface it as 429 instead of letting
|
||||
// both succeed and writing last_username_change twice.
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
cooldownColumnExists
|
||||
? "UPDATE users SET username = ?, last_username_change = ? "
|
||||
+ "WHERE id = ? "
|
||||
+ " AND (last_username_change = 0 OR last_username_change + ? <= ?) "
|
||||
+ "LIMIT 1"
|
||||
: "UPDATE users SET username = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setString(1, newUsername);
|
||||
if (cooldownColumnExists) {
|
||||
upd.setInt(2, now);
|
||||
upd.setInt(3, userId);
|
||||
upd.setLong(4, cooldownSeconds);
|
||||
upd.setInt(5, now);
|
||||
} else {
|
||||
upd.setInt(2, userId);
|
||||
}
|
||||
try {
|
||||
rowsUpdated = upd.executeUpdate();
|
||||
} catch (SQLException dup) {
|
||||
// ER_DUP_ENTRY (1062) / SQLState 23000 — the UNIQUE KEY on users.username
|
||||
// raced us between the pre-check and the UPDATE. Surface as 409 instead of
|
||||
// a generic 500 so the UI can show "username taken".
|
||||
if (dup.getErrorCode() == 1062 || "23000".equals(dup.getSQLState())) {
|
||||
duplicateName = true;
|
||||
} else {
|
||||
throw dup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateName || (cooldownColumnExists && rowsUpdated == 0)) {
|
||||
if (!duplicateName) cooldownRace = true;
|
||||
conn.rollback();
|
||||
} else {
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE rooms SET owner_name = ? WHERE owner_id = ?")) {
|
||||
upd.setString(1, newUsername);
|
||||
upd.setInt(2, userId);
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE rooms_for_sale SET owner_name = ? WHERE user_id = ?")) {
|
||||
upd.setString(1, newUsername);
|
||||
upd.setInt(2, userId);
|
||||
upd.executeUpdate();
|
||||
} catch (SQLException roomsForSale) {
|
||||
// rooms_for_sale is optional — only swallow "table doesn't exist".
|
||||
if (roomsForSale.getErrorCode() != 1146
|
||||
&& !"42S02".equals(roomsForSale.getSQLState())) {
|
||||
throw roomsForSale;
|
||||
}
|
||||
}
|
||||
|
||||
conn.commit();
|
||||
}
|
||||
} catch (SQLException txError) {
|
||||
try { conn.rollback(); } catch (SQLException ignore) {}
|
||||
throw txError;
|
||||
} finally {
|
||||
conn.setAutoCommit(previousAutoCommit);
|
||||
}
|
||||
|
||||
if (duplicateName) {
|
||||
LOGGER.info("[auth/change-username] dup-entry race for user id={} wanted='{}'", userId, newUsername);
|
||||
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
|
||||
errorPayload("That username is already taken."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cooldownRace) {
|
||||
LOGGER.info("[auth/change-username] cooldown race for user id={} (concurrent rename rejected)", userId);
|
||||
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
|
||||
errorPayload("Rename already in progress — please wait."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh in-memory caches BEFORE we drop the client. On disconnect, Habbo.disconnect()
|
||||
// calls HabboInfo.run() which persists every field of HabboInfo back to the users row
|
||||
// (including username) — so if the in-memory object still holds the old name it will
|
||||
// overwrite the DB update we just committed. We also need to re-key the
|
||||
// HabboManager.onlineHabbosByName map and update any loaded Room.ownerName fields so
|
||||
// navigator / room cards stop showing the old name without a server restart.
|
||||
try {
|
||||
if (Emulator.getGameServer() != null && Emulator.getGameServer().getGameClientManager() != null
|
||||
&& Emulator.getGameEnvironment() != null && Emulator.getGameEnvironment().getHabboManager() != null) {
|
||||
com.eu.habbo.habbohotel.users.Habbo habbo =
|
||||
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (habbo != null) {
|
||||
// Re-key the username map AND update the in-memory username so the
|
||||
// disconnect-time HabboInfo.run() persists the new name, not the old one.
|
||||
Emulator.getGameEnvironment().getHabboManager().removeHabbo(habbo);
|
||||
habbo.getHabboInfo().setUsername(newUsername);
|
||||
Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo);
|
||||
}
|
||||
}
|
||||
} catch (Exception cacheError) {
|
||||
LOGGER.warn("[auth/change-username] failed to refresh HabboManager cache", cacheError);
|
||||
}
|
||||
|
||||
try {
|
||||
if (Emulator.getGameEnvironment() != null && Emulator.getGameEnvironment().getRoomManager() != null) {
|
||||
for (com.eu.habbo.habbohotel.rooms.Room room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) {
|
||||
if (room.getOwnerId() == userId) {
|
||||
room.setOwnerName(newUsername);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception cacheError) {
|
||||
LOGGER.warn("[auth/change-username] failed to refresh Room.ownerName cache", cacheError);
|
||||
}
|
||||
|
||||
// Marketplace offer-list pages bake the seller name into the serialized payload,
|
||||
// so invalidate it; the next /offers request will rebuild it with the new name.
|
||||
try {
|
||||
com.eu.habbo.messages.incoming.catalog.marketplace.RequestOffersEvent.cachedResults.clear();
|
||||
} catch (Exception cacheError) {
|
||||
LOGGER.warn("[auth/change-username] failed to clear marketplace cache", cacheError);
|
||||
}
|
||||
|
||||
// Drop any active session so the user reconnects with the fresh identity
|
||||
// (friend list, navigator, messenger threads all refresh on relogin).
|
||||
try (PreparedStatement clear = conn.prepareStatement(
|
||||
"UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) {
|
||||
clear.setInt(1, userId);
|
||||
clear.executeUpdate();
|
||||
}
|
||||
|
||||
if (Emulator.getGameServer() != null
|
||||
&& Emulator.getGameServer().getGameClientManager() != null) {
|
||||
com.eu.habbo.habbohotel.users.Habbo habbo =
|
||||
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (habbo != null && habbo.getClient() != null) {
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
|
||||
}
|
||||
}
|
||||
|
||||
AuthRateLimiter.recordSuccess(ip);
|
||||
LOGGER.info("[auth/change-username] '{}' -> '{}' (user id={}, ip='{}')",
|
||||
currentUsername, newUsername, userId, ip);
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("message", "Username updated. Please log in again with your new name.");
|
||||
ok.addProperty("username", newUsername);
|
||||
ok.addProperty("relogin", true);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[auth/change-username] failed for user id=" + userId, e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) {
|
||||
String ssoTicket = readString(body, "ssoTicket");
|
||||
String rememberToken = readString(body, "rememberToken").trim();
|
||||
@@ -1135,11 +1664,20 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
|
||||
if (origin != null && !origin.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Origin", origin);
|
||||
response.headers().set("Vary", "Origin");
|
||||
response.headers().set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
|
||||
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
|
||||
|
||||
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
|
||||
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
|
||||
} else {
|
||||
response.headers().set("Access-Control-Allow-Headers",
|
||||
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
|
||||
}
|
||||
|
||||
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
|
||||
response.headers().set("Access-Control-Max-Age", "600");
|
||||
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -267,11 +267,20 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler {
|
||||
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
|
||||
if (origin != null && !origin.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Origin", origin);
|
||||
response.headers().set("Vary", "Origin");
|
||||
response.headers().set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
|
||||
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
|
||||
|
||||
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
|
||||
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
|
||||
} else {
|
||||
response.headers().set("Access-Control-Allow-Headers",
|
||||
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
|
||||
}
|
||||
|
||||
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
|
||||
response.headers().set("Access-Control-Max-Age", "600");
|
||||
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -299,10 +299,18 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter {
|
||||
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
|
||||
if (origin != null && !origin.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Origin", origin);
|
||||
response.headers().set("Vary", "Origin");
|
||||
}
|
||||
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
|
||||
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Nitro-Key");
|
||||
|
||||
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
|
||||
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
|
||||
} else {
|
||||
response.headers().set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Nitro-Key");
|
||||
}
|
||||
|
||||
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
|
||||
response.headers().set("Access-Control-Max-Age", "600");
|
||||
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user