Add username/password auth and login UI
This commit is contained in:
121
wwwroot/app.js
121
wwwroot/app.js
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user