80bedbcf180ec262a2b7b9b941f2f80d71c1551b
hiram
  Fri Jul 24 11:34:48 2026 -0700
add password reveal button to hgLogin form refs ##37921

diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c
index c10d1496378..cc67f339de7 100644
--- src/hg/hgLogin/hgLogin.c
+++ src/hg/hgLogin/hgLogin.c
@@ -42,30 +42,32 @@
 /* The excludeVars are not saved to the cart. */
 char *excludeVars[] = { "submit", "Submit", "debug", "fixMembers", "update", 
      "hgLogin_password", "hgLogin_password2", "hgLogin_newPassword1",
      "hgLogin_newPassword2", NULL };
 struct cart *cart;	/* This holds cgi and other variables between clicks. */
 char *database;		/* Name of genome database - hg15, mm3, or the like. */
 struct hash *oldCart;	/* Old cart hash. */
 char *errMsg = NULL;    /* Error message to show user when form data rejected */
 char brwName[64];
 char brwAddr[256];
 char signature[256];
 char returnAddr[256];
 char *hgLoginUrl = NULL; /* full absolute URL to hgLogin as seen from browser,
     e.g. http://genome.ucsc.edu/cgi-bin/hgLogin. Can be a relative URL /cgi-bin/hgLogin if
     hg.conf login.relativeLink is on. */
+boolean pwdEyeIconEnabled = FALSE; /* show/hide eye icon on password fields;
+    set from hg.conf login.pwdEyeIcon in doMiddle() */
 
 /* for earlyBotCheck() function at the beginning of main() */
 #define delayFraction   1.0    /* standard penalty is 1.0 for most CGIs */
 
 /* ---- Global helper functions ---- */
 char *browserName()
 /* Return the browser name like 'UCSC Genome Browser' */
 {
 if isEmpty(cfgOption(CFG_LOGIN_BROWSER_NAME))
     return cloneString("NULL_browserName");
 else
     return cloneString(cfgOption(CFG_LOGIN_BROWSER_NAME));
 }
 
 char *browserAddr()
@@ -302,31 +304,34 @@
         }
     if (*c <= ' ' || *c >= 127) return 0;
     if (strchr(rfc822_specials, *c)) return 0;
     } while (*++c);
 
 return (count >= 1);
 }
 
 struct dyString *getLoginCookieJS(char *userName, uint idx)
 /* returns javascript statements that set the cookies associated with
  * logging in as a particular user */
 {
 struct dyString *result = dyStringNew(1024);
 struct slName *newCookies = loginLoginUser(userName, idx), *sl;
 for (sl = newCookies;  sl != NULL;  sl = sl->next)
+{
+verbose(0, "DBG login cookie: '%s'\n", sl->name);
     dyStringPrintf(result, " document.cookie = '%s';", sl->name);
+}
 return result; 
 }
 
 static boolean isValidReturnUrl(char *returnUrl)
 /* Verify that returnUrl startswith an hg.conf approved set of hosts. */
 {
 struct slName *approvedHosts = slNameListFromComma(cfgOptionDefault(CFG_APPROVED_HOSTS, NULL));
 slAddHead(&approvedHosts, slNameNew(hLoginHostCgiBinUrl()));
 if (approvedHosts)
     {
     struct slName *approvedStart;
     for (approvedStart = approvedHosts; approvedStart != NULL; approvedStart = approvedStart->next)
         {
         if (startsWith(approvedStart->name, returnUrl))
             return TRUE;
@@ -699,62 +704,111 @@
 void setupNewAccount(struct sqlConnection *conn, char *email, char *username)
 /* Set up  new user account and send activation mail to user */
 {
 char query[256];
 char *token = generateRandomPassword();
 char *tokenMD5 = generateTokenMD5(token);
 sqlSafef(query,sizeof(query), "UPDATE gbMembers SET lastUse=NOW(),emailToken='%s', emailTokenExpires=DATE_ADD(NOW(), INTERVAL 7 DAY), accountActivated='N' WHERE userName='%s'",
     tokenMD5,
     username
     );
 sqlUpdate(conn, query);
 sendActivateMail(email, username, tokenMD5);
 return;
 }
 
+void printPwdEyeIcon(char *iconId, char *slashId)
+/* print a clickable eye icon, absolutely positioned inside a password
+ * input's wrapper span; slashId is the <line> toggled to show "hidden".
+ * No-op if disabled via hg.conf login.pwdEyeIcon. */
+{
+if (!pwdEyeIconEnabled)
+    return;
+hPrintf(
+    "<span id=\"%s\" title=\"Show/hide password\" "
+    "style=\"position:absolute; right:8px; top:50%%; transform:translateY(-50%%); "
+    "cursor:pointer; user-select:none;\">"
+    "<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" "
+    "stroke=\"#666\" stroke-width=\"2\">"
+    "<path d=\"M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z\"/>"
+    "<circle cx=\"12\" cy=\"12\" r=\"3\"/>"
+    "<line id=\"%s\" x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\" style=\"display:none;\"/>"
+    "</svg>"
+    "</span>", iconId, slashId);
+}
+
+void printPwdToggleJS()
+/* define the password show/hide toggle function used by all eye icons */
+{
+jsInline(
+    "function togglePwdVisibility(inputId, slashId) {\n"
+    "  var inp = document.getElementById(inputId);\n"
+    "  var slash = document.getElementById(slashId);\n"
+    "  if (inp.type === 'password') {\n"
+    "    inp.type = 'text';\n"
+    "    slash.style.display = 'inline';\n"
+    "  } else {\n"
+    "    inp.type = 'password';\n"
+    "    slash.style.display = 'none';\n"
+    "  }\n"
+    "}\n"
+    );
+}
+
 void displayLoginPage(struct sqlConnection *conn)
 /* draw the account login page */
 {
+verbose(0, "DBG hgLogin displayLoginPage\n");
 char *username = cartUsualString(cart, "hgLogin_userName", "");
 hPrintf("<div id=\"loginBox\" class=\"centeredContainer formBox\">"
     "\n"
     "<h2>%s</h2>"
     "\n", brwName);
 hPrintf(
     "<h3>Login</h3>"
     "\n");
 if (errMsg && sameString(errMsg, "Your account has been activated."))
     hPrintf("<span style='color:green;'>%s</span>\n", errMsg ? errMsg : "");
 else
     hPrintf("<span style='color:red;'>%s</span>\n", errMsg ? errMsg : "");
 hPrintf("<form method=post action=\"%s\" name=\"accountLoginForm\" id=\"accountLoginForm\">"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"userName\">Username</label>"
     "<input type=text name=\"hgLogin_userName\" value=\"%s\" size=\"30\" id=\"userName\">"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"password\">Password</label>"
+    "<span style=\"position:relative; display:inline-block;\">"
     "<input type=password name=\"hgLogin_password\" value=\"\" size=\"30\" id=\"password\">"
+    , hgLoginUrl, username);
+printPwdEyeIcon("pwdEyeIcon", "pwdEyeSlash");
+hPrintf(
+    "</span>"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "   <input type=\"submit\" name=\"hgLogin.do.displayLogin\" value=\"Login\" class=\"largeButton\">"
     "    &nbsp;<a href=\"%s\">Cancel</a>"
     "</div>"
-    , hgLoginUrl, username, getReturnToURL());
+    , getReturnToURL());
+if (pwdEyeIconEnabled)
+    {
+    printPwdToggleJS();
+    jsOnEventById("click", "pwdEyeIcon", "togglePwdVisibility('password','pwdEyeSlash');");
+    }
 cartSaveSession(cart);
 hPrintf(
     "</form>"
     "\n"
     "\n"
     "<div id=\"helpBox\">"
     "<a href=\"%s?hgLogin.do.displayAccHelpPage=1\">Can't access your account?</a><br>"
     "Need an account? <a href=\"%s?hgLogin.do.signupPage=1\">Sign up</a>.<br>"
     "</div><!-- END - helpBox -->"
     "</div><!-- END - loginBox -->"
     "\n"
     "\n"
     "</body>"
     "</html>", hgLoginUrl, hgLoginUrl);
 }
@@ -799,52 +853,71 @@
     "<h3>Change Password</h3>"
     "\n"
     "<p> <span style='color:red;'>%s</span> </p>"
     "\n"
     "<form method=\"post\" action=\"%s\" name=\"changePasswordForm\" id=\"changePasswordForm\">"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"userName\">Username</label>"
     "<input type=\"text\" name=\"hgLogin_userName\" size=\"30\" value=\"%s\" id=\"email\">"
     "</div>"
     "\n", errMsg ? errMsg : "", hgLoginUrl,
     cartUsualString(cart, "hgLogin_userName", ""));
 hPrintf("<div class=\"inputGroup\">"
     "\n"
     "<label for=\"currentPw\">Current or Emailed Password</label>"
-    "<input type=\"password\" name=\"hgLogin_password\" value=\"\" size=\"30\" id=\"currentPw\">"
+    "<span style=\"position:relative; display:inline-block;\">"
+    "<input type=\"password\" name=\"hgLogin_password\" value=\"\" size=\"30\" id=\"currentPw\">");
+printPwdEyeIcon("curPwEyeIcon", "curPwEyeSlash");
+hPrintf(
+    "</span>"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"newPw1\">New Password</label>"
-    "<input type=\"password\" name=\"hgLogin_newPassword1\" value=\"\" size=\"30\" id=\"newPw\">"
+    "<span style=\"position:relative; display:inline-block;\">"
+    "<input type=\"password\" name=\"hgLogin_newPassword1\" value=\"\" size=\"30\" id=\"newPw1\">");
+printPwdEyeIcon("newPw1EyeIcon", "newPw1EyeSlash");
+hPrintf(
+    "</span>"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"newPw2\">Re-enter New Password</label>"
-    "<input type=\"password\" name=\"hgLogin_newPassword2\" value=\"\" size=\"30\" id=\"newPw\">"
+    "<span style=\"position:relative; display:inline-block;\">"
+    "<input type=\"password\" name=\"hgLogin_newPassword2\" value=\"\" size=\"30\" id=\"newPw2\">");
+printPwdEyeIcon("newPw2EyeIcon", "newPw2EyeSlash");
+hPrintf(
+    "</span>"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "    <input type=\"submit\" name=\"hgLogin.do.changePassword\" value=\"Change Password\" class=\"largeButton\"> &nbsp; "
     "    <a href=\"%s\">Cancel</a>"
     "\n"
     "</div>"
     "</form>"
     "\n"
     "</div><!-- END - changePwBox -->"
     "\n", getReturnToURL());
+if (pwdEyeIconEnabled)
+    {
+    printPwdToggleJS();
+    jsOnEventById("click", "curPwEyeIcon", "togglePwdVisibility('currentPw','curPwEyeSlash');");
+    jsOnEventById("click", "newPw1EyeIcon", "togglePwdVisibility('newPw1','newPw1EyeSlash');");
+    jsOnEventById("click", "newPw2EyeIcon", "togglePwdVisibility('newPw2','newPw2EyeSlash');");
+    }
 cartSaveSession(cart);
 }
 
 void changePassword(struct sqlConnection *conn)
 /* process the change password form */
 {
 char query[256];
 char *user = cartUsualString(cart, "hgLogin_userName", "");
 char *currentPassword = cartUsualString(cart, "hgLogin_password", "");
 char *newPassword1 = cartUsualString(cart, "hgLogin_newPassword1", "");
 char *newPassword2 = cartUsualString(cart, "hgLogin_newPassword2", "");
 char *changeRequired = cartUsualString(cart, "hgLogin_changeRequired", "");
 if (!user || sameString(user,""))
     {
     freez(&errMsg);
@@ -955,48 +1028,62 @@
     "<label for=\"reenterEmail\">Re-enter Email address</label>"
     "<input type=text name=\"hgLogin_email2\" value=\"%s\" size=\"30\" id=\"emailCheck\">"
     "</div>\n",
     cartUsualString(cart, "hgLogin_userName", ""), cartUsualString(cart, "hgLogin_email", ""),
     cartUsualString(cart, "hgLogin_email2", ""));
 
 if (sqlFieldIndex(conn, "gbMembers", "recovEmail") != -1)
     hPrintf("<div class=\"inputGroup\">"
         "<label for=\"recovEmail\">Optional Secondary Recovery Email</label>"
         "<input type=text name=\"hgLogin_recovEmail\" size=\"30\" id=\"recovEmail\">"
         "</div>"
         "\n");
 
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"password\">Password <small>(must be at least 5 characters)</small></label>"
-    "<input type=password name=\"hgLogin_password\" value=\"%s\" size=\"30\" id=\"password\">"
+    "<span style=\"position:relative; display:inline-block;\">"
+    "<input type=password name=\"hgLogin_password\" value=\"%s\" size=\"30\" id=\"password\">",
+    cartUsualString(cart, "hgLogin_password", ""));
+printPwdEyeIcon("signupPwEyeIcon", "signupPwEyeSlash");
+hPrintf(
+    "</span>"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
-    "<label for=\"password\">Re-enter Password</label>"
-    "<input type=password name=\"hgLogin_password2\" value=\"%s\" size=\"30\" id=\"passwordCheck\">"
+    "<label for=\"passwordCheck\">Re-enter Password</label>"
+    "<span style=\"position:relative; display:inline-block;\">"
+    "<input type=password name=\"hgLogin_password2\" value=\"%s\" size=\"30\" id=\"passwordCheck\">",
+    cartUsualString(cart, "hgLogin_password2", ""));
+printPwdEyeIcon("signupPwCheckEyeIcon", "signupPwCheckEyeSlash");
+hPrintf(
+    "</span>"
     "\n"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "    <input type=\"submit\" name=\"hgLogin.do.signup\" value=\"Sign Up\" class=\"largeButton\"> &nbsp; "
     "    <a href=\"%s\">Cancel</a>"
     "</div>"
     "</form>"
     "</div><!-- END - signUpBox -->",
-    cartUsualString(cart, "hgLogin_password", ""), 
-    cartUsualString(cart, "hgLogin_password2", ""),
     getReturnToURL());
+if (pwdEyeIconEnabled)
+    {
+    printPwdToggleJS();
+    jsOnEventById("click", "signupPwEyeIcon", "togglePwdVisibility('password','signupPwEyeSlash');");
+    jsOnEventById("click", "signupPwCheckEyeIcon", "togglePwdVisibility('passwordCheck','signupPwCheckEyeSlash');");
+    }
 cartSaveSession(cart);
 }
 
 void signup(struct sqlConnection *conn)
 /* process the signup form */
 {
 char query[1024];
 char *user = cartUsualString(cart, "hgLogin_userName", "");
 char *encUserName = cgiEncodeFull(user);
 if (!user || sameString(user,""))
     {
     freez(&errMsg);
     errMsg = cloneString("User name cannot be blank.");
     signupPage(conn);
     return;
@@ -1299,62 +1386,67 @@
     cartSetString(cart, "hgLogin_changeRequired", "YES");
     changePasswordPage(conn);
     } 
 else
     {
     errMsg = cloneString(incorrectUsernameOrPassword);
     displayLoginPage(conn);
     return;
     }
 gbMembersFree(&m);
 }
 
 void  displayLogoutSuccess()
 /* display logout success msg, and reset cookie */
 {
+verbose(0, "DBG hgLogin displayLogoutSuccess\n");
 hPrintf("<h2>%s Sign Out</h2>", brwName);
 hPrintf(
     "<p align=\"left\">"
     "</p>"
     "<span style='color:red;'></span>"
     "\n");
 struct dyString *javascript = dyStringNew(1024);
 struct slName *newCookies = loginLogoutUser(), *sl;
 for (sl = newCookies;  sl != NULL;  sl = sl->next)
+{
+verbose(0, "DBG logout cookie: '%s'\n", sl->name);
     dyStringPrintf(javascript, " document.cookie = '%s';", sl->name);
+}
 jsInline(javascript->string);
 /* return to "returnto" URL */
 returnToURL(150);
 }
 
 void doMiddle(struct cart *theCart)
 /* Write the middle parts of the HTML page.
  * This routine sets up some globals and then
  * dispatches to the appropriate page-maker. */
 {
 struct sqlConnection *conn = hConnectCentral();
 
 // on mirrors, try to add the field 'recovEmail' to gbMembers. This may or may not work, depending on their config
 if (sqlFieldIndex(conn, "gbMembers", "recovEmail") == -1) {
     autoUpgradeTableAddColumn(conn, "gbMembers", "recovEmail", "varchar(255)", FALSE, "''");
 }
 
 cart = theCart;
 safecpy(brwName,sizeof(brwName), browserName());
 safecpy(brwAddr,sizeof(brwAddr), browserAddr());
 safecpy(signature,sizeof(signature), mailSignature());
 safecpy(returnAddr,sizeof(returnAddr), mailReturnAddr());
+pwdEyeIconEnabled = cfgOptionBooleanDefault(CFG_LOGIN_PWD_EYE_ICON, FALSE);
 
 if (cartVarExists(cart, "hgLogin.do.changePasswordPage"))
     changePasswordPage(conn);
 else if (cartVarExists(cart, "hgLogin.do.changePassword"))
     changePassword(conn);
 else if (cartVarExists(cart, "hgLogin.do.displayAccHelpPage"))
     displayAccHelpPage(conn);
 else if (cartVarExists(cart, "hgLogin.do.accountHelp"))
     accountHelp(conn);
 else if (cartVarExists(cart, "hgLogin.do.activateAccount"))
     activateAccount(conn);
 else if (cartVarExists(cart, "hgLogin.do.displayActMailSuccess"))
     displayActMailSuccess();
 else if (cartVarExists(cart, "hgLogin.do.displayMailSuccess"))
     displayMailSuccess();