a1331212cb
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.
116 lines
4.5 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
}
|