diff --git a/build.gradle b/build.gradle
index 086613b..d7a34ff 100644
--- a/build.gradle
+++ b/build.gradle
@@ -25,6 +25,7 @@ dependencies {
implementation 'org.postgresql:postgresql'
compile "io.springfox:springfox-swagger2:2.9.2"
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
+ compile "org.springframework.boot:spring-boot-configuration-processor"
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json'
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java
new file mode 100644
index 0000000..424d2d3
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java
@@ -0,0 +1,90 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import javax.validation.constraints.NotNull;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * Class to interface with `email.*` properties in application.properties. This properties are used
+ * for generating the email to send on password reset or registration
+ *
+ * @see ch.usi.inf.sa4.sanmarinoes.smarthut.controller.UserAccountController
+ */
+@Component
+@EnableConfigurationProperties
+@ConfigurationProperties(prefix = "email")
+public class EmailConfigurationService {
+
+ /** The email subject for a registration email */
+ @NotNull private String registrationSubject;
+
+ /** The text in the email body preceding the confirmation URL for a registration email */
+ @NotNull private String registration;
+
+ /**
+ * The URL to follow for registration email confirmation. Has to end with the start of a query
+ * parameter
+ */
+ @NotNull private String registrationPath;
+
+ /** The email subject for a reset password email */
+ @NotNull private String resetPasswordSubject;
+
+ /** The text in the email body preceding the confirmation URL for a reset password email */
+ @NotNull private String resetPassword;
+
+ /**
+ * The URL to follow for password reset email confirmation. Has to end with the start of a query
+ * parameter
+ */
+ @NotNull private String resetPasswordPath;
+
+ public String getRegistrationSubject() {
+ return registrationSubject;
+ }
+
+ public void setRegistrationSubject(String registrationSubject) {
+ this.registrationSubject = registrationSubject;
+ }
+
+ public String getRegistration() {
+ return registration;
+ }
+
+ public void setRegistration(String registration) {
+ this.registration = registration;
+ }
+
+ public String getRegistrationPath() {
+ return registrationPath;
+ }
+
+ public void setRegistrationPath(String registrationPath) {
+ this.registrationPath = registrationPath;
+ }
+
+ public String getResetPasswordSubject() {
+ return resetPasswordSubject;
+ }
+
+ public void setResetPasswordSubject(String resetPasswordSubject) {
+ this.resetPasswordSubject = resetPasswordSubject;
+ }
+
+ public String getResetPassword() {
+ return resetPassword;
+ }
+
+ public void setResetPassword(String resetPassword) {
+ this.resetPassword = resetPassword;
+ }
+
+ public String getResetPasswordPath() {
+ return resetPasswordPath;
+ }
+
+ public void setResetPasswordPath(String resetPasswordPath) {
+ this.resetPasswordPath = resetPasswordPath;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
index e38d0df..253998d 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
@@ -52,10 +52,11 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.authorizeRequests()
.antMatchers(
"/auth/login",
- "/auth/register",
"/swagger-ui.html",
"/register",
"/register/confirm-account",
+ "/register/init-reset-password",
+ "/register/reset-password",
"/v2/api-docs",
"/webjars/**",
"/swagger-resources/**",
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
index e238f16..ebf354f 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
@@ -1,9 +1,13 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.EmailConfigurationService;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.InitPasswordResetRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.PasswordResetRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.EmailTokenNotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationToken;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
@@ -11,30 +15,66 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.EmailSenderService;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-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.springframework.web.bind.annotation.*;
+/** Unauthenticated set of endpoints to handle registration and password reset */
@RestController
@EnableAutoConfiguration
@RequestMapping("/register")
public class UserAccountController {
- @Autowired private UserRepository userRepository;
+ private final UserRepository userRepository;
- @Autowired private ConfirmationTokenRepository confirmationTokenRepository;
+ private final ConfirmationTokenRepository confirmationTokenRepository;
- @Autowired private EmailSenderService emailSenderService;
+ private final EmailSenderService emailSenderService;
- @Autowired private BCryptPasswordEncoder encoder;
+ private final BCryptPasswordEncoder encoder;
+ private final EmailConfigurationService emailConfig;
+
+ public UserAccountController(
+ UserRepository userRepository,
+ ConfirmationTokenRepository confirmationTokenRepository,
+ EmailSenderService emailSenderService,
+ BCryptPasswordEncoder encoder,
+ EmailConfigurationService emailConfig) {
+ this.userRepository = userRepository;
+ this.confirmationTokenRepository = confirmationTokenRepository;
+ this.emailSenderService = emailSenderService;
+ this.encoder = encoder;
+ this.emailConfig = emailConfig;
+ }
+
+ private void sendEmail(String email, ConfirmationToken token, boolean isRegistration) {
+ SimpleMailMessage mailMessage = new SimpleMailMessage();
+ mailMessage.setTo(email);
+ mailMessage.setSubject(
+ isRegistration
+ ? emailConfig.getRegistrationSubject()
+ : emailConfig.getResetPasswordSubject());
+ mailMessage.setFrom("smarthut.sm@gmail.com");
+ mailMessage.setText(
+ (isRegistration ? emailConfig.getRegistration() : emailConfig.getResetPassword())
+ + " "
+ + (isRegistration
+ ? emailConfig.getRegistrationPath()
+ : emailConfig.getResetPasswordPath())
+ + token.getConfirmationToken());
+
+ emailSenderService.sendEmail(mailMessage);
+ }
+
+ /**
+ * Unauthenticated endpoint to call to send a password reset email
+ *
+ * @param registrationData registration data of the new user
+ * @return success
+ * @throws DuplicateRegistrationException if a user exists with same email or username
+ */
@PostMapping
public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
throws DuplicateRegistrationException {
@@ -60,35 +100,101 @@ public class UserAccountController {
toSave.setEmail(registrationData.getEmail());
userRepository.save(toSave);
- ConfirmationToken confirmationToken = new ConfirmationToken(toSave);
+ ConfirmationToken token;
+ do {
+ token = new ConfirmationToken(toSave);
+ } while (confirmationTokenRepository.findByConfirmationToken(
+ token.getConfirmationToken())
+ != null);
- confirmationTokenRepository.save(confirmationToken);
+ confirmationTokenRepository.save(token);
- SimpleMailMessage mailMessage = new SimpleMailMessage();
- mailMessage.setTo(registrationData.getEmail());
- mailMessage.setSubject("Complete Registration!");
- mailMessage.setFrom("smarthut.sm@gmail.com");
- mailMessage.setText(
- "To confirm your account, please click here : "
- + "http://localhost:8080/register/confirm-account?token="
- + confirmationToken.getConfirmationToken());
-
- emailSenderService.sendEmail(mailMessage);
+ sendEmail(toSave.getEmail(), token, true);
return new OkResponse();
}
}
+ /**
+ * Unauthenticated endpoint to call to send a password reset email
+ *
+ * @param resetRequest a JSON object containing the email of the user to reset
+ * @return success
+ * @throws UserNotFoundException if given email does not belong to any user
+ */
+ @PostMapping("/init-reset-password")
+ public OkResponse initResetPassword(@Valid @RequestBody InitPasswordResetRequest resetRequest)
+ throws UserNotFoundException {
+ final User toReset = userRepository.findByEmailIgnoreCase(resetRequest.getEmail());
+
+ // Check if an User with the same email already exists
+ if (toReset == null) {
+ throw new UserNotFoundException();
+ }
+
+ ConfirmationToken token;
+ do {
+ token = new ConfirmationToken(toReset);
+ token.setResetPassword(true);
+ } while (confirmationTokenRepository.findByConfirmationToken(token.getConfirmationToken())
+ != null);
+
+ // Delete existing email password reset tokens
+ confirmationTokenRepository.deleteByUserAndResetPassword(toReset, true);
+
+ // Save new token
+ confirmationTokenRepository.save(token);
+
+ sendEmail(toReset.getEmail(), token, false);
+
+ return new OkResponse();
+ }
+
+ /**
+ * Unauthenticated endpoint to call with token sent by email to reset password
+ *
+ * @param resetRequest the token given via email and the new password
+ * @return success
+ * @throws EmailTokenNotFoundException if given token is not a valid token for password reset
+ */
+ @PutMapping("/reset-password")
+ public OkResponse resetPassword(@Valid @RequestBody PasswordResetRequest resetRequest)
+ throws EmailTokenNotFoundException {
+ final ConfirmationToken token =
+ confirmationTokenRepository.findByConfirmationToken(
+ resetRequest.getConfirmationToken());
+
+ if (token == null || !token.getResetPassword()) {
+ throw new EmailTokenNotFoundException();
+ }
+
+ final User user = token.getUser();
+ user.setPassword(encoder.encode(resetRequest.getPassword()));
+ userRepository.save(user);
+
+ // Delete token to prevent further password changes
+ confirmationTokenRepository.delete(token);
+
+ return new OkResponse();
+ }
+
+ /**
+ * Unauthenticated endpoint to call with token sent by email to enable user
+ *
+ * @param confirmationToken the token given via email
+ * @return success
+ * @throws EmailTokenNotFoundException if given token is not a valid token for email
+ * confirmation
+ */
@GetMapping(value = "/confirm-account")
public OkResponse confirmUserAccount(@RequestParam("token") @NotNull String confirmationToken)
throws EmailTokenNotFoundException {
final ConfirmationToken token =
confirmationTokenRepository.findByConfirmationToken(confirmationToken);
- if (token != null) {
- final User user = userRepository.findByEmailIgnoreCase(token.getUser().getEmail());
- user.setEnabled(true);
- userRepository.save(user);
+ if (token != null && !token.getResetPassword()) {
+ token.getUser().setEnabled(true);
+ userRepository.save(token.getUser());
// TODO: redirect to frontend
return new OkResponse();
} else {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
new file mode 100644
index 0000000..d82c4f0
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
@@ -0,0 +1,25 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+/** DTO for password reset request */
+public class InitPasswordResetRequest {
+ /**
+ * The user's email (validated according to criteria used in >input type="email"<>
+ *
, technically not RFC 5322 compliant
+ */
+ @NotEmpty(message = "Please provide an email")
+ @Email(message = "Please provide a valid email address")
+ @Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
+ private String email;
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
new file mode 100644
index 0000000..bf5bccf
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
@@ -0,0 +1,34 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.*;
+
+/** DTO for password reset request */
+public class PasswordResetRequest {
+
+ @NotNull private String confirmationToken;
+
+ /** A properly salted way to store the password */
+ @NotNull
+ @NotEmpty(message = "Please provide a password")
+ @Size(
+ min = 6,
+ max = 255,
+ message = "Your password should be at least 6 characters long and up to 255 chars long")
+ private String password;
+
+ public String getConfirmationToken() {
+ return confirmationToken;
+ }
+
+ public void setConfirmationToken(String confirmationToken) {
+ this.confirmationToken = confirmationToken;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
index f6c86a0..d324724 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
@@ -21,7 +21,7 @@ public class ConfirmationToken {
@Column(name = "id", updatable = false, nullable = false)
private Long id;
- @Column(name = "confirmation_token")
+ @Column(name = "confirmation_token", unique = true)
private String confirmationToken;
@Temporal(TemporalType.TIMESTAMP)
@@ -31,10 +31,14 @@ public class ConfirmationToken {
@JoinColumn(nullable = false, name = "user_id")
private User user;
+ @Column(nullable = false)
+ private Boolean resetPassword;
+
public ConfirmationToken(User user) {
this.user = user;
createdDate = new Date();
confirmationToken = UUID.randomUUID().toString();
+ resetPassword = false;
}
/** Constructor for hibernate reflective stuff things whatever */
@@ -71,4 +75,12 @@ public class ConfirmationToken {
public void setUser(User user) {
this.user = user;
}
+
+ public Boolean getResetPassword() {
+ return resetPassword;
+ }
+
+ public void setResetPassword(Boolean resetPassword) {
+ this.resetPassword = resetPassword;
+ }
}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
index 4bc18ce..9bf3791 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
@@ -1,7 +1,11 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+import javax.transaction.Transactional;
import org.springframework.data.repository.CrudRepository;
public interface ConfirmationTokenRepository extends CrudRepository {
ConfirmationToken findByConfirmationToken(String confirmationToken);
+
+ @Transactional
+ void deleteByUserAndResetPassword(User user, boolean resetPassword);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 9bfe2a7..14c91cd 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -22,4 +22,12 @@ spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
-spring.mail.properties.mail.smtp.writetimeout=5000
\ No newline at end of file
+spring.mail.properties.mail.smtp.writetimeout=5000
+
+email.registrationsubject=Complete your SmartHut.sm registration
+email.registration=To confirm your registration, please click here:
+email.registraionpath=http://localhost:8080/register/confirm-account?token=
+
+email.resetpasswordsubject=SmartHut.sm password reset
+email.resetpassword=To reset your password, please click here:
+email.resetpasswordpath=http://localhost:3000/password-reset?token=
\ No newline at end of file