SSO and turn gate

This commit is contained in:
2026-10-30 16:36:36 -04:00
parent c4d9d1b86c
commit b96be7076e
8 changed files with 273 additions and 132 deletions
+32 -55
View File
@@ -1,6 +1,5 @@
"use strict";
const TOKEN_KEY = "thebutton-token";
const $ = (id) => document.getElementById(id);
const callbackParams = new URLSearchParams(window.location.hash.slice(1));
@@ -11,10 +10,10 @@ if (callbackError) {
let game = null; // latest /api/state payload
let clockOffset = 0; // serverTime - clientTime
let token = localStorage.getItem(TOKEN_KEY);
let oidcMode = false;
let oidcLoginUrl = "";
let oidcRegisterUrl = "";
let token = false;
let oidcLoginUrl = "/auth/oidc/login";
let oidcRegisterUrl = "https://lists.eom.dev/accounts/register/";
localStorage.removeItem("thebutton-token");
let statsLoaded = false;
let subscribed = false;
let currentShareText = "";
@@ -198,7 +197,6 @@ function syncPresence() {
if (token && sharePresence) $("presence-error").textContent = "Couldn't update online status. Retrying automatically.";
if (error.status === 401) {
token = null;
localStorage.removeItem(TOKEN_KEY);
lastPresenceUpdate = null;
updateButton();
}
@@ -218,13 +216,10 @@ window.addEventListener("storage", (event) => {
alertObservation = null;
renderAlertSettings();
}
if (event.key === PRESENCE_KEY || event.key === TOKEN_KEY) {
if (event.key === PRESENCE_KEY) {
sharePresence = localStorage.getItem(PRESENCE_KEY) !== "false";
if (event.key === TOKEN_KEY) isAdmin = false;
token = localStorage.getItem(TOKEN_KEY);
updateButton();
syncPresence();
if (event.key === TOKEN_KEY && token) loadMe();
}
});
@@ -247,19 +242,16 @@ function renderSections() {
const id = button.dataset.section;
button.hidden = !available[id];
button.setAttribute("aria-expanded", String(selectedSection === id));
if (id === "account-section") button.textContent = token ? "Your score" : oidcMode ? "Sign in" : "Log in / Join";
if (id === "account-section") button.textContent = token ? "Your score" : "Sign in";
});
document.querySelectorAll("[data-panel]").forEach((panel) => {
panel.hidden = panel.id !== selectedSection;
});
$("mailing-list").hidden = !token || subscribed;
$("register-section").hidden = !!token || oidcMode;
$("oidc-login-section").hidden = !!token || !oidcMode;
if (oidcMode) {
$("oidc-login-link").href = oidcLoginUrl;
$("oidc-register-link").href = oidcRegisterUrl || "#";
$("oidc-register-link").hidden = !oidcRegisterUrl;
}
$("oidc-login-section").hidden = !!token;
$("oidc-login-link").href = oidcLoginUrl;
$("oidc-register-link").href = oidcRegisterUrl || "#";
$("oidc-register-link").hidden = !oidcRegisterUrl;
$("chat-form").hidden = !token;
$("chat-login").hidden = !!token;
$("section-hint").hidden = selectedSection !== null;
@@ -282,7 +274,6 @@ $("section-nav").addEventListener("click", (event) => {
async function api(method, path, body) {
const headers = { "Content-Type": "application/json" };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(path, { method, headers, body: body ? JSON.stringify(body) : undefined });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw Object.assign(new Error(data.error || res.statusText), { status: res.status });
@@ -449,7 +440,18 @@ async function refreshState() {
}
function updateButton() {
$("the-button").disabled = !token || !game || game.ended;
const eligibility = game?.press_eligibility;
$("the-button").disabled = !token || !game || game.ended || eligibility?.eligible !== true;
$("press-eligibility").hidden = !token;
if (token) {
$("press-eligibility").textContent = !eligibility
? "Checking when you can press..."
: !eligibility.has_pressed
? "Your first press this season is ready."
: eligibility.eligible
? "Your turn is ready: two different players have pressed since your last turn."
: `Waiting for ${eligibility.other_players_remaining} more different ${eligibility.other_players_remaining === 1 ? "player" : "players"} to press.`;
}
$("me-section").hidden = !token;
$("my-subscribed").hidden = !token || !subscribed;
renderSections();
@@ -512,11 +514,11 @@ function renderAchievements(achievements) {
}
async function loadMe() {
if (!token) return;
const requestedToken = token;
try {
const me = await api("GET", "/api/me");
if (token !== requestedToken) return;
if (token !== requestedToken && token) return;
token = true;
isAdmin = me.is_admin === true;
$("my-name").textContent = me.name;
$("my-points").textContent = me.total_points.toLocaleString();
@@ -549,6 +551,7 @@ async function loadMe() {
}
subscribed = me.subscribed;
updateButton();
syncPresence();
fillTable($("my-zones"), ["Zone", "Points each", "Your presses"], zoneRows(me.zone_counts));
fillTable(
$("my-history"),
@@ -561,7 +564,6 @@ async function loadMe() {
if (e.status === 401) {
token = null;
isAdmin = false;
localStorage.removeItem(TOKEN_KEY);
updateButton();
}
}
@@ -879,7 +881,6 @@ $("chat-form").addEventListener("submit", async (event) => {
$("chat-error").textContent = error.message;
if (error.status === 401) {
token = null;
localStorage.removeItem(TOKEN_KEY);
updateButton();
}
} finally {
@@ -934,6 +935,7 @@ $("the-button").addEventListener("click", async () => {
try {
const r = await api("POST", "/api/press");
game.deadline = r.deadline;
game.press_eligibility = r.press_eligibility;
currentShareText = `I pushed #TheButton at ${formatPrecise(r.press.remaining_ms)}.`;
$("press-result").textContent =
`You pressed in the ${r.press.zone} zone with ${formatPrecise(r.press.remaining_ms)} remaining: +${r.press.points} points`;
@@ -954,15 +956,10 @@ $("mastodon-instance").addEventListener("input", updateShareLinks);
async function loadAuthConfig() {
try {
const response = await fetch("/api/config", { cache: "no-store" });
if (!response.ok) return;
if (!response.ok) throw new Error("Identity configuration is unavailable");
const config = await response.json();
oidcMode = config.auth_mode === "oidc";
if (oidcMode) {
token = null;
localStorage.removeItem(TOKEN_KEY);
}
oidcLoginUrl = config.login_url || "";
oidcRegisterUrl = config.register_url || "";
oidcLoginUrl = config.login_url || "/auth/oidc/login";
oidcRegisterUrl = config.register_url || "https://lists.eom.dev/accounts/register/";
renderSections();
if (callbackError) {
const errorMessage = callbackError === "access_denied"
@@ -973,36 +970,16 @@ async function loadAuthConfig() {
$("oidc-error").textContent = errorMessage;
}
} catch {
// Keep the local login UI available if the server has no OIDC config endpoint.
// Never fall back to local credentials when Dex is unavailable.
renderSections();
}
}
$("register-form").addEventListener("submit", async (ev) => {
ev.preventDefault();
$("register-error").textContent = "";
const action = ev.submitter?.value === "register" ? "register" : "login";
try {
const r = await api("POST", `/api/${action}`, {
name: $("name-input").value,
password: $("password-input").value,
});
token = r.token;
localStorage.setItem(TOKEN_KEY, token);
syncPresence();
$("password-input").value = "";
updateButton();
await loadMe();
await loadLeaderboard();
} catch (e) {
$("register-error").textContent = e.message;
}
});
$("logout-button").addEventListener("click", async () => {
await api("POST", "/api/logout").catch(() => {});
token = null;
subscribed = false;
localStorage.removeItem(TOKEN_KEY);
isAdmin = false;
$("me-section").hidden = true;
$("press-result").textContent = "";
$("subscribe-result").textContent = "";