Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
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 @@ -93,6 +93,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 +170,16 @@ 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
@Setter
private String verificationCode;

public String getCoreUsername() {
return (firstname + lastname).replaceAll("[\\s-]+", "");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

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

@GetMapping("/is-verified")
@Secured({ "ROLE_ANONYMOUS" })
public boolean isVerifiedTeacherOrNonTeacher(@RequestParam String username) {
User user = userService.retrieveTeacherByUsername(username);
return !this.isTeacher(user) || this.isTeacherVerified(user); // Only teachers need to verify their accounts
}

protected boolean isTeacher(User user) {
return user != null && !user.getRoles().contains("ROLE_STUDENT");
}

private boolean isTeacherVerified(User user) {
TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails();
return tud.isVerified();
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package org.wise.portal.presentation.web.controllers.teacher;

import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

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

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.Schoollevel;
import org.wise.portal.domain.authentication.impl.TeacherUserDetails;
Expand All @@ -26,12 +31,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 @@ -47,60 +46,26 @@ ResponseEntity<Map<String, Object>> createTeacherAccount(
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.mailService.sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), username,
true, locale, request);
} else {
this.mailService.sendVerifyTeacherEmail(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)
Expand Down Expand Up @@ -133,7 +98,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 +113,8 @@ private TeacherUserDetails createTeacherUserDetails(Map<String, String> teacherF
tud.setLanguage(locale.getLanguage());
setPassword(teacherFields, tud);
tud.setEmailValid(true);
tud.setVerified(isSocialAccount);
setVerificationCode(tud);
return tud;
}

Expand All @@ -164,6 +131,18 @@ private void setPassword(Map<String, String> teacherFields, TeacherUserDetails t
tud.setPassword(teacherFields.get("password"));
}
}

private void setVerificationCode(TeacherUserDetails tud) {
boolean isCodeSet = false;
while (!isCodeSet) {
try {
tud.setVerificationCode(UUID.randomUUID().toString());
isCodeSet = true;
} catch (DataIntegrityViolationException e) {
continue;
}
}
}

private boolean isSet(String value) {
return value != null && !value.isEmpty();
Expand All @@ -172,4 +151,70 @@ private boolean isSet(String value) {
private void setRandomPassword(TeacherUserDetails tud) {
tud.setPassword(RandomStringUtils.random(10, true, true));
}

@GetMapping("/verify")
@Secured({ "ROLE_ANONYMOUS" })
public void verifyTeacherAndRedirect(@RequestParam String code, HttpServletResponse response,
HttpServletRequest request) throws IOException {
User user = userService.retrieveTeacherByVerificationCode(code);
String link = verifyTeacherIfNecessaryAndGetLoginLink(request, user);
sendWelcomeEmailIfNecessary(user, link, request);
response.sendRedirect(link);
}

private String verifyTeacherIfNecessaryAndGetLoginLink(HttpServletRequest request, User user) {
String link;
if (user == null) {
link = "/login?verified=error";
} else if (!isTeacher(user)) {
link = getRedirectLink(user, false);
} else {
TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails();
boolean verified = verifyTeacherAccount(user, tud, request);
link = getRedirectLink(user, verified);
}
return link;
}

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

private String getRedirectLink(User user, boolean verified) {
return String.format("/login?verified=%s&username=%s",
verified, user.getUserDetails().getUsername());
}

private boolean verifyTeacherAccount(User user, TeacherUserDetails tud, HttpServletRequest request) {
if (!tud.isVerified()) {
tud.setVerified(true);
userService.updateUser(user);
return true;
} else {
return false;
}
}

@PostMapping("send-verify-email")
@Secured({ "ROLE_ANONYMOUS" })
ResponseEntity<Map<String, Object>> sendVerificationEmail(@RequestParam String username,
HttpServletRequest request) {
User user = userService.retrieveTeacherByUsername(username);
if (isTeacher(user)) {
TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails();
if (tud.isVerified()) {
return ResponseEntityGenerator.createError("Teacher already verified");
} else {
this.mailService.sendVerifyTeacherEmail(tud.getEmailAddress(), tud.getVerificationCode(),
request.getLocale(), request);
return createRegisterSuccessResponse(username);
}
} else {
return ResponseEntityGenerator.createError("Not a teacher");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ HashMap<String, Object> getUserInfo(Authentication auth,
info.put("country", tud.getCountry());
info.put("schoolName", tud.getSchoolname());
info.put("schoolLevel", tud.getSchoollevel());
info.put("isVerified", tud.isVerified());
}
return info;
} else {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/wise/portal/service/mail/IMailFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
*/
package org.wise.portal.service.mail;

import java.util.Locale;

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

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

/**
* @author Anthony Perritano
Expand All @@ -46,4 +51,9 @@ void postMail(String recipients[], String subject, String message , String from)
void postMail(String recipients[], String subject, String message , String from, String[] cc)
throws MessagingException;

void sendWelcomeTeacherEmail(String email, String displayName, String username,
boolean socialAccount, Locale locale, HttpServletRequest request);

void sendVerifyTeacherEmail(String email, String verificationCode, Locale locale,
HttpServletRequest request);
}
Loading
Loading