Add username/password auth and login UI

This commit is contained in:
2026-01-29 01:01:13 +01:00
parent ca25d4f0ee
commit f1534b7631
21 changed files with 690 additions and 50 deletions

View File

@@ -1,6 +1,8 @@
import { api, adminApi } from "./js/api.js";
const state = {
isAuthenticated: false,
authMode: "login",
me: null,
phase: null,
counts: null,
@@ -21,11 +23,54 @@ function toast(msg, isError = false) {
setTimeout(() => toastEl.classList.add("hidden"), 2000);
}
function setAuthUI(isAuthed) {
const main = document.querySelector("main");
const statusBar = document.querySelector(".status-bar");
const authCard = $("auth-card");
[main, statusBar].forEach(el => el?.classList.toggle("hidden", !isAuthed));
if (authCard) authCard.classList.toggle("hidden", isAuthed);
const adminToggle = $("admin-toggle");
if (adminToggle) adminToggle.classList.toggle("hidden", !isAuthed);
}
function setAuthMode(mode) {
state.authMode = mode;
document.querySelectorAll(".auth-form").forEach(form => {
form.classList.toggle("hidden", form.dataset.mode !== mode);
});
document.querySelectorAll("[data-auth-tab]").forEach(btn => {
btn.classList.toggle("active", btn.dataset.authTab === mode);
});
}
function clearUserState() {
state.me = null;
state.phase = null;
state.counts = null;
state.mySuggestions = [];
state.allSuggestions = [];
state.myVotes = [];
state.results = [];
}
function handleAuthError(err) {
if (err?.status === 401) {
clearUserState();
state.isAuthenticated = false;
setAuthUI(false);
return true;
}
toast(err?.message || "Unexpected error", true);
return false;
}
async function loadState() {
const [me, stateData] = await Promise.all([api.me(), api.state()]);
state.isAuthenticated = true;
state.me = me;
state.phase = stateData.currentPhase;
state.counts = stateData;
setAuthUI(true);
renderPhasePill();
renderCounts();
const nameInput = $("name-input");
@@ -181,6 +226,52 @@ function renderResults() {
}
function setupHandlers() {
document.querySelectorAll("[data-auth-tab]").forEach(btn => {
btn.addEventListener("click", () => setAuthMode(btn.dataset.authTab));
});
setAuthMode(state.authMode);
const loginForm = $("login-form");
if (loginForm) {
loginForm.addEventListener("submit", async (e) => {
e.preventDefault();
const username = $("login-username").value.trim();
const password = $("login-password").value;
if (!username || !password) return toast("Username and password required", true);
try {
await api.login({ username, password });
state.isAuthenticated = true;
setAuthUI(true);
await refreshPhaseData();
toast("Logged in");
} catch (err) {
if (err?.status === 401) return toast("Invalid username or password", true);
if (handleAuthError(err)) return;
}
});
}
const registerForm = $("register-form");
if (registerForm) {
registerForm.addEventListener("submit", async (e) => {
e.preventDefault();
const username = $("register-username").value.trim();
const password = $("register-password").value;
const displayName = $("register-displayName").value.trim();
if (!username || !password) return toast("Username and password required", true);
try {
await api.register({ username, password, displayName });
state.isAuthenticated = true;
setAuthUI(true);
await refreshPhaseData();
toast("Registered");
} catch (err) {
if (handleAuthError(err)) return;
toast(err.message, true);
}
});
}
const nameInput = $("name-input");
if (nameInput) {
["focus", "input"].forEach(evt => {
@@ -241,6 +332,20 @@ function setupHandlers() {
$("reset").addEventListener("click", () => adminAction(adminApi.reset, "Reset complete"));
$("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, "Factory reset complete"));
const logoutBtn = $("logout");
if (logoutBtn) {
logoutBtn.addEventListener("click", async () => {
try {
await api.logout();
} catch (err) {
toast(err.message, true);
}
clearUserState();
state.isAuthenticated = false;
setAuthUI(false);
});
}
const adminToggle = $("admin-toggle");
const adminCard = $("admin-card");
const adminClose = $("admin-close");
@@ -263,8 +368,13 @@ async function adminAction(fn, successMessage) {
}
async function refreshPhaseData() {
await loadState();
await Promise.all([loadSuggestData(), loadRevealData(), loadVoteData(), loadResults()]);
try {
await loadState();
await Promise.all([loadSuggestData(), loadRevealData(), loadVoteData(), loadResults()]);
} catch (err) {
if (handleAuthError(err)) return;
throw err;
}
}
function buildCard(s, { showAuthor = false, allowDelete = false }) {
@@ -328,6 +438,7 @@ function openLightbox(url, title) {
}
function applyNameRequirementUI() {
if (!state.isAuthenticated) return;
const requiresName = !state.me?.displayName?.trim();
const warning = $("name-warning");
if (warning) warning.classList.toggle("hidden", !requiresName);
@@ -359,7 +470,11 @@ async function main() {
} catch (err) {
toast(err.message, true);
}
setInterval(refreshPhaseData, 4000);
setInterval(() => {
refreshPhaseData().catch(err => {
if (!handleAuthError(err)) toast(err.message, true);
});
}, 4000);
}
main();