330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
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" });
|
|
}
|
|
}
|