import { api } from "@rustpad/api"; import * as sessionStore from "@rustpad/session"; import { askInput, showMessage } from "./modal.js"; const { getAuthToken, setAuthSession, setNickname } = sessionStore; const clearAuthSession = sessionStore.clearAuthSession || (() => { localStorage.removeItem("rustpad:auth-token"); sessionStorage.removeItem("rustpad:auth-token"); sessionStorage.removeItem("rustpad:nickname"); }); export function bindIdentityDialog({ dialog, onIdentity, initialMode = "login" }) { if (dialog.querySelector("#auth-panel")) return bindLegacyIdentityDialog({ dialog, onIdentity }); const form = dialog.querySelector("#identity-form"); const nickname = dialog.querySelector("#nickname"); const message = dialog.querySelector("#identity-error"); const title = dialog.querySelector("#identity-title"); const copy = dialog.querySelector("#identity-copy"); const emailField = dialog.querySelector("#auth-email-field"); const email = dialog.querySelector("#auth-email"); const password = dialog.querySelector("#auth-password"); const submit = dialog.querySelector("#auth-submit"); const switchMode = dialog.querySelector("#auth-switch-mode"); const resetButton = dialog.querySelector("#show-reset"); const backButton = dialog.querySelector("#reset-back"); const registrationEnabled = document.body.dataset.registrationEnabled === "true"; let mode = initialMode; const setMode = (nextMode) => { mode = nextMode === "register" && !registrationEnabled ? "login" : nextMode; const resetting = mode === "reset"; const registering = mode === "register"; title.textContent = resetting ? "Reset password" : registering ? "Register nickname" : "Log in"; copy.textContent = resetting ? "Enter the e-mail address assigned to your account." : registering ? "Reserve your nickname with an e-mail address and password." : "Use the e-mail address and password assigned to your account."; nickname.closest("label").hidden = !registering; nickname.disabled = !registering; emailField.hidden = false; email.disabled = false; password.closest("label").hidden = resetting; password.disabled = resetting; submit.textContent = resetting ? "Send reset link" : registering ? "Create account" : "Log in"; switchMode.hidden = resetting || !registrationEnabled; switchMode.textContent = registering ? "Already registered? Log in" : "Create an account"; resetButton.hidden = resetting || registering; backButton.hidden = !resetting; const loginMode = mode === "login"; form.autocomplete = loginMode ? "on" : "off"; nickname.autocomplete = "off"; nickname.dataset.bwignore = "true"; email.autocomplete = loginMode ? "username" : "off"; password.autocomplete = loginMode ? "current-password" : "off"; if (loginMode) { email.removeAttribute("data-bwignore"); password.removeAttribute("data-bwignore"); } else { email.dataset.bwignore = "true"; password.dataset.bwignore = "true"; } message.textContent = ""; queueMicrotask(() => { (registering ? nickname : email).focus(); }); }; switchMode?.addEventListener("click", () => setMode(mode === "register" ? "login" : "register")); resetButton?.addEventListener("click", () => setMode("reset")); backButton?.addEventListener("click", () => setMode("login")); dialog.querySelector("#close-identity")?.addEventListener("click", () => dialog.close()); dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); }); dialog.addEventListener("close", () => { form.reset(); message.textContent = ""; setMode(initialMode); }); form.addEventListener("submit", async (event) => { event.preventDefault(); message.textContent = ""; submit.disabled = true; try { if (mode === "reset") { const result = await api("/api/auth/password-reset", { method: "POST", body: JSON.stringify({ email: email.value.trim() }), }); message.classList.remove("error"); message.classList.add("success"); message.textContent = result.message; return; } const endpoint = mode === "register" ? "/api/auth/register" : "/api/auth/login"; const payload = { email: email.value.trim(), password: password.value }; if (mode === "register") payload.nickname = nickname.value.trim(); const session = await api(endpoint, { method: "POST", body: JSON.stringify(payload) }); if (mode === "register" && session.confirmation_required) { message.classList.remove("error"); message.classList.add("success"); message.textContent = session.message; password.value = ""; return; } setAuthSession(session); await onIdentity(session.nickname, session); dialog.close(); } catch (error) { message.classList.remove("success"); message.classList.add("error"); message.textContent = error.message; } finally { submit.disabled = false; } }); setMode(initialMode); return { setMode }; } function bindLegacyIdentityDialog({ dialog, onIdentity }) { const form = dialog.querySelector("#identity-form"); const nickname = dialog.querySelector("#nickname"); const authPanel = dialog.querySelector("#auth-panel"); const message = dialog.querySelector("#identity-error"); const modeTitle = dialog.querySelector("#auth-mode-title"); const emailField = dialog.querySelector("#auth-email-field"); const email = dialog.querySelector("#auth-email"); const password = dialog.querySelector("#auth-password"); const authSubmit = dialog.querySelector("#auth-submit"); const guestSubmit = dialog.querySelector("#guest-continue"); const registerButton = dialog.querySelector("#show-register"); const loginButton = dialog.querySelector("#show-login"); const backButton = dialog.querySelector("#auth-back"); const logoutButton = dialog.querySelector("#logout-account"); const registrationEnabled = document.body.dataset.registrationEnabled === "true"; let mode = "login"; const updateActions = () => { if (logoutButton) logoutButton.hidden = !getAuthToken(); if (registerButton) registerButton.hidden = !registrationEnabled; }; const collapse = () => { authPanel.hidden = true; nickname.disabled = false; nickname.required = true; password.value = ""; message.textContent = ""; guestSubmit?.focus(); }; const showAuth = (nextMode) => { mode = nextMode === "register" && !registrationEnabled ? "login" : nextMode; authPanel.hidden = false; // Keep the guest nickname form visible so the user can still continue anonymously. nickname.disabled = mode === "login"; nickname.required = mode === "register"; emailField.hidden = false; email.disabled = false; modeTitle.textContent = mode === "register" ? "Register nickname" : "Log in"; authSubmit.textContent = mode === "register" ? "Register and continue" : "Log in and continue"; const loginMode = mode === "login"; form.autocomplete = loginMode ? "on" : "off"; nickname.autocomplete = "off"; nickname.dataset.bwignore = "true"; email.autocomplete = loginMode ? "username" : "off"; password.autocomplete = loginMode ? "current-password" : "off"; if (loginMode) { email.removeAttribute("data-bwignore"); password.removeAttribute("data-bwignore"); } else { email.dataset.bwignore = "true"; password.dataset.bwignore = "true"; } message.textContent = ""; queueMicrotask(() => (mode === "register" ? nickname : email).focus()); }; registerButton?.addEventListener("click", () => showAuth("register")); loginButton?.addEventListener("click", () => showAuth("login")); backButton?.addEventListener("click", collapse); dialog.querySelector("#close-identity")?.addEventListener("click", () => dialog.close()); dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); }); dialog.addEventListener("close", collapse); dialog.addEventListener("cancel", collapse); dialog.querySelector("#show-reset")?.addEventListener("click", async () => { const value = email.value.trim() || await askInput({ title: "Reset password", message: "Enter the e-mail address assigned to your account.", label: "E-mail", type: "email", autocomplete: "off", confirmText: "Send reset link", bitwardenIgnore: true, }) || ""; if (!value) return; try { const result = await api("/api/auth/password-reset", { method: "POST", body: JSON.stringify({ email: value }) }); message.classList.remove("error"); message.classList.add("success"); message.textContent = result.message; } catch (error) { message.classList.remove("success"); message.classList.add("error"); message.textContent = error.message; } }); logoutButton?.addEventListener("click", async () => { await logoutCurrentSession(); updateActions(); collapse(); nickname.value = ""; message.textContent = "Logged out."; }); form.addEventListener("submit", async (event) => { event.preventDefault(); message.textContent = ""; const name = nickname.value.trim(); const authRequested = event.submitter === authSubmit; try { if (authRequested) { const endpoint = mode === "register" ? "/api/auth/register" : "/api/auth/login"; const payload = { email: email.value.trim(), password: password.value }; if (mode === "register") payload.nickname = name; const session = await api(endpoint, { method: "POST", body: JSON.stringify(payload) }); if (mode === "register" && session.confirmation_required) { message.classList.remove("error"); message.classList.add("success"); message.textContent = session.message; password.value = ""; return; } setAuthSession(session); await onIdentity(session.nickname, session); return; } nickname.disabled = false; const result = await api("/api/auth/identity", { method: "POST", body: JSON.stringify({ nickname: name, session_token: getAuthToken() || null }) }); setNickname(result.nickname); await onIdentity(result.nickname, null); } catch (error) { message.classList.remove("success"); message.classList.add("error"); message.textContent = error.message; if (/registered|session|account/i.test(error.message)) showAuth("login"); } }); updateActions(); collapse(); return { setMode: showAuth }; } export async function validateCurrentSession() { const token = getAuthToken(); if (!token) return null; try { return await api("/api/auth/me", { headers: { Authorization: `Bearer ${token}` } }); } catch { clearAuthSession(); return null; } } export async function logoutCurrentSession() { const token = getAuthToken(); if (token) { try { await api("/api/auth/logout", { method: "POST", headers: { Authorization: `Bearer ${token}` } }); } catch { } } clearAuthSession(); } export async function handleAccountConfirmationToken() { const url = new URL(location.href); const token = url.searchParams.get("confirm_token"); if (!token) return; url.searchParams.delete("confirm_token"); history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`); try { const result = await api("/api/auth/confirm-account", { method: "POST", body: JSON.stringify({ token }) }); await showMessage(result.message, { title: "Account confirmed" }); } catch (error) { await showMessage(error.message, { title: "Account confirmation failed" }); } } export async function handleResetToken() { const url = new URL(location.href); const token = url.searchParams.get("reset_token"); if (!token) return; // Remove the token immediately. Refreshing or navigating back must not reopen the reset dialog. url.searchParams.delete("reset_token"); history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`); const password = await askInput({ title: "Set a new password", message: "The password must contain at least 8 characters.", label: "New password", type: "password", autocomplete: "off", minLength: 8, confirmText: "Change password", bitwardenIgnore: true, }); if (!password) return; try { await api("/api/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, password }) }); await showMessage("Password changed. The reset link has been used and cannot be opened again.", { title: "Password changed" }); } catch (error) { await showMessage(error.message, { title: "Password reset failed" }); } }