Merge pull request #3 from geneticglitch1/codex/repair-craftdeck-console-uploads

Support offline-name Forge logins and add the friend installation guide
This commit is contained in:
Aryan Singh authored and GitHub committed 2026-10-16 20:52:19 -05:00 Unverified
commit a51e9ef4cd
6 files changed
+86 -7

No files matched your search

+10 -3
View File
@@ -21,6 +21,8 @@ type AccessData = {
approved: ApprovedPlayer[];
whitelist: string[] | null;
registered: string[] | null;
authProvider: "easyauth" | "serverlogin";
registrationCommand: string;
};
const STATUS_BADGE: Record<ApprovedPlayer["status"], { color: "good" | "warn" | "crit" | "muted"; label: string }> = {
@@ -80,7 +82,7 @@ export default function AccessPage() {
<div>
<h1 className="text-xl font-bold">Access &amp; Auth</h1>
<p className="mt-0.5 text-xs text-ink2">
Only approved usernames can join. New players must <code>/register</code> within the window, then{" "}
Only approved usernames can join. New players must register within the window using <code>{data?.registrationCommand ?? "/register <password> <password>"}</code>, then{" "}
<code>/login</code> on every visit.
</p>
</div>
@@ -105,7 +107,7 @@ export default function AccessPage() {
</div>
<p className="mt-2 text-[11px] leading-relaxed text-muted">
Approving whitelists the name and starts the registration countdown. Tell your friend to join{" "}
<em>right away</em> and run <code>/register &lt;password&gt; &lt;password&gt;</code>. If the timer
<em>right away</em> and run <code>{data?.registrationCommand ?? "/register <password> <password>"}</code>. If the timer
expires first, the name is automatically removed from the whitelist.
</p>
</Card>
@@ -183,7 +185,7 @@ export default function AccessPage() {
<RefreshCw size={12} /> Re-open window
</Button>
)}
{p.status === "registered" && (
{p.status === "registered" && data?.authProvider !== "serverlogin" && (
<>
<Button
size="sm"
@@ -205,6 +207,11 @@ export default function AccessPage() {
</Button>
</>
)}
{p.status === "registered" && data?.authProvider === "serverlogin" && (
<Button size="sm" onClick={() => void run(`reregister-${p.username}`, { op: "force-reregister", username: p.username })} busy={busy === `reregister-${p.username}`}>
<KeyRound size={12} /> Re-register password
</Button>
)}
{p.status !== "revoked" && (
<Button size="sm" variant="danger" onClick={() => setConfirmRevoke(p.username)}>
Revoke
+2 -2
View File
@@ -8,7 +8,7 @@ import {
forceReregister,
importFromWhitelist,
} from "@/server/authflow";
import { whitelistList, easyauthList } from "@/server/mc";
import { whitelistList, easyauthList, authProvider, registrationCommand } from "@/server/mc";
import { ok, fail, readJson, handle } from "@/server/api";
export const dynamic = "force-dynamic";
@@ -23,7 +23,7 @@ export async function GET() {
try {
registered = await easyauthList();
} catch {}
return ok({ approved: listApproved(), whitelist, registered });
return ok({ approved: listApproved(), whitelist, registered, authProvider: authProvider(), registrationCommand: registrationCommand() });
});
}
+3 -2
View File
@@ -13,6 +13,7 @@ import {
easyauthList,
easyauthRemove,
easyauthSetPassword,
registrationCommand,
} from "./mc";
/**
@@ -72,7 +73,7 @@ export async function approvePlayer(username: string, note?: string): Promise<Ap
)
.run(username, now, expires, note ?? null);
logActivity("approve", username, `Registration window open for ${Math.round(windowMs() / 60000)} min`);
notify("info", `Approved ${username}`, "They can now join and /register — the window is ticking.");
notify("info", `Approved ${username}`, `They can now join and use ${registrationCommand()} — the window is ticking.`);
return db().prepare("SELECT * FROM approved_players WHERE username = ?").get(username) as unknown as ApprovedPlayer;
}
@@ -127,7 +128,7 @@ export async function resetPassword(username: string): Promise<string> {
export async function forceReregister(username: string): Promise<void> {
await easyauthRemove(username);
try {
await kickPlayer(username, "Please rejoin and /register a new password");
await kickPlayer(username, `Please rejoin and use ${registrationCommand()} to choose a new password`);
} catch {}
await whitelistAdd(username);
db()
+22
View File
@@ -384,9 +384,30 @@ export async function kickAll(reason: string): Promise<number> {
// ── EasyAuth ────────────────────────────────────────────────────────────
export function authProvider() {
return fs.existsSync(path.join(env.mcDataDir, "config", "serverlogin-common.toml")) ? "serverlogin" : "easyauth";
}
export function registrationCommand() {
return authProvider() === "serverlogin" ? "/login <password> <password>" : "/register <password> <password>";
}
export function parseServerLoginAccounts(text: string): string[] {
const data = JSON.parse(text) as { formatVersion?: number; users?: Record<string, { lastKnownName?: string }> };
if (data.formatVersion !== 1 || !data.users || Array.isArray(data.users) || typeof data.users !== "object") throw new Error("Unsupported Server Login account database");
return Object.values(data.users).map(account => account?.lastKnownName).filter((name): name is string => typeof name === "string" && isValidUsername(name));
}
/** Registered account names according to EasyAuth. */
export async function easyauthList(): Promise<string[]> {
if (authProvider() === "serverlogin") {
// Confirm the server is reachable before reconciling registration windows.
await execChecked("auth count");
const file = path.join(env.mcDataDir, "world", "serverlogin", "users.json");
if (!fs.existsSync(file)) return [];
return parseServerLoginAccounts(fs.readFileSync(file, "utf8"));
}
const out = stripColors(await rcon().exec("auth list"));
if (/unknown|incorrect|expected/i.test(out)) throw new Error("Authentication provider did not return an account list");
// Output is a list of registered usernames; extract all name-like tokens
// after a colon if present, otherwise from the whole string.
const colon = out.indexOf(":");
@@ -404,6 +425,7 @@ export async function easyauthRemove(name: string): Promise<string> {
export async function easyauthSetPassword(name: string, password: string): Promise<string> {
assertValidUsername(name);
if (authProvider() === "serverlogin") throw new Error("Use Force re-register, then let the player choose a new password in-game");
if (!/^[\x21-\x7e]{1,64}$/.test(password)) throw new Error("Unsupported password characters");
return stripColors(await rcon().exec(`auth update ${name} ${password}`));
}
+22
View File
@@ -0,0 +1,22 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
vi.mock("../src/server/env", () => ({ env: { mcDataDir: "" } }));
vi.mock("../src/server/rcon", () => ({ rcon: () => ({ exec: vi.fn() }), stripColors: (s: string) => s, RconError: class extends Error {} }));
import { env } from "../src/server/env";
import { authProvider, registrationCommand, parseServerLoginAccounts, easyauthSetPassword } from "../src/server/mc";
let dir: string;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), "craftdeck-auth-")); env.mcDataDir = dir; });
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
it("uses the Forge registration flow and never sends an unsupported password reset command", async () => {
expect(authProvider()).toBe("easyauth");
fs.mkdirSync(path.join(dir, "config")); fs.writeFileSync(path.join(dir, "config/serverlogin-common.toml"), "");
expect(registrationCommand()).toBe("/login <password> <password>");
await expect(easyauthSetPassword("aryan", "test-only")).rejects.toThrow("Force re-register");
});
it("returns names only from the documented account database and rejects unknown formats", () => {
expect(parseServerLoginAccounts(JSON.stringify({ formatVersion: 1, users: { aryan: { lastKnownName: "aryan", salt: "private", hash: "private" } } }))).toEqual(["aryan"]);
expect(() => parseServerLoginAccounts('{"formatVersion":2,"users":{}}')).toThrow();
expect(() => parseServerLoginAccounts('{"formatVersion":1,"users":[]}')).toThrow();
});
+27
View File
@@ -0,0 +1,27 @@
# Play From the Fog Reimagined
**Server:** `minecraft.ayush-jain.dev`
**Pack:** From the Fog Reimagined **6.3.2** by SoumXD
**Minecraft:** 1.20.1 · Forge 47.4.10 · Java 17
## Install once
1. Open your launcher’s modpack installer. For offline-name accounts, [SKlauncher 4.0](https://docs.skmedix.pl/4.0/) supports CurseForge packs and automatic Java setup. Its 4.0 release is currently beta. If you use Microsoft sign-in, the [CurseForge app](https://www.curseforge.com/download/app) can install the pack directly.
2. Search **From the Fog Reimagined** by **SoumXD**, select **6.3.2**, and install the whole pack. [Exact official client release](https://www.curseforge.com/minecraft/modpacks/from-the-fog-reimagined/files/9191040).
3. Launch that pack. Give the game 6 GB of memory if your computer has room; the pack author lists 4 GB as the client minimum. Shaders are optional—turn them off if performance is poor.
4. Multiplayer → Add Server → `minecraft.ayush-jain.dev`.
Use the exact username the admin approved, including capitalization. Start with `aryan` or `jainster` as appropriate. Ask the admin to approve any additional name in CraftDeck → Access & Auth immediately before your first join.
On first join, follow the server prompt: `/login YOUR_PASSWORD YOUR_PASSWORD`. On later joins: `/login YOUR_PASSWORD`. Choose your own password in the game; do not send it to the admin or reuse an important account password. If the registration window expires, the admin can reopen it in CraftDeck.
## If joining fails
- **Not whitelisted:** check the exact username and ask the admin to reopen approval.
- **Missing/incompatible mods:** launch the complete **6.3.2** pack, rather than vanilla or a standalone From the Fog datapack. Both sides must use 1.20.1 Forge.
- **Out of memory / slow:** allocate memory in this pack’s launcher settings and disable shaders first.
- **Cannot connect:** ask the admin to check the Minecraft container and TCPShield route. The CraftDeck admin address is private and is not the game server address.
The server uses the pack author’s matching server pack, plus a server-only login mod. Friends do not need to copy server files or install that login mod. Download pack content from the author through your launcher; no separate mod ZIP collection or hosted mirror is needed.
This is the official Reimagined pack baseline. An exact CaseOh-specific mod/shader list has not been verified.