Updated email verification workflow to current code practices
This commit is contained in:
parent
51e6bb1f90
commit
fcf7189e44
13 changed files with 325 additions and 43 deletions
|
@ -10,6 +10,7 @@ sourceCompatibility = "11"
|
|||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -21,6 +22,8 @@ dependencies {
|
|||
implementation 'io.jsonwebtoken:jjwt:0.9.1'
|
||||
implementation 'org.springframework.security:spring-security-web'
|
||||
implementation 'org.postgresql:postgresql'
|
||||
compile "io.springfox:springfox-swagger2:2.9.2"
|
||||
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
|
||||
|
||||
implementation('org.springframework.boot:spring-boot-starter-web') {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.Service;
|
||||
package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.lang.reflect.Type;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter;
|
||||
import springfox.documentation.spring.web.json.Json;
|
||||
|
||||
/**
|
||||
* Spring configuration in order to register the GSON type adapter needed to avoid serializing twice
|
||||
* Springfox Swagger JSON output (see: https://stackoverflow.com/a/30220562)
|
||||
*/
|
||||
@Configuration
|
||||
public class GsonConfig {
|
||||
@Bean
|
||||
public GsonHttpMessageConverter gsonHttpMessageConverter() {
|
||||
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
|
||||
converter.setGson(gson());
|
||||
return converter;
|
||||
}
|
||||
|
||||
private Gson gson() {
|
||||
final GsonBuilder builder = new GsonBuilder();
|
||||
builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter());
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
/** GSON type adapter needed to avoid serializing twice Springfox Swagger JSON output */
|
||||
class SpringfoxJsonToGsonAdapter implements JsonSerializer<Json> {
|
||||
@Override
|
||||
public JsonElement serialize(Json json, Type type, JsonSerializationContext context) {
|
||||
return JsonParser.parseString(json.value());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
|
||||
|
||||
import static springfox.documentation.builders.PathSelectors.regex;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.service.ApiInfo;
|
||||
import springfox.documentation.service.ApiKey;
|
||||
import springfox.documentation.service.SecurityScheme;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
/**
|
||||
* This class configures the automated REST documentation tool Swagger for this project. The
|
||||
* documentation can be seen by going to http://localhost:8080/swaggeer-ui.html
|
||||
*/
|
||||
@Configuration
|
||||
@EnableSwagger2
|
||||
@ComponentScan("ch.usi.inf.sa4.sanmarinoes.smarthut")
|
||||
public class SpringFoxConfig {
|
||||
|
||||
/**
|
||||
* Main definition of Springfox / swagger configuration
|
||||
*
|
||||
* @return a Docket object containing the swagger configuration
|
||||
*/
|
||||
@Bean
|
||||
public Docket api() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
.select()
|
||||
.apis(RequestHandlerSelectors.any())
|
||||
.paths(paths()::test)
|
||||
.build()
|
||||
.apiInfo(apiInfo())
|
||||
.securitySchemes(securitySchemes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the documentation about the smarthut authentication system
|
||||
*
|
||||
* @return a list of springfox authentication configurations
|
||||
*/
|
||||
private static List<? extends SecurityScheme> securitySchemes() {
|
||||
return List.of(new ApiKey("Bearer", "Authorization", "header"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the paths the documentation must be generated for. Add a path here only when the
|
||||
* spec has been totally defined.
|
||||
*
|
||||
* @return A predicate that tests whether a path must be included or not
|
||||
*/
|
||||
private Predicate<String> paths() {
|
||||
return ((Predicate<String>) regex("/auth.*")::apply).or(regex("/register.*")::apply);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metadata about the smarthut project
|
||||
*
|
||||
* @return metadata about smarthut
|
||||
*/
|
||||
private ApiInfo apiInfo() {
|
||||
return new ApiInfoBuilder()
|
||||
.title("SmartHut.sm API")
|
||||
.description("Backend API for the SanMariones version of the SA4 SmartHut project")
|
||||
.termsOfServiceUrl("https://www.youtube.com/watch?v=9KxTcDsy9Gs")
|
||||
.license("WTFPL")
|
||||
.version("dev branch")
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
|
||||
|
||||
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RoomSaveRequest;
|
||||
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.*;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
@ -28,27 +29,38 @@ public class RoomController {
|
|||
return roomRepository.findById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Room save(@Valid @RequestBody Room r) {
|
||||
final Object principal =
|
||||
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
|
||||
if (!(principal instanceof UserDetails)) {
|
||||
throw new IllegalStateException("User is not logged in");
|
||||
}
|
||||
private Room save(final RoomSaveRequest r, final Principal principal, boolean setWhenNull) {
|
||||
Room newRoom = new Room();
|
||||
|
||||
final String username = ((UserDetails) principal).getUsername();
|
||||
final Long userId = userRepository.findByUsername(username).getId();
|
||||
final String img = r.getImage();
|
||||
final String icon = r.getIcon();
|
||||
|
||||
r.setUserId(userId);
|
||||
r.setUser(null);
|
||||
newRoom.setUserId(userId);
|
||||
newRoom.setUser(null);
|
||||
if (img != null) {
|
||||
newRoom.setImage(img.getBytes());
|
||||
} else if (setWhenNull) {
|
||||
newRoom.setImage(null);
|
||||
}
|
||||
if (icon != null) {
|
||||
newRoom.setIcon(icon.getBytes());
|
||||
} else if (setWhenNull) {
|
||||
newRoom.setIcon(null);
|
||||
}
|
||||
|
||||
return roomRepository.save(r);
|
||||
return roomRepository.save(newRoom);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Room create(@Valid @RequestBody RoomSaveRequest r, final Principal principal) {
|
||||
return this.save(r, principal, true);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public Room update(@Valid @RequestBody Room r) {
|
||||
return roomRepository.save(r);
|
||||
public Room update(@Valid @RequestBody RoomSaveRequest r, final Principal principal) {
|
||||
return this.save(r, principal, false);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
|
||||
|
||||
import ch.usi.inf.sa4.sanmarinoes.smarthut.Service.EmailSenderService;
|
||||
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse;
|
||||
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateEmailRegistrationException;
|
||||
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.models.ConfirmationToken;
|
||||
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.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;
|
||||
|
@ -20,7 +21,6 @@ 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.servlet.ModelAndView;
|
||||
|
||||
@RestController
|
||||
@EnableAutoConfiguration
|
||||
|
@ -35,33 +35,37 @@ public class UserAccountController {
|
|||
|
||||
@Autowired private BCryptPasswordEncoder encoder;
|
||||
|
||||
@GetMapping
|
||||
public ModelAndView displayRegistration(ModelAndView modelAndView, User user) {
|
||||
modelAndView.addObject("user", user);
|
||||
modelAndView.setViewName("register");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public OkResponse registerUser(@Valid @RequestBody User user)
|
||||
throws DuplicateEmailRegistrationException {
|
||||
System.out.println(user);
|
||||
User existingUser = userRepository.findByEmailIgnoreCase(user.getEmail());
|
||||
public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
|
||||
throws DuplicateRegistrationException {
|
||||
final User existingEmailUser =
|
||||
userRepository.findByEmailIgnoreCase(registrationData.getEmail());
|
||||
final User existingUsernameUser =
|
||||
userRepository.findByUsername(registrationData.getUsername());
|
||||
|
||||
// Check if an User with the same email already exists
|
||||
if (existingUser != null) {
|
||||
throw new DuplicateEmailRegistrationException();
|
||||
if (existingEmailUser != null || existingUsernameUser != null) {
|
||||
throw new DuplicateRegistrationException();
|
||||
} else {
|
||||
// encode user's password
|
||||
user.setPassword(encoder.encode(user.getPassword()));
|
||||
userRepository.save(user);
|
||||
final User toSave = new User();
|
||||
// disable the user (it will be enabled on email confiration)
|
||||
toSave.setEnabled(false);
|
||||
|
||||
ConfirmationToken confirmationToken = new ConfirmationToken(user);
|
||||
// encode user's password
|
||||
toSave.setPassword(encoder.encode(registrationData.getPassword()));
|
||||
|
||||
// set other fields
|
||||
toSave.setName(registrationData.getName());
|
||||
toSave.setUsername(registrationData.getUsername());
|
||||
toSave.setEmail(registrationData.getEmail());
|
||||
userRepository.save(toSave);
|
||||
|
||||
ConfirmationToken confirmationToken = new ConfirmationToken(toSave);
|
||||
|
||||
confirmationTokenRepository.save(confirmationToken);
|
||||
|
||||
SimpleMailMessage mailMessage = new SimpleMailMessage();
|
||||
mailMessage.setTo(user.getEmail());
|
||||
mailMessage.setTo(registrationData.getEmail());
|
||||
mailMessage.setSubject("Complete Registration!");
|
||||
mailMessage.setFrom("smarthut.sm@gmail.com");
|
||||
mailMessage.setText(
|
||||
|
@ -85,6 +89,7 @@ public class UserAccountController {
|
|||
final User user = userRepository.findByEmailIgnoreCase(token.getUser().getEmail());
|
||||
user.setEnabled(true);
|
||||
userRepository.save(user);
|
||||
// TODO: redirect to frontend
|
||||
return new OkResponse();
|
||||
} else {
|
||||
throw new EmailTokenNotFoundException();
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
|
||||
|
||||
import javax.persistence.Lob;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class RoomSaveRequest {
|
||||
/**
|
||||
* Icon and image are to be given as byte[]. In order to get an encoded string from it, the
|
||||
* Base64.getEncoder().encodeToString(byte[] content) should be used. For further information:
|
||||
* https://www.baeldung.com/java-base64-image-string
|
||||
* https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html
|
||||
*/
|
||||
@Lob private String icon;
|
||||
|
||||
@Lob private String image;
|
||||
|
||||
/** The user given name of this room (e.g. 'Master bedroom') */
|
||||
@NotNull private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public String getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(String image) {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
public class UserRegistrationRequest {
|
||||
|
||||
/** The full name of the user */
|
||||
@NotNull
|
||||
@NotEmpty(message = "Please provide a full name")
|
||||
private String name;
|
||||
|
||||
/** The full name of the user */
|
||||
@NotNull
|
||||
@NotEmpty(message = "Please provide a username")
|
||||
private String username;
|
||||
|
||||
/** 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;
|
||||
|
||||
/**
|
||||
* The user's email (validated according to criteria used in <code>>input type="email"<>
|
||||
* </code>, technically not RFC 5322 compliant
|
||||
*/
|
||||
@NotNull
|
||||
@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 getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@ import org.springframework.http.HttpStatus;
|
|||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
|
||||
public class DuplicateEmailRegistrationException extends Exception {
|
||||
public DuplicateEmailRegistrationException() {
|
||||
super("Email already belonging to another user");
|
||||
public class DuplicateRegistrationException extends Exception {
|
||||
public DuplicateRegistrationException() {
|
||||
super("Email or username already belonging to another user");
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
|
@ -21,12 +22,14 @@ public abstract class Device {
|
|||
/** Device identifier */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@Column(name = "id", updatable = false, nullable = false)
|
||||
@Column(name = "id", updatable = false, nullable = false, unique = true)
|
||||
@ApiModelProperty(hidden = true)
|
||||
private long id;
|
||||
|
||||
/** The room this device belongs in */
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "room_id", nullable = false, updatable = false, insertable = false)
|
||||
@ApiModelProperty(hidden = true)
|
||||
private Room room;
|
||||
|
||||
/**
|
||||
|
@ -46,13 +49,17 @@ public abstract class Device {
|
|||
* The name for the category of this particular device (e.g 'dimmer'). Not stored in the
|
||||
* database but set thanks to constructors
|
||||
*/
|
||||
@Transient private final String kind;
|
||||
@ApiModelProperty(hidden = true)
|
||||
@Transient
|
||||
private final String kind;
|
||||
|
||||
/**
|
||||
* The way this device behaves in the automation flow. Not stored in the database but set thanks
|
||||
* to constructors
|
||||
*/
|
||||
@Transient private final FlowType flowType;
|
||||
@ApiModelProperty(hidden = true)
|
||||
@Transient
|
||||
private final FlowType flowType;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import java.util.Set;
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
@ -11,6 +12,7 @@ public class Room {
|
|||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@Column(name = "id", updatable = false, nullable = false)
|
||||
@ApiModelProperty(hidden = true)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
|
@ -41,12 +43,14 @@ public class Room {
|
|||
private String name;
|
||||
|
||||
/** Collection of devices present in this room */
|
||||
@ApiModelProperty(hidden = true)
|
||||
@OneToMany(mappedBy = "room")
|
||||
private Set<Device> devices;
|
||||
|
||||
/** User that owns the house this room is in */
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id", nullable = false, updatable = false, insertable = false)
|
||||
@ApiModelProperty(hidden = true)
|
||||
private User user;
|
||||
|
||||
public Long getId() {
|
||||
|
|
|
@ -1,11 +1,35 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
|
||||
/** A switch input device TODO: define switch behaviour (push button vs on/off state) */
|
||||
@Entity
|
||||
public class Switch extends InputDevice {
|
||||
|
||||
/** The state of this switch */
|
||||
@Column(nullable = false, name = "switch_on")
|
||||
private boolean on;
|
||||
|
||||
public Switch() {
|
||||
super("switch");
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter method for this Switch
|
||||
*
|
||||
* @param state The state to be set
|
||||
*/
|
||||
void setState(boolean state) {
|
||||
on = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method for this Switch
|
||||
*
|
||||
* @return This Switch on state
|
||||
*/
|
||||
boolean getState() {
|
||||
return on;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import java.util.Set;
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.Email;
|
||||
|
@ -15,6 +16,7 @@ public class User {
|
|||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@Column(name = "id", updatable = false, nullable = false)
|
||||
@ApiModelProperty(hidden = true)
|
||||
private Long id;
|
||||
|
||||
/** The full name of the user */
|
||||
|
@ -43,7 +45,7 @@ public class User {
|
|||
* The user's email (validated according to criteria used in <code>>input type="email"<>
|
||||
* </code>, technically not RFC 5322 compliant
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
@Column(nullable = false, unique = true)
|
||||
@NotNull
|
||||
@NotEmpty(message = "Please provide an email")
|
||||
@Email(message = "Please provide a valid email address")
|
||||
|
@ -52,9 +54,11 @@ public class User {
|
|||
|
||||
/** All rooms in the user's house */
|
||||
@OneToMany(mappedBy = "user")
|
||||
@ApiModelProperty(hidden = true)
|
||||
private Set<Room> rooms;
|
||||
|
||||
@Column(nullable = false)
|
||||
@ApiModelProperty(hidden = true)
|
||||
private Boolean isEnabled = false;
|
||||
|
||||
public Long getId() {
|
||||
|
|
Loading…
Reference in a new issue