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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user