Files
brass-and-sigil/server/wwwroot/modules/auth.js
T
Matt Sijbers a1331212cb Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.

- launcher/  Avalonia 12 desktop client; single-file win-x64 publish.
             Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
             portable install path, sidecar settings.
- server/    brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
             MC subprocess, embedded Kestrel admin panel with cookie
             auth + rate limiting, RCON bridge, scheduled backups,
             BlueMap CLI integration with player markers + skin proxy,
             friend-side whitelist request flow, world wipe with seed
             selection (keep current / random / custom).
- pack/      pack.lock.json (Modrinth + manual CurseForge entries),
             data-only tweak source under tweaks/, build outputs in
             overrides/ (gitignored).
- scripts/   Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
             plus Deploy-Brass.ps1 unified one-shot deploy with
             version-bump pre-flight and daemon-state detection.
2026-05-05 00:19:05 +01:00

116 lines
4.5 KiB
JavaScript

// Login overlay + Account panel (logout / change password).
//
// Cookie is set server-side as HttpOnly so JS never sees it -- that defeats
// XSS-based exfiltration. We only briefly hold the password during input.
"use strict";
// We deliberately don't import apiJson here -- change-password returns its
// own error messages and we want to surface them verbatim to the user.
let overlayShown = false;
function showOverlay() {
if (overlayShown) return;
overlayShown = true;
const overlay = document.getElementById("loginOverlay");
if (overlay) {
overlay.hidden = false;
document.getElementById("loginPassword")?.focus();
}
}
export function setupAuth() {
document.addEventListener("authrequired", showOverlay);
setupLoginForm();
setupAccountPanel();
}
function setupLoginForm() {
const overlay = document.getElementById("loginOverlay");
const input = document.getElementById("loginPassword");
const button = document.getElementById("loginSubmit");
const errorEl = document.getElementById("loginError");
if (!overlay || !input || !button || !errorEl) return;
async function tryLogin() {
const pw = input.value;
if (!pw) return;
errorEl.textContent = "";
button.disabled = true;
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: pw }),
});
if (res.status === 401) { errorEl.textContent = "Wrong password."; input.select(); return; }
if (res.status === 429) { errorEl.textContent = "Too many attempts. Wait a minute."; return; }
if (!res.ok) { errorEl.textContent = `Error ${res.status}`; return; }
// Server set the cookie. Reload so SSE / pollers pick it up.
location.reload();
} catch (e) {
errorEl.textContent = e.message;
} finally {
button.disabled = false;
input.value = "";
}
}
button.addEventListener("click", tryLogin);
input.addEventListener("keydown", e => { if (e.key === "Enter") tryLogin(); });
}
function setupAccountPanel() {
const logoutBtn = document.getElementById("acctLogout");
const changeBtn = document.getElementById("acctChangePw");
const form = document.getElementById("acctChangeForm");
const cur = document.getElementById("acctCurrent");
const nxt = document.getElementById("acctNew");
const cnf = document.getElementById("acctConfirm");
const submit = document.getElementById("acctSubmit");
const cancel = document.getElementById("acctCancel");
const msg = document.getElementById("acctMsg");
if (!logoutBtn || !changeBtn) return;
logoutBtn.addEventListener("click", async () => {
if (!confirm("Log out of the panel?")) return;
try { await fetch("/api/auth/logout", { method: "POST" }); }
finally { location.reload(); }
});
changeBtn.addEventListener("click", () => {
form.hidden = !form.hidden;
msg.textContent = "";
if (!form.hidden) cur.focus();
});
cancel.addEventListener("click", () => {
form.hidden = true;
cur.value = nxt.value = cnf.value = "";
msg.textContent = "";
});
submit.addEventListener("click", async () => {
msg.className = "acct-msg";
msg.textContent = "";
if (nxt.value.length < 8) { msg.textContent = "New password must be at least 8 characters."; return; }
if (nxt.value !== cnf.value) { msg.textContent = "New password and confirmation don't match."; return; }
submit.disabled = true;
try {
const res = await fetch("/api/auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ current: cur.value, next: nxt.value }),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) { msg.textContent = body.error || `Error ${res.status}`; return; }
msg.className = "acct-msg ok";
msg.textContent = "Password changed.";
cur.value = nxt.value = cnf.value = "";
setTimeout(() => { form.hidden = true; msg.textContent = ""; }, 1500);
} catch (e) {
msg.textContent = e.message;
} finally {
submit.disabled = false;
}
});
}