big features
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
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;
|
||||
password.autocomplete = registering ? "new-password" : "current-password";
|
||||
message.textContent = "";
|
||||
|
||||
queueMicrotask(() => {
|
||||
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) });
|
||||
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";
|
||||
password.autocomplete = mode === "register" ? "new-password" : "current-password";
|
||||
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) });
|
||||
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 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" });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user