From 54ab686d1db3dd52672e0e6503be329cf13b49de Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 14 Jul 2026 22:37:57 -0700 Subject: [PATCH 01/20] Added new columns to teacher data and modified welcome email --- .../org/wise/portal/dao/user/UserDao.java | 1 + .../dao/user/impl/HibernateUserDao.java | 12 +++++++++-- .../impl/TeacherUserDetails.java | 16 ++++++++++++++ .../teacher/TeacherAPIController.java | 21 ++++++++++++++----- .../controllers/user/UserAPIController.java | 1 + .../wise/portal/service/user/UserService.java | 2 ++ .../service/user/impl/UserServiceImpl.java | 4 ++++ src/main/resources/i18n/i18n.properties | 2 +- 8 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/wise/portal/dao/user/UserDao.java b/src/main/java/org/wise/portal/dao/user/UserDao.java index b0d8715c3b..fbc10f77d3 100644 --- a/src/main/java/org/wise/portal/dao/user/UserDao.java +++ b/src/main/java/org/wise/portal/dao/user/UserDao.java @@ -46,6 +46,7 @@ List retrieveStudentsByNameAndBirthday(String firstName, String lastName, List retrieveTeachersByFirstName(String firstName); List retrieveTeachersByLastName(String lastName); User retrieveTeacherByUsername(String username); + User retrieveTeacherByVerificationCode(String verificationCode); List retrieveTeachersByDisplayName(String displayName); List retrieveTeachersByCity(String city); List retrieveTeachersByState(String state); diff --git a/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java b/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java index 6a97b31d53..e6d090b429 100644 --- a/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java +++ b/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java @@ -148,8 +148,11 @@ public List retrieveTeachersByLastName(String lastName) { } public User retrieveTeacherByUsername(String username) { - List resultList = retrieveTeachersByFieldValue("username", username); - return resultList.isEmpty() ? null : resultList.get(0); + return retrieveTeacherByFieldValue("username", username); + } + + public User retrieveTeacherByVerificationCode(String verificationCode) { + return retrieveTeacherByFieldValue("verificationCode", verificationCode); } public List retrieveTeachersByDisplayName(String displayName) { @@ -176,6 +179,11 @@ public List retrieveTeachersByEmail(String emailAddress) { return retrieveTeachersByFieldValue("emailAddress", emailAddress); } + private User retrieveTeacherByFieldValue(String field, String value) { + List resultList = retrieveTeachersByFieldValue(field, value); + return resultList.isEmpty() ? null : resultList.get(0); + } + @SuppressWarnings("unchecked") private List retrieveTeachersByFieldValue(String field, String value) { CriteriaBuilder cb = getCriteriaBuilder(); diff --git a/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java b/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java index 76f7e51340..52b53ff702 100644 --- a/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java +++ b/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java @@ -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; @@ -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) + @Getter + @Setter + private String verificationCode; + public String getCoreUsername() { return (firstname + lastname).replaceAll("[\\s-]+", ""); } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 559ba9040a..87c1f3d701 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.UUID; import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; @@ -178,19 +179,22 @@ ResponseEntity> createTeacherAccount( tud.setHowDidYouHearAboutUs(teacherFields.get("howDidYouHearAboutUs")); Locale locale = request.getLocale(); tud.setLanguage(locale.getLanguage()); + tud.setVerified(false); + String verificationCode = UUID.randomUUID().toString(); + tud.setVerificationCode(verificationCode); User createdUser = this.userService.createUser(tud); String username = createdUser.getUserDetails().getUsername(); String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); - Boolean iSendEmailEnabled = Boolean.valueOf(sendEmailEnabledStr); + Boolean isSendEmailEnabled = Boolean.valueOf(sendEmailEnabledStr); boolean socialAccount = this.isSet(googleUserId) || this.isSet(microsoftUserId); - if (iSendEmailEnabled) { - sendCreateTeacherAccountEmail(email, displayName, username, socialAccount, locale, request); + if (isSendEmailEnabled) { + sendCreateTeacherAccountEmail(email, displayName, username, socialAccount, locale, verificationCode, request); } return createRegisterSuccessResponse(username); } private void sendCreateTeacherAccountEmail(String email, String displayName, String username, - boolean socialAccount, Locale locale, HttpServletRequest request) { + boolean socialAccount, Locale locale, String verificationCode, HttpServletRequest request) { String fromEmail = appProperties.getProperty("portalemailaddress"); String[] recipients = { email }; String defaultSubject = messageSource.getMessage( @@ -209,9 +213,10 @@ private void sendCreateTeacherAccountEmail(String email, String displayName, Str "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername", new Object[] { displayName, gettingStartedUrl }, defaultBody, locale); } else { + String verificationUrl = getVerificationUrl(request, username, verificationCode); message = messageSource.getMessage( "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", - new Object[] { displayName, username, gettingStartedUrl }, defaultBody, locale); + new Object[] { displayName, username, verificationUrl, gettingStartedUrl }, defaultBody, locale); } try { mailService.postMail(recipients, subject, message, fromEmail); @@ -220,6 +225,12 @@ private void sendCreateTeacherAccountEmail(String email, String displayName, Str } } + private String getVerificationUrl(HttpServletRequest request, String username, + String verificationCode) { + return String.format("%s/api/teacher/verify?username=%s&code=%s", + ControllerUtil.getPortalUrlString(request), username, verificationCode); + } + private String getGettingStartedUrl(HttpServletRequest request) { return ControllerUtil.getPortalUrlString(request) + "/help/getting-started"; } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java index ca965b81fb..a08b962c13 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java @@ -118,6 +118,7 @@ HashMap 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 { diff --git a/src/main/java/org/wise/portal/service/user/UserService.java b/src/main/java/org/wise/portal/service/user/UserService.java index 0d1cdbcc8e..4bcd3aa627 100644 --- a/src/main/java/org/wise/portal/service/user/UserService.java +++ b/src/main/java/org/wise/portal/service/user/UserService.java @@ -155,6 +155,8 @@ List retrieveStudentsByNameAndBirthday(String firstName, String lastName, User retrieveTeacherByUsername(String username); + User retrieveTeacherByVerificationCode(String verificationCode); + List retrieveTeachersByDisplayName(String displayName); List retrieveTeachersByCity(String city); diff --git a/src/main/java/org/wise/portal/service/user/impl/UserServiceImpl.java b/src/main/java/org/wise/portal/service/user/impl/UserServiceImpl.java index 79446d49a5..d7c9a0d479 100644 --- a/src/main/java/org/wise/portal/service/user/impl/UserServiceImpl.java +++ b/src/main/java/org/wise/portal/service/user/impl/UserServiceImpl.java @@ -208,6 +208,10 @@ public User retrieveTeacherByUsername(String username) { return userDao.retrieveTeacherByUsername(username); } + public User retrieveTeacherByVerificationCode(String verificationCode) { + return userDao.retrieveTeacherByVerificationCode(verificationCode); + } + public List retrieveTeachersByDisplayName(String displayName) { return userDao.retrieveTeachersByDisplayName(displayName); } diff --git a/src/main/resources/i18n/i18n.properties b/src/main/resources/i18n/i18n.properties index 92d0bd5d2f..9f5c52d00a 100644 --- a/src/main/resources/i18n/i18n.properties +++ b/src/main/resources/i18n/i18n.properties @@ -3756,7 +3756,7 @@ presentation.web.controllers.teacher.project.customized.ShareProjectController.s presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject=Welcome to WISE! presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject.description=Text "Welcome to WISE!" to welcome new teachers in a welcome email. -presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody=Hi {0}!\n\nYour username is: {1}\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nFor more information on how to get started, please visit our Getting Started page.\n{2}\n\nThank you for joining WISE,\nWISE Team +presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody=Hi {0}!\n\nYour username is: {1}\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nPlease click the following link to verify your email address and sign in: {2}\n\nFor more information on how to get started, please visit our Getting Started page.\n{3}\n\nThank you for joining WISE,\nWISE Team presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody.description=Body of the Welcome email message sent to new WISE teachers. presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername=Hi {0}!\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nFor more information on how to get started, please visit our Getting Started page.\n{1}\n\nThank you for joining WISE,\nWISE Team presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername.description=Body of the Welcome email message sent to new WISE teachers that use Google ID to register. From 9f8cfb0f3f3ba9680ca267b9a7a577e5d6409a83 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 14 Jul 2026 22:52:06 -0700 Subject: [PATCH 02/20] Verification endpoints --- .../user/UserVerificationAPIController.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java new file mode 100644 index 0000000000..d784290489 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java @@ -0,0 +1,59 @@ +package org.wise.portal.presentation.web.controllers.user; + +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.service.user.UserService; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +@RestController +@RequestMapping("/api/user") +public class UserVerificationAPIController { + + private final UserService userService; + + public UserVerificationAPIController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/verify") + public void openVerificationLink(@RequestParam String code, @RequestParam String username, + HttpServletResponse response) throws IOException { + this.verifyTeacherAccount(code); + response.sendRedirect("/login?username=" + username); + } + + private void verifyTeacherAccount(String verificationCode) { + User user = userService.retrieveTeacherByVerificationCode(verificationCode); + if (this.isTeacher(user)) { + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + if (!tud.isVerified()) { + tud.setVerified(true); + userService.updateUser(user); + } + } + } + + private boolean isTeacher(User user) { + return user != null && !user.getRoles().contains("ROLE_STUDENT"); + } + + @GetMapping("/is-verified") + public boolean isUserVerified(@RequestParam String username) { + User user = userService.retrieveTeacherByUsername(username); + return !this.isTeacher(user) || this.isTeacherVerified(user); // Only teachers need to verify their accounts + } + + private boolean isTeacherVerified(User user) { + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + return tud.isVerified(); + } +} From 8e2d7025d241c98f4d01d4135c24380f5c1ead5d Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 14 Jul 2026 23:04:09 -0700 Subject: [PATCH 03/20] Update verification URL to match endpoint --- .../web/controllers/teacher/TeacherAPIController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 87c1f3d701..f453aff06c 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -227,7 +227,7 @@ private void sendCreateTeacherAccountEmail(String email, String displayName, Str private String getVerificationUrl(HttpServletRequest request, String username, String verificationCode) { - return String.format("%s/api/teacher/verify?username=%s&code=%s", + return String.format("%s/api/user/verify?username=%s&code=%s", ControllerUtil.getPortalUrlString(request), username, verificationCode); } From ac5ed8094b2392994e7537c1c7be20d26f29c99c Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Wed, 15 Jul 2026 15:41:27 -0700 Subject: [PATCH 04/20] Show a confirmation when a user's account has just been verified --- .../controllers/user/UserVerificationAPIController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java index d784290489..207c3380eb 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java @@ -27,19 +27,21 @@ public UserVerificationAPIController(UserService userService) { @GetMapping("/verify") public void openVerificationLink(@RequestParam String code, @RequestParam String username, HttpServletResponse response) throws IOException { - this.verifyTeacherAccount(code); - response.sendRedirect("/login?username=" + username); + boolean verified = this.verifyTeacherAccount(code); + response.sendRedirect("/login?verified=" + verified + "&username=" + username); } - private void verifyTeacherAccount(String verificationCode) { + private boolean verifyTeacherAccount(String verificationCode) { User user = userService.retrieveTeacherByVerificationCode(verificationCode); if (this.isTeacher(user)) { TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); if (!tud.isVerified()) { tud.setVerified(true); userService.updateUser(user); + return true; } } + return false; } private boolean isTeacher(User user) { From 8cce7f5e729af3fb884b1a22bef29a3a29c3028d Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Thu, 16 Jul 2026 15:48:59 -0700 Subject: [PATCH 05/20] Added endpoint to resend the verification email --- .../teacher/TeacherAPIController.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index f453aff06c..a303db9ce3 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -239,6 +239,26 @@ private boolean isSet(String value) { return value != null && !value.isEmpty(); } + @PostMapping("/verify-email") + @Secured({ "ROLE_ANONYMOUS" }) + ResponseEntity> resendCreateTeacherAccountEmail(@RequestParam String username, HttpServletRequest request) { + User user = userService.retrieveTeacherByUsername(username); + if (user == null) { + return ResponseEntityGenerator.createError("usernameNotFound"); + } + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + if (tud.isVerified()) { + return ResponseEntityGenerator.createError("accountAlreadyVerified"); + } + String email = tud.getEmailAddress(); + String displayName = tud.getFirstname() + " " + tud.getLastname(); + String verificationCode = tud.getVerificationCode(); + userService.updateUser(user); + Locale locale = request.getLocale(); + sendCreateTeacherAccountEmail(email, displayName, username, false, locale, verificationCode, request); + return createRegisterSuccessResponse(username); + } + private List> getRunSharedOwnersList(Run run) { List> sharedOwners = new ArrayList>(); for (User sharedOwner : run.getSharedowners()) { From 6d52c633a3be03bc9ebc10639d78ffb3105f5211 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Thu, 16 Jul 2026 16:21:29 -0700 Subject: [PATCH 06/20] Minor change to email --- src/main/resources/i18n/i18n.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/i18n/i18n.properties b/src/main/resources/i18n/i18n.properties index 9f5c52d00a..ec43dd3f67 100644 --- a/src/main/resources/i18n/i18n.properties +++ b/src/main/resources/i18n/i18n.properties @@ -3756,7 +3756,7 @@ presentation.web.controllers.teacher.project.customized.ShareProjectController.s presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject=Welcome to WISE! presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject.description=Text "Welcome to WISE!" to welcome new teachers in a welcome email. -presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody=Hi {0}!\n\nYour username is: {1}\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nPlease click the following link to verify your email address and sign in: {2}\n\nFor more information on how to get started, please visit our Getting Started page.\n{3}\n\nThank you for joining WISE,\nWISE Team +presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody=Hi {0}!\n\nYour username is: {1}\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nPlease click the following link to verify your email address and sign in:\n{2}\n\nFor more information on how to get started, please visit our Getting Started page.\n{3}\n\nThank you for joining WISE,\nWISE Team presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody.description=Body of the Welcome email message sent to new WISE teachers. presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername=Hi {0}!\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nFor more information on how to get started, please visit our Getting Started page.\n{1}\n\nThank you for joining WISE,\nWISE Team presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername.description=Body of the Welcome email message sent to new WISE teachers that use Google ID to register. From 92e45aba109fd0ee30506fa441a7b151b41533b4 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 09:56:23 -0700 Subject: [PATCH 07/20] Move teacher registration into separate class --- .../teacher/TeacherAPIController.java | 109 --------------- .../TeacherRegistrationAPIController.java | 127 ++++++++++++++++++ .../teacher/TeacherAPIControllerTest.java | 43 ------ .../TeacherRegistrationAPIControllerTest.java | 77 +++++++++++ 4 files changed, 204 insertions(+), 152 deletions(-) create mode 100644 src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java create mode 100644 src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 559ba9040a..92e6baadd5 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -4,24 +4,19 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.TreeSet; -import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.acls.model.Permission; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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; @@ -32,13 +27,8 @@ import org.wise.portal.domain.group.Group; import org.wise.portal.domain.run.Run; import org.wise.portal.domain.user.User; -import org.wise.portal.domain.usertag.UserTag; -import org.wise.portal.presentation.web.controllers.ControllerUtil; import org.wise.portal.presentation.web.controllers.user.UserAPIController; -import org.wise.portal.presentation.web.exception.InvalidNameException; -import org.wise.portal.presentation.web.response.ResponseEntityGenerator; import org.wise.portal.presentation.web.response.SimpleResponse; -import org.wise.portal.service.authentication.DuplicateUsernameException; import org.wise.portal.service.authentication.UserDetailsService; import org.wise.portal.service.usertags.UserTagsService; @@ -129,105 +119,6 @@ List getAllTeacherUsernames() { return userDetailsService.retrieveAllTeacherUsernames(); } - @PostMapping("/register") - @Secured({ "ROLE_ANONYMOUS" }) - ResponseEntity> createTeacherAccount( - @RequestBody Map teacherFields, HttpServletRequest request) - throws DuplicateUsernameException, InvalidNameException { - if (ControllerUtil.isReCaptchaEnabled()) { - String token = teacherFields.get("token"); - if (!ControllerUtil.isReCaptchaResponseValid(token)) { - return ResponseEntityGenerator.createError("recaptchaResponseInvalid"); - } - } - TeacherUserDetails tud = new TeacherUserDetails(); - String firstName = teacherFields.get("firstName"); - String lastName = teacherFields.get("lastName"); - if (!isFirstNameAndLastNameValid(firstName, lastName)) { - String messageCode = this.getInvalidNameMessageCode(firstName, lastName); - throw new InvalidNameException(messageCode); - } - tud.setFirstname(firstName); - tud.setLastname(lastName); - String email = teacherFields.get("email"); - tud.setEmailAddress(email); - tud.setCity(teacherFields.get("city")); - tud.setState(teacherFields.get("state")); - tud.setCountry(teacherFields.get("country")); - String googleUserId = teacherFields.get("googleUserId"); - String microsoftUserId = teacherFields.get("microsoftUserId"); - if (isSet(googleUserId)) { - tud.setGoogleUserId(googleUserId); - tud.setPassword(RandomStringUtils.random(10, true, true)); - } else if (isSet(microsoftUserId)) { - tud.setMicrosoftUserId(microsoftUserId); - tud.setPassword(RandomStringUtils.random(10, true, true)); - } else { - String password = teacherFields.get("password"); - if (!passwordService.isValid(password)) { - return ResponseEntityGenerator.createError(passwordService.getErrors(password)); - } else { - tud.setPassword(password); - } - } - String displayName = firstName + " " + lastName; - tud.setDisplayname(displayName); - tud.setEmailValid(true); - tud.setSchoollevel(Schoollevel.valueOf(teacherFields.get("schoolLevel"))); - tud.setSchoolname(teacherFields.get("schoolName")); - tud.setHowDidYouHearAboutUs(teacherFields.get("howDidYouHearAboutUs")); - Locale locale = request.getLocale(); - tud.setLanguage(locale.getLanguage()); - User createdUser = this.userService.createUser(tud); - String username = createdUser.getUserDetails().getUsername(); - String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); - Boolean iSendEmailEnabled = Boolean.valueOf(sendEmailEnabledStr); - boolean socialAccount = this.isSet(googleUserId) || this.isSet(microsoftUserId); - if (iSendEmailEnabled) { - sendCreateTeacherAccountEmail(email, displayName, username, socialAccount, locale, request); - } - return createRegisterSuccessResponse(username); - } - - private void sendCreateTeacherAccountEmail(String email, String displayName, String username, - boolean socialAccount, Locale locale, HttpServletRequest request) { - String fromEmail = appProperties.getProperty("portalemailaddress"); - String[] recipients = { email }; - String defaultSubject = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject", - null, Locale.US); - String subject = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject", - null, defaultSubject, locale); - String defaultBody = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", - new Object[] { username }, Locale.US); - String gettingStartedUrl = getGettingStartedUrl(request); - String message; - if (socialAccount) { - message = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername", - new Object[] { displayName, gettingStartedUrl }, defaultBody, locale); - } else { - message = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", - new Object[] { displayName, username, gettingStartedUrl }, defaultBody, locale); - } - try { - mailService.postMail(recipients, subject, message, fromEmail); - } catch (MessagingException e) { - e.printStackTrace(); - } - } - - private String getGettingStartedUrl(HttpServletRequest request) { - return ControllerUtil.getPortalUrlString(request) + "/help/getting-started"; - } - - private boolean isSet(String value) { - return value != null && !value.isEmpty(); - } - private List> getRunSharedOwnersList(Run run) { List> sharedOwners = new ArrayList>(); for (User sharedOwner : run.getSharedowners()) { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java new file mode 100644 index 0000000000..d5dc363c56 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -0,0 +1,127 @@ +package org.wise.portal.presentation.web.controllers.teacher; + +import java.util.Locale; +import java.util.Map; + +import javax.mail.MessagingException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +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.RestController; +import org.wise.portal.domain.authentication.Schoollevel; +import org.wise.portal.domain.authentication.impl.TeacherUserDetails; +import org.wise.portal.domain.user.User; +import org.wise.portal.presentation.web.controllers.ControllerUtil; +import org.wise.portal.presentation.web.exception.InvalidNameException; +import org.wise.portal.presentation.web.response.ResponseEntityGenerator; +import org.wise.portal.service.authentication.DuplicateUsernameException; + + +@RestController +@RequestMapping("/api/teacher/register") +public class TeacherRegistrationAPIController extends TeacherAPIController { + + @PostMapping() + @Secured({ "ROLE_ANONYMOUS" }) + ResponseEntity> createTeacherAccount( + @RequestBody Map teacherFields, HttpServletRequest request) + throws DuplicateUsernameException, InvalidNameException { + if (ControllerUtil.isReCaptchaEnabled()) { + String token = teacherFields.get("token"); + if (!ControllerUtil.isReCaptchaResponseValid(token)) { + return ResponseEntityGenerator.createError("recaptchaResponseInvalid"); + } + } + TeacherUserDetails tud = new TeacherUserDetails(); + String firstName = teacherFields.get("firstName"); + String lastName = teacherFields.get("lastName"); + if (!isFirstNameAndLastNameValid(firstName, lastName)) { + String messageCode = this.getInvalidNameMessageCode(firstName, lastName); + throw new InvalidNameException(messageCode); + } + tud.setFirstname(firstName); + tud.setLastname(lastName); + String email = teacherFields.get("email"); + tud.setEmailAddress(email); + tud.setCity(teacherFields.get("city")); + tud.setState(teacherFields.get("state")); + tud.setCountry(teacherFields.get("country")); + String googleUserId = teacherFields.get("googleUserId"); + String microsoftUserId = teacherFields.get("microsoftUserId"); + if (isSet(googleUserId)) { + tud.setGoogleUserId(googleUserId); + tud.setPassword(RandomStringUtils.random(10, true, true)); + } else if (isSet(microsoftUserId)) { + tud.setMicrosoftUserId(microsoftUserId); + tud.setPassword(RandomStringUtils.random(10, true, true)); + } else { + String password = teacherFields.get("password"); + if (!passwordService.isValid(password)) { + return ResponseEntityGenerator.createError(passwordService.getErrors(password)); + } else { + tud.setPassword(password); + } + } + String displayName = firstName + " " + lastName; + tud.setDisplayname(displayName); + tud.setEmailValid(true); + tud.setSchoollevel(Schoollevel.valueOf(teacherFields.get("schoolLevel"))); + tud.setSchoolname(teacherFields.get("schoolName")); + tud.setHowDidYouHearAboutUs(teacherFields.get("howDidYouHearAboutUs")); + Locale locale = request.getLocale(); + tud.setLanguage(locale.getLanguage()); + User createdUser = this.userService.createUser(tud); + String username = createdUser.getUserDetails().getUsername(); + String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); + Boolean iSendEmailEnabled = Boolean.valueOf(sendEmailEnabledStr); + boolean socialAccount = this.isSet(googleUserId) || this.isSet(microsoftUserId); + if (iSendEmailEnabled) { + sendCreateTeacherAccountEmail(email, displayName, username, socialAccount, locale, request); + } + return createRegisterSuccessResponse(username); + } + + private void sendCreateTeacherAccountEmail(String email, String displayName, String username, + boolean socialAccount, Locale locale, HttpServletRequest request) { + String fromEmail = appProperties.getProperty("portalemailaddress"); + String[] recipients = { email }; + String defaultSubject = messageSource.getMessage( + "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject", + null, Locale.US); + String subject = messageSource.getMessage( + "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject", + null, defaultSubject, locale); + String defaultBody = messageSource.getMessage( + "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", + new Object[] { username }, Locale.US); + String gettingStartedUrl = getGettingStartedUrl(request); + String message; + if (socialAccount) { + message = messageSource.getMessage( + "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername", + new Object[] { displayName, gettingStartedUrl }, defaultBody, locale); + } else { + message = messageSource.getMessage( + "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", + new Object[] { displayName, username, gettingStartedUrl }, defaultBody, locale); + } + try { + mailService.postMail(recipients, subject, message, fromEmail); + } catch (MessagingException e) { + e.printStackTrace(); + } + } + + private String getGettingStartedUrl(HttpServletRequest request) { + return ControllerUtil.getPortalUrlString(request) + "/help/getting-started"; + } + + private boolean isSet(String value) { + return value != null && !value.isEmpty(); + } +} diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java index d070760700..76902310e4 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java @@ -16,7 +16,6 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import org.easymock.EasyMockExtension; @@ -25,8 +24,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import org.wise.portal.dao.ObjectNotFoundException; import org.wise.portal.domain.PeriodNotFoundException; @@ -35,9 +32,7 @@ import org.wise.portal.domain.run.Run; import org.wise.portal.domain.user.User; import org.wise.portal.presentation.web.controllers.APIControllerTest; -import org.wise.portal.presentation.web.exception.InvalidNameException; import org.wise.portal.presentation.web.response.SimpleResponse; -import org.wise.portal.service.authentication.DuplicateUsernameException; import org.wise.portal.service.authentication.UserDetailsService; import org.wise.portal.service.password.impl.PasswordServiceImpl; import org.wise.portal.service.usertags.UserTagsService; @@ -375,33 +370,6 @@ public void createRun_ThreePeriods_CreateRun() throws Exception { verify(runService); } - @Test - public void createTeacherAccount_InvalidPassword_ReturnError() - throws DuplicateUsernameException, InvalidNameException { - HashMap teacherFields = createDefaultTeacherFields(); - teacherFields.put("password", PasswordServiceImpl.INVALID_PASSWORD_TOO_SHORT); - ResponseEntity> response = teacherAPIController - .createTeacherAccount(teacherFields, request); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertEquals("invalidPassword", response.getBody().get("messageCode")); - } - - @Test - public void createTeacherAccount_WithGoogleUserId_CreateUser() - throws DuplicateUsernameException, InvalidNameException { - HashMap teacherFields = createDefaultTeacherFields(); - teacherFields.put("googleUserId", "123456789"); - expect(request.getLocale()).andReturn(Locale.US); - replay(request); - expect(userService.createUser(isA(TeacherUserDetails.class))).andReturn(teacher1); - replay(userService); - ResponseEntity> response = teacherAPIController - .createTeacherAccount(teacherFields, request); - assertEquals(TEACHER_USERNAME, response.getBody().get("username")); - verify(request); - verify(userService); - } - @Test public void editRunEndTime_WithNullEndTime_ChangeEndTime() throws ObjectNotFoundException { expect(userService.retrieveUserByUsername(teacherAuth.getName())).andReturn(teacher1); @@ -456,17 +424,6 @@ public void editRunIsLockedAfterEndDate_WithFalse_ChangeValue() throws ObjectNot verify(runService); } - private HashMap createDefaultTeacherFields() { - HashMap fields = new HashMap(); - fields.put("firstName", TEACHER_FIRSTNAME); - fields.put("lastName", TEACHER_LASTNAME); - fields.put("schoolLevel", "COLLEGE"); - fields.put("birthMonth", "1"); - fields.put("birthDay", "1"); - fields.put("gender", "MALE"); - return fields; - } - private void expectGetRunMapToBeCalled() { expect(projectService.getProjectPath(isA(Project.class))).andReturn("/1/project.json"); expect(projectService.getProjectSharedOwnersList(isA(Project.class))) diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java new file mode 100644 index 0000000000..032b815512 --- /dev/null +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java @@ -0,0 +1,77 @@ +package org.wise.portal.presentation.web.controllers.teacher; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.easymock.EasyMockExtension; +import org.easymock.Mock; +import org.easymock.TestSubject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.wise.portal.domain.authentication.impl.TeacherUserDetails; +import org.wise.portal.presentation.web.controllers.APIControllerTest; +import org.wise.portal.presentation.web.exception.InvalidNameException; +import org.wise.portal.service.authentication.DuplicateUsernameException; +import org.wise.portal.service.authentication.UserDetailsService; +import org.wise.portal.service.password.impl.PasswordServiceImpl; +import org.wise.portal.service.usertags.UserTagsService; + +@ExtendWith(EasyMockExtension.class) +public class TeacherRegistrationAPIControllerTest extends APIControllerTest { + + @TestSubject + private TeacherRegistrationAPIController teacherRegistrationAPIController = new TeacherRegistrationAPIController(); + + @Mock + private UserDetailsService userDetailsService; + + @Mock + private UserTagsService userTagsService; + + @Test + public void createTeacherAccount_InvalidPassword_ReturnError() + throws DuplicateUsernameException, InvalidNameException { + HashMap teacherFields = createDefaultTeacherFields(); + teacherFields.put("password", PasswordServiceImpl.INVALID_PASSWORD_TOO_SHORT); + ResponseEntity> response = teacherRegistrationAPIController + .createTeacherAccount(teacherFields, request); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("invalidPassword", response.getBody().get("messageCode")); + } + + @Test + public void createTeacherAccount_WithGoogleUserId_CreateUser() + throws DuplicateUsernameException, InvalidNameException { + HashMap teacherFields = createDefaultTeacherFields(); + teacherFields.put("googleUserId", "123456789"); + expect(request.getLocale()).andReturn(Locale.US); + replay(request); + expect(userService.createUser(isA(TeacherUserDetails.class))).andReturn(teacher1); + replay(userService); + ResponseEntity> response = teacherRegistrationAPIController + .createTeacherAccount(teacherFields, request); + assertEquals(TEACHER_USERNAME, response.getBody().get("username")); + verify(request); + verify(userService); + } + + private HashMap createDefaultTeacherFields() { + HashMap fields = new HashMap(); + fields.put("firstName", TEACHER_FIRSTNAME); + fields.put("lastName", TEACHER_LASTNAME); + fields.put("schoolLevel", "COLLEGE"); + fields.put("birthMonth", "1"); + fields.put("birthDay", "1"); + fields.put("gender", "MALE"); + return fields; + } +} From 907d6836eb5f95cb23e0fa88e878923b54eea9ef Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 09:58:48 -0700 Subject: [PATCH 08/20] Refactor teacher registration --- .../TeacherRegistrationAPIController.java | 185 +++++++++++------- 1 file changed, 115 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index d5dc363c56..3f3fe84582 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -18,110 +18,155 @@ import org.wise.portal.domain.user.User; import org.wise.portal.presentation.web.controllers.ControllerUtil; import org.wise.portal.presentation.web.exception.InvalidNameException; +import org.wise.portal.presentation.web.exception.InvalidPasswordException; +import org.wise.portal.presentation.web.exception.RecaptchaVerificationException; import org.wise.portal.presentation.web.response.ResponseEntityGenerator; import org.wise.portal.service.authentication.DuplicateUsernameException; - @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" }) ResponseEntity> createTeacherAccount( @RequestBody Map teacherFields, HttpServletRequest request) throws DuplicateUsernameException, InvalidNameException { + try { + validateTeacherFields(teacherFields); + } catch (RecaptchaVerificationException e) { + return ResponseEntityGenerator.createError("recaptchaResponseInvalid"); + } catch (InvalidPasswordException e) { + return ResponseEntityGenerator.createError( + passwordService.getErrors(teacherFields.get("password"))); + } + Locale locale = request.getLocale(); + TeacherUserDetails tud = createTeacherUserDetails(teacherFields, locale); + User createdUser = this.userService.createUser(tud); + String username = createdUser.getUserDetails().getUsername(); + if (isSendEmailEnabled()) { + boolean socialAccount = isSet(tud.getGoogleUserId()) || isSet(tud.getMicrosoftUserId()); + sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), username, + socialAccount, locale, request); + } + return createRegisterSuccessResponse(username); + } + + private Boolean isSendEmailEnabled() { + String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); + return Boolean.valueOf(sendEmailEnabledStr); + } + + 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 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 void validateTeacherFields(Map teacherFields) + throws RecaptchaVerificationException, InvalidNameException, InvalidPasswordException { + validateReCaptcha(teacherFields.get("token")); + validateFirstAndLastName(teacherFields.get("firstName"), teacherFields.get("lastName")); + validatePassword(teacherFields.get("password")); + } + + private void validateReCaptcha(String token) throws RecaptchaVerificationException { if (ControllerUtil.isReCaptchaEnabled()) { - String token = teacherFields.get("token"); if (!ControllerUtil.isReCaptchaResponseValid(token)) { - return ResponseEntityGenerator.createError("recaptchaResponseInvalid"); + throw new RecaptchaVerificationException("Invalid ReCaptcha Response"); } } - TeacherUserDetails tud = new TeacherUserDetails(); - String firstName = teacherFields.get("firstName"); - String lastName = teacherFields.get("lastName"); + } + + private void validateFirstAndLastName(String firstName, String lastName) + throws InvalidNameException { if (!isFirstNameAndLastNameValid(firstName, lastName)) { String messageCode = this.getInvalidNameMessageCode(firstName, lastName); throw new InvalidNameException(messageCode); } - tud.setFirstname(firstName); - tud.setLastname(lastName); - String email = teacherFields.get("email"); - tud.setEmailAddress(email); + } + + private void validatePassword(String password) throws InvalidPasswordException { + if (password != null && !passwordService.isValid(password)) { + throw new InvalidPasswordException(); + } + } + + private TeacherUserDetails createTeacherUserDetails(Map teacherFields, + Locale locale) { + TeacherUserDetails tud = new TeacherUserDetails(); + tud.setFirstname(teacherFields.get("firstName")); + tud.setLastname(teacherFields.get("lastName")); + tud.setEmailAddress(teacherFields.get("email")); tud.setCity(teacherFields.get("city")); tud.setState(teacherFields.get("state")); tud.setCountry(teacherFields.get("country")); - String googleUserId = teacherFields.get("googleUserId"); - String microsoftUserId = teacherFields.get("microsoftUserId"); - if (isSet(googleUserId)) { - tud.setGoogleUserId(googleUserId); - tud.setPassword(RandomStringUtils.random(10, true, true)); - } else if (isSet(microsoftUserId)) { - tud.setMicrosoftUserId(microsoftUserId); - tud.setPassword(RandomStringUtils.random(10, true, true)); - } else { - String password = teacherFields.get("password"); - if (!passwordService.isValid(password)) { - return ResponseEntityGenerator.createError(passwordService.getErrors(password)); - } else { - tud.setPassword(password); - } - } - String displayName = firstName + " " + lastName; - tud.setDisplayname(displayName); - tud.setEmailValid(true); + tud.setDisplayname(tud.getFirstname() + " " + tud.getLastname()); tud.setSchoollevel(Schoollevel.valueOf(teacherFields.get("schoolLevel"))); tud.setSchoolname(teacherFields.get("schoolName")); tud.setHowDidYouHearAboutUs(teacherFields.get("howDidYouHearAboutUs")); - Locale locale = request.getLocale(); tud.setLanguage(locale.getLanguage()); - User createdUser = this.userService.createUser(tud); - String username = createdUser.getUserDetails().getUsername(); - String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); - Boolean iSendEmailEnabled = Boolean.valueOf(sendEmailEnabledStr); - boolean socialAccount = this.isSet(googleUserId) || this.isSet(microsoftUserId); - if (iSendEmailEnabled) { - sendCreateTeacherAccountEmail(email, displayName, username, socialAccount, locale, request); - } - return createRegisterSuccessResponse(username); + setPassword(teacherFields, tud); + tud.setEmailValid(true); + return tud; } - private void sendCreateTeacherAccountEmail(String email, String displayName, String username, - boolean socialAccount, Locale locale, HttpServletRequest request) { - String fromEmail = appProperties.getProperty("portalemailaddress"); - String[] recipients = { email }; - String defaultSubject = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject", - null, Locale.US); - String subject = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject", - null, defaultSubject, locale); - String defaultBody = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", - new Object[] { username }, Locale.US); - String gettingStartedUrl = getGettingStartedUrl(request); - String message; - if (socialAccount) { - message = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername", - new Object[] { displayName, gettingStartedUrl }, defaultBody, locale); + private void setPassword(Map teacherFields, TeacherUserDetails tud) { + String googleUserId = teacherFields.get("googleUserId"); + String microsoftUserId = teacherFields.get("microsoftUserId"); + if (isSet(googleUserId)) { + tud.setGoogleUserId(googleUserId); + setRandomPassword(tud); + } else if (isSet(microsoftUserId)) { + tud.setMicrosoftUserId(microsoftUserId); + setRandomPassword(tud); } else { - message = messageSource.getMessage( - "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody", - new Object[] { displayName, username, gettingStartedUrl }, defaultBody, locale); - } - try { - mailService.postMail(recipients, subject, message, fromEmail); - } catch (MessagingException e) { - e.printStackTrace(); + tud.setPassword(teacherFields.get("password")); } } - private String getGettingStartedUrl(HttpServletRequest request) { - return ControllerUtil.getPortalUrlString(request) + "/help/getting-started"; - } - private boolean isSet(String value) { return value != null && !value.isEmpty(); } + + private void setRandomPassword(TeacherUserDetails tud) { + tud.setPassword(RandomStringUtils.random(10, true, true)); + } } From ad53a48f759567e18e5ee223921c0df5c2ac93a2 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 09:58:55 -0700 Subject: [PATCH 09/20] Fix tests --- .../TeacherRegistrationAPIControllerTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java index 032b815512..87bb71b758 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java @@ -22,12 +22,13 @@ import org.wise.portal.presentation.web.exception.InvalidNameException; import org.wise.portal.service.authentication.DuplicateUsernameException; import org.wise.portal.service.authentication.UserDetailsService; +import org.wise.portal.service.password.PasswordService; import org.wise.portal.service.password.impl.PasswordServiceImpl; import org.wise.portal.service.usertags.UserTagsService; @ExtendWith(EasyMockExtension.class) public class TeacherRegistrationAPIControllerTest extends APIControllerTest { - + @TestSubject private TeacherRegistrationAPIController teacherRegistrationAPIController = new TeacherRegistrationAPIController(); @@ -37,11 +38,19 @@ public class TeacherRegistrationAPIControllerTest extends APIControllerTest { @Mock private UserTagsService userTagsService; + @Mock + private PasswordService passwordService; + @Test public void createTeacherAccount_InvalidPassword_ReturnError() throws DuplicateUsernameException, InvalidNameException { HashMap teacherFields = createDefaultTeacherFields(); teacherFields.put("password", PasswordServiceImpl.INVALID_PASSWORD_TOO_SHORT); + expect(passwordService.isValid(PasswordServiceImpl.INVALID_PASSWORD_TOO_SHORT)).andReturn(false); + Map errors = new HashMap<>(); + errors.put("messageCode", "invalidPassword"); + expect(passwordService.getErrors(PasswordServiceImpl.INVALID_PASSWORD_TOO_SHORT)).andReturn(errors); + replay(passwordService); ResponseEntity> response = teacherRegistrationAPIController .createTeacherAccount(teacherFields, request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -62,7 +71,7 @@ public void createTeacherAccount_WithGoogleUserId_CreateUser() assertEquals(TEACHER_USERNAME, response.getBody().get("username")); verify(request); verify(userService); - } + } private HashMap createDefaultTeacherFields() { HashMap fields = new HashMap(); From 92b951b18d07502a0ea83d5406a9b02809a4ed07 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 10:03:28 -0700 Subject: [PATCH 10/20] Minor change --- .../teacher/TeacherRegistrationAPIController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index 3f3fe84582..cc18cd734e 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -51,18 +51,21 @@ ResponseEntity> createTeacherAccount( User createdUser = this.userService.createUser(tud); String username = createdUser.getUserDetails().getUsername(); if (isSendEmailEnabled()) { - boolean socialAccount = isSet(tud.getGoogleUserId()) || isSet(tud.getMicrosoftUserId()); sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), username, - socialAccount, locale, request); + isSocialAccount(tud), locale, request); } return createRegisterSuccessResponse(username); } - private Boolean isSendEmailEnabled() { + 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) { From 7a8f57ae865144bd6d585958aab18f7a8bcd1c6f Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 11:38:25 -0700 Subject: [PATCH 11/20] Move contents of UserVerificationAPIController into more appropriate classes --- .../teacher/TeacherAPIController.java | 15 +++++ .../TeacherRegistrationAPIController.java | 22 +++++++ .../user/UserVerificationAPIController.java | 61 ------------------- 3 files changed, 37 insertions(+), 61 deletions(-) delete mode 100644 src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 1dd53d19ef..3fcfd083e7 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -327,4 +327,19 @@ HashMap editRunIsLockedAfterEndDate(Authentication authenticatio } return response; } + + @GetMapping("/is-verified") + public boolean isUserVerified(@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(); + } } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index cc18cd734e..116881497a 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -172,4 +172,26 @@ private boolean isSet(String value) { private void setRandomPassword(TeacherUserDetails tud) { tud.setPassword(RandomStringUtils.random(10, true, true)); } + + @GetMapping("/verify") + public void openVerificationLink(@RequestParam String code, @RequestParam String username, + HttpServletResponse response) throws IOException { + boolean verified = this.verifyTeacherAccount(code); + response.sendRedirect("/login?verified=" + verified + "&username=" + username); + } + + private boolean verifyTeacherAccount(String verificationCode) { + User user = userService.retrieveTeacherByVerificationCode(verificationCode); + if (this.isTeacher(user)) { + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + if (!tud.isVerified()) { + tud.setVerified(true); + userService.updateUser(user); + return true; + } + } + return false; + } + + } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java deleted file mode 100644 index 207c3380eb..0000000000 --- a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserVerificationAPIController.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.wise.portal.presentation.web.controllers.user; - -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.service.user.UserService; - -import java.io.IOException; - -import javax.servlet.http.HttpServletResponse; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - - -@RestController -@RequestMapping("/api/user") -public class UserVerificationAPIController { - - private final UserService userService; - - public UserVerificationAPIController(UserService userService) { - this.userService = userService; - } - - @GetMapping("/verify") - public void openVerificationLink(@RequestParam String code, @RequestParam String username, - HttpServletResponse response) throws IOException { - boolean verified = this.verifyTeacherAccount(code); - response.sendRedirect("/login?verified=" + verified + "&username=" + username); - } - - private boolean verifyTeacherAccount(String verificationCode) { - User user = userService.retrieveTeacherByVerificationCode(verificationCode); - if (this.isTeacher(user)) { - TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); - if (!tud.isVerified()) { - tud.setVerified(true); - userService.updateUser(user); - return true; - } - } - return false; - } - - private boolean isTeacher(User user) { - return user != null && !user.getRoles().contains("ROLE_STUDENT"); - } - - @GetMapping("/is-verified") - public boolean isUserVerified(@RequestParam String username) { - User user = userService.retrieveTeacherByUsername(username); - return !this.isTeacher(user) || this.isTeacherVerified(user); // Only teachers need to verify their accounts - } - - private boolean isTeacherVerified(User user) { - TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); - return tud.isVerified(); - } -} From 9d941f70776d51a6daa9c712f41db607e67d69e1 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 12:20:58 -0700 Subject: [PATCH 12/20] Welcome email only after verifying account --- .../teacher/TeacherAPIController.java | 1 + .../TeacherRegistrationAPIController.java | 81 +++++++++++++++---- src/main/resources/i18n/i18n.properties | 7 +- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 3fcfd083e7..02fe6992ce 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -329,6 +329,7 @@ HashMap editRunIsLockedAfterEndDate(Authentication authenticatio } @GetMapping("/is-verified") + @Secured({ "ROLE_ANONYMOUS" }) public boolean isUserVerified(@RequestParam String username) { User user = userService.retrieveTeacherByUsername(username); return !this.isTeacher(user) || this.isTeacherVerified(user); // Only teachers need to verify their accounts diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index 116881497a..99ac519811 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -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.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; @@ -28,10 +33,12 @@ 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"; + "presentation.web.controllers.teacher.registerTeacherController."; + private final String verifyBodyCode = this.emailCodePrefix + "verifyTeacherEmailBody"; + private final String verifySubjectCode = this.emailCodePrefix + "verifyTeacherEmailSubject"; + private final String welcomeBodyCode = this.emailCodePrefix + "welcomeTeacherEmailBody"; + private final String welcomeSocialAccountBodyCode = this.emailCodePrefix + "welcomeTeacherEmailBodyNoUsername"; + private final String welcomeSubjectCode = this.emailCodePrefix + "welcomeTeacherEmailSubject"; @PostMapping() @Secured({ "ROLE_ANONYMOUS" }) @@ -47,12 +54,17 @@ ResponseEntity> 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); + if (isSocialAccount) { + sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), username, + true, locale, request); + } else { + sendVerifyTeacherEmail(tud.getEmailAddress(), tud.getVerificationCode(), locale, request); + } } return createRegisterSuccessResponse(username); } @@ -62,8 +74,8 @@ private boolean isSendEmailEnabled() { return Boolean.valueOf(sendEmailEnabledStr); } - private boolean isSocialAccount(TeacherUserDetails tud) { - return isSet(tud.getGoogleUserId()) || isSet(tud.getMicrosoftUserId()); + private boolean isSocialAccount(Map teacherFields) { + return isSet(teacherFields.get("googleUserId")) || isSet(tud.getMicrosoftUserId()); } private void sendWelcomeTeacherEmail(String email, String displayName, String username, @@ -93,6 +105,20 @@ private String getGettingStartedUrl(HttpServletRequest request) { return ControllerUtil.getPortalUrlString(request) + "/help/getting-started"; } + private void sendVerifyTeacherEmail(String email, String verificationCode, + Locale locale, HttpServletRequest request) { + String subject = getEmailMessage(this.verifySubjectCode, this.verifySubjectCode, null, locale); + String verificationUrl = getVerificationUrl(verificationCode, request); + Object[] args = new Object[] { verificationUrl }; + String body = getEmailMessage(this.verifyBodyCode, this.verifyBodyCode, args, locale); + this.sendEmail(email, subject, body); + } + + private String getVerificationUrl(String verificationCode, HttpServletRequest request) { + return String.format("%s/api/teacher/register/verify?code=%s", + ControllerUtil.getPortalUrlString(request), verificationCode); + } + private void sendEmail(String email, String subject, String body) { String fromEmail = appProperties.getProperty("portalemailaddress"); String[] recipients = { email }; @@ -133,7 +159,7 @@ private void validatePassword(String password) throws InvalidPasswordException { } private TeacherUserDetails createTeacherUserDetails(Map teacherFields, - Locale locale) { + boolean isSocialAccount, Locale locale) { TeacherUserDetails tud = new TeacherUserDetails(); tud.setFirstname(teacherFields.get("firstName")); tud.setLastname(teacherFields.get("lastName")); @@ -148,6 +174,8 @@ private TeacherUserDetails createTeacherUserDetails(Map teacherF tud.setLanguage(locale.getLanguage()); setPassword(teacherFields, tud); tud.setEmailValid(true); + tud.setVerified(isSocialAccount); + tud.setVerificationCode(UUID.randomUUID().toString()); return tud; } @@ -174,24 +202,45 @@ private void setRandomPassword(TeacherUserDetails tud) { } @GetMapping("/verify") - public void openVerificationLink(@RequestParam String code, @RequestParam String username, - HttpServletResponse response) throws IOException { - boolean verified = this.verifyTeacherAccount(code); - response.sendRedirect("/login?verified=" + verified + "&username=" + username); + @Secured({ "ROLE_ANONYMOUS" }) + public void openVerificationLink(@RequestParam String code, HttpServletResponse response, + HttpServletRequest request) throws IOException { + boolean verified = this.verifyTeacherAccount(code, request); + User user = userService.retrieveTeacherByVerificationCode(code); + response.sendRedirect("/login?verified=" + verified + "&username=" + user.getUserDetails().getUsername()); } - private boolean verifyTeacherAccount(String verificationCode) { + private boolean verifyTeacherAccount(String verificationCode, HttpServletRequest request) { User user = userService.retrieveTeacherByVerificationCode(verificationCode); if (this.isTeacher(user)) { TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); if (!tud.isVerified()) { tud.setVerified(true); userService.updateUser(user); + sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(), + false, request.getLocale(), request); return true; } } return false; } - + @PostMapping("send-verify-email") + @Secured({ "ROLE_ANONYMOUS" }) + ResponseEntity> sendVerificationEmail(@RequestParam String username, + HttpServletRequest request) { + User user = userService.retrieveTeacherByUsername(username); + if (this.isTeacher(user)) { + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + if (tud.isVerified()) { + return ResponseEntityGenerator.createError("Teacher already verified"); //TODO: error message codes point where? + } else { + sendVerifyTeacherEmail(tud.getEmailAddress(), tud.getVerificationCode(), + request.getLocale(), request); + return createRegisterSuccessResponse(username); + } + } else { + return ResponseEntityGenerator.createError("Not a teacher"); //TODO: error message codes point where? + } + } } diff --git a/src/main/resources/i18n/i18n.properties b/src/main/resources/i18n/i18n.properties index ec43dd3f67..270f3a530a 100644 --- a/src/main/resources/i18n/i18n.properties +++ b/src/main/resources/i18n/i18n.properties @@ -3754,9 +3754,14 @@ presentation.web.controllers.teacher.project.customized.ShareProjectController.s presentation.web.controllers.teacher.project.customized.ShareProjectController.shareProjectConfirmationEmailBody={0} shared a project with you on WISE:\n\nProject Name: {1}\nProject ID: {2}\nShared with username: {3}\nDate this project was shared: {4}\n\nGo to this URL to preview this project: {5}\n\nThanks,\nWISE Team presentation.web.controllers.teacher.project.customized.ShareProjectController.shareProjectConfirmationEmailBody.description=Message body of the email notifying user that someone shared a project with them. +presentation.web.controllers.teacher.registerTeacherController.verifyTeacherEmailSubject=Verify your WISE account +presentation.web.controllers.teacher.registerTeacherController.verifyTeacherEmailSubject.description=Subject line of the email including the link that teachers must be click to verify their account. +presentation.web.controllers.teacher.registerTeacherController.verifyTeacherEmailBody=Please click the following link to verify your email address and complete the registration process:\n{0}\n\nIf you did not make this request, you can ignore this email. +presentation.web.controllers.teacher.registerTeacherController.verifyTeacherEmailBody.description=Message body of the email including the link that teachers must be click to verify their account. + presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject=Welcome to WISE! presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailSubject.description=Text "Welcome to WISE!" to welcome new teachers in a welcome email. -presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody=Hi {0}!\n\nYour username is: {1}\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nPlease click the following link to verify your email address and sign in:\n{2}\n\nFor more information on how to get started, please visit our Getting Started page.\n{3}\n\nThank you for joining WISE,\nWISE Team +presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody=Hi {0}!\n\nYour username is: {1}\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nFor more information on how to get started, please visit our Getting Started page.\n{2}\n\nThank you for joining WISE,\nWISE Team presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBody.description=Body of the Welcome email message sent to new WISE teachers. presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername=Hi {0}!\n\nWelcome to our WISE learning environment. Our research over the past many years demonstrates that students have significant learning gains when using WISE projects.\n\nFor more information on how to get started, please visit our Getting Started page.\n{1}\n\nThank you for joining WISE,\nWISE Team presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername.description=Body of the Welcome email message sent to new WISE teachers that use Google ID to register. From 97e49d1ab0b12912887671df8b2b7202d522b303 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 12:33:30 -0700 Subject: [PATCH 13/20] Should keep trying to set verification code until unique constraint is satisfied --- .../authentication/impl/TeacherUserDetails.java | 2 +- .../teacher/TeacherRegistrationAPIController.java | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java b/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java index 52b53ff702..cd7babf46a 100644 --- a/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java +++ b/src/main/java/org/wise/portal/domain/authentication/impl/TeacherUserDetails.java @@ -175,7 +175,7 @@ public class TeacherUserDetails extends PersistentUserDetails implements Mutable @Setter private boolean verified = false; - @Column(name = TeacherUserDetails.COLUMN_NAME_VERIFICATION_CODE) + @Column(name = TeacherUserDetails.COLUMN_NAME_VERIFICATION_CODE, unique = true) @Getter @Setter private String verificationCode; diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index 99ac519811..b446307675 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -10,6 +10,7 @@ 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; @@ -175,7 +176,7 @@ private TeacherUserDetails createTeacherUserDetails(Map teacherF setPassword(teacherFields, tud); tud.setEmailValid(true); tud.setVerified(isSocialAccount); - tud.setVerificationCode(UUID.randomUUID().toString()); + setVerificationCode(tud); return tud; } @@ -192,6 +193,18 @@ private void setPassword(Map 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(); From 29095d9e73ec0d4e0ab9ad72a6fd95f4c6eb0a3a Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 17 Jul 2026 17:57:49 -0700 Subject: [PATCH 14/20] Minor bug --- .../controllers/teacher/TeacherRegistrationAPIController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index b446307675..f38344d031 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -76,7 +76,7 @@ private boolean isSendEmailEnabled() { } private boolean isSocialAccount(Map teacherFields) { - return isSet(teacherFields.get("googleUserId")) || isSet(tud.getMicrosoftUserId()); + return isSet(teacherFields.get("googleUserId")) || isSet(teacherFields.get("microsoftUserId")); } private void sendWelcomeTeacherEmail(String email, String displayName, String username, From d2b80422f6f38b290378af2b91e8dd5b0c225805 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 20 Jul 2026 21:48:40 -0700 Subject: [PATCH 15/20] Redirect to login with an error flag if verification code doesn't match any users in database --- .../TeacherRegistrationAPIController.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index f38344d031..7a95fa42fa 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -216,15 +216,24 @@ private void setRandomPassword(TeacherUserDetails tud) { @GetMapping("/verify") @Secured({ "ROLE_ANONYMOUS" }) - public void openVerificationLink(@RequestParam String code, HttpServletResponse response, - HttpServletRequest request) throws IOException { - boolean verified = this.verifyTeacherAccount(code, request); + public void verifyTeacherAndRedirect(@RequestParam String code, HttpServletResponse response, + HttpServletRequest request) throws IOException { User user = userService.retrieveTeacherByVerificationCode(code); - response.sendRedirect("/login?verified=" + verified + "&username=" + user.getUserDetails().getUsername()); + if (user == null) { + response.sendRedirect("/login?verified=error"); + } else { + boolean verified = this.verifyTeacherAccount(user, request); + String redirectLink = getRedirectLink(user, verified); + response.sendRedirect(redirectLink); + } + } + + private String getRedirectLink(User user, boolean verified) { + return String.format("/login?verified=%s&username=%s", + verified, user.getUserDetails().getUsername()); } - private boolean verifyTeacherAccount(String verificationCode, HttpServletRequest request) { - User user = userService.retrieveTeacherByVerificationCode(verificationCode); + private boolean verifyTeacherAccount(User user, HttpServletRequest request) { if (this.isTeacher(user)) { TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); if (!tud.isVerified()) { From 896a3bcca995364184b5c8abeccfa3fdb9b5e3d4 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 20 Jul 2026 23:19:47 -0700 Subject: [PATCH 16/20] Move email specific code to MailService --- .../dao/user/impl/HibernateUserDao.java | 8 +- .../TeacherRegistrationAPIController.java | 124 +++++------------- .../wise/portal/service/mail/IMailFacade.java | 10 ++ .../wise/portal/service/mail/MailService.java | 80 +++++++++++ 4 files changed, 129 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java b/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java index e6d090b429..8a8f64f0de 100644 --- a/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java +++ b/src/main/java/org/wise/portal/dao/user/impl/HibernateUserDao.java @@ -151,10 +151,6 @@ public User retrieveTeacherByUsername(String username) { return retrieveTeacherByFieldValue("username", username); } - public User retrieveTeacherByVerificationCode(String verificationCode) { - return retrieveTeacherByFieldValue("verificationCode", verificationCode); - } - public List retrieveTeachersByDisplayName(String displayName) { return retrieveTeachersByFieldValue("displayname", displayName); } @@ -179,6 +175,10 @@ public List retrieveTeachersByEmail(String emailAddress) { return retrieveTeachersByFieldValue("emailAddress", emailAddress); } + public User retrieveTeacherByVerificationCode(String verificationCode) { + return retrieveTeacherByFieldValue("verificationCode", verificationCode); + } + private User retrieveTeacherByFieldValue(String field, String value) { List resultList = retrieveTeachersByFieldValue(field, value); return resultList.isEmpty() ? null : resultList.get(0); diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index 7a95fa42fa..beef5d7092 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.UUID; -import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -32,14 +31,6 @@ @RestController @RequestMapping("/api/teacher/register") public class TeacherRegistrationAPIController extends TeacherAPIController { - - private final String emailCodePrefix = - "presentation.web.controllers.teacher.registerTeacherController."; - private final String verifyBodyCode = this.emailCodePrefix + "verifyTeacherEmailBody"; - private final String verifySubjectCode = this.emailCodePrefix + "verifyTeacherEmailSubject"; - private final String welcomeBodyCode = this.emailCodePrefix + "welcomeTeacherEmailBody"; - private final String welcomeSocialAccountBodyCode = this.emailCodePrefix + "welcomeTeacherEmailBodyNoUsername"; - private final String welcomeSubjectCode = this.emailCodePrefix + "welcomeTeacherEmailSubject"; @PostMapping() @Secured({ "ROLE_ANONYMOUS" }) @@ -59,77 +50,24 @@ ResponseEntity> createTeacherAccount( TeacherUserDetails tud = createTeacherUserDetails(teacherFields, isSocialAccount, locale); User createdUser = this.userService.createUser(tud); String username = createdUser.getUserDetails().getUsername(); - if (isSendEmailEnabled()) { - if (isSocialAccount) { - sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), username, - true, locale, request); - } else { - sendVerifyTeacherEmail(tud.getEmailAddress(), tud.getVerificationCode(), 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 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 boolean isSocialAccount(Map teacherFields) { return isSet(teacherFields.get("googleUserId")) || isSet(teacherFields.get("microsoftUserId")); } - 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 sendVerifyTeacherEmail(String email, String verificationCode, - Locale locale, HttpServletRequest request) { - String subject = getEmailMessage(this.verifySubjectCode, this.verifySubjectCode, null, locale); - String verificationUrl = getVerificationUrl(verificationCode, request); - Object[] args = new Object[] { verificationUrl }; - String body = getEmailMessage(this.verifyBodyCode, this.verifyBodyCode, args, locale); - this.sendEmail(email, subject, body); - } - - private String getVerificationUrl(String verificationCode, HttpServletRequest request) { - return String.format("%s/api/teacher/register/verify?code=%s", - ControllerUtil.getPortalUrlString(request), verificationCode); - } - - 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 void validateTeacherFields(Map teacherFields) throws RecaptchaVerificationException, InvalidNameException, InvalidPasswordException { validateReCaptcha(teacherFields.get("token")); @@ -219,12 +157,24 @@ private void setRandomPassword(TeacherUserDetails tud) { public void verifyTeacherAndRedirect(@RequestParam String code, HttpServletResponse response, HttpServletRequest request) throws IOException { User user = userService.retrieveTeacherByVerificationCode(code); + String link; if (user == null) { - response.sendRedirect("/login?verified=error"); + link = "/login?verified=error"; + } else if (!isTeacher(user)) { + link = getRedirectLink(user, false); } else { - boolean verified = this.verifyTeacherAccount(user, request); - String redirectLink = getRedirectLink(user, verified); - response.sendRedirect(redirectLink); + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + boolean verified = verifyTeacherAccount(user, tud, request); + sendWelcomeEmailIfNecessary(tud, verified, request); + link = getRedirectLink(user, verified); + } + response.sendRedirect(link); + } + + private void sendWelcomeEmailIfNecessary(TeacherUserDetails tud, boolean verified, HttpServletRequest request) { + if (verified) { + this.mailService.sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(), + false, request.getLocale(), request); } } @@ -233,18 +183,14 @@ private String getRedirectLink(User user, boolean verified) { verified, user.getUserDetails().getUsername()); } - private boolean verifyTeacherAccount(User user, HttpServletRequest request) { - if (this.isTeacher(user)) { - TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); - if (!tud.isVerified()) { - tud.setVerified(true); - userService.updateUser(user); - sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(), - false, request.getLocale(), request); - return true; - } + private boolean verifyTeacherAccount(User user, TeacherUserDetails tud, HttpServletRequest request) { + if (!tud.isVerified()) { + tud.setVerified(true); + userService.updateUser(user); + return true; + } else { + return false; } - return false; } @PostMapping("send-verify-email") @@ -252,13 +198,13 @@ private boolean verifyTeacherAccount(User user, HttpServletRequest request) { ResponseEntity> sendVerificationEmail(@RequestParam String username, HttpServletRequest request) { User user = userService.retrieveTeacherByUsername(username); - if (this.isTeacher(user)) { + if (isTeacher(user)) { TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); if (tud.isVerified()) { return ResponseEntityGenerator.createError("Teacher already verified"); //TODO: error message codes point where? } else { - sendVerifyTeacherEmail(tud.getEmailAddress(), tud.getVerificationCode(), - request.getLocale(), request); + this.mailService.sendVerifyTeacherEmail(tud.getEmailAddress(), tud.getVerificationCode(), + request.getLocale(), request); return createRegisterSuccessResponse(username); } } else { diff --git a/src/main/java/org/wise/portal/service/mail/IMailFacade.java b/src/main/java/org/wise/portal/service/mail/IMailFacade.java index 02af361d9b..8036ddd6ef 100644 --- a/src/main/java/org/wise/portal/service/mail/IMailFacade.java +++ b/src/main/java/org/wise/portal/service/mail/IMailFacade.java @@ -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 @@ -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); } diff --git a/src/main/java/org/wise/portal/service/mail/MailService.java b/src/main/java/org/wise/portal/service/mail/MailService.java index 9a9c764cfb..b6bda64f05 100644 --- a/src/main/java/org/wise/portal/service/mail/MailService.java +++ b/src/main/java/org/wise/portal/service/mail/MailService.java @@ -23,13 +23,20 @@ */ package org.wise.portal.service.mail; +import java.util.Locale; + import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.core.env.Environment; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +import org.wise.portal.domain.authentication.impl.TeacherUserDetails; +import org.wise.portal.presentation.web.controllers.ControllerUtil; /** * Compose and send email using the JavaMail Framework @@ -38,9 +45,23 @@ @Service public class MailService implements IMailFacade { + private final String emailCodePrefix = + "presentation.web.controllers.teacher.registerTeacherController."; + private final String verifyBodyCode = this.emailCodePrefix + "verifyTeacherEmailBody"; + private final String verifySubjectCode = this.emailCodePrefix + "verifyTeacherEmailSubject"; + private final String welcomeBodyCode = this.emailCodePrefix + "welcomeTeacherEmailBody"; + private final String welcomeSocialAccountBodyCode = this.emailCodePrefix + "welcomeTeacherEmailBodyNoUsername"; + private final String welcomeSubjectCode = this.emailCodePrefix + "welcomeTeacherEmailSubject"; + + @Autowired + protected Environment appProperties; + @Autowired private JavaMailSender javaMailSender; + @Autowired + private MessageSource messageSource; + public void postMail(String[] recipients, String subject, String message, String from) throws MessagingException { postMail(recipients, subject, message, from, null); @@ -65,4 +86,63 @@ public void postMail(String[] recipients, String subject, String message, } } } + + public void sendWelcomeTeacherEmail(String email, String displayName, String username, + boolean socialAccount, Locale locale, HttpServletRequest request) { + if (isSendEmailEnabled()) { + String subject = getEmailMessage(this.welcomeSubjectCode, this.welcomeSubjectCode, null, locale); + String body = getWelcomeTeacherBody(displayName, username, socialAccount, locale, request); + this.sendEmail(email, subject, body); + } + } + + public void sendVerifyTeacherEmail(String email, String verificationCode, + Locale locale, HttpServletRequest request) { + if (isSendEmailEnabled()) { + String subject = getEmailMessage(this.verifySubjectCode, this.verifySubjectCode, null, locale); + String verificationUrl = getVerificationUrl(verificationCode, request); + Object[] args = new Object[] { verificationUrl }; + String body = getEmailMessage(this.verifyBodyCode, this.verifyBodyCode, args, locale); + this.sendEmail(email, subject, body); + } + } + + private boolean isSendEmailEnabled() { + String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); + return Boolean.valueOf(sendEmailEnabledStr); + } + + 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 String getVerificationUrl(String verificationCode, HttpServletRequest request) { + return String.format("%s/api/teacher/register/verify?code=%s", + ControllerUtil.getPortalUrlString(request), verificationCode); + } + + private void sendEmail(String email, String subject, String body) { + String fromEmail = appProperties.getProperty("portalemailaddress"); + String[] recipients = { email }; + try { + this.postMail(recipients, subject, body, fromEmail); + } catch (MessagingException e) { + e.printStackTrace(); + } + } } From dad7c6e4a0a0bf52785a870bc566171c3603e229 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 21 Jul 2026 10:48:42 -0700 Subject: [PATCH 17/20] Added tests (some untested) --- .../teacher/TeacherAPIController.java | 2 +- .../TeacherRegistrationAPIController.java | 4 +- .../dao/user/impl/HibernateUserDaoTest.java | 35 +++++- .../web/controllers/APIControllerTest.java | 13 ++- .../teacher/TeacherAPIControllerTest.java | 40 +++++++ .../TeacherRegistrationAPIControllerTest.java | 102 ++++++++++++++++++ 6 files changed, 184 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 02fe6992ce..a9355122f5 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -330,7 +330,7 @@ HashMap editRunIsLockedAfterEndDate(Authentication authenticatio @GetMapping("/is-verified") @Secured({ "ROLE_ANONYMOUS" }) - public boolean isUserVerified(@RequestParam String username) { + 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 } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index beef5d7092..8b46edce3c 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -201,14 +201,14 @@ ResponseEntity> sendVerificationEmail(@RequestParam String u if (isTeacher(user)) { TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); if (tud.isVerified()) { - return ResponseEntityGenerator.createError("Teacher already verified"); //TODO: error message codes point where? + 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"); //TODO: error message codes point where? + return ResponseEntityGenerator.createError("Not a teacher"); } } } diff --git a/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java b/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java index 641be7c6f0..d66d40e16a 100644 --- a/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java +++ b/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java @@ -32,8 +32,10 @@ import org.springframework.security.core.userdetails.UserDetails; import org.wise.portal.domain.authentication.Gender; import org.wise.portal.domain.authentication.Schoollevel; +import org.wise.portal.domain.authentication.impl.TeacherUserDetails; import org.wise.portal.domain.user.User; import org.wise.portal.junit.AbstractTransactionalDbTests; +import org.wise.portal.service.authentication.DuplicateUsernameException; /** * @author Geoffrey Kwan @@ -48,17 +50,30 @@ public class HibernateUserDaoTest extends AbstractTransactionalDbTests { @BeforeEach public void setUp() throws Exception { - teacher1 = createTeacherUser("Mrs", "Puff", "MrsPuff", "Mrs. Puff", "boat", "Bikini Bottom", + teacher1 = createVerificationTeacherUser("Mrs", "Puff", "MrsPuff", "Mrs. Puff", "boat", "Bikini Bottom", "Water State", "Pacific Ocean", "mrspuff@bikinibottom.com", "Boating School", - Schoollevel.COLLEGE, "1234567890"); - teacher2 = createTeacherUser("Mr", "Krabs", "MrKrabs", "Mr. Krabs", "restaurant", + Schoollevel.COLLEGE, "1234567890", "abcd1234", true); + teacher2 = createVerificationTeacherUser("Mr", "Krabs", "MrKrabs", "Mr. Krabs", "restaurant", "Bikini Bottom", "Water State", "Pacific Ocean", "mrkrabs@bikinibottom.com", "Krusty Krab", - Schoollevel.HIGH_SCHOOL, "abcdefghij"); + Schoollevel.HIGH_SCHOOL, "abcdefghij", "1234abcd", false); student1 = createStudentUser("Spongebob", "Squarepants", "SpongebobS0101", "burger", 1, 1, Gender.MALE); student2 = createStudentUser("Patrick", "Star", "PatrickS0101", "rock", 1, 1, Gender.MALE); } + public User createVerificationTeacherUser(String firstName, String lastName, String username, + String displayName, String password, String city, String state, String country, String email, + String schoolName, Schoollevel schoolLevel, String googleUserId, String verificationCode, + boolean isVerified) + throws DuplicateUsernameException { + User teacher = createTeacherUser(firstName, lastName, username, displayName, password, city, + state, country, email, schoolName, schoolLevel, googleUserId); + TeacherUserDetails tud = (TeacherUserDetails) teacher.getUserDetails(); + tud.setVerificationCode(verificationCode); + tud.setVerified(isVerified); + return teacher; + } + @Test public void retrieveByUserDetails_ShouldReturnTheUser() { UserDetails userDetails = teacher1.getUserDetails(); @@ -244,6 +259,18 @@ public void retrieveTeachersByEmail_WithExistingEmail_ShouldSucceed() { assertEquals(1, users.size()); } + @Test + public void retrieveTeacherByVerificationCode_WithNonExistingCode_ShouldNotReturnAnyUser() { + User user = userDao.retrieveTeacherByVerificationCode("wxyz6789"); + assertNull(user); + } + + @Test + public void retrieveTeacherByVerificationCode_WithExistingCode_ShouldSucceed() { + User user = userDao.retrieveTeacherByVerificationCode("abcd1234"); + assertEquals(teacher1.getId(), user.getId()); + } + @Test public void retrieveTeachersBySchoolLevel_WithNotUsedSchoolLevel_ShouldNotReturnAnyUser() { List users = userDao diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java index 4a9c0cf5f0..0780308806 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java @@ -136,7 +136,7 @@ public void setUp() { createWorkgroups(); } - private void createStudents() { + protected void createStudents() { student1UserDetails = createStudentUserDetails(STUDENT_FIRSTNAME, STUDENT_LASTNAME, STUDENT_USERNAME, Gender.MALE, 5, STUDENT1_GOOGLE_ID, STUDENT1_ACCOUNT_ANSWER); student1 = createStudent(student1Id, student1UserDetails); @@ -147,13 +147,13 @@ private void createStudents() { studentAuth2 = createAuthentication(student2UserDetails); } - private void createTeachers() { + protected void createTeachers() { teacher1UserDetails = createTeacherUserDetails(TEACHER_FIRSTNAME, TEACHER_LASTNAME, - TEACHER_USERNAME, Schoollevel.COLLEGE, 5); + TEACHER_USERNAME, Schoollevel.COLLEGE, 5, true, "abcd1234"); teacher1 = createTeacher(teacher1Id, teacher1UserDetails); teacherAuth = createAuthentication(teacher1UserDetails); teacher2UserDetails = createTeacherUserDetails(TEACHER2_FIRSTNAME, TEACHER2_LASTNAME, - TEACHER2_USERNAME, Schoollevel.COLLEGE, 5); + TEACHER2_USERNAME, Schoollevel.COLLEGE, 5, false, "efgh5678"); teacher2 = createTeacher(teacher2Id, teacher2UserDetails); teacherAuth2 = createAuthentication(teacher2UserDetails); } @@ -262,13 +262,16 @@ protected User createTeacher(Long id, TeacherUserDetails teacherUserDetails) { } protected TeacherUserDetails createTeacherUserDetails(String firstName, String lastName, - String username, Schoollevel schoolLevel, Integer numberOfLogins) { + String username, Schoollevel schoolLevel, Integer numberOfLogins, boolean isVerified, + String verificationCode) { TeacherUserDetails teacherUserDetails = new TeacherUserDetails(); teacherUserDetails.setFirstname(firstName); teacherUserDetails.setLastname(lastName); teacherUserDetails.setUsername(username); teacherUserDetails.setSchoollevel(schoolLevel); teacherUserDetails.setNumberOfLogins(numberOfLogins); + teacherUserDetails.setVerified(isVerified); + teacherUserDetails.setVerificationCode(verificationCode); PersistentGrantedAuthority teacherAuthority = new PersistentGrantedAuthority(); teacherAuthority.setAuthority(UserDetailsService.TEACHER_ROLE); teacherUserDetails.setAuthorities(new GrantedAuthority[] { teacherAuthority }); diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java index 76902310e4..dfb5a8b0a7 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIControllerTest.java @@ -6,7 +6,9 @@ import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; @@ -433,4 +435,42 @@ private void expectGetRunMapToBeCalled() { expect(projectService.getLicensePath(isA(Project.class))) .andReturn("http://localhost:8080/curriculum/1/license.txt"); } + + @Test + public void isVerifiedTeacherOrNonTeacher_UserIsAStudent_ReturnTrue() { + expect(userService.retrieveTeacherByUsername(STUDENT_USERNAME)).andReturn(null); + replay(userService); + boolean isVerifiedTeacher = teacherAPIController.isVerifiedTeacherOrNonTeacher(STUDENT_USERNAME); + assertTrue(isVerifiedTeacher); + verify(userService); + } + + @Test + public void isVerifiedTeacherOrNonTeacher_UserDoesNotExist_ReturnTrue() { + expect(userService.retrieveTeacherByUsername("")).andReturn(null); + replay(userService); + boolean isVerifiedTeacher = teacherAPIController.isVerifiedTeacherOrNonTeacher(""); + assertTrue(isVerifiedTeacher); + verify(userService); + } + + @Test + public void isVerifiedTeacherOrNonTeacher_TeacherIsVerified_ReturnTrue() { + this.createTeachers(); + expect(userService.retrieveTeacherByUsername(TEACHER_USERNAME)).andReturn(teacher1); + replay(userService); + boolean isVerifiedTeacher = teacherAPIController.isVerifiedTeacherOrNonTeacher(TEACHER_USERNAME); + assertTrue(isVerifiedTeacher); + verify(userService); + } + + @Test + public void isVerifiedTeacherOrNonTeacher_TeacherIsUnverified_ReturnFalse() { + this.createTeachers(); + expect(userService.retrieveTeacherByUsername(TEACHER2_USERNAME)).andReturn(teacher2); + replay(userService); + boolean isVerifiedTeacher = teacherAPIController.isVerifiedTeacherOrNonTeacher(TEACHER2_USERNAME); + assertFalse(isVerifiedTeacher); + verify(userService); + } } diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java index 87bb71b758..9e94e56af3 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIControllerTest.java @@ -1,20 +1,25 @@ package org.wise.portal.presentation.web.controllers.teacher; import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.isA; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import javax.servlet.http.HttpServletResponse; + import org.easymock.EasyMockExtension; import org.easymock.Mock; import org.easymock.TestSubject; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.context.MessageSource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.wise.portal.domain.authentication.impl.TeacherUserDetails; @@ -22,6 +27,7 @@ import org.wise.portal.presentation.web.exception.InvalidNameException; import org.wise.portal.service.authentication.DuplicateUsernameException; import org.wise.portal.service.authentication.UserDetailsService; +import org.wise.portal.service.mail.MailService; import org.wise.portal.service.password.PasswordService; import org.wise.portal.service.password.impl.PasswordServiceImpl; import org.wise.portal.service.usertags.UserTagsService; @@ -32,6 +38,15 @@ public class TeacherRegistrationAPIControllerTest extends APIControllerTest { @TestSubject private TeacherRegistrationAPIController teacherRegistrationAPIController = new TeacherRegistrationAPIController(); + @Mock + private HttpServletResponse response; + + @Mock + private MailService mailService; + + @Mock + private MessageSource messageSource; + @Mock private UserDetailsService userDetailsService; @@ -66,11 +81,16 @@ public void createTeacherAccount_WithGoogleUserId_CreateUser() replay(request); expect(userService.createUser(isA(TeacherUserDetails.class))).andReturn(teacher1); replay(userService); + mailService.sendWelcomeTeacherEmail("", TEACHER_FIRSTNAME + " " + TEACHER_LASTNAME, TEACHER_USERNAME, + true, Locale.US, request); + expectLastCall(); + replay(mailService); ResponseEntity> response = teacherRegistrationAPIController .createTeacherAccount(teacherFields, request); assertEquals(TEACHER_USERNAME, response.getBody().get("username")); verify(request); verify(userService); + verify(mailService); } private HashMap createDefaultTeacherFields() { @@ -83,4 +103,86 @@ private HashMap createDefaultTeacherFields() { fields.put("gender", "MALE"); return fields; } + + @Test + public void verifyTeacherAndRedirect_TeacherUnverified_RedirectsWithVerifiedTrue() throws IOException { + createTeachers(); + expect(userService.retrieveTeacherByVerificationCode("efgh5678")).andReturn(teacher2); + userService.updateUser(teacher2); + expectLastCall(); + replay(userService); + TeacherUserDetails tud = (TeacherUserDetails) teacher2.getUserDetails(); + mailService.sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(), false, null, request); + expectLastCall(); + replay(mailService); + response.sendRedirect("/login?verified=true&username=" + tud.getUsername()); + expectLastCall(); + replay(response); + teacherRegistrationAPIController.verifyTeacherAndRedirect("efgh5678", response, request); + verify(userService); + verify(mailService); + verify(response); + } + + @Test + public void verifyTeacherAndRedirect_TeacherAlreadyVerified_RedirectsWithVerifiedFalse() throws IOException { + createTeachers(); + expect(userService.retrieveTeacherByVerificationCode("abcd1234")).andReturn(teacher1); + replay(userService); + response.sendRedirect("/login?verified=false&username=" + teacher1.getUserDetails().getUsername()); + expectLastCall(); + replay(response); + teacherRegistrationAPIController.verifyTeacherAndRedirect("abcd1234", response, request); + verify(userService); + verify(response); + } + + @Test + public void verifyTeacherAndRedirect_InvalidVerificationCode_RedirectsWithVerificationError() throws IOException { + expect(userService.retrieveTeacherByVerificationCode("")).andReturn(null); + replay(userService); + response.sendRedirect("/login?verified=error"); + expectLastCall(); + replay(response); + teacherRegistrationAPIController.verifyTeacherAndRedirect("", response, request); + verify(userService); + verify(response); + } + + @Test + public void sendVerificationEmail_UserIsUnverifiedTeacher_SendEmail() { + this.createTeachers(); + expect(userService.retrieveTeacherByUsername(TEACHER2_USERNAME)).andReturn(teacher2); + replay(userService); + mailService.sendVerifyTeacherEmail("", "efgh5678", null, request); + expectLastCall(); + replay(mailService); + ResponseEntity> response = + teacherRegistrationAPIController.sendVerificationEmail(TEACHER2_USERNAME, request); + assertEquals(TEACHER2_USERNAME, response.getBody().get("username")); + verify(userService); + verify(mailService); + + } + + @Test + public void sendVerificationEmail_UserIsVerifiedTeacher_ReturnError() { + this.createTeachers(); + expect(userService.retrieveTeacherByUsername(TEACHER_USERNAME)).andReturn(teacher1); + replay(userService); + ResponseEntity> response = + teacherRegistrationAPIController.sendVerificationEmail(TEACHER_USERNAME, request); + assertEquals("Teacher already verified", response.getBody().get("messageCode")); + verify(userService); + } + + @Test + public void sendVerificationEmail_UserIsNotTeacher_ReturnError() { + expect(userService.retrieveTeacherByUsername(STUDENT_USERNAME)).andReturn(null); + replay(userService); + ResponseEntity> response = + teacherRegistrationAPIController.sendVerificationEmail(STUDENT_USERNAME, request); + assertEquals("Not a teacher", response.getBody().get("messageCode")); + verify(userService); + } } From a499a64b66e80da103178a6629087159d6f29c75 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 21 Jul 2026 12:50:14 -0700 Subject: [PATCH 18/20] Separate sending welcome email from getting login link to make steps of method more clear --- .../teacher/TeacherRegistrationAPIController.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index 8b46edce3c..655e8bb6d1 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -157,6 +157,12 @@ private void setRandomPassword(TeacherUserDetails tud) { 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"; @@ -165,14 +171,14 @@ public void verifyTeacherAndRedirect(@RequestParam String code, HttpServletRespo } else { TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); boolean verified = verifyTeacherAccount(user, tud, request); - sendWelcomeEmailIfNecessary(tud, verified, request); link = getRedirectLink(user, verified); } - response.sendRedirect(link); + return link; } - private void sendWelcomeEmailIfNecessary(TeacherUserDetails tud, boolean verified, HttpServletRequest request) { - if (verified) { + private void sendWelcomeEmailIfNecessary(User user, String link, HttpServletRequest request) { + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); + if (link.contains("verified=true")) { this.mailService.sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(), false, request.getLocale(), request); } From 6fc8b46270c920d63d69443876c469546cc9e793 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 21 Jul 2026 13:05:53 -0700 Subject: [PATCH 19/20] Can't get user details for null user bug --- .../controllers/teacher/TeacherRegistrationAPIController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java index 655e8bb6d1..9537dc4e27 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherRegistrationAPIController.java @@ -177,8 +177,8 @@ private String verifyTeacherIfNecessaryAndGetLoginLink(HttpServletRequest reques } private void sendWelcomeEmailIfNecessary(User user, String link, HttpServletRequest request) { - TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); if (link.contains("verified=true")) { + TeacherUserDetails tud = (TeacherUserDetails) user.getUserDetails(); this.mailService.sendWelcomeTeacherEmail(tud.getEmailAddress(), tud.getDisplayname(), tud.getUsername(), false, request.getLocale(), request); } From 0845082ee6550a56236d0bea4190a2c373aed4a2 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Tue, 21 Jul 2026 13:10:13 -0700 Subject: [PATCH 20/20] Changed old tests that are failing --- .../dao/authentication/impl/HibernateUserDetailsDaoTest.java | 2 +- .../org/wise/portal/dao/user/impl/HibernateUserDaoTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDaoTest.java b/src/test/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDaoTest.java index 796f3c981e..48099afcf2 100644 --- a/src/test/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDaoTest.java +++ b/src/test/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDaoTest.java @@ -77,7 +77,7 @@ public void retrieveByName_WithExistingStudentName_ShouldReturnUser() { @Test public void retrieveAllTeacherUsernames_WhenThereAreTeachers_ShouldReturnTeacherUsernames() { List usernames = userDetailsDao.retrieveAllTeacherUsernames(); - assertEquals(4, usernames.size()); + assertEquals(2, usernames.size()); } @Test diff --git a/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java b/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java index d66d40e16a..92115478d9 100644 --- a/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java +++ b/src/test/java/org/wise/portal/dao/user/impl/HibernateUserDaoTest.java @@ -136,7 +136,7 @@ public void retrieveDisabledUsers_WhenThereIsOneDisabledUser_ShouldReturnOneUser @Test public void retrieveAllTeachers_ShouldReturnAllTeachers() { List users = userDao.retrieveAllTeachers(); - assertEquals(4, users.size()); + assertEquals(2, users.size()); } @Test @@ -281,7 +281,7 @@ public void retrieveTeachersBySchoolLevel_WithNotUsedSchoolLevel_ShouldNotReturn @Test public void retrieveTeachersBySchoolLevel_WithExistingSchoolLevel_ShouldSucceed() { List users = userDao.retrieveTeachersBySchoolLevel(Schoollevel.COLLEGE.toString()); - assertEquals(3, users.size()); + assertEquals(1, users.size()); } @Test