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:
6 files changed
+86
-7
No files matched your search
@@ -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 & 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 <password> <password></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
|
||||
|
||||
@@ -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() });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user