Wire account verification end-to-end and fix an RFC 5322 mail bug (#1)
This commit is contained in:
parent
f4651e2c8d
commit
4e52a9c418
2 changed files with 55 additions and 0 deletions
|
|
@ -45,6 +45,8 @@ async function readJson(res) {
|
||||||
if (detail && typeof detail === "object" && detail.reason) msg = detail.reason;
|
if (detail && typeof detail === "object" && detail.reason) msg = detail.reason;
|
||||||
else if (detail === "LOGIN_BAD_CREDENTIALS") msg = "Wrong email or password.";
|
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 === "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 if (typeof detail === "string") msg = detail.replace(/_/g, " ").toLowerCase();
|
||||||
else msg = `Request failed (${res.status}).`;
|
else msg = `Request failed (${res.status}).`;
|
||||||
throw new Error(msg);
|
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 } });
|
const res = await apiFetch("api/v2/auth/register", { method: "POST", json: { email, password } });
|
||||||
await readJson(res);
|
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() {
|
export async function logout() {
|
||||||
try { await apiFetch("api/v2/auth/logout", { method: "POST" }); } catch (e) {}
|
try { await apiFetch("api/v2/auth/logout", { method: "POST" }); } catch (e) {}
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
|
|
@ -375,6 +381,27 @@ function escapeHtml(s) {
|
||||||
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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 --------------------------------------------------------------------
|
// --- boot --------------------------------------------------------------------
|
||||||
(async function initAccount() {
|
(async function initAccount() {
|
||||||
ensureAcctEl();
|
ensureAcctEl();
|
||||||
|
|
@ -382,4 +409,21 @@ function escapeHtml(s) {
|
||||||
await refreshUser(); // then confirm session from the cookie
|
await refreshUser(); // then confirm session from the cookie
|
||||||
renderHeader();
|
renderHeader();
|
||||||
emitAuth();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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 page (subscriptions) --- */
|
||||||
.alerts-loading { color: var(--muted); }
|
.alerts-loading { color: var(--muted); }
|
||||||
.alerts-gate {
|
.alerts-gate {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue