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
@@ -190,7 +190,7 @@ public class CommandHandler {
|
||||
addCommand(new ControlCommand());
|
||||
addCommand(new CoordsCommand());
|
||||
addCommand(new CreditsCommand());
|
||||
addCommand(new DanceCommand());
|
||||
addCommand(new DanceCommand());
|
||||
addCommand(new DiagonalCommand());
|
||||
addCommand(new DisconnectCommand());
|
||||
addCommand(new EjectAllCommand());
|
||||
@@ -230,7 +230,7 @@ public class CommandHandler {
|
||||
addCommand(new MutePetsCommand());
|
||||
addCommand(new PetInfoCommand());
|
||||
addCommand(new PickallCommand());
|
||||
addCommand(new PingCommand());
|
||||
addCommand(new PingCommand());
|
||||
addCommand(new PixelCommand());
|
||||
addCommand(new PluginsCommand());
|
||||
addCommand(new PointsCommand());
|
||||
@@ -253,6 +253,7 @@ public class CommandHandler {
|
||||
addCommand(new SayCommand());
|
||||
addCommand(new SetMaxCommand());
|
||||
addCommand(new SetPollCommand());
|
||||
addCommand(new SetRoomTemplateCommand());
|
||||
addCommand(new SetSpeedCommand());
|
||||
addCommand(new ShoutAllCommand());
|
||||
addCommand(new ShoutCommand());
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
public class SetRoomTemplateCommand extends Command {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SetRoomTemplateCommand.class);
|
||||
|
||||
public SetRoomTemplateCommand() {
|
||||
super("cmd_setroom_template", Emulator.getTexts().getValue("commands.keys.cmd_setroom_template").split(";"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom();
|
||||
if (room == null) {
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.error.cmd_setroom_template.no_room"),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String yes = Emulator.getTexts().getValue("generic.yes");
|
||||
|
||||
if (params.length < 2 || !params[1].equalsIgnoreCase(yes)) {
|
||||
gameClient.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template.verify")
|
||||
.replace("%generic.yes%", yes)
|
||||
.replace("%roomname%", room.getName()));
|
||||
return true;
|
||||
}
|
||||
|
||||
int newTemplateId = 0;
|
||||
int itemsCopied = 0;
|
||||
int itemsSkipped = 0;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement insTemplate = connection.prepareStatement(
|
||||
"INSERT INTO room_templates (title, description, thumbnail, sort_order, enabled, " +
|
||||
"name, room_description, model, password, state, users_max, category, " +
|
||||
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
|
||||
"moodlight_data, override_model, trade_mode) " +
|
||||
"(SELECT name, description, '', 0, '1', " +
|
||||
"name, description, model, password, state, users_max, category, " +
|
||||
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
|
||||
"moodlight_data, override_model, trade_mode " +
|
||||
"FROM rooms WHERE id = ?)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
insTemplate.setInt(1, room.getId());
|
||||
insTemplate.executeUpdate();
|
||||
try (ResultSet keys = insTemplate.getGeneratedKeys()) {
|
||||
if (keys.next()) newTemplateId = keys.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (newTemplateId <= 0) {
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (room.hasCustomLayout()) {
|
||||
try (PreparedStatement updLayout = connection.prepareStatement(
|
||||
"UPDATE room_templates t " +
|
||||
"JOIN room_models_custom c ON c.id = ? " +
|
||||
"SET t.heightmap = c.heightmap, t.door_x = c.door_x, " +
|
||||
" t.door_y = c.door_y, t.door_dir = c.door_dir " +
|
||||
"WHERE t.template_id = ?")) {
|
||||
updLayout.setInt(1, room.getId());
|
||||
updLayout.setInt(2, newTemplateId);
|
||||
updLayout.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
try (PreparedStatement insItems = connection.prepareStatement(
|
||||
"INSERT INTO room_templates_items (template_id, item_id, wall_pos, x, y, z, rot, extra_data, wired_data) " +
|
||||
"SELECT ?, i.item_id, i.wall_pos, i.x, i.y, i.z, i.rot, i.extra_data, i.wired_data " +
|
||||
"FROM items i JOIN items_base ib ON ib.id = i.item_id " +
|
||||
"WHERE i.room_id = ?")) {
|
||||
insItems.setInt(1, newTemplateId);
|
||||
insItems.setInt(2, room.getId());
|
||||
itemsCopied = insItems.executeUpdate();
|
||||
}
|
||||
|
||||
try (PreparedStatement countTotal = connection.prepareStatement(
|
||||
"SELECT COUNT(*) FROM items WHERE room_id = ?")) {
|
||||
countTotal.setInt(1, room.getId());
|
||||
try (ResultSet rs = countTotal.executeQuery()) {
|
||||
if (rs.next()) itemsSkipped = Math.max(0, rs.getInt(1) - itemsCopied);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("cmd_setroom_template failed for roomId=" + room.getId(), e);
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template")
|
||||
.replace("%id%", Integer.toString(newTemplateId))
|
||||
.replace("%items%", Integer.toString(itemsCopied))
|
||||
.replace("%skipped%", Integer.toString(itemsSkipped)),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+460
-26
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@@ -31,10 +32,13 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final String LOGOUT_PATH = "/api/auth/logout";
|
||||
private static final String CHECK_EMAIL_PATH = "/api/auth/check-email";
|
||||
private static final String CHECK_USERNAME_PATH = "/api/auth/check-username";
|
||||
private static final String ROOM_TEMPLATES_PATH = "/api/auth/room-templates";
|
||||
private static final String REMEMBER_PATH = "/api/auth/remember";
|
||||
private static final String HEALTH_PATH = "/api/health";
|
||||
|
||||
private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
|
||||
private static final Pattern EMAIL_RE = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
|
||||
private static final Pattern FIGURE_RE = Pattern.compile("^[A-Za-z0-9.\\-]{1,200}$");
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
private static final int MAX_BODY_BYTES = 8 * 1024;
|
||||
|
||||
@@ -50,6 +54,8 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
if (!path.equals(LOGIN_PATH) && !path.equals(REGISTER_PATH)
|
||||
&& !path.equals(FORGOT_PATH) && !path.equals(LOGOUT_PATH)
|
||||
&& !path.equals(CHECK_EMAIL_PATH) && !path.equals(CHECK_USERNAME_PATH)
|
||||
&& !path.equals(ROOM_TEMPLATES_PATH)
|
||||
&& !path.equals(REMEMBER_PATH)
|
||||
&& !path.equals(HEALTH_PATH)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
@@ -79,6 +85,15 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.equals(ROOM_TEMPLATES_PATH)) {
|
||||
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
|
||||
return;
|
||||
}
|
||||
handleRoomTemplates(ctx, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method() != HttpMethod.POST) {
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use POST."));
|
||||
return;
|
||||
@@ -120,6 +135,10 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
handleCheckUsername(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
if (path.equals(REMEMBER_PATH)) {
|
||||
handleRemember(ctx, req, body, ip);
|
||||
return;
|
||||
}
|
||||
|
||||
String turnstileToken = readString(body, "turnstileToken");
|
||||
if (!TurnstileVerifier.verify(turnstileToken, ip)) {
|
||||
@@ -220,51 +239,210 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) {
|
||||
String ssoTicket = readString(body, "ssoTicket");
|
||||
String rememberToken = readString(body, "rememberToken").trim();
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("message", "Logged out.");
|
||||
|
||||
if (ssoTicket == null || ssoTicket.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
lookup.setString(1, ssoTicket);
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
int userId = 0;
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) userId = rs.getInt("id");
|
||||
}
|
||||
|
||||
if (userId > 0) {
|
||||
try (PreparedStatement clear = conn.prepareStatement(
|
||||
"UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) {
|
||||
clear.setInt(1, userId);
|
||||
clear.executeUpdate();
|
||||
if (ssoTicket != null && !ssoTicket.isEmpty()) {
|
||||
try (PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
lookup.setString(1, ssoTicket);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) userId = rs.getInt("id");
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
if (userId > 0) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete only the specific remember token for this device.
|
||||
// Other devices keep their tokens and can still silent-login.
|
||||
if (!rememberToken.isEmpty()) {
|
||||
String hash = sha256Hex(rememberToken);
|
||||
if (hash != null) {
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE token_hash = ?")) {
|
||||
del.setString(1, hash);
|
||||
del.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Logout cleanup failed for ticket", e);
|
||||
LOGGER.error("Logout cleanup failed", e);
|
||||
}
|
||||
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
|
||||
/* ─── Remember me ─── */
|
||||
|
||||
private void handleRemember(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String rememberToken = readString(body, "rememberToken").trim();
|
||||
if (rememberToken.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
|
||||
return;
|
||||
}
|
||||
|
||||
String hash = sha256Hex(rememberToken);
|
||||
if (hash == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
return;
|
||||
}
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
int userId = 0;
|
||||
int tokenRowId = 0;
|
||||
|
||||
try (PreparedStatement sel = conn.prepareStatement(
|
||||
"SELECT id, user_id, expires_at FROM users_remember_tokens WHERE token_hash = ? LIMIT 1")) {
|
||||
sel.setString(1, hash);
|
||||
try (ResultSet rs = sel.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
if (rs.getInt("expires_at") > now) {
|
||||
userId = rs.getInt("user_id");
|
||||
tokenRowId = rs.getInt("id");
|
||||
} else {
|
||||
tokenRowId = rs.getInt("id"); // expired - still purge below
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userId <= 0) {
|
||||
if (tokenRowId > 0) {
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE id = ?")) {
|
||||
del.setInt(1, tokenRowId);
|
||||
del.executeUpdate();
|
||||
}
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = null;
|
||||
try (PreparedStatement usr = conn.prepareStatement(
|
||||
"SELECT username FROM users WHERE id = ? LIMIT 1")) {
|
||||
usr.setInt(1, userId);
|
||||
try (ResultSet rs = usr.executeQuery()) {
|
||||
if (rs.next()) username = rs.getString("username");
|
||||
}
|
||||
}
|
||||
|
||||
if (username == null) {
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE id = ?")) {
|
||||
del.setInt(1, tokenRowId);
|
||||
del.executeUpdate();
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
|
||||
return;
|
||||
}
|
||||
|
||||
String ssoTicket = mintSsoTicket();
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setString(1, ssoTicket);
|
||||
upd.setString(2, ip == null ? "" : ip);
|
||||
upd.setInt(3, userId);
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
// Rotate: drop the consumed token and issue a new one.
|
||||
try (PreparedStatement del = conn.prepareStatement(
|
||||
"DELETE FROM users_remember_tokens WHERE id = ?")) {
|
||||
del.setInt(1, tokenRowId);
|
||||
del.executeUpdate();
|
||||
}
|
||||
|
||||
String newToken = issueRememberToken(conn, userId, ip);
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("ssoTicket", ssoTicket);
|
||||
ok.addProperty("username", username);
|
||||
if (newToken != null) ok.addProperty("rememberToken", newToken);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Remember login failed", e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a fresh remember-me token for a user, stores the hash,
|
||||
* and returns the raw base64url string to embed in the response.
|
||||
* Returns null on failure (the login still succeeds).
|
||||
*/
|
||||
private static String issueRememberToken(Connection conn, int userId, String ip) {
|
||||
byte[] buf = new byte[32];
|
||||
RNG.nextBytes(buf);
|
||||
String raw = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
String hash = sha256Hex(raw);
|
||||
if (hash == null) return null;
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
int days = Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
|
||||
int expiresAt = now + (days * 24 * 60 * 60);
|
||||
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users_remember_tokens (user_id, token_hash, created_at, expires_at, ip_address) VALUES (?, ?, ?, ?, ?)")) {
|
||||
ins.setInt(1, userId);
|
||||
ins.setString(2, hash);
|
||||
ins.setInt(3, now);
|
||||
ins.setInt(4, expiresAt);
|
||||
ins.setString(5, ip == null ? "" : ip);
|
||||
ins.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to persist remember token for userId=" + userId, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static String sha256Hex(String input) {
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder(digest.length * 2);
|
||||
for (byte b : digest) {
|
||||
String h = Integer.toHexString(b & 0xff);
|
||||
if (h.length() == 1) sb.append('0');
|
||||
sb.append(h);
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("sha256Hex failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Login ─── */
|
||||
|
||||
private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
String username = readString(body, "username").trim();
|
||||
String password = readString(body, "password");
|
||||
boolean rememberMe = readBoolean(body, "remember", false);
|
||||
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials."));
|
||||
@@ -309,11 +487,14 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
upd.executeUpdate();
|
||||
}
|
||||
|
||||
String rememberToken = rememberMe ? issueRememberToken(conn, userId, ip) : null;
|
||||
|
||||
AuthRateLimiter.recordSuccess(ip);
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("ssoTicket", ssoTicket);
|
||||
ok.addProperty("username", rs.getString("username"));
|
||||
if (rememberToken != null) ok.addProperty("rememberToken", rememberToken);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -333,6 +514,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
String username = readString(body, "username").trim();
|
||||
String email = readString(body, "email").trim();
|
||||
String password = readString(body, "password");
|
||||
String figure = readString(body, "figure").trim();
|
||||
String gender = readString(body, "gender").trim().toUpperCase();
|
||||
int templateId = readInt(body, "templateId", 0);
|
||||
|
||||
if (!USERNAME_RE.matcher(username).matches()) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
|
||||
@@ -392,11 +576,19 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
String defaultMotto = Emulator.getConfig().getValue("register.default.motto", "I love Habbo!");
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
String finalLook = (figure.isEmpty() || !FIGURE_RE.matcher(figure).matches()) ? defaultLook : figure;
|
||||
String finalGender = (gender.equals("M") || gender.equals("F")) ? gender : "M";
|
||||
|
||||
int startingCredits = Math.max(0, Emulator.getConfig().getInt("new_user_credits", 0));
|
||||
int startingDuckets = Math.max(0, Emulator.getConfig().getInt("new_user_duckets", 0));
|
||||
int startingDiamonds = Math.max(0, Emulator.getConfig().getInt("new_user_diamonds", 0));
|
||||
|
||||
int newUserId = 0;
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users (username, password, mail, account_created, " +
|
||||
"ip_register, ip_current, last_online, last_login, motto, look, gender, " +
|
||||
"credits, `rank`, home_room, machine_id, auth_ticket, online) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'M', 0, 1, 0, '', '', '0')",
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, '', '', '0')",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
ins.setString(1, username);
|
||||
ins.setString(2, hashed);
|
||||
@@ -407,8 +599,26 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
ins.setInt(7, now);
|
||||
ins.setInt(8, now);
|
||||
ins.setString(9, defaultMotto);
|
||||
ins.setString(10, defaultLook);
|
||||
ins.setString(10, finalLook);
|
||||
ins.setString(11, finalGender);
|
||||
ins.setInt(12, startingCredits);
|
||||
ins.executeUpdate();
|
||||
try (ResultSet keys = ins.getGeneratedKeys()) {
|
||||
if (keys.next()) newUserId = keys.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (newUserId > 0 && (startingDuckets > 0 || startingDiamonds > 0)) {
|
||||
seedUserCurrencies(conn, newUserId, startingDuckets, startingDiamonds);
|
||||
}
|
||||
|
||||
LOGGER.info("[auth/register] user created id={} username='{}' templateId={} credits={} duckets={} diamonds={}",
|
||||
newUserId, username, templateId, startingCredits, startingDuckets, startingDiamonds);
|
||||
|
||||
if (newUserId > 0 && templateId > 0) {
|
||||
cloneTemplateForUser(conn, templateId, newUserId, username);
|
||||
} else if (templateId > 0) {
|
||||
LOGGER.warn("[auth/register] skipping template clone: user insert did not return an id (username='{}')", username);
|
||||
}
|
||||
|
||||
AvailabilityCache.invalidateEmail(email);
|
||||
@@ -423,6 +633,209 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the template carries a custom heightmap (override_model='1' and a
|
||||
* non-empty heightmap), creates the matching room_models_custom row keyed
|
||||
* by the new room id and renames the room's model to custom_<newRoomId>.
|
||||
* Without this, cloned rooms reference a layout that doesn't exist
|
||||
* (the source room's id) and load as a black screen.
|
||||
*/
|
||||
private static void materializeCustomLayout(Connection conn, int templateId, int newRoomId) {
|
||||
String overrideModel = "0";
|
||||
String heightmap = "";
|
||||
int doorX = 0, doorY = 0, doorDir = 2;
|
||||
try (PreparedStatement sel = conn.prepareStatement(
|
||||
"SELECT override_model, heightmap, door_x, door_y, door_dir " +
|
||||
"FROM room_templates WHERE template_id = ? LIMIT 1")) {
|
||||
sel.setInt(1, templateId);
|
||||
try (ResultSet rs = sel.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
overrideModel = rs.getString("override_model");
|
||||
heightmap = rs.getString("heightmap");
|
||||
doorX = rs.getInt("door_x");
|
||||
doorY = rs.getInt("door_y");
|
||||
doorDir = rs.getInt("door_dir");
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] reading template layout failed templateId=" + templateId, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"1".equals(overrideModel) || heightmap == null || heightmap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String customName = "custom_" + newRoomId;
|
||||
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO room_models_custom (id, name, door_x, door_y, door_dir, heightmap) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE name = VALUES(name), door_x = VALUES(door_x), " +
|
||||
"door_y = VALUES(door_y), door_dir = VALUES(door_dir), heightmap = VALUES(heightmap)")) {
|
||||
ins.setInt(1, newRoomId);
|
||||
ins.setString(2, customName);
|
||||
ins.setInt(3, doorX);
|
||||
ins.setInt(4, doorY);
|
||||
ins.setInt(5, doorDir);
|
||||
ins.setString(6, heightmap);
|
||||
ins.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] room_models_custom insert failed roomId=" + newRoomId, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE rooms SET model = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setString(1, customName);
|
||||
upd.setInt(2, newRoomId);
|
||||
upd.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] rooms.model rename failed roomId=" + newRoomId, e);
|
||||
}
|
||||
|
||||
LOGGER.info("[auth/register] materialized custom layout '{}' for roomId={}", customName, newRoomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds starting balances into users_currency for duckets (type=0) and
|
||||
* diamonds (type=5). Only inserts when the amount is > 0. Credits live
|
||||
* in users.credits and are set directly during the register INSERT.
|
||||
*/
|
||||
private static void seedUserCurrencies(Connection conn, int userId, int duckets, int diamonds) {
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE amount = VALUES(amount)")) {
|
||||
if (duckets > 0) {
|
||||
ins.setInt(1, userId);
|
||||
ins.setInt(2, 0);
|
||||
ins.setInt(3, duckets);
|
||||
ins.addBatch();
|
||||
}
|
||||
if (diamonds > 0) {
|
||||
ins.setInt(1, userId);
|
||||
ins.setInt(2, 5);
|
||||
ins.setInt(3, diamonds);
|
||||
ins.addBatch();
|
||||
}
|
||||
ins.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] seeding users_currency failed userId=" + userId
|
||||
+ " duckets=" + duckets + " diamonds=" + diamonds, e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Room templates (registration step 3) ─── */
|
||||
|
||||
private void handleRoomTemplates(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
JsonArray templates = new JsonArray();
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"SELECT template_id, title, description, thumbnail " +
|
||||
"FROM room_templates WHERE enabled = '1' " +
|
||||
"ORDER BY sort_order ASC, template_id ASC")) {
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
JsonObject t = new JsonObject();
|
||||
t.addProperty("templateId", rs.getInt("template_id"));
|
||||
t.addProperty("title", rs.getString("title"));
|
||||
t.addProperty("description", rs.getString("description"));
|
||||
t.addProperty("thumbnail", rs.getString("thumbnail"));
|
||||
templates.add(t);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("room-templates list failed", e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
|
||||
return;
|
||||
}
|
||||
JsonObject res = new JsonObject();
|
||||
res.add("templates", templates);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones a room_templates entry + its room_templates_items into the new
|
||||
* user's rooms/items rows, then points their home_room at the new room.
|
||||
* Failures here do not abort registration; the account is still created.
|
||||
*/
|
||||
private static void cloneTemplateForUser(Connection conn, int templateId, int userId, String userName) {
|
||||
LOGGER.info("[auth/register] cloning template id={} for user id={} name='{}'", templateId, userId, userName);
|
||||
|
||||
try (PreparedStatement check = conn.prepareStatement(
|
||||
"SELECT 1 FROM room_templates WHERE template_id = ? AND enabled = '1' LIMIT 1")) {
|
||||
check.setInt(1, templateId);
|
||||
try (ResultSet rs = check.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
LOGGER.warn("[auth/register] unknown/disabled room template id={} for user id={}", templateId, userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] template lookup failed for templateId=" + templateId, e);
|
||||
return;
|
||||
}
|
||||
|
||||
int newRoomId = 0;
|
||||
int roomsInserted = 0;
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO rooms (owner_id, owner_name, name, description, model, password, state, " +
|
||||
"users_max, category, paper_floor, paper_wall, paper_landscape, thickness_wall, " +
|
||||
"thickness_floor, moodlight_data, override_model, trade_mode) " +
|
||||
"(SELECT ?, ?, name, room_description, model, password, state, " +
|
||||
"users_max, category, paper_floor, paper_wall, paper_landscape, thickness_wall, " +
|
||||
"thickness_floor, moodlight_data, override_model, trade_mode " +
|
||||
"FROM room_templates WHERE template_id = ?)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
ins.setInt(1, userId);
|
||||
ins.setString(2, userName);
|
||||
ins.setInt(3, templateId);
|
||||
roomsInserted = ins.executeUpdate();
|
||||
try (ResultSet keys = ins.getGeneratedKeys()) {
|
||||
if (keys.next()) newRoomId = keys.getInt(1);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] clone rooms failed templateId=" + templateId + " userId=" + userId, e);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("[auth/register] rooms insert: rowsAffected={} newRoomId={}", roomsInserted, newRoomId);
|
||||
|
||||
if (newRoomId <= 0) {
|
||||
LOGGER.warn("[auth/register] clone aborted - no roomId returned (templateId={}, userId={})", templateId, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
materializeCustomLayout(conn, templateId, newRoomId);
|
||||
|
||||
int itemsInserted = 0;
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO items (user_id, room_id, item_id, wall_pos, x, y, z, rot, " +
|
||||
"extra_data, wired_data, limited_data, guild_id) " +
|
||||
"(SELECT ?, ?, item_id, wall_pos, x, y, z, rot, extra_data, wired_data, '0:0', 0 " +
|
||||
"FROM room_templates_items WHERE template_id = ?)")) {
|
||||
ins.setInt(1, userId);
|
||||
ins.setInt(2, newRoomId);
|
||||
ins.setInt(3, templateId);
|
||||
itemsInserted = ins.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] clone items failed templateId=" + templateId
|
||||
+ " roomId=" + newRoomId + " userId=" + userId, e);
|
||||
}
|
||||
|
||||
LOGGER.info("[auth/register] items insert: rowsAffected={} roomId={}", itemsInserted, newRoomId);
|
||||
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users SET home_room = ? WHERE id = ? LIMIT 1")) {
|
||||
upd.setInt(1, newRoomId);
|
||||
upd.setInt(2, userId);
|
||||
int rows = upd.executeUpdate();
|
||||
LOGGER.info("[auth/register] home_room update: rowsAffected={} userId={} roomId={}", rows, userId, newRoomId);
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/register] setting home_room failed userId=" + userId + " roomId=" + newRoomId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Forgot password ─── */
|
||||
|
||||
private void handleForgot(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
|
||||
@@ -510,6 +923,27 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private static int readInt(JsonObject obj, String key, int defaultValue) {
|
||||
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsInt();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean readBoolean(JsonObject obj, String key, boolean defaultValue) {
|
||||
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
com.google.gson.JsonElement el = obj.get(key);
|
||||
if (el.getAsJsonPrimitive().isBoolean()) return el.getAsBoolean();
|
||||
String s = el.getAsString();
|
||||
return "1".equals(s) || "true".equalsIgnoreCase(s);
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
String ipHeader = Emulator.getConfig() != null
|
||||
? Emulator.getConfig().getValue("ws.ip.header", "")
|
||||
|
||||
Reference in New Issue
Block a user