d33d0e1c19117a0347dbadb99a77e9f12483c735
max
Tue Sep 1 06:56:09 2026 -0700
hgLogin: let users set or change their recovery email address, refs #38197
Adds a page where a signed-in user can set or change the recovery address on
their account, offered in the account menu next to "Change email" -- both in
the top right blue bar popup and on the session page. It is off by default:
set login.recovEmailChange=on in hg.conf to offer it. The page also needs
login.cookieSalt, working outbound mail and the recovEmailVerified column,
and stays hidden where any of those is missing.
The new address is confirmed by mail before it takes effect, so whatever is
on the account keeps working until the link is opened and a typo costs the
user nothing. An account that has a password must supply it, since a
confirmed recovery address can sign in. Once the address does change, the
account's main address is told, the same notice that a change of the main
address already sends.
One signature now covers both the address given at signup and a later
change, so there is a single confirmation path rather than two.
diff --git src/hg/js/topLinks.js src/hg/js/topLinks.js
index 99b5248175e..b00ccd58325 100644
--- src/hg/js/topLinks.js
+++ src/hg/js/topLinks.js
@@ -1,386 +1,389 @@
// topLinks.js - behavior for the two top-right menu-bar links, "Login" and "Share a link".
//
// Login: when the user is logged in, the menu item shows their username and opens a small
// account dialog (change password, sign out). When logged out it is a plain link to hgSession,
// so this script does nothing for it.
//
// Share a link (browser pages only): saves the current view as a session and shows a copyable
// short link. Logged-in users get to name the session and choose whether others may open it;
// logged-out users get an anonymous link created on the fly.
//
// The C code that builds the menu bar (lib/web.c menuBar() for CGI pages, hgMenubar.c for static
// pages) fills in the data-* attributes that this script reads. The dialog is a self-contained
// overlay so it works on static pages too (no jQuery UI dependency).
/* jshint esversion: 8 */
/* global $, document, window, URL, getHgsid, copyToClipboard */
var topLinks = (function() {
"use strict";
// Small helper to build an element with properties and inline styles set from the DOM
// (assigning to element.style.* is CSP-safe, unlike a style="" attribute in markup).
function el(tag, props, styles) {
var e = document.createElement(tag);
var k;
if (props)
for (k in props)
if (props.hasOwnProperty(k)) e[k] = props[k];
if (styles)
for (k in styles)
if (styles.hasOwnProperty(k)) e.style[k] = styles[k];
return e;
}
var closeCurrent = null; // closer for the currently open modal, if any
function closeModal() {
if (closeCurrent) { closeCurrent(); closeCurrent = null; }
}
// Show a centered modal with the given title and body node. Returns the body element so the
// caller can replace its contents later (e.g. swap a spinner for the resulting link).
function showModal(titleText, bodyNode, widthPx) {
closeModal();
var overlay = el("div", {}, {
position: "fixed", top: "0", left: "0", right: "0", bottom: "0",
background: "rgba(0,0,0,0.4)", zIndex: "10000"
});
var box = el("div", {}, {
position: "fixed", top: "90px", left: "50%", transform: "translateX(-50%)",
background: "#fff", color: "#000", border: "1px solid #888", borderRadius: "4px",
boxShadow: "0 2px 12px rgba(0,0,0,0.4)", width: (widthPx || 430) + "px", maxWidth: "92%",
zIndex: "10001", fontSize: "14px"
});
var titleBar = el("div", {}, {
background: "#00457c", color: "#fff", padding: "8px 12px", fontWeight: "bold",
borderTopLeftRadius: "4px", borderTopRightRadius: "4px", position: "relative"
});
titleBar.appendChild(document.createTextNode(titleText));
var x = el("span", {title: "Close", textContent: "×"}, {
position: "absolute", right: "10px", top: "5px", cursor: "pointer",
fontSize: "18px", lineHeight: "18px"
});
x.addEventListener("click", closeModal);
titleBar.appendChild(x);
var body = el("div", {}, {padding: "14px"});
if (bodyNode) body.appendChild(bodyNode);
box.appendChild(titleBar);
box.appendChild(body);
overlay.appendChild(box);
document.body.appendChild(overlay);
// Swallow Escape and clicks so they don't also reach an underlying dialog's own
// close-on-escape / click-outside handlers (e.g. the hgc item-details popup).
function onKey(ev) {
if (ev.key === "Escape") {
ev.preventDefault();
ev.stopImmediatePropagation();
closeModal();
}
}
overlay.addEventListener("click", function(ev) {
if (ev.target === overlay)
closeModal();
ev.stopPropagation();
});
document.addEventListener("keydown", onKey, true); // capture: run before other handlers
closeCurrent = function() {
document.removeEventListener("keydown", onKey, true);
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
};
return body;
}
// ---- Login / account dialog --------------------------------------------------------------
function showLoginDialog(link) {
var user = link.getAttribute("data-username");
var logoutUrl = link.getAttribute("data-logouturl");
var changePwUrl = link.getAttribute("data-changepwurl");
var changeEmailUrl = link.getAttribute("data-changeemailurl");
+ var changeRecovEmailUrl = link.getAttribute("data-changerecovemailurl");
var body = document.createElement("div");
var p = el("p");
p.appendChild(document.createTextNode("Signed in as "));
p.appendChild(el("b", {textContent: user})); // textContent avoids HTML injection
p.appendChild(document.createTextNode("."));
body.appendChild(p);
// helper: add an