Merge branch 'dev' into 'web-socket-feature'

This commit is contained in:
Claudio Maggioni 2020-03-14 20:09:58 +01:00
commit 873ef54e13
73 changed files with 1783 additions and 620 deletions

View file

@ -1,17 +1,61 @@
code_quality: #Trying to set up the CI, probably won't work
image: docker:stable image: gradle:jdk13
variables:
DOCKER_DRIVER: overlay2 stages:
allow_failure: true - build
- test
- code_quality
- deploy
#Sets up the docker
smarthut_deploy:
stage: deploy
image: docker:latest
services: services:
- docker:stable-dind - docker:dind
variables:
DOCKER_DRIVER: overlay
before_script:
- docker version
- docker info
- docker login -u smarthutsm -p $CI_DOCKER_PASS #GiovanniRoberto
script: script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - "docker build -t smarthutsm/smarthut:${CI_COMMIT_BRANCH} --pull ."
- docker run - "docker push smarthutsm/smarthut:${CI_COMMIT_BRANCH}"
--env SOURCE_CODE="$PWD" after_script:
--volume "$PWD":/code - docker logout
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
#base checks for the code
build:
stage: build
script:
- gradle clean
- gradle assemble
artifacts: artifacts:
paths:
- build/libs/*.jar
expire_in: 1 week
#Runs the various tests and creates a report on the test coverage
test:
stage: test
script:
- gradle test
artifacts:
paths:
- build/test-results/test/TEST-*.xml
reports: reports:
codequality: gl-code-quality-report.json junit: build/test-results/test/TEST-*.xml
#Runs a quality check on the code and creates a report on the codes
code_quality:
stage: code_quality
script:
- gradle cpdCheck
artifacts:
paths:
- build/reports/cpd/cpdCheck.xml
#create a report on the quality of the code
expose_as: 'Code Quality Report'
allow_failure: true

8
Dockerfile Normal file
View file

@ -0,0 +1,8 @@
FROM openjdk:13-jdk-alpine
ENV DB_URL ""
ENV DB_USER ""
ENV DB_PASS ""
ARG JAR_FILE=build/libs/smarthut*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

View file

@ -1,37 +1,49 @@
plugins { plugins {
id 'org.springframework.boot' version '2.2.4.RELEASE' id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "de.aaschmid.cpd" version "3.1"
id 'java' id 'java'
} }
group = 'ch.usi.inf.sa4.sanmarinoes.' group = 'ch.usi.inf.sa4.sanmarinoes'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
sourceCompatibility = "11" sourceCompatibility = '11'
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
} }
dependencies { dependencies {
compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
compile "org.springframework.boot:spring-boot-starter-websocket"
implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter'
implementation 'com.sun.mail:javax.mail:1.6.2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-mail'
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" implementation 'com.google.code.gson:gson'
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' compile 'io.springfox:springfox-swagger2: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'
} }
implementation 'com.google.code.gson:gson'
testImplementation('org.springframework.boot:spring-boot-starter-test') { testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
} }
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.h2database:h2:1.4.200'
// Fixes https://stackoverflow.com/a/60455550
testImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.11'
} }
test { test {

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -1,6 +1,5 @@
#Thu Feb 20 21:04:58 CET 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip

View file

@ -0,0 +1,51 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
import java.io.IOException;
import java.util.List;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
/**
* Add CORS headers to each response in order to please the frontend requests, coming from a
* different host for now (thanks to the difference in ports). Andrea would you please stop
* complaining now
*/
@Component
public class CORSFilter implements Filter {
static void setCORSHeaders(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "HEAD, PUT, POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader(
"Access-Control-Allow-Headers",
String.join(
",",
List.of(
"Access-Control-Allow-Headers",
"Origin",
"Accept",
"X-Requested-With",
"Authorization",
"Content-Type",
"Access-Control-Request-Method",
"Access-Control-Request-Headers")));
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) res;
setCORSHeaders(response);
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}

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

@ -23,6 +23,7 @@ public class GsonConfig {
private Gson gson() { private Gson gson() {
final GsonBuilder builder = new GsonBuilder(); final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter());
builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy());
return builder.create(); return builder.create();
} }
} }
@ -34,3 +35,16 @@ class SpringfoxJsonToGsonAdapter implements JsonSerializer<Json> {
return JsonParser.parseString(json.value()); return JsonParser.parseString(json.value());
} }
} }
/** GSON exclusion strategy to exclude attributes with @GsonExclude */
class AnnotationExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getAnnotation(GsonExclude.class) != null;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}

View file

@ -0,0 +1,10 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface GsonExclude {}

View file

@ -16,6 +16,10 @@ public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
HttpServletResponse response, HttpServletResponse response,
AuthenticationException authException) AuthenticationException authException)
throws IOException { throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); if (!"OPTIONS".equals(request.getMethod())) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
} else {
CORSFilter.setCORSHeaders(response);
}
} }
} }

View file

@ -1,6 +1,5 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.config; package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
import static springfox.documentation.builders.PathSelectors.regex;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -8,11 +7,11 @@ 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.*;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType; import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2; import springfox.documentation.swagger2.annotations.EnableSwagger2;
@ -38,7 +37,8 @@ public class SpringFoxConfig {
.paths(paths()::test) .paths(paths()::test)
.build() .build()
.apiInfo(apiInfo()) .apiInfo(apiInfo())
.securitySchemes(securitySchemes()); .securitySchemes(securitySchemes())
.securityContexts(List.of(securityContext()));
} }
/** /**
@ -50,14 +50,32 @@ public class SpringFoxConfig {
return List.of(new ApiKey("Bearer", "Authorization", "header")); return List.of(new ApiKey("Bearer", "Authorization", "header"));
} }
/** private SecurityContext securityContext() {
* Return a Java functional API predicate for regex matches return SecurityContext.builder()
* .securityReferences(defaultAuth())
* @param regex the regex to match on .forPaths(authenticatedPaths()::test)
* @return a Java functional API predicate .build();
*/ }
private Predicate<String> regexPredicate(final String regex) {
return regex(regex)::apply; private List<SecurityReference> defaultAuth() {
final AuthorizationScope authorizationScope =
new AuthorizationScope("global", "accessEverything");
return List.of(
new SecurityReference("Bearer", new AuthorizationScope[] {authorizationScope}));
}
private Predicate<String> authenticatedPaths() {
return ((Predicate<String>) PathSelectors.regex("/auth/update")::apply)
.or(PathSelectors.regex("/room.*")::apply)
.or(PathSelectors.regex("/device.*")::apply)
.or(PathSelectors.regex("/buttonDimmer.*")::apply)
.or(PathSelectors.regex("/dimmableLight.*")::apply)
.or(PathSelectors.regex("/knobDimmer.*")::apply)
.or(PathSelectors.regex("/regularLight.*")::apply)
.or(PathSelectors.regex("/sensor.*")::apply)
.or(PathSelectors.regex("/smartPlug.*")::apply)
.or(PathSelectors.regex("/switch.*")::apply)
.or(PathSelectors.regex("/motionSensor.*")::apply);
} }
/** /**
@ -67,9 +85,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.*"));
} }
/** /**

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

@ -4,6 +4,8 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest; 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.error.UnauthorizedException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import io.swagger.annotations.Authorization; import io.swagger.annotations.Authorization;
import java.security.Principal; import java.security.Principal;
@ -17,7 +19,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@CrossOrigin
@RequestMapping("/auth") @RequestMapping("/auth")
public class AuthenticationController { public class AuthenticationController {
@ -35,8 +36,7 @@ public class AuthenticationController {
AuthenticationManager authenticationManager, AuthenticationManager authenticationManager,
UserRepository userRepository, UserRepository userRepository,
JWTTokenUtil jwtTokenUtil, JWTTokenUtil jwtTokenUtil,
JWTUserDetailsService userDetailsService, JWTUserDetailsService userDetailsService) {
UserRepository users) {
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
this.userRepository = userRepository; this.userRepository = userRepository;
this.jwtTokenUtil = jwtTokenUtil; this.jwtTokenUtil = jwtTokenUtil;
@ -44,10 +44,30 @@ public class AuthenticationController {
} }
@PostMapping("/login") @PostMapping("/login")
public JWTResponse login(@RequestBody JWTRequest authenticationRequest) throws Exception { public JWTResponse login(@Valid @RequestBody JWTRequest authenticationRequest)
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); throws UnauthorizedException, UserNotFoundException {
final UserDetails userDetails = final UserDetails userDetails;
userDetailsService.loadUserByUsername(authenticationRequest.getUsername()); if (authenticationRequest.getUsernameOrEmail().contains("@")) {
// usernameOrEmail contains an email, so fetch the corresponding username
final User user =
userRepository.findByEmailIgnoreCase(
authenticationRequest.getUsernameOrEmail());
if (user == null) {
throw new UserNotFoundException();
}
authenticate(user.getUsername(), authenticationRequest.getPassword());
userDetails = userDetailsService.loadUserByUsername(user.getUsername());
} else {
// usernameOrEmail contains a username, authenticate with that then
authenticate(
authenticationRequest.getUsernameOrEmail(),
authenticationRequest.getPassword());
userDetails =
userDetailsService.loadUserByUsername(
authenticationRequest.getUsernameOrEmail());
}
final String token = jwtTokenUtil.generateToken(userDetails); final String token = jwtTokenUtil.generateToken(userDetails);
return new JWTResponse(token); return new JWTResponse(token);
} }
@ -67,16 +87,14 @@ public class AuthenticationController {
return userRepository.save(oldUser); return userRepository.save(oldUser);
} }
private void authenticate(String username, String password) throws Exception { private void authenticate(String username, String password) throws UnauthorizedException {
try { try {
authenticationManager.authenticate( authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)); new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) { } catch (DisabledException e) {
e.printStackTrace(); throw new UnauthorizedException(true);
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) { } catch (BadCredentialsException e) {
e.printStackTrace(); throw new UnauthorizedException(false);
throw new Exception("INVALID_CREDENTIALS", e);
} }
} }
} }

View file

@ -2,11 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerDimRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmer; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmerRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Set;
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.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -15,37 +16,77 @@ import org.springframework.web.bind.annotation.*;
@RestController @RestController
@EnableAutoConfiguration @EnableAutoConfiguration
@RequestMapping("/buttonDimmer") @RequestMapping("/buttonDimmer")
public class ButtonDimmerController { public class ButtonDimmerController
@Autowired private ButtonDimmerRepository buttonDimmerService; extends InputDeviceConnectionController<ButtonDimmer, DimmableLight> {
private ButtonDimmerRepository buttonDimmerRepository;
private DimmableLightRepository dimmableLightRepository;
@Autowired
protected ButtonDimmerController(
ButtonDimmerRepository inputRepository, DimmableLightRepository outputRepository) {
super(
inputRepository,
outputRepository,
DimmableLight.BUTTON_DIMMER_DIMMABLE_LIGHT_CONNECTOR);
this.buttonDimmerRepository = inputRepository;
this.dimmableLightRepository = outputRepository;
}
@GetMapping @GetMapping
public List<ButtonDimmer> findAll() { public List<ButtonDimmer> findAll() {
return toList(buttonDimmerService.findAll()); return toList(buttonDimmerRepository.findAll());
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<ButtonDimmer> findById(@PathVariable("id") long id) { public ButtonDimmer findById(@PathVariable("id") long id) throws NotFoundException {
return buttonDimmerService.findById(id); return buttonDimmerRepository.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping @PostMapping
public ButtonDimmer create(@Valid @RequestBody final ButtonDimmerSaveRequest bd) { public ButtonDimmer create(@Valid @RequestBody final GenericDeviceSaveReguest bd) {
ButtonDimmer newBD = new ButtonDimmer(); ButtonDimmer newBD = new ButtonDimmer();
newBD.setLights(bd.getLights());
newBD.setId(bd.getId());
newBD.setName(bd.getName()); newBD.setName(bd.getName());
newBD.setRoomId(bd.getRoomId()); newBD.setRoomId(bd.getRoomId());
return buttonDimmerService.save(newBD); return buttonDimmerRepository.save(newBD);
} }
@PutMapping @PutMapping("/dim")
public ButtonDimmer update(@Valid @RequestBody ButtonDimmerSaveRequest bd) { public Set<DimmableLight> dim(@Valid @RequestBody final ButtonDimmerDimRequest bd)
return this.create(bd); throws NotFoundException {
final ButtonDimmer buttonDimmer =
buttonDimmerRepository.findById(bd.getId()).orElseThrow(NotFoundException::new);
switch (bd.getDimType()) {
case UP:
buttonDimmer.increaseIntensity();
break;
case DOWN:
buttonDimmer.decreaseIntensity();
break;
}
dimmableLightRepository.saveAll(buttonDimmer.getOutputs());
return buttonDimmer.getOutputs();
}
@PostMapping("/{id}/lights")
public Set<? extends OutputDevice> addLight(
@PathVariable("id") long inputId, @RequestParam("lightId") Long lightId)
throws NotFoundException {
return addOutput(inputId, lightId);
}
@DeleteMapping("/{id}/lights")
public Set<? extends OutputDevice> removeLight(
@PathVariable("id") long inputId, @RequestParam("lightId") Long lightId)
throws NotFoundException {
return removeOutput(inputId, lightId);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id) { public void delete(@PathVariable("id") long id) {
buttonDimmerService.deleteById(id); buttonDimmerRepository.deleteById(id);
} }
} }

View file

@ -0,0 +1,44 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DeviceSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.BadDataException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RoomRepository;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@EnableAutoConfiguration
@RequestMapping("/device")
public class DeviceController {
@Autowired private DeviceRepository<Device> deviceRepository;
@Autowired private RoomRepository roomRepository;
@PutMapping
public Device update(@Valid @RequestBody DeviceSaveRequest deviceSaveRequest)
throws NotFoundException, BadDataException {
final Device d =
deviceRepository
.findById(deviceSaveRequest.getId())
.orElseThrow(NotFoundException::new);
// check if roomId is valid
roomRepository
.findById(deviceSaveRequest.getRoomId())
.orElseThrow(() -> new BadDataException("roomId is not a valid room id"));
d.setRoomId(deviceSaveRequest.getRoomId());
d.setName(deviceSaveRequest.getName());
deviceRepository.save(d);
return d;
}
}

View file

@ -3,10 +3,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableLightSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableLightSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLightRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLightRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
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.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -25,24 +25,28 @@ public class DimmableLightController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<DimmableLight> findById(@PathVariable("id") long id) { public DimmableLight findById(@PathVariable("id") long id) throws NotFoundException {
return dimmableLightService.findById(id); return dimmableLightService.findById(id).orElseThrow(NotFoundException::new);
}
private DimmableLight save(DimmableLight initial, DimmableLightSaveRequest dl) {
initial.setIntensity(dl.getIntensity());
initial.setName(dl.getName());
initial.setRoomId(dl.getRoomId());
return dimmableLightService.save(initial);
} }
@PostMapping @PostMapping
public DimmableLight create(@Valid @RequestBody DimmableLightSaveRequest dl) { public DimmableLight create(@Valid @RequestBody DimmableLightSaveRequest dl) {
DimmableLight newDL = new DimmableLight(); return save(new DimmableLight(), dl);
newDL.setIntensity(dl.getIntensity());
newDL.setId(dl.getId());
newDL.setName(dl.getName());
newDL.setRoomId(dl.getRoomId());
return dimmableLightService.save(newDL);
} }
@PutMapping @PutMapping
public DimmableLight update(@Valid @RequestBody DimmableLightSaveRequest dl) { public DimmableLight update(@Valid @RequestBody DimmableLightSaveRequest sp)
return this.create(dl); throws NotFoundException {
return save(
dimmableLightService.findById(sp.getId()).orElseThrow(NotFoundException::new), sp);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

View file

@ -0,0 +1,92 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.Set;
/**
* An abstract controller for an input device that has output connected to it. Aids to create the
* output add and output remove route
*
* @param <I> the type of device this controller is for
* @param <O> the output device attached to I
*/
public abstract class InputDeviceConnectionController<
I extends InputDevice, O extends OutputDevice> {
private class IOPair {
private final I input;
private final O output;
private IOPair(I input, O output) {
this.input = input;
this.output = output;
}
}
private DeviceRepository<I> inputRepository;
private DeviceRepository<O> outputReposiory;
private Connector<I, O> connector;
/**
* Contstructs the controller by requiring essential object for the controller implementation
*
* @param inputRepository the input device repository
* @param outputRepository the output device repository
* @param connector a appropriate Connector instance for the I and O tyoes.
*/
protected InputDeviceConnectionController(
DeviceRepository<I> inputRepository,
DeviceRepository<O> outputRepository,
Connector<I, O> connector) {
this.inputRepository = inputRepository;
this.outputReposiory = outputRepository;
this.connector = connector;
}
private IOPair checkConnectionIDs(Long inputId, Long outputId) throws NotFoundException {
final I input =
inputRepository
.findById(inputId)
.orElseThrow(() -> new NotFoundException("input device"));
final O output =
outputReposiory
.findById(outputId)
.orElseThrow(() -> new NotFoundException("output device"));
return new IOPair(input, output);
}
/**
* Implements the output device connection creation (add) route
*
* @param inputId input device id
* @param outputId output device id
* @return the list of output devices attached to the input device of id inputId
* @throws NotFoundException if inputId or outputId are not valid
*/
protected Set<? extends OutputDevice> addOutput(Long inputId, Long outputId)
throws NotFoundException {
final IOPair pair = checkConnectionIDs(inputId, outputId);
connector.connect(pair.input, pair.output, true);
outputReposiory.save(pair.output);
return pair.input.getOutputs();
}
/**
* Implements the output device connection destruction (remove) route
*
* @param inputId input device id
* @param outputId output device id
* @return the list of output devices attached to the input device of id inputId
* @throws NotFoundException if inputId or outputId are not valid
*/
protected Set<? extends OutputDevice> removeOutput(Long inputId, Long outputId)
throws NotFoundException {
final IOPair pair = checkConnectionIDs(inputId, outputId);
connector.connect(pair.input, pair.output, false);
outputReposiory.save(pair.output);
return pair.input.getOutputs();
}
}

View file

@ -2,11 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmer; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerDimRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmerRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Set;
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.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -15,38 +16,70 @@ import org.springframework.web.bind.annotation.*;
@RestController @RestController
@EnableAutoConfiguration @EnableAutoConfiguration
@RequestMapping("/knobDimmer") @RequestMapping("/knobDimmer")
public class KnobDimmerController { public class KnobDimmerController
extends InputDeviceConnectionController<KnobDimmer, DimmableLight> {
@Autowired private KnobDimmerRepository knobDimmerService; @Autowired private KnobDimmerRepository knobDimmerRepository;
@Autowired private DimmableLightRepository dimmableLightRepository;
@Autowired
protected KnobDimmerController(
KnobDimmerRepository inputRepository, DimmableLightRepository outputRepository) {
super(
inputRepository,
outputRepository,
DimmableLight.KNOB_DIMMER_DIMMABLE_LIGHT_CONNECTOR);
this.knobDimmerRepository = inputRepository;
this.dimmableLightRepository = outputRepository;
}
@GetMapping @GetMapping
public List<KnobDimmer> findAll() { public List<KnobDimmer> findAll() {
return toList(knobDimmerService.findAll()); return toList(knobDimmerRepository.findAll());
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<KnobDimmer> findById(@PathVariable("id") long id) { public KnobDimmer findById(@PathVariable("id") long id) throws NotFoundException {
return knobDimmerService.findById(id); return knobDimmerRepository.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping @PostMapping
public KnobDimmer create(@Valid @RequestBody KnobDimmerSaveRequest kd) { public KnobDimmer create(@Valid @RequestBody GenericDeviceSaveReguest kd) {
KnobDimmer newKD = new KnobDimmer(); KnobDimmer newKD = new KnobDimmer();
newKD.setLights(kd.getLights());
newKD.setId(kd.getId());
newKD.setName(kd.getName()); newKD.setName(kd.getName());
newKD.setRoomId(kd.getRoomId()); newKD.setRoomId(kd.getRoomId());
return knobDimmerService.save(newKD); return knobDimmerRepository.save(newKD);
} }
@PutMapping @PutMapping("/dimTo")
public KnobDimmer update(@Valid @RequestBody KnobDimmerSaveRequest kd) { public Set<DimmableLight> dimTo(@Valid @RequestBody final KnobDimmerDimRequest bd)
return this.create(kd); throws NotFoundException {
final KnobDimmer dimmer =
knobDimmerRepository.findById(bd.getId()).orElseThrow(NotFoundException::new);
dimmer.setLightIntensity(bd.getIntensity());
dimmableLightRepository.saveAll(dimmer.getOutputs());
return dimmer.getOutputs();
}
@PostMapping("/{id}/lights")
public Set<? extends OutputDevice> addLight(
@PathVariable("id") long inputId, @RequestParam("lightId") Long lightId)
throws NotFoundException {
return addOutput(inputId, lightId);
}
@DeleteMapping("/{id}/lights")
public Set<? extends OutputDevice> removeLight(
@PathVariable("id") long inputId, @RequestParam("lightId") Long lightId)
throws NotFoundException {
return removeOutput(inputId, lightId);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id) { public void delete(@PathVariable("id") long id) {
knobDimmerService.deleteById(id); knobDimmerRepository.deleteById(id);
} }
} }

View file

@ -2,11 +2,11 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.MotionSensorSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensor; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensor;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
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.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -25,26 +25,19 @@ public class MotionSensorController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<MotionSensor> findById(@PathVariable("id") long id) { public MotionSensor findById(@PathVariable("id") long id) throws NotFoundException {
return motionSensorService.findById(id); return motionSensorService.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping @PostMapping
public MotionSensor create(@Valid @RequestBody MotionSensorSaveRequest ms) { public MotionSensor create(@Valid @RequestBody GenericDeviceSaveReguest ms) {
MotionSensor newMS = new MotionSensor(); MotionSensor newMS = new MotionSensor();
newMS.setDetected(ms.isDetected());
newMS.setId(ms.getId());
newMS.setName(ms.getName()); newMS.setName(ms.getName());
newMS.setRoomId(ms.getRoomId()); newMS.setRoomId(ms.getRoomId());
return motionSensorService.save(newMS); return motionSensorService.save(newMS);
} }
@PutMapping
public MotionSensor update(@Valid @RequestBody MotionSensorSaveRequest ms) {
return this.create(ms);
}
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id) { public void delete(@PathVariable("id") long id) {
motionSensorService.deleteById(id); motionSensorService.deleteById(id);

View file

@ -3,10 +3,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RegularLightSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RegularLightSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLightRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLightRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
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.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -32,14 +32,11 @@ public class RegularLightController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<RegularLight> findById(@PathVariable("id") long id) { public RegularLight findById(@PathVariable("id") long id) throws NotFoundException {
return regularLightService.findById(id); return regularLightService.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping private RegularLight save(RegularLight newRL, RegularLightSaveRequest rl) {
public RegularLight create(@Valid @RequestBody RegularLightSaveRequest rl) {
RegularLight newRL = new RegularLight();
newRL.setId(rl.getId());
newRL.setName(rl.getName()); newRL.setName(rl.getName());
newRL.setRoomId(rl.getRoomId()); newRL.setRoomId(rl.getRoomId());
newRL.setOn(rl.isOn()); newRL.setOn(rl.isOn());
@ -47,9 +44,16 @@ public class RegularLightController {
return regularLightService.save(newRL); return regularLightService.save(newRL);
} }
@PostMapping
public RegularLight create(@Valid @RequestBody RegularLightSaveRequest rl) {
return save(new RegularLight(), rl);
}
@PutMapping @PutMapping
public RegularLight update(@Valid @RequestBody RegularLightSaveRequest rl) { public RegularLight update(@Valid @RequestBody RegularLightSaveRequest rl)
return this.create(rl); throws NotFoundException {
return save(
regularLightService.findById(rl.getId()).orElseThrow(NotFoundException::new), rl);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

View file

@ -3,13 +3,13 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RoomSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RoomSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.security.Principal; 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.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@ -29,26 +29,27 @@ public class RoomController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public @ResponseBody Optional<Room> findById(@PathVariable("id") long id) { public @ResponseBody Room findById(@PathVariable("id") long id) throws NotFoundException {
return roomRepository.findById(id); return roomRepository.findById(id).orElseThrow(NotFoundException::new);
} }
private Room save(final RoomSaveRequest r, final Principal principal, boolean setWhenNull) { private Room save(final RoomSaveRequest r, final Principal principal, boolean setWhenNull) {
Room newRoom = new Room(); Room newRoom = new Room();
final String username = ((UserDetails) principal).getUsername(); final String username = principal.getName();
final Long userId = userRepository.findByUsername(username).getId(); final Long userId = userRepository.findByUsername(username).getId();
final String img = r.getImage(); final String img = r.getImage();
final String icon = r.getIcon(); final Room.Icon icon = r.getIcon();
newRoom.setUserId(userId); newRoom.setUserId(userId);
newRoom.setName(r.getName());
if (img != null) { if (img != null) {
newRoom.setImage(img.getBytes()); newRoom.setImage(img);
} else if (setWhenNull) { } else if (setWhenNull) {
newRoom.setImage(null); newRoom.setImage(null);
} }
if (icon != null) { if (icon != null) {
newRoom.setIcon(icon.getBytes()); newRoom.setIcon(icon);
} else if (setWhenNull) { } else if (setWhenNull) {
newRoom.setIcon(null); newRoom.setIcon(null);
} }

View file

@ -3,6 +3,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
@ -24,27 +25,20 @@ public class SensorController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<Sensor> findById(@PathVariable("id") long id) { public Sensor findById(@PathVariable("id") long id) throws NotFoundException {
return sensorRepository.findById(id); return sensorRepository.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping @PostMapping
public Sensor create(@Valid @RequestBody SensorSaveRequest s) { public Sensor create(@Valid @RequestBody SensorSaveRequest s) {
Sensor newSensor = new Sensor(); Sensor newSensor = new Sensor();
newSensor.setSensor(s.getSensor()); newSensor.setSensor(s.getSensor());
newSensor.setValue(s.getValue());
newSensor.setId(s.getId());
newSensor.setName(s.getName()); newSensor.setName(s.getName());
newSensor.setRoomId(s.getRoomId()); newSensor.setRoomId(s.getRoomId());
return sensorRepository.save(newSensor); return sensorRepository.save(newSensor);
} }
@PutMapping
public Sensor update(@Valid @RequestBody SensorSaveRequest s) {
return this.create(s);
}
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id) { public void deleteById(@PathVariable("id") long id) {
sensorRepository.deleteById(id); sensorRepository.deleteById(id);

View file

@ -3,6 +3,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SmartPlugSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SmartPlugSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
@ -24,13 +25,11 @@ public class SmartPlugController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<SmartPlug> findById(@PathVariable("id") long id) { public SmartPlug findById(@PathVariable("id") long id) throws NotFoundException {
return smartPlugRepository.findById(id); return smartPlugRepository.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping private SmartPlug save(SmartPlug newSP, SmartPlugSaveRequest sp) {
public SmartPlug create(@Valid @RequestBody SmartPlugSaveRequest sp) {
SmartPlug newSP = new SmartPlug();
newSP.setOn(sp.isOn()); newSP.setOn(sp.isOn());
newSP.setId(sp.getId()); newSP.setId(sp.getId());
newSP.setName(sp.getName()); newSP.setName(sp.getName());
@ -39,9 +38,15 @@ public class SmartPlugController {
return smartPlugRepository.save(newSP); return smartPlugRepository.save(newSP);
} }
@PostMapping
public SmartPlug create(@Valid @RequestBody SmartPlugSaveRequest sp) {
return save(new SmartPlug(), sp);
}
@PutMapping @PutMapping
public SmartPlug update(@Valid @RequestBody SmartPlugSaveRequest sp) { public SmartPlug update(@Valid @RequestBody SmartPlugSaveRequest sp) throws NotFoundException {
return this.create(sp); return save(
smartPlugRepository.findById(sp.getId()).orElseThrow(NotFoundException::new), sp);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

View file

@ -2,7 +2,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchOperationRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
@ -14,9 +16,24 @@ import org.springframework.web.bind.annotation.*;
@RestController @RestController
@EnableAutoConfiguration @EnableAutoConfiguration
@RequestMapping("/switch") @RequestMapping("/switch")
public class SwitchController { public class SwitchController extends InputDeviceConnectionController<Switch, Switchable> {
@Autowired private SwitchRepository switchRepository; private SwitchRepository switchRepository;
private SwitchableRepository<Switchable> switchableRepository;
/**
* Contstructs the controller by requiring essential object for the controller implementation
*
* @param inputRepository the input device repository
* @param outputRepository the output device repository
*/
@Autowired
protected SwitchController(
SwitchRepository inputRepository, SwitchableRepository<Switchable> outputRepository) {
super(inputRepository, outputRepository, Switchable.SWITCH_SWITCHABLE_CONNECTOR);
this.switchRepository = inputRepository;
this.switchableRepository = outputRepository;
}
@GetMapping @GetMapping
public List<Switch> findAll() { public List<Switch> findAll() {
@ -24,24 +41,53 @@ public class SwitchController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Optional<Switch> findById(@PathVariable("id") long id) { public Switch findById(@PathVariable("id") long id) throws NotFoundException {
return switchRepository.findById(id); return switchRepository.findById(id).orElseThrow(NotFoundException::new);
} }
@PostMapping @PostMapping
public Switch create(@Valid @RequestBody SwitchSaveRequest s) { public Switch create(@Valid @RequestBody GenericDeviceSaveReguest s) {
Switch newSwitch = new Switch(); Switch newSwitch = new Switch();
newSwitch.setId(s.getId());
newSwitch.setName(s.getName()); newSwitch.setName(s.getName());
newSwitch.setRoomId(s.getRoomId()); newSwitch.setRoomId(s.getRoomId());
newSwitch.setOn(s.isOn());
return switchRepository.save(newSwitch); return switchRepository.save(newSwitch);
} }
@PutMapping @PutMapping("/operate")
public Switch update(@Valid @RequestBody SwitchSaveRequest s) { public Set<Switchable> operate(@Valid @RequestBody final SwitchOperationRequest sr)
return this.create(s); throws NotFoundException {
final Switch s = switchRepository.findById(sr.getId()).orElseThrow(NotFoundException::new);
switch (sr.getType()) {
case ON:
s.setOn(true);
break;
case OFF:
s.setOn(false);
break;
case TOGGLE:
s.toggle();
break;
}
switchableRepository.saveAll(s.getOutputs());
return s.getOutputs();
}
@PostMapping("/{id}/lights")
public Set<? extends OutputDevice> addSwitchable(
@PathVariable("id") long inputId, @RequestParam("switchableId") Long switchableId)
throws NotFoundException {
return addOutput(inputId, switchableId);
}
@DeleteMapping("/{id}/lights")
public Set<? extends OutputDevice> removeSwitchable(
@PathVariable("id") long inputId, @RequestParam("switchableId") Long switchableId)
throws NotFoundException {
return removeOutput(inputId, switchableId);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

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

@ -1,23 +0,0 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import java.util.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.web.bind.annotation.*;
@RestController
@EnableAutoConfiguration
// @Mapping("/light")
public class WelcomeController {
@GetMapping
List<Device> testDevices() {
return Arrays.asList(
new KnobDimmer(),
new RegularLight(),
new MotionSensor(),
new Sensor(),
new SmartPlug(),
new Switch());
}
}

View file

@ -0,0 +1,34 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import javax.validation.constraints.NotNull;
/** A 'dim' event from a button dimmer. */
public class ButtonDimmerDimRequest {
/** The device id */
@NotNull private Long id;
public enum DimType {
UP,
DOWN;
}
/** Whether the dim is up or down */
@NotNull private DimType dimType;
public DimType getDimType() {
return dimType;
}
public void setDimType(DimType dimType) {
this.dimType = dimType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View file

@ -1,67 +0,0 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
public class ButtonDimmerSaveRequest {
@Lob private Set<DimmableLight> lights = new HashSet<DimmableLight>();
/** Device identifier */
private long id;
/** The room this device belongs in */
private Room room;
/**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call.
*/
@NotNull private Long roomId;
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name;
public Set<DimmableLight> getLights() {
return this.lights;
}
public void setLights(Set<DimmableLight> newLights) {
this.lights = newLights;
}
public void setId(long id) {
this.id = id;
}
public void setRoom(Room room) {
this.room = room;
}
public void setRoomId(Long roomId) {
this.roomId = roomId;
}
public void setName(String name) {
this.name = name;
}
public long getId() {
return id;
}
public Room getRoom() {
return room;
}
public Long getRoomId() {
return roomId;
}
public String getName() {
return name;
}
}

View file

@ -1,11 +1,9 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class SwitchSaveRequest { public class DeviceSaveRequest {
/** The state of this switch */
private boolean on;
/** Device identifier */ /** Device identifier */
private long id; private long id;
@ -16,37 +14,29 @@ public class SwitchSaveRequest {
@NotNull private Long roomId; @NotNull private Long roomId;
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name; @NotNull @NotEmpty private String name;
public void setId(long id) {
this.id = id;
}
public void setRoomId(Long roomId) {
this.roomId = roomId;
}
public void setName(String name) {
this.name = name;
}
public long getId() { public long getId() {
return id; return id;
} }
public void setId(long id) {
this.id = id;
}
public Long getRoomId() { public Long getRoomId() {
return roomId; return roomId;
} }
public void setRoomId(Long roomId) {
this.roomId = roomId;
}
public String getName() { public String getName() {
return name; return name;
} }
public boolean isOn() { public void setName(String name) {
return on; this.name = name;
}
public void setOn(boolean on) {
this.on = on;
} }
} }

View file

@ -1,24 +1,20 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class DimmableLightSaveRequest { public class DimmableLightSaveRequest {
/** Device id (used only for update requests) */
private Long id;
/** The light intensity value. Goes from 0 (off) to 100 (on) */ /** The light intensity value. Goes from 0 (off) to 100 (on) */
@NotNull @NotNull
@Min(1) @Min(1)
@Max(100) @Max(100)
private Integer intensity = 0; private Integer intensity = 0;
/** Device identifier */
private long id;
/** The room this device belongs in */
private Room room;
/** /**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from * The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call. * a REST call.
@ -28,14 +24,6 @@ public class DimmableLightSaveRequest {
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name; @NotNull private String name;
public void setId(long id) {
this.id = id;
}
public void setRoom(Room room) {
this.room = room;
}
public void setRoomId(Long roomId) { public void setRoomId(Long roomId) {
this.roomId = roomId; this.roomId = roomId;
} }
@ -44,14 +32,6 @@ public class DimmableLightSaveRequest {
this.name = name; this.name = name;
} }
public long getId() {
return id;
}
public Room getRoom() {
return room;
}
public Long getRoomId() { public Long getRoomId() {
return roomId; return roomId;
} }
@ -64,10 +44,15 @@ public class DimmableLightSaveRequest {
return intensity; return intensity;
} }
public void setIntensity(Integer intensity) throws IllegalArgumentException { public void setIntensity(Integer intensity) {
if (intensity < 0 || intensity > 100) {
throw new IllegalArgumentException("The intensity level can't go below 0 or above 100");
}
this.intensity = intensity; this.intensity = intensity;
} }
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
} }

View file

@ -2,12 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class MotionSensorSaveRequest { public class GenericDeviceSaveReguest {
private boolean detected;
/** Device identifier */
private long id;
/** /**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from * The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call. * a REST call.
@ -17,10 +12,6 @@ public class MotionSensorSaveRequest {
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name; @NotNull private String name;
public void setId(long id) {
this.id = id;
}
public void setRoomId(Long roomId) { public void setRoomId(Long roomId) {
this.roomId = roomId; this.roomId = roomId;
} }
@ -29,10 +20,6 @@ public class MotionSensorSaveRequest {
this.name = name; this.name = name;
} }
public long getId() {
return id;
}
public Long getRoomId() { public Long getRoomId() {
return roomId; return roomId;
} }
@ -40,12 +27,4 @@ public class MotionSensorSaveRequest {
public String getName() { public String getName() {
return name; return name;
} }
public boolean isDetected() {
return detected;
}
public void setDetected(boolean detected) {
this.detected = detected;
}
} }

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

@ -1,15 +1,17 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
public class JWTRequest { import javax.validation.constraints.NotNull;
private String username;
private String password;
public String getUsername() { public class JWTRequest {
return this.username; @NotNull private String usernameOrEmail;
@NotNull private String password;
public String getUsernameOrEmail() {
return this.usernameOrEmail;
} }
public void setUsername(String username) { public void setUsernameOrEmail(String usernameOrEmail) {
this.username = username; this.usernameOrEmail = usernameOrEmail;
} }
public String getPassword() { public String getPassword() {
@ -19,4 +21,16 @@ public class JWTRequest {
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; this.password = password;
} }
@Override
public String toString() {
return "JWTRequest{"
+ "usernameOrEmail='"
+ usernameOrEmail
+ '\''
+ ", password='"
+ password
+ '\''
+ '}';
}
} }

View file

@ -0,0 +1,33 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class KnobDimmerDimRequest {
/** The device id */
@NotNull private Long id;
/** The absolute intensity value */
@NotNull
@Min(0)
@Max(100)
private Integer intensity;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getIntensity() {
return intensity;
}
public void setIntensity(Integer intensity) {
this.intensity = intensity;
}
}

View file

@ -1,55 +0,0 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Lob;
import javax.validation.constraints.NotNull;
public class KnobDimmerSaveRequest {
@Lob private Set<DimmableLight> lights = new HashSet<DimmableLight>();
/** Device identifier */
private long id;
/**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call.
*/
@NotNull private Long roomId;
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name;
public void setId(long id) {
this.id = id;
}
public void setRoomId(Long roomId) {
this.roomId = roomId;
}
public void setName(String name) {
this.name = name;
}
public long getId() {
return id;
}
public Long getRoomId() {
return roomId;
}
public String getName() {
return name;
}
public void setLights(Set<DimmableLight> lights) {
this.lights = lights;
}
public Set<DimmableLight> getLights() {
return lights;
}
}

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

@ -18,10 +18,6 @@ public class RegularLightSaveRequest {
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name; @NotNull private String name;
public void setId(long id) {
this.id = id;
}
public void setRoomId(Long roomId) { public void setRoomId(Long roomId) {
this.roomId = roomId; this.roomId = roomId;
} }
@ -49,4 +45,8 @@ public class RegularLightSaveRequest {
public void setOn(boolean on) { public void setOn(boolean on) {
this.on = on; this.on = on;
} }
public void setId(long id) {
this.id = id;
}
} }

View file

@ -1,17 +1,19 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class RoomSaveRequest { public class RoomSaveRequest {
@NotNull private Room.Icon icon;
/** /**
* Icon and image are to be given as byte[]. In order to get an encoded string from it, the * Image is 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: * Base64.getEncoder().encodeToString(byte[] content) should be used. For further information:
* https://www.baeldung.com/java-base64-image-string * https://www.baeldung.com/java-base64-image-string
* https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html * https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html
*/ */
@Lob private String icon;
@Lob private String image; @Lob private String image;
/** The user given name of this room (e.g. 'Master bedroom') */ /** The user given name of this room (e.g. 'Master bedroom') */
@ -25,11 +27,11 @@ public class RoomSaveRequest {
this.name = name; this.name = name;
} }
public String getIcon() { public Room.Icon getIcon() {
return icon; return icon;
} }
public void setIcon(String icon) { public void setIcon(Room.Icon icon) {
this.icon = icon; this.icon = icon;
} }

View file

@ -23,17 +23,11 @@ public class SensorSaveRequest {
LIGHT LIGHT
} }
/** The value of this sensor according to its sensor type */
private int value;
/** The type of this sensor */ /** The type of this sensor */
@NotNull @NotNull
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
private Sensor.SensorType sensor; private Sensor.SensorType sensor;
/** Device identifier */
private long id;
/** /**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from * The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call. * a REST call.
@ -43,10 +37,6 @@ public class SensorSaveRequest {
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name; @NotNull private String name;
public void setId(long id) {
this.id = id;
}
public void setRoomId(Long roomId) { public void setRoomId(Long roomId) {
this.roomId = roomId; this.roomId = roomId;
} }
@ -55,10 +45,6 @@ public class SensorSaveRequest {
this.name = name; this.name = name;
} }
public long getId() {
return id;
}
public Long getRoomId() { public Long getRoomId() {
return roomId; return roomId;
} }
@ -74,12 +60,4 @@ public class SensorSaveRequest {
public void setSensor(Sensor.SensorType sensor) { public void setSensor(Sensor.SensorType sensor) {
this.sensor = sensor; this.sensor = sensor;
} }
public int getValue() {
return this.value;
}
public void setValue(int newValue) {
this.value = newValue;
}
} }

View file

@ -18,10 +18,6 @@ public class SmartPlugSaveRequest {
/** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
@NotNull private String name; @NotNull private String name;
public void setId(long id) {
this.id = id;
}
public void setRoomId(Long roomId) { public void setRoomId(Long roomId) {
this.roomId = roomId; this.roomId = roomId;
} }
@ -49,4 +45,8 @@ public class SmartPlugSaveRequest {
public void setOn(boolean on) { public void setOn(boolean on) {
this.on = on; this.on = on;
} }
public void setId(long id) {
this.id = id;
}
} }

View file

@ -0,0 +1,35 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import javax.validation.constraints.NotNull;
/** An on/off/toggle operation on a switch */
public class SwitchOperationRequest {
/** The device id */
@NotNull private Long id;
public enum OperationType {
ON,
OFF,
TOGGLE
}
/** The type of switch operation */
@NotNull private SwitchOperationRequest.OperationType type;
public OperationType getType() {
return type;
}
public void setType(OperationType type) {
this.type = type;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View file

@ -12,6 +12,9 @@ public class UserRegistrationRequest {
/** The full name of the user */ /** The full name of the user */
@NotNull @NotNull
@NotEmpty(message = "Please provide a username") @NotEmpty(message = "Please provide a username")
@Pattern(
regexp = "[A-Za-z0-9_\\-]+",
message = "Username can contain only letters, numbers, '_' and '-'")
private String username; private String username;
/** A properly salted way to store the password */ /** A properly salted way to store the password */

View file

@ -0,0 +1,11 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public class BadDataException extends Exception {
public BadDataException(String message) {
super(message);
}
}

View file

@ -0,0 +1,15 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class NotFoundException extends Exception {
public NotFoundException() {
super("Not found");
}
public NotFoundException(String what) {
super(what + " not found");
}
}

View file

@ -0,0 +1,18 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public class UnauthorizedException extends Exception {
private final boolean isUserDisabled;
public UnauthorizedException(boolean isDisabled) {
super("Access denied: " + (isDisabled ? "user is disabled" : "wrong credentials"));
this.isUserDisabled = isDisabled;
}
public boolean isUserDisabled() {
return isUserDisabled;
}
}

View file

@ -0,0 +1,11 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public class UserNotFoundException extends Exception {
public UserNotFoundException() {
super("No user found with given email");
}
}

View file

@ -1,9 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.OneToMany;
/** /**
* Represents a dimmer that can only instruct an increase or decrease of intensity (i.e. like a * Represents a dimmer that can only instruct an increase or decrease of intensity (i.e. like a
@ -11,60 +8,25 @@ import javax.persistence.OneToMany;
*/ */
@Entity @Entity
public class ButtonDimmer extends Dimmer { public class ButtonDimmer extends Dimmer {
/** The delta amount to apply to a increase or decrease intensity */
private static final int DIM_INCREMENT = 10;
public ButtonDimmer() { public ButtonDimmer() {
super("button-dimmer"); super("button-dimmer");
} }
@OneToMany(mappedBy = "dimmer") /** Increases the current intensity level of the dimmable light by DIM_INCREMENT */
private Set<DimmableLight> lights;
/** Increases the current intensity level of the dimmable light by 1 */
public void increaseIntensity() { public void increaseIntensity() {
for (DimmableLight dl : lights) { for (DimmableLight dl : getOutputs()) {
dl.setIntensity(dl.getIntensity() + 1); dl.setIntensity(dl.getIntensity() + DIM_INCREMENT);
} }
} }
/** Decreases the current intensity level of the dimmable light by 1 */ /** Decreases the current intensity level of the dimmable light by DIM_INCREMENT */
public void decreaseIntensity() { public void decreaseIntensity() {
for (DimmableLight dl : lights) { for (DimmableLight dl : getOutputs()) {
dl.setIntensity(dl.getIntensity() - 1); dl.setIntensity(dl.getIntensity() - DIM_INCREMENT);
} }
} }
/**
* Adds a DimmableLight to this set of DimmableLights
*
* @param dl The DimmableLight to be added
*/
public void addLight(DimmableLight dl) {
lights.add(dl);
}
/**
* Removes the given DimmableLight
*
* @param dl The DimmableLight to be removed
*/
public void removeLight(DimmableLight dl) {
lights.remove(dl);
}
/** Clears this set */
public void clearSet() {
lights.clear();
}
/**
* Get the lights
*
* @return duh
*/
public Set<DimmableLight> getLights() {
return this.lights;
}
public void setLights(Set<DimmableLight> newLights) {
this.lights = newLights;
}
} }

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

@ -0,0 +1,47 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* A rule on how to connect an input device type to an output device type
*
* @param <I> the input device type
* @param <O> the output device type
*/
@FunctionalInterface
public interface Connector<I extends InputDevice, O extends OutputDevice> {
/**
* Connects or disconnects input to output
*
* @param input the input device
* @param output the output device
* @param connect true if connection, false if disconnection
*/
void connect(I input, O output, boolean connect);
/**
* Produces a basic implementation of a connector, assuming there is a OneToMany relationship
* between J and K
*
* @param outputsGetter the getter method of the set of outputs on the input class
* @param inputSetter the setter method for the input id on the output class
* @param <J> the input device type
* @param <K> the output device type
* @return a Connector implementation for the pair of types J and K
*/
static <J extends InputDevice, K extends OutputDevice> Connector<J, K> basic(
Function<J, Set<? super K>> outputsGetter, BiConsumer<K, Long> inputSetter) {
return (i, o, connect) -> {
if (connect) {
outputsGetter.apply(i).add(o);
} else {
outputsGetter.apply(i).remove(o);
}
inputSetter.accept(o, connect ? i.getId() : null);
};
}
}

View file

@ -1,7 +1,9 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
@ -9,18 +11,31 @@ import javax.validation.constraints.NotNull;
/** Represent a dimmable light */ /** Represent a dimmable light */
@Entity @Entity
public class DimmableLight extends Light { public class DimmableLight extends Switchable {
public static final Connector<ButtonDimmer, DimmableLight>
BUTTON_DIMMER_DIMMABLE_LIGHT_CONNECTOR =
Connector.basic(ButtonDimmer::getOutputs, DimmableLight::setDimmerId);
public static final Connector<KnobDimmer, DimmableLight> KNOB_DIMMER_DIMMABLE_LIGHT_CONNECTOR =
Connector.basic(KnobDimmer::getOutputs, DimmableLight::setDimmerId);
public DimmableLight() { public DimmableLight() {
super("light"); super("light");
} }
@ManyToOne private Dimmer dimmer; @ManyToOne
@GsonExclude
@JoinColumn(name = "dimmer_id", updatable = false, insertable = false)
private Dimmer dimmer;
@Column(name = "dimmer_id")
private Long dimmerId;
/** The light intensity value. Goes from 0 (off) to 100 (on) */ /** The light intensity value. Goes from 0 (off) to 100 (on) */
@NotNull @NotNull
@Column(nullable = false) @Column(nullable = false)
@Min(1) @Min(0)
@Max(100) @Max(100)
private Integer intensity = 0; private Integer intensity = 0;
@ -28,10 +43,41 @@ public class DimmableLight extends Light {
return intensity; return intensity;
} }
public void setIntensity(Integer intensity) throws IllegalArgumentException { /**
if (intensity < 0 || intensity > 100) { * Sets the intensity to a certain level. Out of bound values are corrected to the respective
throw new IllegalArgumentException("The intensity level can't go below 0 or above 100"); * extremums. An intensity level of 0 turns the light off, but keeps the old intensity level
* stored.
*
* @param intensity the intensity level (may be out of bounds)
*/
public void setIntensity(Integer intensity) {
if (intensity <= 0) {
this.intensity = 0;
} else if (intensity > 100) {
this.intensity = 100;
} else {
this.intensity = intensity;
} }
this.intensity = intensity; }
@Override
public boolean isOn() {
return intensity != 0;
}
@Override
public void setOn(boolean on) {
intensity = on ? 100 : 0;
}
public void setDimmerId(Long dimmerId) {
this.dimmerId = dimmerId;
super.setSwitchId(null);
};
@Override
public void setSwitchId(Long switchId) {
super.setSwitchId(switchId);
this.dimmerId = null;
} }
} }

View file

@ -1,3 +1,3 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
public interface DimmableLightRepository extends DeviceRepository<DimmableLight> {} public interface DimmableLightRepository extends SwitchableRepository<DimmableLight> {}

View file

@ -1,8 +1,10 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Inheritance; import javax.persistence.Inheritance;
import javax.persistence.InheritanceType; import javax.persistence.InheritanceType;
import javax.persistence.OneToMany;
/** Represents a generic dimmer input device */ /** Represents a generic dimmer input device */
@Entity @Entity
@ -11,4 +13,17 @@ public abstract class Dimmer extends InputDevice {
public Dimmer(String kind) { public Dimmer(String kind) {
super(kind); super(kind);
} }
@OneToMany(mappedBy = "dimmer")
private Set<DimmableLight> lights;
/**
* Get the lights connected to this dimmer
*
* @return duh
*/
@Override
public Set<DimmableLight> getOutputs() {
return this.lights;
}
} }

View file

@ -1,14 +1,22 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
/** /**
* A generic abstraction for an input device, i.e. something that captures input either from the * A generic abstraction for an input device, i.e. something that captures input either from the
* environment (sensor) or the user (switch / dimmer). * environment (sensor) or the user (switch / dimmer).
*/ */
@Entity @Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class InputDevice extends Device { public abstract class InputDevice extends Device {
public InputDevice(String kind) { public InputDevice(String kind) {
super(kind, FlowType.INPUT); super(kind, FlowType.INPUT);
} }
public Set<? extends OutputDevice> getOutputs() {
return Set.of();
}
} }

View file

@ -15,7 +15,7 @@ public class JWTUserDetailsService implements UserDetailsService {
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User toReturn = repository.findByUsername(username); User toReturn = repository.findByUsername(username);
if (toReturn != null) { if (toReturn != null && toReturn.getEnabled()) {
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
toReturn.getUsername(), toReturn.getPassword(), Set.of()); toReturn.getUsername(), toReturn.getPassword(), Set.of());
} else { } else {

View file

@ -1,8 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.OneToMany;
/** /**
* Represents a dimmer able to set absolute intensity values (i.e. knowing the absolute intensity * Represents a dimmer able to set absolute intensity values (i.e. knowing the absolute intensity
@ -10,62 +8,19 @@ import javax.persistence.OneToMany;
*/ */
@Entity @Entity
public class KnobDimmer extends Dimmer { public class KnobDimmer extends Dimmer {
public KnobDimmer() { public KnobDimmer() {
super("knob-dimmer"); super("knob-dimmer");
} }
@OneToMany(mappedBy = "dimmer")
private Set<DimmableLight> lights;
/** /**
* Increases or decreases the current intensity level by 5, moving between absolute multiples of * Sets absolutely the intensity level of all lights connected
* 5 between 0 and 100, of all dimmable lights mapped to this knob
* *
* @param inc The direction the knob is turned with * @param intensity the intensity (must be from 0 to 100)
*/ */
public void modifyIntensity(boolean inc) { public void setLightIntensity(int intensity) {
for (DimmableLight dl : getOutputs()) {
for (DimmableLight dl : lights) { dl.setIntensity(intensity);
int remainder = dl.getIntensity() / 5;
if (inc) {
dl.setIntensity(dl.getIntensity() - remainder);
dl.setIntensity((dl.getIntensity() + 5) % 105);
} else {
dl.setIntensity(dl.getIntensity() + (5 - remainder));
dl.setIntensity((dl.getIntensity() - 5) % 105);
}
} }
} }
/**
* Adds a DimmableLight to this set of DimmableLights
*
* @param dl The DimmableLight to be added
*/
public void addLight(DimmableLight dl) {
lights.add(dl);
}
/**
* Removes the given DimmableLight
*
* @param dl The DimmableLight to be removed
*/
public void removeLight(DimmableLight dl) {
lights.remove(dl);
}
/** Clears this set */
public void clearSet() {
lights.clear();
}
public void setLights(Set<DimmableLight> lights) {
this.lights = lights;
}
public Set<DimmableLight> getLights() {
return lights;
}
} }

View file

@ -1,31 +0,0 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.validation.constraints.NotNull;
/** Represents a generic light */
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Light extends OutputDevice {
/** Whether the light is on or not */
@Column(name = "light_on", nullable = false)
@NotNull
boolean on;
protected Light(String kind) {
super(kind);
this.on = false;
}
public boolean isOn() {
return on;
}
public void setOn(boolean on) {
this.on = on;
}
}

View file

@ -1,8 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import javax.persistence.Entity; import javax.persistence.*;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
/** /**
* Represents a generic output device, i.e. something that causes some behaviour (light, smartPlugs, * Represents a generic output device, i.e. something that causes some behaviour (light, smartPlugs,

View file

@ -1,11 +1,30 @@
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;
import javax.validation.constraints.NotNull;
/** Represents a standard non-dimmable light */ /** Represents a standard non-dimmable light */
@Entity @Entity
public class RegularLight extends Light { public class RegularLight extends Switchable {
/** Whether the light is on or not */
@Column(name = "light_on", nullable = false)
@NotNull
boolean on;
public RegularLight() { public RegularLight() {
super("regular-light"); super("regular-light");
this.on = false;
}
@Override
public boolean isOn() {
return on;
}
@Override
public void setOn(boolean on) {
this.on = on;
} }
} }

View file

@ -1,3 +1,3 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
public interface RegularLightRepository extends DeviceRepository<RegularLight> {} public interface RegularLightRepository extends SwitchableRepository<RegularLight> {}

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import com.google.gson.annotations.SerializedName;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -8,25 +9,124 @@ import javax.validation.constraints.NotNull;
@Entity @Entity
public class Room { public class Room {
/** A collection of Semantic UI icons */
@SuppressWarnings("unused")
public enum Icon {
@SerializedName("home")
HOME("home"),
@SerializedName("coffee")
COFFEE("coffee"),
@SerializedName("beer")
BEER("beer"),
@SerializedName("glass martini")
GLASS_MARTINI("glass martini"),
@SerializedName("film")
FILM("film"),
@SerializedName("video")
VIDEO("video"),
@SerializedName("music")
MUSIC("music"),
@SerializedName("headphones")
HEADPHONES("headphones"),
@SerializedName("fax")
FAX("fax"),
@SerializedName("phone")
PHONE("phone"),
@SerializedName("laptop")
LAPTOP("laptop"),
@SerializedName("bath")
BATH("bath"),
@SerializedName("shower")
SHOWER("shower"),
@SerializedName("bed")
BED("bed"),
@SerializedName("child")
CHILD("child"),
@SerializedName("warehouse")
WAREHOUSE("warehouse"),
@SerializedName("car")
CAR("car"),
@SerializedName("bicycle")
BICYCLE("bicycle"),
@SerializedName("motorcycle")
MOTORCYCLE("motorcycle"),
@SerializedName("archive")
ARCHIVE("archive"),
@SerializedName("boxes")
BOXES("boxes"),
@SerializedName("cubes")
CUBES("cubes"),
@SerializedName("chess")
CHESS("chess"),
@SerializedName("gamepad")
GAMEPAD("gamepad"),
@SerializedName("futbol")
FUTBOL("futbol"),
@SerializedName("table tennis")
TABLE_TENNIS("table tennis"),
@SerializedName("server")
SERVER("server"),
@SerializedName("tv")
TV("tv"),
@SerializedName("heart")
HEART("heart"),
@SerializedName("camera")
CAMERA("camera"),
@SerializedName("trophy")
TROPHY("trophy"),
@SerializedName("wrench")
WRENCH("wrench"),
@SerializedName("image")
IMAGE("image"),
@SerializedName("book")
BOOK("book"),
@SerializedName("university")
UNIVERSITY("university"),
@SerializedName("medkit")
MEDKIT("medkit"),
@SerializedName("paw")
PAW("paw"),
@SerializedName("tree")
TREE("tree"),
@SerializedName("utensils")
UTENSILS("utensils"),
@SerializedName("male")
MALE("male"),
@SerializedName("female")
FEMALE("female"),
@SerializedName("life ring outline")
LIFE_RING_OUTLINE("life ring outline");
private String iconName;
Icon(String s) {
this.iconName = s;
}
@Override
public String toString() {
return iconName;
}
}
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false, unique = true) @Column(name = "id", updatable = false, nullable = false, unique = true)
@ApiModelProperty(hidden = true) @ApiModelProperty(hidden = true)
private Long id; private Long id;
/** The room icon, out of a set of Semantic UI icons */
@Column private Icon icon;
/** /**
* Icon and image are to be given as byte[]. In order to get an encoded string from it, the * Image is 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: * Base64.getEncoder().encodeToString(byte[] content) should be used. For further information:
* https://www.baeldung.com/java-base64-image-string * https://www.baeldung.com/java-base64-image-string
* https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html * https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html
*/ */
@Lob
@Column(name = "icon", columnDefinition = "TEXT")
private byte[] icon;
@Lob @Lob
@Column(name = "image", columnDefinition = "TEXT") @Column(name = "image", columnDefinition = "TEXT")
private byte[] image; private String image;
/** /**
* User that owns the house this room is in as a foreign key id. To use when updating and * User that owns the house this room is in as a foreign key id. To use when updating and
@ -65,19 +165,19 @@ public class Room {
this.name = name; this.name = name;
} }
public byte[] getIcon() { public Icon getIcon() {
return icon; return icon;
} }
public void setIcon(byte[] icon) { public void setIcon(Icon icon) {
this.icon = icon; this.icon = icon;
} }
public byte[] getImage() { public String getImage() {
return image; return image;
} }
public void setImage(byte[] image) { public void setImage(String image) {
this.image = image; this.image = image;
} }

View file

@ -42,6 +42,8 @@ public class Sensor extends InputDevice {
public void setSensor(SensorType sensor) { public void setSensor(SensorType sensor) {
this.sensor = sensor; this.sensor = sensor;
// TODO: setup hook for sockets live update
} }
public int getValue() { public int getValue() {

View file

@ -6,17 +6,19 @@ import javax.validation.constraints.NotNull;
/** A smart plug that can be turned either on or off */ /** A smart plug that can be turned either on or off */
@Entity @Entity
public class SmartPlug extends OutputDevice { public class SmartPlug extends Switchable {
/** Whether the smart plug is on */ /** Whether the smart plug is on */
@Column(name = "smart_plug_on", nullable = false) @Column(name = "smart_plug_on", nullable = false)
@NotNull @NotNull
private boolean on; private boolean on;
@Override
public boolean isOn() { public boolean isOn() {
return on; return on;
} }
@Override
public void setOn(boolean on) { public void setOn(boolean on) {
this.on = on; this.on = on;
} }

View file

@ -1,3 +1,3 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
public interface SmartPlugRepository extends DeviceRepository<SmartPlug> {} public interface SmartPlugRepository extends SwitchableRepository<SmartPlug> {}

View file

@ -1,12 +1,18 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.OneToMany;
/** A switch input device */ /** A switch input device */
@Entity @Entity
public class Switch extends InputDevice { public class Switch extends InputDevice {
@OneToMany(mappedBy = "switchDevice")
private Set<Switchable> switchables = new HashSet<>();
/** The state of this switch */ /** The state of this switch */
@Column(nullable = false, name = "switch_on") @Column(nullable = false, name = "switch_on")
private boolean on; private boolean on;
@ -22,6 +28,15 @@ public class Switch extends InputDevice {
*/ */
public void setOn(boolean state) { public void setOn(boolean state) {
on = state; on = state;
for (final Switchable s : switchables) {
s.setOn(on);
}
}
/** Toggle between on and off state */
public void toggle() {
setOn(!isOn());
} }
/** /**
@ -32,4 +47,8 @@ public class Switch extends InputDevice {
public boolean isOn() { public boolean isOn() {
return on; return on;
} }
public Set<Switchable> getOutputs() {
return switchables;
}
} }

View file

@ -0,0 +1,47 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
import javax.persistence.*;
/** A device that can be turned either on or off */
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Switchable extends OutputDevice {
public static final Connector<Switch, Switchable> SWITCH_SWITCHABLE_CONNECTOR =
Connector.basic(Switch::getOutputs, Switchable::setSwitchId);
@ManyToOne
@GsonExclude
@JoinColumn(name = "switch_id", updatable = false, insertable = false)
private Switch switchDevice;
@Column(name = "switch_id")
private Long switchId;
protected Switchable(String kind) {
super(kind);
}
/**
* Returns whether the device is on (true) or not (false)
*
* @return whether the device is on (true) or not (false)
*/
public abstract boolean isOn();
/**
* Sets the on status of the device
*
* @param on the new on status: true for on, false for off
*/
public abstract void setOn(boolean on);
public Long getSwitchId() {
return switchId;
}
public void setSwitchId(Long switchId) {
this.switchId = switchId;
}
}

View file

@ -0,0 +1,7 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
/**
* SwitchableRepository acts as a superclass for the other repositories so to mirror in the database
* the class inheritance present among the various switchable devices.
*/
public interface SwitchableRepository<T extends Switchable> extends DeviceRepository<T> {}

View file

@ -2,11 +2,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
/** A user of the Smarthut application */ /** A user of the Smarthut application */
@Entity(name = "smarthutuser") @Entity(name = "smarthutuser")
@ -19,25 +14,15 @@ public class User {
private Long id; private Long id;
/** The full name of the user */ /** The full name of the user */
@NotNull
@Column(nullable = false) @Column(nullable = false)
@NotEmpty(message = "Please provide a full name")
private String name; private String name;
/** The full username of the user */ /** The full username of the user */
@NotNull
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
@NotEmpty(message = "Please provide a username")
private String username; private String username;
/** A properly salted way to store the password */ /** A properly salted way to store the password */
@NotNull
@Column(nullable = false) @Column(nullable = false)
@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; private String password;
/** /**
@ -45,10 +30,6 @@ public class User {
* </code>, technically not RFC 5322 compliant * </code>, technically not RFC 5322 compliant
*/ */
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
@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; private String email;
@Column(nullable = false) @Column(nullable = false)

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=

View file

@ -0,0 +1,233 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut;
import static org.assertj.core.api.Assertions.assertThat;
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.OkResponse;
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.UnauthorizedException;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
public class AuthenticationTests extends SmartHutTest {
@Autowired private TestRestTemplate restTemplate;
private UserRegistrationRequest getDisabledUser() {
final UserRegistrationRequest disabledUser = new UserRegistrationRequest();
disabledUser.setName("Disabled User");
disabledUser.setEmail("disabled@example.com");
disabledUser.setUsername("disabled");
disabledUser.setPassword("password");
return disabledUser;
}
private static final UserRegistrationRequest enabledUser = new UserRegistrationRequest();
static {
enabledUser.setName("Enabled User");
enabledUser.setEmail("enabled@example.com");
enabledUser.setUsername("enabled");
enabledUser.setPassword("password");
}
@Override
protected void setUp() {
final ResponseEntity<OkResponse> res =
this.restTemplate.postForEntity(
this.url("/register"), getDisabledUser(), OkResponse.class);
assertThat(res.getStatusCode().equals(HttpStatus.OK));
final ResponseEntity<OkResponse> res2 =
this.restTemplate.postForEntity(
this.url("/register"), enabledUser, OkResponse.class);
assertThat(res2.getStatusCode().equals(HttpStatus.OK));
// TODO: email confirmation for enabledUser
}
@Test
public void registrationShouldReturnBadRequestWithIncorrectFields() {
final Map<String, Object> badJSON = Map.of("luciano", "goretti", "danilo", "malusa");
assertThat(
this.restTemplate
.postForEntity(url("/register"), badJSON, JWTResponse.class)
.getStatusCode()
.equals(HttpStatus.BAD_REQUEST));
}
@Test
public void registrationShouldReturnBadRequestWithShortPassword() {
final UserRegistrationRequest request = new UserRegistrationRequest();
request.setName("Mario Goretti");
request.setEmail("test@example.com");
request.setUsername("mgo");
request.setPassword("passw");
final ResponseEntity<JsonObject> res =
this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
final JsonArray errors = res.getBody().getAsJsonArray("errors");
assertThat(errors.size() == 1);
assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("password"));
}
@Test
public void registrationShouldReturnBadRequestWithWrongEmail() {
final UserRegistrationRequest request = new UserRegistrationRequest();
request.setName("Mario Goretti");
request.setEmail("test@example");
request.setUsername("mgo");
request.setPassword("password");
final ResponseEntity<JsonObject> res =
this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
final JsonArray errors = res.getBody().getAsJsonArray("errors");
assertThat(errors.size() == 1);
assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("email"));
}
@Test
public void registrationShouldReturnBadRequestWithNoName() {
final UserRegistrationRequest request = new UserRegistrationRequest();
request.setEmail("test@example.com");
request.setUsername("mgo");
request.setPassword("password");
final ResponseEntity<JsonObject> res =
this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
final JsonArray errors = res.getBody().getAsJsonArray("errors");
assertThat(errors.size() == 1);
assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("name"));
}
@Test
public void registrationShouldReturnBadRequestWithNoUsername() {
final UserRegistrationRequest request = new UserRegistrationRequest();
request.setName("Mario Goretti");
request.setEmail("test@example.com");
request.setPassword("password");
final ResponseEntity<JsonObject> res =
this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
final JsonArray errors = res.getBody().getAsJsonArray("errors");
assertThat(errors.size() == 1);
assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("username"));
}
@Test
public void registrationShouldReturnBadRequestWithDuplicateData() {
{
final ResponseEntity<DuplicateRegistrationException> res =
this.restTemplate.postForEntity(
url("/register"),
getDisabledUser(),
DuplicateRegistrationException.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
}
{
final UserRegistrationRequest disabledUserDifferentMail = getDisabledUser();
enabledUser.setEmail("another@example.com");
final ResponseEntity<DuplicateRegistrationException> res =
this.restTemplate.postForEntity(
url("/register"),
disabledUserDifferentMail,
DuplicateRegistrationException.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
}
{
final UserRegistrationRequest disabledUserDifferentUsername = getDisabledUser();
enabledUser.setUsername("another");
final ResponseEntity<DuplicateRegistrationException> res =
this.restTemplate.postForEntity(
url("/register"),
disabledUserDifferentUsername,
DuplicateRegistrationException.class);
assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
assertThat(res.getBody() != null);
}
}
@Test
public void registrationShouldReturnOkWithCorrectData() {
final UserRegistrationRequest request = new UserRegistrationRequest();
request.setName("Registration Test");
request.setUsername("smarthut");
request.setEmail("smarthut.sm@example.com");
request.setPassword("password");
final ResponseEntity<OkResponse> res =
this.restTemplate.postForEntity(url("/register"), request, OkResponse.class);
assertThat(res.getStatusCode().equals(HttpStatus.OK));
assertThat(res.getBody() != null);
}
@Test
public void loginShouldReturnBadRequestWithIncorrectFields() {
final Map<String, Object> badJSON = Map.of("badkey", 3, "password", "ciaomamma");
assertThat(
this.restTemplate
.postForEntity(url("/auth/login"), badJSON, JWTResponse.class)
.getStatusCode()
.equals(HttpStatus.BAD_REQUEST));
}
@Test
public void loginShouldReturnUnauthorizedWithNonExistantUser() {
final JWTRequest request = new JWTRequest();
request.setUsernameOrEmail("roberto");
request.setPassword("ciaomamma");
final ResponseEntity<UnauthorizedException> res =
this.restTemplate.postForEntity(
url("/auth/login"), request, UnauthorizedException.class);
assertThat(res.getStatusCode().equals(HttpStatus.UNAUTHORIZED));
assertThat(res.getBody() != null);
assertThat(!res.getBody().isUserDisabled());
}
@Test
public void loginShouldReturnUnauthorizedWithDisabledUser() {
final JWTRequest request = new JWTRequest();
request.setUsernameOrEmail("disabled");
request.setPassword("password");
final ResponseEntity<UnauthorizedException> res =
this.restTemplate.postForEntity(
url("/auth/login"), request, UnauthorizedException.class);
assertThat(res.getStatusCode().equals(HttpStatus.UNAUTHORIZED));
assertThat(res.getBody() != null);
assertThat(res.getBody().isUserDisabled());
}
}

View file

@ -0,0 +1,25 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut;
import org.junit.jupiter.api.BeforeEach;
public abstract class SmartHutTest {
private boolean setupDone = false;
protected final String getBaseURL() {
return "http://localhost:2000/";
}
protected final String url(final String url) {
return getBaseURL() + url;
}
protected void setUp() {}
@BeforeEach
void setUpHack() {
if (!setupDone) {
setUp();
setupDone = true;
}
}
}

View file

@ -1,11 +1,26 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut; package ch.usi.inf.sa4.sanmarinoes.smarthut;
import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest import org.junit.jupiter.api.Test;
class SmarthutApplicationTests { import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
public class SmarthutApplicationTests extends SmartHutTest {
@Autowired private TestRestTemplate restTemplate;
@Test @Test
void contextLoads() {} public void anonymousGreetingShouldNotBeAuthorized() throws Exception {
assertThat(
this.restTemplate
.getForEntity(getBaseURL(), Void.class)
.getStatusCode()
.equals(HttpStatus.UNAUTHORIZED));
}
} }

View file

@ -0,0 +1,35 @@
spring.http.converters.preferred-json-mapper=gson
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
# Hibernate properties
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
spring.jpa.properties.hibernate.format_sql=true
jwt.secret=thiskeymustbeverylongorthethingcomplainssoiamjustgoingtowritehereabunchofgarbageciaomamma
spring.mail.test-connection=true
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.username=smarthut.sm@gmail.com
spring.mail.password=dcadvbagqfkwbfts
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
server.port = 2000
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=