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
@@ -87,59 +87,62 @@
         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 <li><a> to a list
         function addLink(ul, href, text) {
             var li = el("li", {}, {margin: "6px 0"});
             li.appendChild(el("a", {href: href, textContent: text}));
             ul.appendChild(li);
         }
 
         // "My Data" navigation: sessions, custom tracks, uploaded track hubs (hubSpace).
         var hgsid = getHgsidSafe();
         var navUl = el("ul", {}, {listStyle: "none", margin: "0 0 10px 0", padding: "0"});
         addLink(navUl, "../cgi-bin/hgSession?hgS_doMainPage=1&hgsid=" + hgsid, "My Sessions");
         addLink(navUl, "../cgi-bin/hgCustom?hgsid=" + hgsid, "My Custom Tracks");
         addLink(navUl, "../cgi-bin/hgHubConnect?hgsid=" + hgsid + "#unlistedHubs", "My Track Hubs");
         body.appendChild(navUl);
 
         // Account actions.
         var ul = el("ul", {}, {listStyle: "none", margin: "0", padding: "0",
                                borderTop: "1px solid #ddd", paddingTop: "8px"});
         if (changePwUrl)
             addLink(ul, changePwUrl, "Change password");
         if (changeEmailUrl)
             addLink(ul, changeEmailUrl, "Change email");
+        if (changeRecovEmailUrl)
+            addLink(ul, changeRecovEmailUrl, "Recovery email");
         addLink(ul, logoutUrl, "Sign out");
         body.appendChild(ul);
         showModal("Account", body);
     }
 
     // ---- Share a link ------------------------------------------------------------------------
 
     function getHgsidSafe() {
         if (typeof getHgsid === "function")
             return getHgsid();
         var m = /[?&]hgsid=([^&]+)/.exec(window.location.search);
         return m ? m[1] : "";
     }
 
     // Clipboard SVG icon (same FontAwesome glyph used by printCopyToClipboardButton in hgSession.c).