Merge branch 'swagger-feature' into 'dev'

Swagger feature

See merge request sa4-2020/the-sanmarinoes/backend!14
This commit is contained in:
Claudio Maggioni 2020-03-01 15:18:51 +01:00
commit 4995c88688
11 changed files with 263 additions and 34 deletions

View file

@ -10,6 +10,7 @@ sourceCompatibility = "11"
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
} }
dependencies { dependencies {
@ -20,6 +21,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.springframework.security:spring-security-web' implementation 'org.springframework.security:spring-security-web'
implementation 'org.postgresql:postgresql' 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') { 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,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());
}
}

View file

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

View file

@ -50,13 +50,17 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.disable() .disable()
// dont authenticate this particular request // dont authenticate this particular request
.authorizeRequests() .authorizeRequests()
.antMatchers("/auth/login") .antMatchers(
"/auth/login",
"/auth/register",
"/swagger-ui.html",
"/v2/api-docs",
"/webjars/**",
"/swagger-resources/**",
"/csrf")
.permitAll() .permitAll()
.antMatchers("/auth/register")
.permitAll()
.
// all other requests need to be authenticated // all other requests need to be authenticated
anyRequest() .anyRequest()
.authenticated() .authenticated()
.and() .and()
. .

View file

@ -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.JWTResponse;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import io.swagger.annotations.Authorization;
import java.security.Principal; import java.security.Principal;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
@ -21,18 +21,31 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/auth") @RequestMapping("/auth")
public class AuthenticationController { 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(); 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") @PostMapping("/login")
public JWTResponse login(@RequestBody JWTRequest authenticationRequest) throws Exception { public JWTResponse login(@RequestBody JWTRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
@ -49,15 +62,18 @@ public class AuthenticationController {
return user; return user;
} }
@Authorization(value = "Bearer")
@PatchMapping("/update") @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()); final User oldUser = userRepository.findByUsername(principal.getName());
if (u.getName() != null) oldUser.setName(u.getName()); if (userData.getName() != null) oldUser.setName(userData.getName());
if (u.getEmail() != null) { if (userData.getEmail() != null) {
oldUser.setEmail(u.getEmail()); oldUser.setEmail(userData.getEmail());
// TODO: handle email verification // 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); return userRepository.save(oldUser);
} }

View file

@ -1,11 +1,12 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; 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 ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.security.Principal;
import java.util.*; import java.util.*;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.*; import org.springframework.boot.autoconfigure.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -28,27 +29,38 @@ public class RoomController {
return roomRepository.findById(id); return roomRepository.findById(id);
} }
@PostMapping private Room save(final RoomSaveRequest r, final Principal principal, boolean setWhenNull) {
public Room save(@Valid @RequestBody Room r) { Room newRoom = new Room();
final Object principal =
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!(principal instanceof UserDetails)) {
throw new IllegalStateException("User is not logged in");
}
final String username = ((UserDetails) principal).getUsername(); final String username = ((UserDetails) principal).getUsername();
final Long userId = userRepository.findByUsername(username).getId(); final Long userId = userRepository.findByUsername(username).getId();
final String img = r.getImage();
final String icon = r.getIcon();
r.setUserId(userId); newRoom.setUserId(userId);
r.setUser(null); 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 @PutMapping
public Room update(@Valid @RequestBody Room r) { public Room update(@Valid @RequestBody RoomSaveRequest r, final Principal principal) {
return roomRepository.save(r); return this.save(r, principal, false);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

View file

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

View file

@ -1,6 +1,7 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import io.swagger.annotations.ApiModelProperty;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -21,12 +22,14 @@ public abstract class Device {
/** Device identifier */ /** Device identifier */
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @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; private long id;
/** The room this device belongs in */ /** The room this device belongs in */
@ManyToOne @ManyToOne
@JoinColumn(name = "room_id", nullable = false, updatable = false, insertable = false) @JoinColumn(name = "room_id", nullable = false, updatable = false, insertable = false)
@ApiModelProperty(hidden = true)
private Room room; 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 * The name for the category of this particular device (e.g 'dimmer'). Not stored in the
* database but set thanks to constructors * 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 * The way this device behaves in the automation flow. Not stored in the database but set thanks
* to constructors * to constructors
*/ */
@Transient private final FlowType flowType; @ApiModelProperty(hidden = true)
@Transient
private final FlowType flowType;
public long getId() { public long getId() {
return id; return id;

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import io.swagger.annotations.ApiModelProperty;
import java.util.Set; import java.util.Set;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -11,6 +12,7 @@ public class Room {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false) @Column(name = "id", updatable = false, nullable = false)
@ApiModelProperty(hidden = true)
private Long id; private Long id;
/** /**
@ -41,12 +43,14 @@ public class Room {
private String name; private String name;
/** Collection of devices present in this room */ /** Collection of devices present in this room */
@ApiModelProperty(hidden = true)
@OneToMany(mappedBy = "room") @OneToMany(mappedBy = "room")
private Set<Device> devices; private Set<Device> devices;
/** User that owns the house this room is in */ /** User that owns the house this room is in */
@ManyToOne @ManyToOne
@JoinColumn(name = "user_id", nullable = false, updatable = false, insertable = false) @JoinColumn(name = "user_id", nullable = false, updatable = false, insertable = false)
@ApiModelProperty(hidden = true)
private User user; private User user;
public Long getId() { public Long getId() {

View file

@ -1,11 +1,35 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
/** A switch input device TODO: define switch behaviour (push button vs on/off state) */ /** A switch input device TODO: define switch behaviour (push button vs on/off state) */
@Entity @Entity
public class Switch extends InputDevice { public class Switch extends InputDevice {
/** The state of this switch */
@Column(nullable = false, name = "switch_on")
private boolean on;
public Switch() { public Switch() {
super("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;
}
} }

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import io.swagger.annotations.ApiModelProperty;
import java.util.Set; import java.util.Set;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.*; import javax.validation.constraints.*;
@ -11,6 +12,7 @@ public class User {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false) @Column(name = "id", updatable = false, nullable = false)
@ApiModelProperty(hidden = true)
private Long id; private Long id;
/** The full name of the user */ /** The full name of the user */
@ -40,7 +42,7 @@ public class User {
* The user's email (validated according to criteria used in <code>&gt;input type="email"&lt;> * The user's email (validated according to criteria used in <code>&gt;input type="email"&lt;>
* </code>, technically not RFC 5322 compliant * </code>, technically not RFC 5322 compliant
*/ */
@Column(nullable = false) @Column(nullable = false, unique = true)
@NotNull @NotNull
@NotEmpty(message = "Please provide an email") @NotEmpty(message = "Please provide an email")
@Email(message = "Please provide a valid email address") @Email(message = "Please provide a valid email address")
@ -49,6 +51,7 @@ public class User {
/** All rooms in the user's house */ /** All rooms in the user's house */
@OneToMany(mappedBy = "user") @OneToMany(mappedBy = "user")
@ApiModelProperty(hidden = true)
private Set<Room> rooms; private Set<Room> rooms;
public Long getId() { public Long getId() {