Wire account verification end-to-end and fix an RFC 5322 mail bug (#1)

This commit is contained in:
emi 2026-07-21 03:57:20 +00:00
parent f4651e2c8d
commit 4e52a9c418
2 changed files with 55 additions and 0 deletions

View file

@ -45,6 +45,8 @@ async function readJson(res) {
if (detail && typeof detail === "object" && detail.reason) msg = detail.reason;
else if (detail === "LOGIN_BAD_CREDENTIALS") msg = "Wrong email or password.";
else if (detail === "REGISTER_USER_ALREADY_EXISTS") msg = "That email is already registered.";
else if (detail === "VERIFY_USER_BAD_TOKEN") msg = "That verification link is invalid or expired.";
else if (detail === "VERIFY_USER_ALREADY_VERIFIED") msg = "This account is already verified.";
else if (typeof detail === "string") msg = detail.replace(/_/g, " ").toLowerCase();
else msg = `Request failed (${res.status}).`;
throw new Error(msg);
@ -84,6 +86,10 @@ async function register(email, password) {
const res = await apiFetch("api/v2/auth/register", { method: "POST", json: { email, password } });
await readJson(res);
}
async function verifyEmail(token) {
const res = await apiFetch("api/v2/auth/verify", { method: "POST", json: { token } });
await readJson(res);
}
export async function logout() {
try { await apiFetch("api/v2/auth/logout", { method: "POST" }); } catch (e) {}
currentUser = null;
@ -375,6 +381,27 @@ function escapeHtml(s) {
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
// --- toast ---------------------------------------------------------------
// One-off status messages (e.g. "email verified") that aren't tied to the auth
// modal, which may not even be open. Success/failure share the same neutral
// style — the text says which, not the color; isError only changes the
// live-region role (alert = assertive, status = polite).
let toastTimer = null;
function showToast(msg, isError = false) {
let el = document.querySelector(".acct-toast");
if (!el) {
el = document.createElement("div");
el.className = "acct-toast";
document.body.appendChild(el);
}
el.textContent = msg;
el.setAttribute("role", isError ? "alert" : "status");
el.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { el.hidden = true; }, 6000);
}
// --- boot --------------------------------------------------------------------
(async function initAccount() {
ensureAcctEl();
@ -382,4 +409,21 @@ function escapeHtml(s) {
await refreshUser(); // then confirm session from the cookie
renderHeader();
emitAuth();
const params = new URLSearchParams(location.search);
const token = params.get("verify_token");
if (token) {
params.delete("verify_token");
const clean = location.pathname + (params.toString() ? `?${params}` : "") + location.hash;
history.replaceState(null, "", clean);
try {
await verifyEmail(token);
await refreshUser();
renderHeader();
emitAuth();
showToast("Email verified — you're all set.");
} catch (err) {
showToast(err.message || "Verification link is invalid or expired.", true);
}
}
})();

View file

@ -402,6 +402,17 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
font: inherit; font-weight: 600; padding: 0; text-decoration: underline;
}
/* One-off status toast (e.g. "email verified") neutral for both success and
failure; the text says which, not the color. */
.acct-toast {
position: fixed; left: 50%; bottom: 24px; z-index: 1200;
transform: translateX(-50%);
max-width: calc(100vw - 32px); padding: 12px 18px; border-radius: 10px;
background: var(--surface); border: 1px solid var(--border); color: var(--text);
font-size: 14px; box-shadow: 0 12px 30px rgba(0, 0, 0, .4);
}
.acct-toast[hidden] { display: none; }
/* --- alerts page (subscriptions) --- */
.alerts-loading { color: var(--muted); }
.alerts-gate {