Whitelist: write allowlist.json+whitelist.json, verify after reload, learn UUIDs from logs
On 26.2 the file whitelist entries load at startup but 'whitelist reload' and shutdown saves use the renamed allowlist.json, so panel writes never took effect live and got wiped on stop. Now the panel writes both file names, verifies the player actually appears in 'whitelist list' after reload, falls back to the vanilla add command, and captures each player's real UUID from 'UUID of player …' log lines for future entries. The auth watcher's self-heal loop reports a loud notification after 3 failed retries instead of silently spamming reloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2ff88b255
commit
b043f03d28
3 files changed
+96
-16
No files matched your search
+55
-14
@@ -65,42 +65,83 @@ export function offlineUuid(name: string): string {
|
||||
|
||||
type WhitelistEntry = { uuid: string; name: string };
|
||||
|
||||
function whitelistPath(): string {
|
||||
return path.join(env.mcDataDir, "whitelist.json");
|
||||
}
|
||||
// 26.x renamed server files as part of the registry cleanup; startup still
|
||||
// migrates the legacy name but `whitelist reload` and shutdown saves use the
|
||||
// new one. Writing both keeps every code path in agreement on any version.
|
||||
const WHITELIST_FILES = ["allowlist.json", "whitelist.json"];
|
||||
|
||||
function readWhitelistFile(): WhitelistEntry[] {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(whitelistPath(), "utf8"));
|
||||
return Array.isArray(data) ? (data as WhitelistEntry[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
const byName = new Map<string, WhitelistEntry>();
|
||||
for (const file of WHITELIST_FILES) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(path.join(env.mcDataDir, file), "utf8"));
|
||||
if (Array.isArray(data)) {
|
||||
for (const e of data as WhitelistEntry[]) {
|
||||
if (e?.name && !byName.has(e.name.toLowerCase())) byName.set(e.name.toLowerCase(), e);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return [...byName.values()];
|
||||
}
|
||||
|
||||
function writeWhitelistFile(entries: WhitelistEntry[]) {
|
||||
fs.writeFileSync(whitelistPath(), JSON.stringify(entries, null, 2) + "\n");
|
||||
try {
|
||||
fs.chownSync(whitelistPath(), 1000, 1000); // keep it writable by the MC user
|
||||
} catch {}
|
||||
const json = JSON.stringify(entries, null, 2) + "\n";
|
||||
for (const file of WHITELIST_FILES) {
|
||||
const p = path.join(env.mcDataDir, file);
|
||||
fs.writeFileSync(p, json);
|
||||
try {
|
||||
fs.chownSync(p, 1000, 1000); // keep it writable by the MC user
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadWhitelist(): Promise<void> {
|
||||
await rcon().exec("whitelist reload");
|
||||
}
|
||||
|
||||
/**
|
||||
* UUIDs the server itself reported in log lines ("UUID of player X is …").
|
||||
* These beat any derivation we could do — if Mojang changes the offline-UUID
|
||||
* scheme again, the panel adapts automatically.
|
||||
*/
|
||||
const observedUuids = new Map<string, string>();
|
||||
|
||||
export function noteObservedUuid(name: string, uuid: string) {
|
||||
observedUuids.set(name.toLowerCase(), uuid.toLowerCase());
|
||||
}
|
||||
|
||||
export async function whitelistAdd(name: string): Promise<string> {
|
||||
assertValidUsername(name);
|
||||
const uuid = observedUuids.get(name.toLowerCase()) ?? offlineUuid(name);
|
||||
const entries = readWhitelistFile().filter((e) => e.name.toLowerCase() !== name.toLowerCase());
|
||||
entries.push({ uuid: offlineUuid(name), name });
|
||||
entries.push({ uuid, name });
|
||||
writeWhitelistFile(entries);
|
||||
await reloadWhitelist();
|
||||
return `Added ${name} to the whitelist (offline UUID)`;
|
||||
|
||||
// Trust nothing: confirm the server actually loaded the entry.
|
||||
let live = await whitelistLive();
|
||||
if (live.some((n) => n.toLowerCase() === name.toLowerCase())) {
|
||||
return `Added ${name} to the whitelist`;
|
||||
}
|
||||
// File route didn't take — fall back to the vanilla command (in-memory,
|
||||
// applies instantly) and re-verify.
|
||||
const out = stripColors(await rcon().exec(`whitelist add ${name}`));
|
||||
live = await whitelistLive();
|
||||
if (live.some((n) => n.toLowerCase() === name.toLowerCase())) {
|
||||
return `Added ${name} to the whitelist (via command)`;
|
||||
}
|
||||
throw new Error(
|
||||
`The server did not accept the whitelist entry for ${name} (reload + command both failed: ${out.trim() || "no output"})`
|
||||
);
|
||||
}
|
||||
|
||||
export async function whitelistRemove(name: string): Promise<string> {
|
||||
assertValidUsername(name);
|
||||
writeWhitelistFile(readWhitelistFile().filter((e) => e.name.toLowerCase() !== name.toLowerCase()));
|
||||
try {
|
||||
await rcon().exec(`whitelist remove ${name}`); // clears any in-memory entry
|
||||
} catch {}
|
||||
await reloadWhitelist();
|
||||
return `Removed ${name} from the whitelist`;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user