diff --git a/.gitignore b/.gitignore index 69ca520..1b1a367 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/** + **/.DS_Store # Compiled class file diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 03128f2..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -smarthut \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index fdc392f..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 7c6191b..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index ea7614f..728859b 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ sourceCompatibility = "11" repositories { mavenCentral() + jcenter() } dependencies { @@ -20,6 +21,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' diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java new file mode 100644 index 0000000..3a2ab37 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java @@ -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 { + @Override + public JsonElement serialize(Json json, Type type, JsonSerializationContext context) { + return JsonParser.parseString(json.value()); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java new file mode 100644 index 0000000..743d119 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java @@ -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 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 paths() { + return regex("/auth.*")::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(); + } +} 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 a8bcb1d..bb548f0 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 @@ -50,13 +50,17 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .disable() // dont authenticate this particular request .authorizeRequests() - .antMatchers("/auth/login") + .antMatchers( + "/auth/login", + "/auth/register", + "/swagger-ui.html", + "/v2/api-docs", + "/webjars/**", + "/swagger-resources/**", + "/csrf") .permitAll() - .antMatchers("/auth/register") - .permitAll() - . // all other requests need to be authenticated - anyRequest() + .anyRequest() .authenticated() .and() . diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java index ed82692..e4c575e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java @@ -5,9 +5,9 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import io.swagger.annotations.Authorization; import java.security.Principal; import javax.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; @@ -21,18 +21,31 @@ import org.springframework.web.bind.annotation.*; @RequestMapping("/auth") public class AuthenticationController { - @Autowired private AuthenticationManager authenticationManager; + private final AuthenticationManager authenticationManager; - @Autowired private UserRepository userRepository; + private final UserRepository userRepository; - @Autowired private JWTTokenUtil jwtTokenUtil; + private final JWTTokenUtil jwtTokenUtil; - @Autowired private JWTUserDetailsService userDetailsService; + private final JWTUserDetailsService userDetailsService; - @Autowired private UserRepository users; + private final UserRepository users; private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + public AuthenticationController( + AuthenticationManager authenticationManager, + UserRepository userRepository, + JWTTokenUtil jwtTokenUtil, + JWTUserDetailsService userDetailsService, + UserRepository users) { + this.authenticationManager = authenticationManager; + this.userRepository = userRepository; + this.jwtTokenUtil = jwtTokenUtil; + this.userDetailsService = userDetailsService; + this.users = users; + } + @PostMapping("/login") public JWTResponse login(@RequestBody JWTRequest authenticationRequest) throws Exception { authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); @@ -49,15 +62,18 @@ public class AuthenticationController { return user; } + @Authorization(value = "Bearer") @PatchMapping("/update") - public User update(@Valid @RequestBody final UserUpdateRequest u, final Principal principal) { + public User update( + @Valid @RequestBody final UserUpdateRequest userData, final Principal principal) { final User oldUser = userRepository.findByUsername(principal.getName()); - if (u.getName() != null) oldUser.setName(u.getName()); - if (u.getEmail() != null) { - oldUser.setEmail(u.getEmail()); + if (userData.getName() != null) oldUser.setName(userData.getName()); + if (userData.getEmail() != null) { + oldUser.setEmail(userData.getEmail()); // TODO: handle email verification } - if (u.getPassword() != null) oldUser.setPassword(encoder.encode(u.getPassword())); + if (userData.getPassword() != null) + oldUser.setPassword(encoder.encode(userData.getPassword())); return userRepository.save(oldUser); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java index b73c55a..c1a4d87 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java @@ -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.*; @@ -30,27 +31,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}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java new file mode 100644 index 0000000..f813b1c --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java @@ -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; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java index 834104d..871b67c 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java @@ -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; diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java index c0fae3f..4a82ab2 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java @@ -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 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() { diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java index fbf1eed..6f0eb99 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java @@ -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; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java index 7909fe8..4b9cb51 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java @@ -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.*; @@ -11,6 +12,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 */ @@ -40,7 +42,7 @@ public class User { * The user's email (validated according to criteria used in >input type="email"<> * , 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") @@ -49,6 +51,7 @@ public class User { /** All rooms in the user's house */ @OneToMany(mappedBy = "user") + @ApiModelProperty(hidden = true) private Set rooms; public Long getId() {