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
fix(housekeeping): align audit log schema
Housekeeping audit writes used an obsolete housekeeping_log schema with operator_id, operator_name, target_user_id and ip columns, while the migration and list composer read actor_id, actor_name, target_type, target_id, target_label, action, detail and success. That made log inserts fail against migrated databases and made auto-created tables unreadable by the client. Align the writer and auto-create DDL with the action-log schema, preserve operator IP in detail, and add a contract test for schema drift.
This commit is contained in:
@@ -11,9 +11,8 @@ import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* Append-only audit trail for privileged housekeeping/admin actions (rank grants,
|
||||
* currency grants, etc.). There was previously no record of which operator did
|
||||
* what to whom. Writes are dispatched off the calling thread; the backing table
|
||||
* is created on first use so no manual migration is required.
|
||||
* currency grants, etc.). Writes are dispatched off the calling thread; the
|
||||
* backing table is created on first use so no manual migration is required.
|
||||
*/
|
||||
public final class HousekeepingAuditLog {
|
||||
|
||||
@@ -43,24 +42,26 @@ public final class HousekeepingAuditLog {
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO housekeeping_log (operator_id, operator_name, action, target_user_id, detail, ip, timestamp) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, operatorId);
|
||||
statement.setString(2, operatorName != null ? operatorName : "");
|
||||
statement.setString(3, action != null ? action : "");
|
||||
"INSERT INTO housekeeping_log (timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success) " +
|
||||
"VALUES (?, ?, ?, 'user', ?, '', ?, ?, 1)")) {
|
||||
statement.setInt(1, Emulator.getIntUnixTimestamp());
|
||||
statement.setInt(2, operatorId);
|
||||
statement.setString(3, operatorName != null ? operatorName : "");
|
||||
statement.setInt(4, targetUserId);
|
||||
statement.setString(5, truncate(detail));
|
||||
statement.setString(6, ip != null ? ip : "");
|
||||
statement.setInt(7, Emulator.getIntUnixTimestamp());
|
||||
statement.setString(5, action != null ? action : "");
|
||||
statement.setString(6, truncate(detail, ip));
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to write housekeeping audit log entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncate(String detail) {
|
||||
if (detail == null) return "";
|
||||
return detail.length() > 512 ? detail.substring(0, 512) : detail;
|
||||
private static String truncate(String detail, String ip) {
|
||||
String value = detail == null ? "" : detail;
|
||||
if (ip != null && !ip.isEmpty()) {
|
||||
value = value.isEmpty() ? "ip=" + ip : value + " ip=" + ip;
|
||||
}
|
||||
return value.length() > 500 ? value.substring(0, 500) : value;
|
||||
}
|
||||
|
||||
private static void ensureTable() {
|
||||
@@ -75,19 +76,19 @@ public final class HousekeepingAuditLog {
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute(
|
||||
"CREATE TABLE IF NOT EXISTS housekeeping_log (" +
|
||||
"id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
|
||||
"operator_id INT NOT NULL, " +
|
||||
"operator_name VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"action VARCHAR(64) NOT NULL, " +
|
||||
"target_user_id INT NOT NULL DEFAULT 0, " +
|
||||
"detail VARCHAR(512) NOT NULL DEFAULT '', " +
|
||||
"ip VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"id INT NOT NULL AUTO_INCREMENT, " +
|
||||
"timestamp INT NOT NULL, " +
|
||||
"actor_id INT NOT NULL, " +
|
||||
"actor_name VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"target_type VARCHAR(16) NOT NULL DEFAULT 'user', " +
|
||||
"target_id INT NOT NULL DEFAULT 0, " +
|
||||
"target_label VARCHAR(128) NOT NULL DEFAULT '', " +
|
||||
"action VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"detail VARCHAR(500) NOT NULL DEFAULT '', " +
|
||||
"success TINYINT NOT NULL DEFAULT 1, " +
|
||||
"PRIMARY KEY (id), " +
|
||||
"KEY idx_operator (operator_id), " +
|
||||
"KEY idx_target (target_user_id), " +
|
||||
"KEY idx_timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
"KEY timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
tableReady = true;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to create housekeeping_log table", e);
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.eu.habbo.habbohotel.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingAuditLogContractTest {
|
||||
private static String auditLogSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writerUsesActionLogSchemaReadByHousekeepingClient() throws Exception {
|
||||
String source = auditLogSource();
|
||||
|
||||
assertTrue(source.contains("actor_id"), "housekeeping_log writer must persist actor_id");
|
||||
assertTrue(source.contains("actor_name"), "housekeeping_log writer must persist actor_name");
|
||||
assertTrue(source.contains("target_type"), "housekeeping_log writer must persist target_type");
|
||||
assertTrue(source.contains("target_id"), "housekeeping_log writer must persist target_id");
|
||||
assertTrue(source.contains("target_label"), "housekeeping_log writer must persist target_label");
|
||||
assertTrue(source.contains("success"), "housekeeping_log writer must persist success");
|
||||
assertFalse(source.contains("operator_id"), "housekeeping_log writer must not use the obsolete operator_id schema");
|
||||
assertFalse(source.contains("target_user_id"), "housekeeping_log writer must not use the obsolete target_user_id schema");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user