Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
54ab686
Added new columns to teacher data and modified welcome email
Aaron-Detre Jul 15, 2026
9f8cfb0
Verification endpoints
Aaron-Detre Jul 15, 2026
8e2d702
Update verification URL to match endpoint
Aaron-Detre Jul 15, 2026
ac5ed80
Show a confirmation when a user's account has just been verified
Aaron-Detre Jul 15, 2026
8cce7f5
Added endpoint to resend the verification email
Aaron-Detre Jul 16, 2026
6d52c63
Minor change to email
Aaron-Detre Jul 16, 2026
92e45ab
Move teacher registration into separate class
Aaron-Detre Jul 17, 2026
907d683
Refactor teacher registration
Aaron-Detre Jul 17, 2026
ad53a48
Fix tests
Aaron-Detre Jul 17, 2026
92b951b
Minor change
Aaron-Detre Jul 17, 2026
46c4666
Merge branch 'refactor-teacher-registration' into email-verification
Aaron-Detre Jul 17, 2026
7a8f57a
Move contents of UserVerificationAPIController into more appropriate …
Aaron-Detre Jul 17, 2026
9d941f7
Welcome email only after verifying account
Aaron-Detre Jul 17, 2026
97e49d1
Should keep trying to set verification code until unique constraint i…
Aaron-Detre Jul 17, 2026
29095d9
Minor bug
Aaron-Detre Jul 18, 2026
d2b8042
Redirect to login with an error flag if verification code doesn't mat…
Aaron-Detre Jul 21, 2026
896a3bc
Move email specific code to MailService
Aaron-Detre Jul 21, 2026
dad7c6e
Added tests (some untested)
Aaron-Detre Jul 21, 2026
eab5572
Merge branch 'develop' into email-verification
Aaron-Detre Jul 21, 2026
a499a64
Separate sending welcome email from getting login link to make steps …
Aaron-Detre Jul 21, 2026
6fc8b46
Can't get user details for null user bug
Aaron-Detre Jul 21, 2026
0845082
Changed old tests that are failing
Aaron-Detre Jul 21, 2026
19bf231
Set teachers as verified if sending mail is not enabled
Aaron-Detre Jul 26, 2026
9755823
Added sql changes to database migrations
Aaron-Detre Jul 26, 2026
30fe780
Merge branch 'develop' into email-verification
Aaron-Detre Jul 26, 2026
2b560da
Merge branch 'develop' into email-verification
Aaron-Detre Jul 30, 2026
efdae2c
Moved isVerified to separate controller
Aaron-Detre Aug 2, 2026
fb967d3
Moved verifyTeacher to separate controller
Aaron-Detre Aug 2, 2026
d949921
Moved sendVerifyTeacherEmail to separate controller
Aaron-Detre Aug 2, 2026
c308af0
Fixed naming and indentation issues
Aaron-Detre Aug 2, 2026
1c5391c
Clean code in TeacherVerificationAPIController
Aaron-Detre Aug 2, 2026
8ab7312
Moved teacher mail methods to separate service
Aaron-Detre Aug 2, 2026
b924f5e
Updated verification url to match new endpoint
Aaron-Detre Aug 8, 2026
c081806
Handle verification check in authentication process
Aaron-Detre Aug 8, 2026
90cf23b
Properly handle duplicate verification codes
Aaron-Detre Aug 9, 2026
9398e20
Properly handle duplicate verification codes
Aaron-Detre Aug 9, 2026
abbebbe
Merge branch 'email-verification' of https://github.com/WISE-Communit…
Aaron-Detre Aug 9, 2026
6923b62
Fixed tests
Aaron-Detre Aug 9, 2026
fdac1d9
Set verification code in createTeacherUser for tests
Aaron-Detre Aug 9, 2026
5a7b8f7
Fixed tests
Aaron-Detre Aug 9, 2026
6d0e666
Added verification code setter that generates random UUID
Aaron-Detre Aug 12, 2026
edd860f
Removed redundant teacher from method names
Aaron-Detre Aug 12, 2026
f72948c
Deleted TeacherIsVerifiedAPIController
Aaron-Detre Aug 12, 2026
f479e70
Removed unused imports
Aaron-Detre Aug 12, 2026
14caee2
Removed spaces from catch blocks
Aaron-Detre Aug 13, 2026
d3ff2a7
Fixed tests
Aaron-Detre Aug 13, 2026
59997ac
Fixed tests
Aaron-Detre Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.wise.portal.dao.authentication;

import org.wise.portal.dao.SimpleDao;
import org.wise.portal.domain.authentication.impl.TeacherUserDetails;

public interface TeacherUserDetailsDao extends SimpleDao<TeacherUserDetails> {

boolean hasVerificationCode(String verificationCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.wise.portal.dao.authentication.impl;

import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

import org.springframework.stereotype.Repository;
import org.wise.portal.dao.authentication.TeacherUserDetailsDao;
import org.wise.portal.dao.impl.AbstractHibernateDao;
import org.wise.portal.domain.authentication.impl.TeacherUserDetails;

@Repository
public class HibernateTeacherUserDetailsDao extends AbstractHibernateDao<TeacherUserDetails>
implements TeacherUserDetailsDao {

@Override
public boolean hasVerificationCode(String verificationCode) {
CriteriaBuilder cb = getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
Root<TeacherUserDetails> teacherUserDetailsRoot = cq.from(TeacherUserDetails.class);
cq.select(cb.count(teacherUserDetailsRoot))
.where(cb.equal(teacherUserDetailsRoot.get("verificationCode"), verificationCode));
TypedQuery<Long> query = entityManager.createQuery(cq);
Long count = query.getSingleResult();
return count > 0;
}

@Override
protected Class<? extends TeacherUserDetails> getDataObjectClass() {
return TeacherUserDetails.class;
}
}
1 change: 1 addition & 0 deletions src/main/java/org/wise/portal/dao/user/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ List<User> retrieveStudentsByNameAndBirthday(String firstName, String lastName,
List<User> retrieveTeachersByFirstName(String firstName);
List<User> retrieveTeachersByLastName(String lastName);
User retrieveTeacherByUsername(String username);
User retrieveTeacherByVerificationCode(String verificationCode);
List<User> retrieveTeachersByDisplayName(String displayName);
List<User> retrieveTeachersByCity(String city);
List<User> retrieveTeachersByState(String state);
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,7 @@ public List<User> retrieveTeachersByLastName(String lastName) {
}

public User retrieveTeacherByUsername(String username) {
List<User> resultList = retrieveTeachersByFieldValue("username", username);
return resultList.isEmpty() ? null : resultList.get(0);
return retrieveTeacherByFieldValue("username", username);
}

public List<User> retrieveTeachersByDisplayName(String displayName) {
Expand All @@ -176,6 +175,15 @@ public List<User> retrieveTeachersByEmail(String emailAddress) {
return retrieveTeachersByFieldValue("emailAddress", emailAddress);
}

public User retrieveTeacherByVerificationCode(String verificationCode) {
return retrieveTeacherByFieldValue("verificationCode", verificationCode);
}

private User retrieveTeacherByFieldValue(String field, String value) {
List<User> resultList = retrieveTeachersByFieldValue(field, value);
return resultList.isEmpty() ? null : resultList.get(0);
}

@SuppressWarnings("unchecked")
private List<User> retrieveTeachersByFieldValue(String field, String value) {
CriteriaBuilder cb = getCriteriaBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.Date;
import java.util.HashMap;
import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Entity;
Expand Down Expand Up @@ -93,6 +94,12 @@ public class TeacherUserDetails extends PersistentUserDetails implements Mutable
@Transient
private static final String COLUMN_NAME_HOW_HEAR = "howDidYouHearAboutUs";

@Transient
private static final String COLUMN_NAME_VERIFIED = "isVerified";

@Transient
private static final String COLUMN_NAME_VERIFICATION_CODE = "verificationCode";

@Transient
private static final long serialVersionUID = 1L;

Expand Down Expand Up @@ -164,6 +171,15 @@ public class TeacherUserDetails extends PersistentUserDetails implements Mutable
@Setter
private String howDidYouHearAboutUs;

@Column(name = TeacherUserDetails.COLUMN_NAME_VERIFIED, nullable = false)
@Getter
@Setter
private boolean verified = false;

@Column(name = TeacherUserDetails.COLUMN_NAME_VERIFICATION_CODE, unique = true)
@Getter
private String verificationCode;

public String getCoreUsername() {
return (firstname + lastname).replaceAll("[\\s-]+", "");
}
Expand All @@ -183,9 +199,7 @@ public String[] getUsernameSuffixes() {
*/
public String getNextUsernameSuffix(String currentUsernameSuffix) {
String nextUsernameSuffix = "";
if (currentUsernameSuffix == null) {
nextUsernameSuffix = "";
} else if ("".equals(currentUsernameSuffix)) {
if ("".equals(currentUsernameSuffix)) {
nextUsernameSuffix = "1";
} else {
try {
Expand Down Expand Up @@ -238,4 +252,8 @@ public boolean isEmailValid() {
public void setEmailValid(boolean emailValid) {
this.emailValid = emailValid;
}

public void setVerificationCode() {
this.verificationCode = UUID.randomUUID().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.wise.portal.presentation.web;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.wise.portal.domain.authentication.impl.TeacherUserDetails;
import org.wise.portal.domain.user.User;
import org.wise.portal.presentation.web.controllers.teacher.TeacherAPIController;

@RestController
@RequestMapping("/api/teacher/verify")
public class TeacherVerificationAPIController extends TeacherAPIController {

@GetMapping()
@Secured({ "ROLE_ANONYMOUS" })
public void verifyTeacherAndRedirect(@RequestParam String code, HttpServletResponse response,
HttpServletRequest request) throws IOException {
User user = userService.retrieveTeacherByVerificationCode(code);
boolean verified = verifyTeacher(user);
String link = getLoginLink(user, verified);
sendWelcomeEmail(user, link, request);
response.sendRedirect(link);
}

private boolean verifyTeacher(User user) {
if (user != null) {
TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails();
return verifyTeacherAccount(user, tud);
} else {
return false;
}
}

private String getLoginLink(User user, boolean verified) {
StringBuilder link = new StringBuilder("/login?verified=");
link.append(user == null ? "error" : verified);
if (user != null) {
link.append("&username=").append(user.getUserDetails().getUsername());
}
return link.toString();
}

private void sendWelcomeEmail(User user, String link, HttpServletRequest request) {
if (link.contains("verified=true")) {
TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails();
this.teacherMailService.sendWelcomeEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(),
false, request.getLocale(), request);
}
}

private boolean verifyTeacherAccount(User user, TeacherUserDetails tud) {
if (!tud.isVerified()) {
tud.setVerified(true);
userService.updateUser(user);
return true;
} else {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.wise.portal.presentation.web.controllers.user.UserAPIController;
import org.wise.portal.presentation.web.response.SimpleResponse;
import org.wise.portal.service.authentication.UserDetailsService;
import org.wise.portal.service.mail.teacher.TeacherMailService;
import org.wise.portal.service.usertags.UserTagsService;

/**
Expand All @@ -44,6 +45,9 @@
@Secured({ "ROLE_TEACHER" })
public class TeacherAPIController extends UserAPIController {

@Autowired
protected TeacherMailService teacherMailService;

@Autowired
private UserDetailsService userDetailsService;

Expand Down Expand Up @@ -326,4 +330,8 @@ HashMap<String, Object> editRunIsLockedAfterEndDate(Authentication authenticatio
}
return response;
}

protected boolean isTeacher(User user) {
return user != null && !user.getRoles().contains("ROLE_STUDENT");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.util.Locale;
import java.util.Map;

import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.RandomStringUtils;
Expand All @@ -26,12 +25,6 @@
@RestController
@RequestMapping("/api/teacher/register")
public class TeacherRegistrationAPIController extends TeacherAPIController {

private final String emailCodePrefix =
"presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmail";
private final String welcomeBodyCode = this.emailCodePrefix + "Body";
private final String welcomeSocialAccountBodyCode = this.emailCodePrefix + "BodyNoUsername";
private final String welcomeSubjectCode = this.emailCodePrefix + "Subject";

@PostMapping()
@Secured({ "ROLE_ANONYMOUS" })
Expand All @@ -44,67 +37,33 @@ ResponseEntity<Map<String, Object>> createTeacherAccount(
return ResponseEntityGenerator.createError("recaptchaResponseInvalid");
} catch (InvalidPasswordException e) {
return ResponseEntityGenerator.createError(
passwordService.getErrors(teacherFields.get("password")));
passwordService.getErrors(teacherFields.get("password")));
}
Locale locale = request.getLocale();
TeacherUserDetails tud = createTeacherUserDetails(teacherFields, locale);
boolean isSocialAccount = isSocialAccount(teacherFields);
TeacherUserDetails tud = createTeacherUserDetails(teacherFields, isSocialAccount, locale);
User createdUser = this.userService.createUser(tud);
String username = createdUser.getUserDetails().getUsername();
if (isSendEmailEnabled()) {
sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), username,
isSocialAccount(tud), locale, request);
}
sendNewTeacherEmail(request, locale, isSocialAccount, tud, username);
return createRegisterSuccessResponse(username);
}

private boolean isSendEmailEnabled() {
String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false");
return Boolean.valueOf(sendEmailEnabledStr);
}

private boolean isSocialAccount(TeacherUserDetails tud) {
return isSet(tud.getGoogleUserId()) || isSet(tud.getMicrosoftUserId());
}

private void sendWelcomeTeacherEmail(String email, String displayName, String username,
boolean socialAccount, Locale locale,
HttpServletRequest request) {
String subject = getEmailMessage(this.welcomeSubjectCode, this.welcomeSubjectCode, null, locale);
String body = getWelcomeTeacherBody(displayName, username, socialAccount, locale, request);
this.sendEmail(email, subject, body);
}

private String getEmailMessage(String defaultCode, String code, Object[] args, Locale locale) {
String defaultMessage = messageSource.getMessage(defaultCode, args, Locale.US);
return messageSource.getMessage(code, args, defaultMessage, locale);
}

private String getWelcomeTeacherBody(String displayName, String username, boolean socialAccount,
Locale locale, HttpServletRequest request) {
String gettingStartedUrl = getGettingStartedUrl(request);
String code = socialAccount ? this.welcomeSocialAccountBodyCode : this.welcomeBodyCode;
Object[] args = socialAccount
? new Object[] { displayName, gettingStartedUrl }
: new Object[] { displayName, username, gettingStartedUrl };
return getEmailMessage(this.welcomeBodyCode, code, args, locale);
}

private String getGettingStartedUrl(HttpServletRequest request) {
return ControllerUtil.getPortalUrlString(request) + "/help/getting-started";
private void sendNewTeacherEmail(HttpServletRequest request, Locale locale, boolean isSocialAccount,
TeacherUserDetails tud, String username) {
if (isSocialAccount) {
this.teacherMailService.sendWelcomeEmail(tud.getEmailAddress(), tud.getDisplayname(), username,
true, locale, request);
} else {
this.teacherMailService.sendVerifyEmail(tud.getEmailAddress(), tud.getVerificationCode(), locale, request);
}
}

private void sendEmail(String email, String subject, String body) {
String fromEmail = appProperties.getProperty("portalemailaddress");
String[] recipients = { email };
try {
mailService.postMail(recipients, subject, body, fromEmail);
} catch (MessagingException e) {
e.printStackTrace();
}
private boolean isSocialAccount(Map<String, String> teacherFields) {
return isSet(teacherFields.get("googleUserId")) || isSet(teacherFields.get("microsoftUserId"));
}

private void validateTeacherFields(Map<String, String> teacherFields)
throws RecaptchaVerificationException, InvalidNameException, InvalidPasswordException {
throws RecaptchaVerificationException, InvalidNameException, InvalidPasswordException {
validateReCaptcha(teacherFields.get("token"));
validateFirstAndLastName(teacherFields.get("firstName"), teacherFields.get("lastName"));
validatePassword(teacherFields.get("password"));
Expand All @@ -119,7 +78,7 @@ private void validateReCaptcha(String token) throws RecaptchaVerificationExcepti
}

private void validateFirstAndLastName(String firstName, String lastName)
throws InvalidNameException {
throws InvalidNameException {
if (!isFirstNameAndLastNameValid(firstName, lastName)) {
String messageCode = this.getInvalidNameMessageCode(firstName, lastName);
throw new InvalidNameException(messageCode);
Expand All @@ -133,7 +92,7 @@ private void validatePassword(String password) throws InvalidPasswordException {
}

private TeacherUserDetails createTeacherUserDetails(Map<String, String> teacherFields,
Locale locale) {
boolean isSocialAccount, Locale locale) {
TeacherUserDetails tud = new TeacherUserDetails();
tud.setFirstname(teacherFields.get("firstName"));
tud.setLastname(teacherFields.get("lastName"));
Expand All @@ -148,6 +107,8 @@ private TeacherUserDetails createTeacherUserDetails(Map<String, String> teacherF
tud.setLanguage(locale.getLanguage());
setPassword(teacherFields, tud);
tud.setEmailValid(true);
tud.setVerified(isSocialAccount || !teacherMailService.isSendEmailEnabled());
tud.setVerificationCode();
return tud;
}

Expand Down
Loading
Loading