Merge branch 'dev' into '21-test-endpoints'

# Conflicts:
#   build.gradle
This commit is contained in:
Claudio Maggioni 2020-03-09 23:14:46 +01:00
commit ca63f0d7df
10 changed files with 316 additions and 35 deletions

View file

@ -26,6 +26,7 @@ dependencies {
implementation 'com.google.code.gson:gson' implementation 'com.google.code.gson:gson'
compile 'io.springfox:springfox-swagger2:2.9.2' compile 'io.springfox:springfox-swagger2:2.9.2'
compile 'io.springfox:springfox-swagger-ui:2.9.2' compile 'io.springfox:springfox-swagger-ui:2.9.2'
compile "org.springframework.boot:spring-boot-configuration-processor"
implementation('org.springframework.boot:spring-boot-starter-web') { implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json' exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json'

View file

@ -0,0 +1,92 @@
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;
import org.springframework.validation.annotation.Validated;
/**
* 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
@Validated
@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;
}
}

View file

@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo; import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey; import springfox.documentation.service.ApiKey;
@ -67,10 +68,7 @@ public class SpringFoxConfig {
* @return A predicate that tests whether a path must be included or not * @return A predicate that tests whether a path must be included or not
*/ */
private Predicate<String> paths() { private Predicate<String> paths() {
return regexPredicate("/auth.*") return PathSelectors.any()::apply;
.or(regexPredicate("/room.*"))
.or(regexPredicate("/register.*"))
.or(regexPredicate("/"));
} }
/** /**

View file

@ -52,10 +52,11 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.authorizeRequests() .authorizeRequests()
.antMatchers( .antMatchers(
"/auth/login", "/auth/login",
"/auth/register",
"/swagger-ui.html", "/swagger-ui.html",
"/register", "/register",
"/register/confirm-account", "/register/confirm-account",
"/register/init-reset-password",
"/register/reset-password",
"/v2/api-docs", "/v2/api-docs",
"/webjars/**", "/webjars/**",
"/swagger-resources/**", "/swagger-resources/**",

View file

@ -1,9 +1,13 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; 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.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.dto.UserRegistrationRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException; 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.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.ConfirmationToken;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; 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 ch.usi.inf.sa4.sanmarinoes.smarthut.service.EmailSenderService;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
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;
/** Unauthenticated set of endpoints to handle registration and password reset */
@RestController @RestController
@EnableAutoConfiguration @EnableAutoConfiguration
@RequestMapping("/register") @RequestMapping("/register")
public class UserAccountController { 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 @PostMapping
public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData) public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
throws DuplicateRegistrationException { throws DuplicateRegistrationException {
@ -60,35 +100,101 @@ public class UserAccountController {
toSave.setEmail(registrationData.getEmail()); toSave.setEmail(registrationData.getEmail());
userRepository.save(toSave); 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(); sendEmail(toSave.getEmail(), token, true);
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);
return new OkResponse(); 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") @GetMapping(value = "/confirm-account")
public OkResponse confirmUserAccount(@RequestParam("token") @NotNull String confirmationToken) public OkResponse confirmUserAccount(@RequestParam("token") @NotNull String confirmationToken)
throws EmailTokenNotFoundException { throws EmailTokenNotFoundException {
final ConfirmationToken token = final ConfirmationToken token =
confirmationTokenRepository.findByConfirmationToken(confirmationToken); confirmationTokenRepository.findByConfirmationToken(confirmationToken);
if (token != null) { if (token != null && !token.getResetPassword()) {
final User user = userRepository.findByEmailIgnoreCase(token.getUser().getEmail()); token.getUser().setEnabled(true);
user.setEnabled(true); userRepository.save(token.getUser());
userRepository.save(user);
// TODO: redirect to frontend // TODO: redirect to frontend
return new OkResponse(); return new OkResponse();
} else { } else {

View file

@ -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 <code>&gt;input type="email"&lt;>
* </code>, 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;
}
}

View file

@ -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;
}
}

View file

@ -21,7 +21,7 @@ public class ConfirmationToken {
@Column(name = "id", updatable = false, nullable = false) @Column(name = "id", updatable = false, nullable = false)
private Long id; private Long id;
@Column(name = "confirmation_token") @Column(name = "confirmation_token", unique = true)
private String confirmationToken; private String confirmationToken;
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@ -31,10 +31,14 @@ public class ConfirmationToken {
@JoinColumn(nullable = false, name = "user_id") @JoinColumn(nullable = false, name = "user_id")
private User user; private User user;
@Column(nullable = false)
private Boolean resetPassword;
public ConfirmationToken(User user) { public ConfirmationToken(User user) {
this.user = user; this.user = user;
createdDate = new Date(); createdDate = new Date();
confirmationToken = UUID.randomUUID().toString(); confirmationToken = UUID.randomUUID().toString();
resetPassword = false;
} }
/** Constructor for hibernate reflective stuff things whatever */ /** Constructor for hibernate reflective stuff things whatever */
@ -71,4 +75,12 @@ public class ConfirmationToken {
public void setUser(User user) { public void setUser(User user) {
this.user = user; this.user = user;
} }
public Boolean getResetPassword() {
return resetPassword;
}
public void setResetPassword(Boolean resetPassword) {
this.resetPassword = resetPassword;
}
} }

View file

@ -1,7 +1,11 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import javax.transaction.Transactional;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
public interface ConfirmationTokenRepository extends CrudRepository<ConfirmationToken, String> { public interface ConfirmationTokenRepository extends CrudRepository<ConfirmationToken, String> {
ConfirmationToken findByConfirmationToken(String confirmationToken); ConfirmationToken findByConfirmationToken(String confirmationToken);
@Transactional
void deleteByUserAndResetPassword(User user, boolean resetPassword);
} }

View file

@ -23,3 +23,11 @@ spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.connectiontimeout=5000 spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000 spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000 spring.mail.properties.mail.smtp.writetimeout=5000
email.registrationSubject=Complete your SmartHut.sm registration
email.registration=To confirm your registration, please click here:
email.registrationPath=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=