Release Healer Man 0.1.5
This commit is contained in:
+98
-2
@@ -3,6 +3,10 @@ import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import {
|
||||
cloneDefaultManastormAdminConfig,
|
||||
normalizeManastormAdminConfig,
|
||||
} from "./manastorm-config.mjs";
|
||||
|
||||
const scrypt = promisify(scryptCallback);
|
||||
|
||||
@@ -33,8 +37,19 @@ async function derivePassword(password, salt) {
|
||||
return Buffer.from(await scrypt(password, salt, 64));
|
||||
}
|
||||
|
||||
const GM_USERNAME_KEYS = new Set(["phenom"]);
|
||||
|
||||
function accountRoles(username) {
|
||||
return GM_USERNAME_KEYS.has(String(username ?? "").trim().toLowerCase()) ? ["gm"] : [];
|
||||
}
|
||||
|
||||
function publicAccount(row) {
|
||||
return { id: row.id, username: row.username, createdAt: Number(row.created_at) };
|
||||
return {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
createdAt: Number(row.created_at ?? row.createdAt),
|
||||
roles: accountRoles(row.username),
|
||||
};
|
||||
}
|
||||
|
||||
export function openGameDatabase(options = {}) {
|
||||
@@ -62,7 +77,21 @@ export function openGameDatabase(options = {}) {
|
||||
account_id TEXT PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
revision INTEGER NOT NULL, data_json TEXT NOT NULL, updated_at INTEGER NOT NULL
|
||||
) STRICT;
|
||||
CREATE TABLE IF NOT EXISTS manastorm_admin_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
schema_version INTEGER NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
config_json TEXT NOT NULL,
|
||||
updated_by_account_id TEXT REFERENCES accounts(id) ON DELETE SET NULL,
|
||||
updated_by_username TEXT,
|
||||
updated_at INTEGER
|
||||
) STRICT;
|
||||
`);
|
||||
database.prepare(`
|
||||
INSERT OR IGNORE INTO manastorm_admin_config (
|
||||
id, schema_version, revision, config_json, updated_by_account_id, updated_by_username, updated_at
|
||||
) VALUES (1, 1, 0, ?, NULL, NULL, NULL)
|
||||
`).run(JSON.stringify(cloneDefaultManastormAdminConfig()));
|
||||
|
||||
const statements = {
|
||||
accountByUsername: database.prepare("SELECT * FROM accounts WHERE username_key = ?"),
|
||||
@@ -82,6 +111,18 @@ export function openGameDatabase(options = {}) {
|
||||
data_json = excluded.data_json, updated_at = excluded.updated_at
|
||||
RETURNING revision, updated_at
|
||||
`),
|
||||
manastormConfig: database.prepare("SELECT * FROM manastorm_admin_config WHERE id = 1"),
|
||||
updateManastormConfig: database.prepare(`
|
||||
UPDATE manastorm_admin_config
|
||||
SET schema_version = 1,
|
||||
revision = revision + 1,
|
||||
config_json = ?,
|
||||
updated_by_account_id = ?,
|
||||
updated_by_username = ?,
|
||||
updated_at = ?
|
||||
WHERE id = 1 AND revision = ?
|
||||
RETURNING revision, updated_at
|
||||
`),
|
||||
};
|
||||
|
||||
function issueSession(accountId) {
|
||||
@@ -110,7 +151,7 @@ export function openGameDatabase(options = {}) {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return { account, ...issueSession(account.id) };
|
||||
return { account: publicAccount(account), ...issueSession(account.id) };
|
||||
}
|
||||
|
||||
async function login(usernameInput, password) {
|
||||
@@ -160,6 +201,58 @@ export function openGameDatabase(options = {}) {
|
||||
return { revision: Number(row.revision), updatedAt: Number(row.updated_at), data };
|
||||
}
|
||||
|
||||
function isGameMaster(account) {
|
||||
// Recompute authority from the server allowlist for every privileged call.
|
||||
return accountRoles(account?.username).includes("gm");
|
||||
}
|
||||
|
||||
function getManastormConfig() {
|
||||
const row = statements.manastormConfig.get();
|
||||
try {
|
||||
return {
|
||||
revision: Number(row?.revision ?? 0),
|
||||
updatedAt: row?.updated_at == null ? null : Number(row.updated_at),
|
||||
updatedBy: row?.updated_by_username ?? null,
|
||||
config: normalizeManastormAdminConfig(JSON.parse(row?.config_json ?? "null")),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Healer Man Manastorm configuration is corrupt; using safe defaults", error);
|
||||
return { revision: 0, updatedAt: null, updatedBy: null, config: cloneDefaultManastormAdminConfig() };
|
||||
}
|
||||
}
|
||||
|
||||
function putManastormConfig(account, expectedRevision, config) {
|
||||
if (!isGameMaster(account)) {
|
||||
throw new GameDatabaseError("Game Master access is required.", 403, "gm_required");
|
||||
}
|
||||
if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
|
||||
throw new GameDatabaseError("A valid expected revision is required.", 400, "invalid_revision");
|
||||
}
|
||||
let normalized;
|
||||
try {
|
||||
normalized = normalizeManastormAdminConfig(config);
|
||||
} catch (error) {
|
||||
throw new GameDatabaseError(error instanceof Error ? error.message : "Invalid Manastorm configuration.", 400, "invalid_manastorm_config");
|
||||
}
|
||||
const now = Date.now();
|
||||
const row = statements.updateManastormConfig.get(
|
||||
JSON.stringify(normalized),
|
||||
account.id,
|
||||
account.username,
|
||||
now,
|
||||
expectedRevision,
|
||||
);
|
||||
if (!row) {
|
||||
throw new GameDatabaseError("The Manastorm configuration changed. Reload and try again.", 409, "revision_conflict");
|
||||
}
|
||||
return {
|
||||
revision: Number(row.revision),
|
||||
updatedAt: Number(row.updated_at),
|
||||
updatedBy: account.username,
|
||||
config: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
databasePath,
|
||||
register,
|
||||
@@ -168,6 +261,9 @@ export function openGameDatabase(options = {}) {
|
||||
logout: (token) => { if (typeof token === "string" && token) statements.deleteSession.run(tokenHash(token)); },
|
||||
getCloudSave,
|
||||
putCloudSave,
|
||||
isGameMaster,
|
||||
getManastormConfig,
|
||||
putManastormConfig,
|
||||
pruneExpiredSessions: () => Number(statements.deleteExpiredSessions.run(Date.now()).changes),
|
||||
close: () => database.close(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user