Skip to content

Commit c79cf0e

Browse files
vagishaclaude
andcommitted
Unified BeginAction and SignUpApiAction signup paths
* Extracted verifyCaptcha, parseAndValidateEmail, sendConfirmationEmail helpers; both actions now run the same sequence (captcha -> blank fields -> email format -> userExists -> send) * Added emailConfirm field to SignupForm; validateSignupForm now also checks blank + match * sendConfirmationEmail wraps the TempUser insert + sendEmail in a DbScope transaction so the row rolls back on send failure * SignUpApiAction now also runs EmailValidator.isValid (closes a real divergence from BeginAction) and no longer falls through to status=USER_ADDED after a sendEmail failure * Rewrote signupPage.jsp using <labkey:form>/<labkey:input> with Confirm Email and kaptcha sections See ai/todos/active/TODO-LK-20260517_add-kaptcha-to-signup.md Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9aef658 commit c79cf0e

2 files changed

Lines changed: 156 additions & 101 deletions

File tree

signup/src/org/labkey/signup/SignUpController.java

Lines changed: 129 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.labkey.signup;
1818

19+
import jakarta.mail.MessagingException;
1920
import org.apache.commons.lang3.StringUtils;
2021
import org.apache.commons.validator.routines.EmailValidator;
2122
import org.apache.logging.log4j.LogManager;
@@ -528,7 +529,8 @@ public class BeginAction extends FormViewAction<SignupForm>
528529
@Override
529530
public void validateCommand(SignupForm form, Errors errors)
530531
{
531-
validateSignupForm(form, errors);
532+
// All validation happens in handlePost so the order matches SignUpApiAction
533+
// (captcha first, then blank-field checks, then email parsing, etc.).
532534
}
533535

534536
@Override
@@ -560,54 +562,39 @@ public ModelAndView getView(SignupForm form, boolean reshow, BindException error
560562
@Override
561563
public boolean handlePost(SignupForm signupForm, BindException errors) throws Exception
562564
{
563-
// Validate with EmailValidator first. ValidEmail(email) will not throw an exception if the domain is
564-
// missing from the email. The default domain configured for the server is appended.
565-
EmailValidator validator = EmailValidator.getInstance();
566-
if(!validator.isValid(signupForm.getEmail()))
565+
String kaptchaError = verifyCaptcha(signupForm.getKaptchaText(), signupForm.getEmail());
566+
if (kaptchaError != null)
567567
{
568-
errors.reject(ERROR_MSG,"'" + signupForm.getEmail() + "' is not a valid email address.");
568+
errors.reject(ERROR_MSG, kaptchaError);
569569
return false;
570570
}
571571

572-
ValidEmail email;
573-
try
572+
validateSignupForm(signupForm, errors);
573+
if (errors.hasErrors())
574574
{
575-
email = new ValidEmail(signupForm.getEmail());
575+
return false;
576576
}
577-
catch (ValidEmail.InvalidEmailException iee)
577+
578+
ValidEmail email = parseAndValidateEmail(signupForm, errors);
579+
if (email == null)
578580
{
579-
errors.reject(ERROR_MSG, iee.getMessage());
580581
return false;
581582
}
582583

583-
if(UserManager.userExists(email))
584+
if (UserManager.userExists(email))
584585
{
585586
// If the user already exists forward them to a page where they can click on a link to recover their password, if required
586587
signupForm.setAccountExists(true);
587588
signupForm.setNewSignUp(false);
588589
return false;
589590
}
590591

591-
// If the user does not exit in LabKey's core database, check in our temporaryusers table
592-
TempUser tempUser = getTempUser(signupForm, email);
593-
594-
// Send email to the user.
595-
ActionURL confirmationUrl = getConfirmationURL(getContainer(), email, tempUser.getKey());
596-
try
592+
if (!createUserAndSendEmail(signupForm, email, errors))
597593
{
598-
User mockUser = new User();
599-
mockUser.setEmail(email.getEmailAddress());
600-
SecurityManager.sendEmail(getContainer(), mockUser, SecurityManager.getRegistrationMessage(null, false), email.getEmailAddress(), confirmationUrl);
601-
}
602-
catch(Exception e)
603-
{
604-
String systemEmail = LookAndFeelProperties.getInstance(getContainer()).getSystemEmailAddress();
605-
errors.reject(ERROR_MSG, "Could not send new user registration email. Please contact your server administrator at " + systemEmail);
606-
errors.reject(ERROR_MSG, e.getMessage());
607594
return false;
608595
}
609596

610-
597+
// Re-render the JSP with the CONFIRMATION_SENT message.
611598
signupForm.setNewSignUp(false);
612599
return false;
613600
}
@@ -642,6 +629,96 @@ private void validateSignupForm(SignupForm form, Errors errors)
642629
{
643630
errors.reject(ERROR_MSG, "Email cannot be blank.");
644631
}
632+
if(StringUtils.isBlank(form.getEmailConfirm()))
633+
{
634+
errors.reject(ERROR_MSG, "Confirm email cannot be blank.");
635+
}
636+
else if(form.getEmail() != null && !form.getEmail().equals(form.getEmailConfirm()))
637+
{
638+
errors.reject(ERROR_MSG, "Email addresses do not match.");
639+
}
640+
}
641+
642+
// On success returns null and clears the session attribute so the captcha cannot be replayed.
643+
// On failure return a a user-facing error message.
644+
// Logging matches LoginController's RegisterUserAction.
645+
private String verifyCaptcha(String submittedText, String emailForLogging)
646+
{
647+
var session = getViewContext().getRequest().getSession(true);
648+
String expected = (String) session.getAttribute(LabKeyKaptchaServlet.SESSION_KEY_VALUE);
649+
if (expected == null)
650+
{
651+
_log.info("Captcha not initialized for signup attempt");
652+
return "Captcha not initialized, please retry.";
653+
}
654+
if (!expected.equalsIgnoreCase(StringUtils.trimToNull(submittedText)))
655+
{
656+
_log.warn("Captcha text did not match for signup attempt for " + emailForLogging);
657+
return "Verification text does not match, please retry.";
658+
}
659+
session.removeAttribute(LabKeyKaptchaServlet.SESSION_KEY_VALUE);
660+
return null;
661+
}
662+
663+
// Returns a parsed ValidEmail, or null if the address is invalid (errors populated).
664+
// Uses EmailValidator first because ValidEmail's constructor does not throw on bare
665+
// strings like "foo" - it silently appends the server's default domain.
666+
private ValidEmail parseAndValidateEmail(SignupForm form, Errors errors)
667+
{
668+
EmailValidator validator = EmailValidator.getInstance();
669+
if (!validator.isValid(form.getEmail()))
670+
{
671+
errors.reject(ERROR_MSG, "'" + form.getEmail() + "' is not a valid email address.");
672+
return null;
673+
}
674+
try
675+
{
676+
return new ValidEmail(form.getEmail());
677+
}
678+
catch (ValidEmail.InvalidEmailException iee)
679+
{
680+
errors.reject(ERROR_MSG, iee.getMessage());
681+
return null;
682+
}
683+
}
684+
685+
// Creates (or reuses) a TempUser row and sends the confirmation email in a single
686+
// transaction. On send failure the transaction is rolled back so a freshly inserted
687+
// TempUser row does not persist when the user never received a confirmation link.
688+
private boolean createUserAndSendEmail(SignupForm form, ValidEmail email, Errors errors) throws java.sql.SQLException
689+
{
690+
try (DbScope.Transaction transaction = SignUpManager.getSchema().getScope().ensureTransaction())
691+
{
692+
TempUser tempUser = getTempUser(form, email);
693+
ActionURL confirmationUrl = getConfirmationURL(getContainer(), email, tempUser.getKey());
694+
try
695+
{
696+
User mockUser = new User();
697+
mockUser.setEmail(email.getEmailAddress());
698+
SecurityManager.sendEmail(getContainer(), mockUser,
699+
SecurityManager.getRegistrationMessage(null, false),
700+
email.getEmailAddress(), confirmationUrl);
701+
}
702+
catch (MessagingException e)
703+
{
704+
String systemEmail = LookAndFeelProperties.getInstance(getContainer()).getSystemEmailAddress();
705+
errors.reject(ERROR_MSG, "Could not send new user registration email. Please contact your server administrator at " + systemEmail);
706+
if (e.getMessage() != null)
707+
{
708+
errors.reject(ERROR_MSG, e.getMessage());
709+
}
710+
return false;
711+
}
712+
transaction.commit();
713+
return true;
714+
}
715+
}
716+
717+
private static List<String> errorsToMessages(Errors errors)
718+
{
719+
return errors.getAllErrors().stream()
720+
.map(e -> e.getDefaultMessage())
721+
.toList();
645722
}
646723

647724
public static ActionURL getConfirmationURL(Container c, ValidEmail email, String key)
@@ -658,6 +735,7 @@ public static class SignupForm extends ReturnUrlForm
658735
private String _lastName;
659736
private String _organization;
660737
private String _email;
738+
private String _emailConfirm;
661739
private boolean _accountExists;
662740
private boolean _newSignUp = true;
663741

@@ -701,6 +779,16 @@ public void setEmail(String email)
701779
_email = email;
702780
}
703781

782+
public String getEmailConfirm()
783+
{
784+
return _emailConfirm;
785+
}
786+
787+
public void setEmailConfirm(String emailConfirm)
788+
{
789+
_emailConfirm = emailConfirm;
790+
}
791+
704792
public boolean isAccountExists()
705793
{
706794
return _accountExists;
@@ -791,72 +879,40 @@ public ApiResponse execute(SignupForm signupForm, BindException errors) throws E
791879
{
792880
ApiSimpleResponse response = new ApiSimpleResponse();
793881

794-
String expectedKaptcha = (String) getViewContext().getRequest().getSession(true)
795-
.getAttribute(LabKeyKaptchaServlet.SESSION_KEY_VALUE);
796-
if (expectedKaptcha == null)
882+
String kaptchaError = verifyCaptcha(signupForm.getKaptchaText(), signupForm.getEmail());
883+
if (kaptchaError != null)
797884
{
798-
_log.info("Captcha not initialized for signup attempt");
799-
response.put("error_message", List.of("Captcha not initialized, please retry."));
885+
response.put("error_message", List.of(kaptchaError));
800886
return response;
801887
}
802-
if (!expectedKaptcha.equalsIgnoreCase(StringUtils.trimToNull(signupForm.getKaptchaText())))
888+
889+
validateSignupForm(signupForm, errors);
890+
if (errors.hasErrors())
803891
{
804-
_log.warn("Captcha text did not match for signup attempt for " + signupForm.getEmail());
805-
response.put("error_message", List.of("Verification text does not match, please retry."));
892+
response.put("error_message", errorsToMessages(errors));
806893
return response;
807894
}
808-
getViewContext().getRequest().getSession(true).removeAttribute(LabKeyKaptchaServlet.SESSION_KEY_VALUE);
809895

810-
ValidEmail email;
811-
try
812-
{
813-
email = new ValidEmail(signupForm.getEmail());
814-
}
815-
catch (ValidEmail.InvalidEmailException iee)
896+
ValidEmail email = parseAndValidateEmail(signupForm, errors);
897+
if (email == null)
816898
{
817-
response.put("error_message", List.of(iee.getMessage()));
899+
response.put("error_message", errorsToMessages(errors));
818900
return response;
819901
}
820902

821-
if(UserManager.userExists(email))
903+
if (UserManager.userExists(email))
822904
{
823905
response.put("status", "USER_EXISTS");
824906
return response;
825907
}
826908

827-
validateSignupForm(signupForm, errors);
828-
if(errors.hasErrors())
909+
if (!createUserAndSendEmail(signupForm, email, errors))
829910
{
830-
List<String> messages = errors.getAllErrors().stream()
831-
.map(e -> e.getDefaultMessage())
832-
.toList();
833-
response.put("error_message", messages);
911+
response.put("error_message", errorsToMessages(errors));
834912
return response;
835913
}
836914

837-
TempUser tempUser = getTempUser(signupForm, email);
838-
839-
840-
// Send email to the user.
841-
ActionURL confirmationUrl = getConfirmationURL(getContainer(), email, tempUser.getKey());
842-
try
843-
{
844-
User mockUser = new User();
845-
mockUser.setEmail(email.getEmailAddress());
846-
SecurityManager.sendEmail(getContainer(), mockUser, SecurityManager.getRegistrationMessage(null, false), email.getEmailAddress(), confirmationUrl);
847-
}
848-
catch(Exception e)
849-
{
850-
String systemEmail = LookAndFeelProperties.getInstance(getContainer()).getSystemEmailAddress();
851-
List<String> messages = new ArrayList<>();
852-
messages.add("Could not send new user registration email. Please contact your server administrator at " + systemEmail);
853-
messages.add(e.getMessage());
854-
response.put("error_message", messages);
855-
}
856-
857-
signupForm.setNewSignUp(false); // TODO: Most likely not needed here
858915
response.put("status", "USER_ADDED");
859-
860916
return response;
861917
}
862918
}
Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,37 @@
1-
<%@ page import="org.labkey.api.view.ActionURL" %>
21
<%@ page import="org.labkey.api.view.HttpView" %>
3-
<%@ page import="org.labkey.signup.SignUpController.BeginAction" %>
42
<%@ page import="org.labkey.signup.SignUpController.SignupForm" %>
53
<%@ page extends="org.labkey.api.jsp.JspBase" %>
64
<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %>
75
<%
86
SignupForm form = (SignupForm)HttpView.currentModel();
9-
ActionURL url = urlFor(BeginAction.class);
7+
String contextPath = request.getContextPath();
108
%>
119

12-
<!-- Display errors here -->
1310
<labkey:errors/>
1411

15-
<form action="<%=h(url)%>" method=post>
16-
<labkey:csrf/>
17-
<table>
18-
<tr>
19-
<td class="labkey-form-label"><label for="firstName">First Name</label> *</td>
20-
<td nowrap><input size="20" type="text" id="firstName" name="firstName" value="<%=h(form.getFirstName())%>"/></td>
21-
</tr>
22-
<tr>
23-
<td class="labkey-form-label"><label for="lastName">Last Name</label> *</td>
24-
<td nowrap><input size="20" type="text" id="lastName" name="lastName" value="<%=h(form.getLastName())%>"/></td>
25-
</tr>
26-
<tr>
27-
<td class="labkey-form-label"><label for="organization">Organization</label> *</td>
28-
<td nowrap><input size="20" type="text" id="organization" name="organization" value="<%=h(form.getOrganization())%>"/></td>
29-
</tr>
30-
<tr>
31-
<td class="labkey-form-label"><label for="email">Email</label> *</td>
32-
<td nowrap><input size="20" type="text" id="email" name="email" value="<%=h(form.getEmail())%>"/></td>
33-
</tr>
34-
<tr>
35-
<td colspan="2"><labkey:button text="Submit" /></td>
36-
</tr>
37-
</table>
38-
</form>
12+
<labkey:form method="post" layout="horizontal" autoComplete="off" style="max-width:600px;">
13+
<labkey:input name="firstName" id="firstName" label="First Name" isRequired="true" size="50" value="<%=form.getFirstName()%>"/>
14+
<labkey:input name="lastName" id="lastName" label="Last Name" isRequired="true" size="50" value="<%=form.getLastName()%>"/>
15+
<labkey:input name="organization" id="organization" label="Organization" isRequired="true" size="50" value="<%=form.getOrganization()%>"/>
16+
<labkey:input name="email" id="email" label="Email" isRequired="true" size="50" value="<%=form.getEmail()%>"/>
17+
<labkey:input name="emailConfirm" id="emailConfirm" label="Confirm Email" isRequired="true" size="50" value="<%=form.getEmailConfirm()%>"/>
18+
19+
<%-- Verification: standalone full-width block --%>
20+
<div style="margin-top:20px;"><strong>Verification</strong></div>
21+
<p style="margin:8px 0 4px 0;">Please enter the six characters shown below (case-insensitive).</p>
22+
<p style="margin:0 0 8px 0;"><a id="kaptchaReload" href="#">Get a new image.</a></p>
23+
<img id="kaptchaImg" src="<%=h(contextPath)%>/kaptcha.jpg" alt="Captcha" width="200" height="50" style="border: 1px solid #ccc; display:block; margin-bottom:6px;"/>
24+
<input type="text" id="kaptchaText" name="kaptchaText" style="width:200px;"/>
25+
26+
<div style="margin-top:20px; clear:both;">
27+
<button type="submit" class="btn btn-default labkey-button">Register</button>
28+
</div>
29+
</labkey:form>
30+
31+
<script type="text/javascript" nonce="<%=getScriptNonce()%>">
32+
document.getElementById("kaptchaReload").addEventListener("click", function(e) {
33+
e.preventDefault();
34+
var img = document.getElementById("kaptchaImg");
35+
img.src = img.src.split("?")[0] + "?ts=" + new Date().getTime();
36+
});
37+
</script>

0 commit comments

Comments
 (0)