diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3f7e467
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,143 @@
+.idea/**
+
+data/**/*
+
+**/.DS_Store
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+.gradle
+/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+# IntelliJ
+*.iml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..309580e
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,65 @@
+#Trying to set up the CI, probably won't work
+image: gradle:jdk13
+
+stages:
+ - build
+ - test
+ - code_quality
+ - deploy
+
+#Sets up the docker
+smarthut_deploy:
+ stage: deploy
+ image: docker:latest
+ tags:
+ - dind
+ services:
+ - docker:dind
+ variables:
+ DOCKER_DRIVER: overlay
+ before_script:
+ - docker version
+ - docker info
+ - docker login -u smarthutsm -p $CI_DOCKER_PASS #GiovanniRoberto
+ script:
+ - "docker build -t smarthutsm/smarthut-backend:${CI_COMMIT_BRANCH} --pull ."
+ - "docker push smarthutsm/smarthut-backend:${CI_COMMIT_BRANCH}"
+ after_script:
+ - docker logout
+ only:
+ - dev
+ - master
+
+#base checks for the code
+build:
+ stage: build
+ script:
+ - gradle clean
+ - gradle assemble
+ 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:
+ 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
+ allow_failure: true
+ script:
+ - gradle cpdCheck
+ artifacts:
+ paths:
+ - build/reports/cpd/cpdCheck.xml
+ #create a report on the quality of the code
+ expose_as: 'Code Quality Report'
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..c931d3a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM openjdk:13-jdk-alpine
+
+ARG JAR_FILE=build/libs/smarthut*.jar
+
+COPY ${JAR_FILE} app.jar
+
+ENV SMARTHUT_THIS_VALUE_IS_PROD_IF_THIS_IS_A_CONTAINER_PIZZOCCHERI=prod
+EXPOSE 8080
+ENTRYPOINT ["java","-jar","/app.jar"]
diff --git a/README.md b/README.md
index d483ec4..f588721 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,53 @@
# backend
+## Installation guide
+
+In order to install a SmartHut.sm, you can use *Docker* and *Docker Compose*
+in order to create che corresponding containers.
+
+Use the following `docker-compose.yml` example file. Change the values
+of `$PASSWORD` and `$SECRET` to respectively the chosen PostgreSQL password
+and the JWT secret used to run the server. `$SECRET` must be at least 64 chars long.
+
+```yaml
+version: '3'
+
+services:
+ smarthutdb:
+ restart: always
+ image: postgres:12-alpine
+ container_name: smarthutdb
+ volumes:
+ - ./data:/var/lib/postgresql/data
+ environment:
+ PGDATA: /var/lib/postgresql/data/data
+ POSTGRES_DB: smarthut
+ POSTGRES_USERNAME: postgres
+ POSTGRES_PASSWORD: $PASSWORD
+
+ smarthutbackend:
+ restart: always
+ image: smarthutsm/smarthut-backend:M1
+ ports:
+ - 8080:8080
+ environment:
+ - POSTGRES_JDBC=jdbc:postgresql://smarthutdb:5432/smarthut
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASS=$PASSWORD
+ - SECRET=$SECRET
+ - MAIL_HOST=smtp.gmail.com
+ - MAIL_PORT=587
+ - MAIL_STARTTLS=true
+ - MAIL_USER=smarthut.sm@gmail.com
+ - MAIL_PASS=dcadvbagqfkwbfts
+ - BACKEND_URL=http://localhost:8080
+ - FRONTEND_URL=http://localhost
+
+ smarthut:
+ restart: always
+ image: smarthutsm/smarthut:M1
+ ports:
+ - 80:80
+ environment:
+ - BACKEND_URL=http://localhost:8080
+```
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..3116923
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'org.springframework.boot' version '2.2.4.RELEASE'
+ id 'io.spring.dependency-management' version '1.0.9.RELEASE'
+ id "de.aaschmid.cpd" version "3.1"
+ id 'java'
+}
+group = 'ch.usi.inf.sa4.sanmarinoes'
+version = '0.0.1-SNAPSHOT'
+sourceCompatibility = '11'
+repositories {
+ mavenCentral()
+}
+dependencies {
+ 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 'com.sun.mail:javax.mail:1.6.2'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-mail'
+ implementation 'org.springframework.boot:spring-boot-starter-websocket'
+ implementation 'org.springframework:spring-websocket:5.2.4.RELEASE'
+ implementation 'io.jsonwebtoken:jjwt:0.9.1'
+ implementation 'org.springframework.security:spring-security-web'
+ implementation 'org.postgresql:postgresql'
+ implementation 'com.google.code.gson:gson'
+ 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'
+ testCompile 'org.springframework.boot:spring-boot-starter-webflux'
+ implementation('org.springframework.boot:spring-boot-starter-web') {
+ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json'
+ }
+ testImplementation('org.springframework.boot:spring-boot-starter-test') {
+ 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 {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/docs/er.pdf b/docs/er.pdf
new file mode 100644
index 0000000..9735d6a
Binary files /dev/null and b/docs/er.pdf differ
diff --git a/git-hooks/format.sh b/git-hooks/format.sh
new file mode 100755
index 0000000..c972530
--- /dev/null
+++ b/git-hooks/format.sh
@@ -0,0 +1,14 @@
+#!/bin/sh -e
+jar_version=1.6
+jar_dir="$HOME/.local/share/java"
+jar_file="$jar_dir/google-java-format-$jar_version-all-deps.jar"
+java_cmd="java"
+
+# download jar file if missing
+if [ ! -f "$jar_file" ]; then
+ mkdir -p "$jar_dir"
+ wget -O "$jar_file" https://github.com/google/google-java-format/releases/download/google-java-format-$jar_version/google-java-format-$jar_version-all-deps.jar
+fi
+
+# execute formatter
+$java_cmd -jar "$jar_file" $@
diff --git a/git-hooks/pre-commit.sh b/git-hooks/pre-commit.sh
new file mode 100755
index 0000000..472c158
--- /dev/null
+++ b/git-hooks/pre-commit.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+set -e
+
+echo "Java formatter running..."
+
+format_cmd="$(dirname $(realpath "$0"))/format.sh"
+
+# skip if NO_VERIFY env var set
+if [ "$NO_VERIFY" ]; then
+ echo 'google-java-format skipped' 1>&2
+ exit 0
+fi
+
+# list all added/copied/modified/renamed java files
+files="`git diff --staged --name-only --diff-filter=ACMR | egrep -a '.java$' | tr \"\\n\" \" \"`"
+for f in $files; do
+ $format_cmd --aosp -i "$f"
+ git add -f "$f"
+done
diff --git a/git-hooks/setup.sh b/git-hooks/setup.sh
new file mode 100755
index 0000000..b04c8d4
--- /dev/null
+++ b/git-hooks/setup.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+if ! git remote get-url origin | grep "lab.si.usi.ch" >/dev/null 2>/dev/null; then
+ echo "Not in the project!"
+ echo "Call this script while in the root directory of the backend project";
+ exit 1;
+elif ! [ -d "./git-hooks" ]; then
+ echo "Not in the right directory!"
+ echo "Call this script while in the root directory of the backend project";
+ exit 1;
+fi;
+
+git config --unset core.hooksPath
+
+this_dir="$(dirname $(realpath "$0"))"
+hook_script="$this_dir/pre-commit.sh"
+ln -svf "$hook_script" "$this_dir/../.git/hooks/pre-commit"
+
+echo "Commit hook installed"
diff --git a/gradle.yml b/gradle.yml
new file mode 100644
index 0000000..51fefa4
--- /dev/null
+++ b/gradle.yml
@@ -0,0 +1,39 @@
+# vim: set ts=2 sw=2 et tw=80:
+image: gradle:jdk13
+
+stages:
+ - build
+ - test
+ - deploy
+
+smarthut_build:
+ stage: build
+ script:
+ - gradle assemble
+ artifacts:
+ paths:
+ - build/libs/*.jar
+ expire_in: 1 week
+
+smarthut_test:
+ stage: test
+ script:
+ - gradle check
+
+smarthut_deploy:
+ stage: deploy
+ image: docker:latest
+ services:
+ - docker:dind
+ variables:
+ DOCKER_DRIVER: overlay
+ before_script:
+ - docker version
+ - docker info
+ - docker login -u smarthutsm -p $CI_DOCKER_PASS
+ script:
+ - "docker build -t smarthutsm/smarthut:${CI_COMMIT_BRANCH} --pull ."
+ - "docker push smarthutsm/smarthut:${CI_COMMIT_BRANCH}"
+ after_script:
+ - docker logout
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f3d88b1
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a2bf131
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..24ca0a7
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'smarthut'
diff --git a/socket_test.html b/socket_test.html
new file mode 100644
index 0000000..687388b
--- /dev/null
+++ b/socket_test.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
Waiting for authentication...
+
+
+
+
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/Service/EmailSenderService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/Service/EmailSenderService.java
new file mode 100644
index 0000000..a51eb5a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/Service/EmailSenderService.java
@@ -0,0 +1,23 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Service("emailSenderService")
+public class EmailSenderService {
+
+ private JavaMailSender javaMailSender;
+
+ @Autowired
+ public EmailSenderService(JavaMailSender javaMailSender) {
+ this.javaMailSender = javaMailSender;
+ }
+
+ @Async
+ public void sendEmail(SimpleMailMessage email) {
+ javaMailSender.send(email);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java
new file mode 100644
index 0000000..57f7b42
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java
@@ -0,0 +1,15 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication
+@EnableScheduling
+@EnableJpaRepositories("ch.usi.inf.sa4.sanmarinoes.smarthut.models")
+public class SmarthutApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SmarthutApplication.class, args);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java
new file mode 100644
index 0000000..d5e19ae
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java
@@ -0,0 +1,40 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import java.io.IOException;
+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 {
+
+ public static void setCORSHeaders(HttpServletResponse response) {
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("Access-Control-Allow-Methods", "*");
+ response.setHeader("Access-Control-Allow-Headers", "*");
+ response.setHeader("Access-Control-Allow-Credentials", "true");
+ response.setHeader("Access-Control-Expose-Headers", "*");
+ response.setHeader("Access-Control-Max-Age", "6".repeat(99));
+ }
+
+ @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() {}
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java
new file mode 100644
index 0000000..a26aeeb
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java
@@ -0,0 +1,112 @@
+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 URL to follow for password reset email confirmation. Has to end with the start of a query
+ * parameter
+ */
+ @NotNull private String resetPasswordPath;
+
+ /** 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;
+
+ @NotNull private String resetPasswordRedirect;
+
+ @NotNull private String registrationRedirect;
+
+ 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;
+ }
+
+ public String getResetPasswordRedirect() {
+ return resetPasswordRedirect;
+ }
+
+ public void setResetPasswordRedirect(String resetPasswordRedirect) {
+ this.resetPasswordRedirect = resetPasswordRedirect;
+ }
+
+ public String getRegistrationRedirect() {
+ return registrationRedirect;
+ }
+
+ public void setRegistrationRedirect(String registrationRedirect) {
+ this.registrationRedirect = registrationRedirect;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java
new file mode 100644
index 0000000..69c4fc9
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java
@@ -0,0 +1,50 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import com.google.gson.*;
+import java.lang.reflect.Type;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.GsonHttpMessageConverter;
+import springfox.documentation.spring.web.json.Json;
+
+/**
+ * Spring configuration in order to register the GSON type adapter needed to avoid serializing twice
+ * Springfox Swagger JSON output (see: https://stackoverflow.com/a/30220562)
+ */
+@Configuration
+public class GsonConfig {
+ @Bean
+ public GsonHttpMessageConverter gsonHttpMessageConverter() {
+ GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
+ converter.setGson(gson());
+ return converter;
+ }
+
+ public static Gson gson() {
+ final GsonBuilder builder = new GsonBuilder();
+ builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter());
+ builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy());
+ return builder.create();
+ }
+}
+
+/** GSON type adapter needed to avoid serializing twice Springfox Swagger JSON output */
+class SpringfoxJsonToGsonAdapter implements JsonSerializer {
+ @Override
+ public JsonElement serialize(Json json, Type type, JsonSerializationContext context) {
+ return JsonParser.parseString(json.value());
+ }
+}
+
+/** 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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonExclude.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonExclude.java
new file mode 100644
index 0000000..1be5551
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonExclude.java
@@ -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 {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTAuthenticationEntryPoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTAuthenticationEntryPoint.java
new file mode 100644
index 0000000..5c91217
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTAuthenticationEntryPoint.java
@@ -0,0 +1,25 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+ @Override
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException)
+ throws IOException {
+ if (!"OPTIONS".equals(request.getMethod())) {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+ } else {
+ CORSFilter.setCORSHeaders(response);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java
new file mode 100644
index 0000000..e0cbb6a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java
@@ -0,0 +1,68 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.JWTUserDetailsService;
+import io.jsonwebtoken.ExpiredJwtException;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+@Component
+public class JWTRequestFilter extends OncePerRequestFilter {
+
+ @Autowired private JWTUserDetailsService jwtUserDetailsService;
+
+ @Autowired private JWTTokenUtils jwtTokenUtils;
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ final String requestTokenHeader = request.getHeader("Authorization");
+ String username = null;
+ String jwtToken = null;
+
+ // JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
+ if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
+ jwtToken = requestTokenHeader.substring(7);
+ try {
+ username = jwtTokenUtils.getUsernameFromToken(jwtToken);
+ } catch (IllegalArgumentException e) {
+ System.out.println("Unable to get JWT Token");
+ } catch (ExpiredJwtException e) {
+ System.out.println("JWT Token has expired");
+ }
+ } else {
+ logger.warn("JWT Token does not begin with Bearer String");
+ }
+
+ // Once we get the token validate it.
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails =
+ this.jwtUserDetailsService.loadUserByUsername(
+ username); // if token is valid configure Spring Security to manually
+ // set authentication
+ if (jwtTokenUtils.validateToken(jwtToken, userDetails)) {
+ UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
+ new UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.getAuthorities());
+ usernamePasswordAuthenticationToken.setDetails(
+ new WebAuthenticationDetailsSource().buildDetails(request));
+ // After setting the Authentication in the context, we specify
+ // that the current user is authenticated. So it passes the
+ // Spring Security Configurations successfully.
+ SecurityContextHolder.getContext()
+ .setAuthentication(usernamePasswordAuthenticationToken);
+ }
+ }
+ chain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java
new file mode 100644
index 0000000..f6943a8
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java
@@ -0,0 +1,84 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.function.Function;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+/** A utility class to handle JWTs */
+@Component
+public class JWTTokenUtils {
+ /** The duration in seconds of the validity of a single token */
+ private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
+
+ /** The secret key used to encrypt all JWTs */
+ @Value("${jwt.secret}")
+ private String secret;
+
+ /**
+ * Retrieves the claimed username from a given token
+ *
+ * @param token the token to inspect
+ * @return the username
+ */
+ public String getUsernameFromToken(String token) {
+ return getClaimFromToken(token, Claims::getSubject);
+ }
+
+ /**
+ * Returns whether the token given is expired or not
+ *
+ * @param token the given token
+ * @return true if expired, false if not
+ */
+ public Boolean isTokenExpired(String token) {
+ final Date expiration = getClaimFromToken(token, Claims::getExpiration);
+ return expiration.before(new Date());
+ }
+
+ /**
+ * Creates a new JWT for a given user. While creating the token - 1. Define claims of the token,
+ * like Issuer, Expiration, Subject, and the ID 2. Sign the JWT using the HS512 algorithm and
+ * secret key. 3. According to JWS Compact Serialization
+ * (https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) compaction of
+ * the JWT to a URL-safe string
+ *
+ * @param user the user to which create a JWT
+ * @return the newly generated token
+ */
+ public String generateToken(UserDetails user) {
+ return Jwts.builder()
+ .setClaims(new HashMap<>())
+ .setSubject(user.getUsername())
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
+ .signWith(SignatureAlgorithm.HS512, secret)
+ .compact();
+ }
+
+ /**
+ * Validates the token given against matching userDetails
+ *
+ * @param token the token given
+ * @param userDetails user details to validate against
+ * @return true if valid, false if not
+ */
+ public Boolean validateToken(String token, UserDetails userDetails) {
+ final String username = getUsernameFromToken(token);
+ return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+ }
+
+ private T getClaimFromToken(String token, Function claimsResolver) {
+ final Claims claims = getAllClaimsFromToken(token);
+ return claimsResolver.apply(claims);
+ }
+
+ private Claims getAllClaimsFromToken(String token) {
+ return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java
new file mode 100644
index 0000000..971a7fb
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java
@@ -0,0 +1,105 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import java.util.List;
+import java.util.function.Predicate;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * This class configures the automated REST documentation tool Swagger for this project. The
+ * documentation can be seen by going to http://localhost:8080/swaggeer-ui.html
+ */
+@Configuration
+@EnableSwagger2
+@ComponentScan("ch.usi.inf.sa4.sanmarinoes.smarthut")
+public class SpringFoxConfig {
+
+ /**
+ * Main definition of Springfox / swagger configuration
+ *
+ * @return a Docket object containing the swagger configuration
+ */
+ @Bean
+ public Docket api() {
+ return new Docket(DocumentationType.SWAGGER_2)
+ .select()
+ .apis(RequestHandlerSelectors.any())
+ .paths(paths()::test)
+ .build()
+ .apiInfo(apiInfo())
+ .securitySchemes(securitySchemes())
+ .securityContexts(List.of(securityContext()));
+ }
+
+ /**
+ * Configures the documentation about the smarthut authentication system
+ *
+ * @return a list of springfox authentication configurations
+ */
+ private static List extends SecurityScheme> securitySchemes() {
+ return List.of(new ApiKey("Bearer", "Authorization", "header"));
+ }
+
+ private SecurityContext securityContext() {
+ return SecurityContext.builder()
+ .securityReferences(defaultAuth())
+ .forPaths(authenticatedPaths()::test)
+ .build();
+ }
+
+ private List defaultAuth() {
+ final AuthorizationScope authorizationScope =
+ new AuthorizationScope("global", "accessEverything");
+ return List.of(
+ new SecurityReference("Bearer", new AuthorizationScope[] {authorizationScope}));
+ }
+
+ private Predicate authenticatedPaths() {
+ return ((Predicate) 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)
+ .or(PathSelectors.regex("/auth/profile.*")::apply);
+ }
+
+ /**
+ * Configures the paths the documentation must be generated for. Add a path here only when the
+ * spec has been totally defined.
+ *
+ * @return A predicate that tests whether a path must be included or not
+ */
+ private Predicate paths() {
+ return PathSelectors.any()::apply;
+ }
+
+ /**
+ * Returns the metadata about the smarthut project
+ *
+ * @return metadata about smarthut
+ */
+ private ApiInfo apiInfo() {
+ return new ApiInfoBuilder()
+ .title("SmartHut.sm API")
+ .description("Backend API for the SanMariones version of the SA4 SmartHut project")
+ .termsOfServiceUrl("https://www.youtube.com/watch?v=9KxTcDsy9Gs")
+ .license("WTFPL")
+ .version("dev branch")
+ .build();
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
new file mode 100644
index 0000000..ec116c3
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
@@ -0,0 +1,83 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.JWTUserDetailsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+ @Autowired private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
+ @Autowired private JWTUserDetailsService jwtUserDetailsService;
+ @Autowired private JWTRequestFilter jwtRequestFilter;
+
+ @Autowired
+ public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
+ // configure AuthenticationManager so that it knows from where to load
+ // user for matching credentials
+ // Use BCryptPasswordEncoder
+ auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+
+ @Override
+ protected void configure(HttpSecurity httpSecurity) throws Exception {
+ // We don't need CSRF for this example
+ httpSecurity
+ .csrf()
+ .disable()
+ // dont authenticate this particular request
+ .authorizeRequests()
+ .antMatchers(
+ "/sensor-socket",
+ "/auth/login",
+ "/swagger-ui.html",
+ "/register",
+ "/register/confirm-account",
+ "/register/init-reset-password",
+ "/register/reset-password",
+ "/v2/api-docs",
+ "/webjars/**",
+ "/swagger-resources/**",
+ "/csrf")
+ .permitAll()
+ // all other requests need to be authenticated
+ .anyRequest()
+ .authenticated()
+ .and()
+ .
+ // make sure we use stateless session; session won't be used to
+ // store user's state.
+ exceptionHandling()
+ .authenticationEntryPoint(jwtAuthenticationEntryPoint)
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(
+ SessionCreationPolicy
+ .STATELESS); // Add a filter to validate the tokens with every
+ // request
+ httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
new file mode 100644
index 0000000..ad48da2
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
@@ -0,0 +1,88 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
+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.error.UnauthorizedException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import java.security.Principal;
+import javax.validation.Valid;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/auth")
+public class AuthenticationController {
+
+ private final AuthenticationManager authenticationManager;
+
+ private final UserRepository userRepository;
+
+ private final JWTTokenUtils jwtTokenUtils;
+
+ private final JWTUserDetailsService userDetailsService;
+
+ private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+
+ public AuthenticationController(
+ AuthenticationManager authenticationManager,
+ UserRepository userRepository,
+ JWTTokenUtils jwtTokenUtils,
+ JWTUserDetailsService userDetailsService) {
+ this.authenticationManager = authenticationManager;
+ this.userRepository = userRepository;
+ this.jwtTokenUtils = jwtTokenUtils;
+ this.userDetailsService = userDetailsService;
+ }
+
+ @PostMapping("/login")
+ public JWTResponse login(@Valid @RequestBody JWTRequest authenticationRequest)
+ throws UnauthorizedException, UserNotFoundException {
+ final UserDetails userDetails;
+ 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 = jwtTokenUtils.generateToken(userDetails);
+ return new JWTResponse(token);
+ }
+
+ @GetMapping("/profile")
+ public User profile(final Principal principal) {
+ return userRepository.findByUsername(principal.getName());
+ }
+
+ private void authenticate(String username, String password) throws UnauthorizedException {
+ try {
+ authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(username, password));
+ } catch (DisabledException e) {
+ throw new UnauthorizedException(true);
+ } catch (BadCredentialsException e) {
+ throw new UnauthorizedException(false);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java
new file mode 100644
index 0000000..944bd8e
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java
@@ -0,0 +1,96 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerDimRequest;
+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.*;
+import java.security.Principal;
+import java.util.List;
+import java.util.Set;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/buttonDimmer")
+public class ButtonDimmerController
+ extends InputDeviceConnectionController {
+ 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
+ public List findAll() {
+ return toList(buttonDimmerRepository.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public ButtonDimmer findById(@PathVariable("id") long id) throws NotFoundException {
+ return buttonDimmerRepository.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ @PostMapping
+ public ButtonDimmer create(@Valid @RequestBody final GenericDeviceSaveReguest bd) {
+ ButtonDimmer newBD = new ButtonDimmer();
+ newBD.setName(bd.getName());
+ newBD.setRoomId(bd.getRoomId());
+
+ return buttonDimmerRepository.save(newBD);
+ }
+
+ @PutMapping("/dim")
+ public Set dim(
+ @Valid @RequestBody final ButtonDimmerDimRequest bd, final Principal principal)
+ throws NotFoundException {
+ final ButtonDimmer buttonDimmer =
+ buttonDimmerRepository
+ .findByIdAndUsername(bd.getId(), principal.getName())
+ .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}")
+ public void delete(@PathVariable("id") long id) {
+ buttonDimmerRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java
new file mode 100644
index 0000000..17bdec7
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java
@@ -0,0 +1,49 @@
+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 java.security.Principal;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/device")
+public class DeviceController {
+
+ @Autowired private DeviceRepository deviceRepository;
+ @Autowired private RoomRepository roomRepository;
+
+ @GetMapping
+ public List getAll(final Principal user) {
+ return deviceRepository.findAllByUsername(user.getName());
+ }
+
+ @PutMapping
+ public Device update(
+ @Valid @RequestBody DeviceSaveRequest deviceSaveRequest, final Principal principal)
+ throws NotFoundException, BadDataException {
+ final Device d =
+ deviceRepository
+ .findByIdAndUsername(deviceSaveRequest.getId(), principal.getName())
+ .orElseThrow(NotFoundException::new);
+
+ // check if roomId is valid
+ roomRepository
+ .findByIdAndUsername(deviceSaveRequest.getRoomId(), principal.getName())
+ .orElseThrow(() -> new BadDataException("roomId is not a valid room id"));
+
+ d.setRoomId(deviceSaveRequest.getRoomId());
+ d.setName(deviceSaveRequest.getName());
+
+ deviceRepository.save(d);
+ return d;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java
new file mode 100644
index 0000000..944b6c2
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java
@@ -0,0 +1,61 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+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.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLightRepository;
+import java.security.Principal;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/dimmableLight")
+public class DimmableLightController {
+
+ @Autowired private DimmableLightRepository dimmableLightService;
+
+ @GetMapping
+ public List findAll() {
+ return toList(dimmableLightService.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public DimmableLight findById(@PathVariable("id") long id) throws NotFoundException {
+ 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
+ public DimmableLight create(@Valid @RequestBody DimmableLightSaveRequest dl) {
+ return save(new DimmableLight(), dl);
+ }
+
+ @PutMapping
+ public DimmableLight update(
+ @Valid @RequestBody DimmableLightSaveRequest sp, final Principal principal)
+ throws NotFoundException {
+ return save(
+ dimmableLightService
+ .findByIdAndUsername(sp.getId(), principal.getName())
+ .orElseThrow(NotFoundException::new),
+ sp);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable("id") long id) {
+ dimmableLightService.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java
new file mode 100644
index 0000000..52f3483
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java
@@ -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 the type of device this controller is for
+ * @param 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 inputRepository;
+
+ private DeviceRepository outputReposiory;
+
+ private Connector 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 inputRepository,
+ DeviceRepository outputRepository,
+ Connector 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();
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java
new file mode 100644
index 0000000..c15d867
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java
@@ -0,0 +1,89 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerDimRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import java.security.Principal;
+import java.util.List;
+import java.util.Set;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/knobDimmer")
+public class KnobDimmerController
+ extends InputDeviceConnectionController {
+
+ @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
+ public List findAll() {
+ return toList(knobDimmerRepository.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public KnobDimmer findById(@PathVariable("id") long id) throws NotFoundException {
+ return knobDimmerRepository.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ @PostMapping
+ public KnobDimmer create(@Valid @RequestBody GenericDeviceSaveReguest kd) {
+ KnobDimmer newKD = new KnobDimmer();
+ newKD.setName(kd.getName());
+ newKD.setRoomId(kd.getRoomId());
+
+ return knobDimmerRepository.save(newKD);
+ }
+
+ @PutMapping("/dimTo")
+ public Set dimTo(
+ @Valid @RequestBody final KnobDimmerDimRequest bd, final Principal principal)
+ throws NotFoundException {
+ final KnobDimmer dimmer =
+ knobDimmerRepository
+ .findByIdAndUsername(bd.getId(), principal.getName())
+ .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}")
+ public void delete(@PathVariable("id") long id) {
+ knobDimmerRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java
new file mode 100644
index 0000000..59c0343
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java
@@ -0,0 +1,79 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
+
+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.MotionSensorRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
+import java.security.Principal;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/motionSensor")
+public class MotionSensorController {
+
+ @Autowired private MotionSensorRepository motionSensorService;
+
+ @Autowired private SensorSocketEndpoint sensorSocketEndpoint;
+
+ @GetMapping
+ public List findAll() {
+ return toList(motionSensorService.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public MotionSensor findById(@PathVariable("id") long id) throws NotFoundException {
+ return motionSensorService.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ @PostMapping
+ public MotionSensor create(@Valid @RequestBody GenericDeviceSaveReguest ms) {
+ MotionSensor newMS = new MotionSensor();
+ newMS.setName(ms.getName());
+ newMS.setRoomId(ms.getRoomId());
+
+ return motionSensorService.save(newMS);
+ }
+
+ /**
+ * Updates detection status of given motion sensor and propagates update throgh socket
+ *
+ * @param sensor the motion sensor to update
+ * @param detected the new detection status
+ * @return the updated motion sensor
+ */
+ public MotionSensor updateDetectionFromMotionSensor(MotionSensor sensor, boolean detected) {
+ sensor.setDetected(detected);
+ final MotionSensor toReturn = motionSensorService.save(sensor);
+
+ sensorSocketEndpoint.broadcast(sensor, motionSensorService.findUser(sensor.getId()));
+
+ return toReturn;
+ }
+
+ @PutMapping("/{id}/detect")
+ public MotionSensor updateDetection(
+ @PathVariable("id") Long sensorId,
+ @RequestParam("detected") boolean detected,
+ final Principal principal)
+ throws NotFoundException {
+
+ return updateDetectionFromMotionSensor(
+ motionSensorService
+ .findByIdAndUsername(sensorId, principal.getName())
+ .orElseThrow(NotFoundException::new),
+ detected);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable("id") long id) {
+ motionSensorService.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java
new file mode 100644
index 0000000..e033555
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java
@@ -0,0 +1,68 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+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.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLightRepository;
+import java.security.Principal;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+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("/regularLight")
+public class RegularLightController {
+
+ @Autowired private RegularLightRepository regularLightService;
+
+ @GetMapping
+ public List findAll() {
+ return toList(regularLightService.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public RegularLight findById(@PathVariable("id") long id) throws NotFoundException {
+ return regularLightService.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ private RegularLight save(RegularLight newRL, RegularLightSaveRequest rl) {
+ newRL.setName(rl.getName());
+ newRL.setRoomId(rl.getRoomId());
+ newRL.setOn(rl.isOn());
+
+ return regularLightService.save(newRL);
+ }
+
+ @PostMapping
+ public RegularLight create(@Valid @RequestBody RegularLightSaveRequest rl) {
+ return save(new RegularLight(), rl);
+ }
+
+ @PutMapping
+ public RegularLight update(
+ @Valid @RequestBody RegularLightSaveRequest rl, final Principal principal)
+ throws NotFoundException {
+ return save(
+ regularLightService
+ .findByIdAndUsername(rl.getId(), principal.getName())
+ .orElseThrow(NotFoundException::new),
+ rl);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable("id") long id) {
+ regularLightService.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java
new file mode 100644
index 0000000..2bfa440
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java
@@ -0,0 +1,104 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+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.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import java.security.Principal;
+import java.util.*;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/room")
+public class RoomController {
+
+ @Autowired private RoomRepository roomRepository;
+
+ @Autowired private UserRepository userRepository;
+
+ @Autowired private DeviceRepository deviceRepository;
+
+ @Autowired private SwitchRepository switchRepository;
+
+ @Autowired private ButtonDimmerRepository buttonDimmerRepository;
+
+ @Autowired private KnobDimmerRepository knobDimmerRepository;
+
+ @GetMapping
+ public List findAll() {
+ return toList(roomRepository.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public @ResponseBody Room findById(@PathVariable("id") long id) throws NotFoundException {
+ return roomRepository.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ @PostMapping
+ public @ResponseBody Room create(
+ @Valid @RequestBody RoomSaveRequest r, final Principal principal) {
+
+ final String username = principal.getName();
+ final Long userId = userRepository.findByUsername(username).getId();
+ final String img = r.getImage();
+ final Room.Icon icon = r.getIcon();
+
+ final Room newRoom = new Room();
+ newRoom.setUserId(userId);
+ newRoom.setName(r.getName());
+ newRoom.setImage(img);
+ newRoom.setIcon(icon);
+
+ return roomRepository.save(newRoom);
+ }
+
+ @PutMapping("/{id}")
+ public @ResponseBody Room update(
+ @PathVariable("id") long id, @RequestBody RoomSaveRequest r, final Principal principal)
+ throws NotFoundException {
+ final Room newRoom =
+ roomRepository
+ .findByIdAndUsername(id, principal.getName())
+ .orElseThrow(NotFoundException::new);
+ final String img = r.getImage();
+ final Room.Icon icon = r.getIcon();
+
+ if (r.getName() != null) {
+ newRoom.setName(r.getName());
+ }
+
+ if ("".equals(img)) {
+ newRoom.setImage(null);
+ } else if (img != null) {
+ newRoom.setImage(img);
+ }
+
+ if (icon != null) {
+ newRoom.setIcon(icon);
+ }
+
+ return roomRepository.save(newRoom);
+ }
+
+ @DeleteMapping("/{id}")
+ public void deleteById(@PathVariable("id") long id) {
+ switchRepository.deleteAllByRoomId(id);
+ knobDimmerRepository.deleteAllByRoomId(id);
+ buttonDimmerRepository.deleteAllByRoomId(id);
+ roomRepository.deleteById(id);
+ }
+
+ /**
+ * Returns a List of all Devices that are present in a given room (identified by its
+ * id).
+ */
+ @GetMapping(path = "/{roomId}/devices")
+ public List getDevices(@PathVariable("roomId") long roomid) {
+ return deviceRepository.findByRoomId(roomid);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java
new file mode 100644
index 0000000..ee1be81
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java
@@ -0,0 +1,81 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+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.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
+import java.math.BigDecimal;
+import java.security.Principal;
+import java.util.*;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/sensor")
+public class SensorController {
+
+ @Autowired private SensorRepository sensorRepository;
+
+ @Autowired private SensorSocketEndpoint sensorSocketEndpoint;
+
+ @GetMapping
+ public List findAll() {
+ return toList(sensorRepository.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public Sensor findById(@PathVariable("id") long id) throws NotFoundException {
+ return sensorRepository.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ @PostMapping
+ public Sensor create(@Valid @RequestBody SensorSaveRequest s) {
+ Sensor newSensor = new Sensor();
+ newSensor.setSensor(s.getSensor());
+ newSensor.setName(s.getName());
+ newSensor.setRoomId(s.getRoomId());
+ newSensor.setValue(s.getValue());
+
+ return sensorRepository.save(newSensor);
+ }
+
+ /**
+ * Updates the sensor with new measurement and propagates update through websocket
+ *
+ * @param sensor the sensor to update
+ * @param value the new measurement
+ * @return the updated sensor
+ */
+ public Sensor updateValueFromSensor(Sensor sensor, BigDecimal value) {
+ sensor.setValue(value);
+ final Sensor toReturn = sensorRepository.save(sensor);
+
+ sensorSocketEndpoint.broadcast(sensor, sensorRepository.findUser(sensor.getId()));
+
+ return toReturn;
+ }
+
+ @PutMapping("/{id}/value")
+ public Sensor updateValue(
+ @PathVariable("id") Long sensorId,
+ @RequestParam("value") BigDecimal value,
+ final Principal principal)
+ throws NotFoundException {
+ return updateValueFromSensor(
+ sensorRepository
+ .findByIdAndUsername(sensorId, principal.getName())
+ .orElseThrow(NotFoundException::new),
+ value);
+ }
+
+ @DeleteMapping("/{id}")
+ public void deleteById(@PathVariable("id") long id) {
+ sensorRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java
new file mode 100644
index 0000000..5ef4eed
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java
@@ -0,0 +1,73 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+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.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import java.security.Principal;
+import java.util.*;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/smartPlug")
+public class SmartPlugController {
+
+ @Autowired private SmartPlugRepository smartPlugRepository;
+
+ @GetMapping
+ public List findAll() {
+ return toList(smartPlugRepository.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public SmartPlug findById(@PathVariable("id") long id) throws NotFoundException {
+ return smartPlugRepository.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ private SmartPlug save(SmartPlug newSP, SmartPlugSaveRequest sp) {
+ newSP.setOn(sp.isOn());
+ newSP.setId(sp.getId());
+ newSP.setName(sp.getName());
+ newSP.setRoomId(sp.getRoomId());
+
+ return smartPlugRepository.save(newSP);
+ }
+
+ @PostMapping
+ public SmartPlug create(@Valid @RequestBody SmartPlugSaveRequest sp) {
+ return save(new SmartPlug(), sp);
+ }
+
+ @PutMapping
+ public SmartPlug update(@Valid @RequestBody SmartPlugSaveRequest sp, final Principal principal)
+ throws NotFoundException {
+ return save(
+ smartPlugRepository
+ .findByIdAndUsername(sp.getId(), principal.getName())
+ .orElseThrow(NotFoundException::new),
+ sp);
+ }
+
+ @DeleteMapping("/{id}/meter")
+ public SmartPlug resetMeter(@PathVariable("id") long id, final Principal principal)
+ throws NotFoundException {
+ final SmartPlug s =
+ smartPlugRepository
+ .findByIdAndUsername(id, principal.getName())
+ .orElseThrow(NotFoundException::new);
+
+ s.resetTotalConsumption();
+ return smartPlugRepository.save(s);
+ }
+
+ @DeleteMapping("/{id}")
+ public void deleteById(@PathVariable("id") long id) {
+ smartPlugRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java
new file mode 100644
index 0000000..fc64ccb
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java
@@ -0,0 +1,102 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
+
+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 java.security.Principal;
+import java.util.*;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/switch")
+public class SwitchController extends InputDeviceConnectionController {
+
+ private SwitchRepository switchRepository;
+ private SwitchableRepository 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 outputRepository) {
+ super(inputRepository, outputRepository, Switchable.SWITCH_SWITCHABLE_CONNECTOR);
+ this.switchRepository = inputRepository;
+ this.switchableRepository = outputRepository;
+ }
+
+ @GetMapping
+ public List findAll() {
+ return toList(switchRepository.findAll());
+ }
+
+ @GetMapping("/{id}")
+ public Switch findById(@PathVariable("id") long id) throws NotFoundException {
+ return switchRepository.findById(id).orElseThrow(NotFoundException::new);
+ }
+
+ @PostMapping
+ public Switch create(@Valid @RequestBody GenericDeviceSaveReguest s) {
+ Switch newSwitch = new Switch();
+ newSwitch.setName(s.getName());
+ newSwitch.setRoomId(s.getRoomId());
+
+ return switchRepository.save(newSwitch);
+ }
+
+ @PutMapping("/operate")
+ public Set operate(
+ @Valid @RequestBody final SwitchOperationRequest sr, final Principal principal)
+ throws NotFoundException {
+ final Switch s =
+ switchRepository
+ .findByIdAndUsername(sr.getId(), principal.getName())
+ .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}")
+ public void deleteById(@PathVariable("id") long id) {
+ switchRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
new file mode 100644
index 0000000..950fb1a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
@@ -0,0 +1,209 @@
+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.PasswordResetRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.EmailTokenNotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationToken;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.service.EmailSenderService;
+import java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+
+/** Unauthenticated set of endpoints to handle registration and password reset */
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/register")
+public class UserAccountController {
+
+ private final UserRepository userRepository;
+
+ private final ConfirmationTokenRepository confirmationTokenRepository;
+
+ private final EmailSenderService emailSenderService;
+
+ 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
+ public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
+ throws DuplicateRegistrationException {
+ final User existingEmailUser =
+ userRepository.findByEmailIgnoreCase(registrationData.getEmail());
+ final User existingUsernameUser =
+ userRepository.findByUsername(registrationData.getUsername());
+
+ // Check if an User with the same email already exists
+ if (existingEmailUser != null || existingUsernameUser != null) {
+ throw new DuplicateRegistrationException();
+ } else {
+ final User toSave = new User();
+ // disable the user (it will be enabled on email confiration)
+ toSave.setEnabled(false);
+
+ // encode user's password
+ toSave.setPassword(encoder.encode(registrationData.getPassword()));
+
+ // set other fields
+ toSave.setName(registrationData.getName());
+ toSave.setUsername(registrationData.getUsername());
+ toSave.setEmail(registrationData.getEmail());
+ userRepository.save(toSave);
+
+ ConfirmationToken token;
+ do {
+ token = new ConfirmationToken(toSave);
+ } while (confirmationTokenRepository.findByConfirmationToken(
+ token.getConfirmationToken())
+ != null);
+
+ confirmationTokenRepository.save(token);
+
+ sendEmail(toSave.getEmail(), token, true);
+
+ 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,
+ final HttpServletResponse response)
+ throws EmailTokenNotFoundException, IOException {
+ 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")
+ public void confirmUserAccount(
+ @RequestParam("token") @NotNull String confirmationToken,
+ final HttpServletResponse response)
+ throws EmailTokenNotFoundException, IOException {
+ final ConfirmationToken token =
+ confirmationTokenRepository.findByConfirmationToken(confirmationToken);
+
+ if (token != null && !token.getResetPassword()) {
+ token.getUser().setEnabled(true);
+ userRepository.save(token.getUser());
+ response.sendRedirect(emailConfig.getRegistrationRedirect());
+ } else {
+ throw new EmailTokenNotFoundException();
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java
new file mode 100644
index 0000000..8e07015
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java
@@ -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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java
new file mode 100644
index 0000000..a975117
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java
@@ -0,0 +1,42 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+public class DeviceSaveRequest {
+ /** 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 @NotEmpty private String name;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public Long getRoomId() {
+ return roomId;
+ }
+
+ public void setRoomId(Long roomId) {
+ this.roomId = roomId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java
new file mode 100644
index 0000000..74e911b
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java
@@ -0,0 +1,58 @@
+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 DimmableLightSaveRequest {
+
+ /** Device id (used only for update requests) */
+ private Long id;
+
+ /** The light intensity value. Goes from 0 (off) to 100 (on) */
+ @NotNull
+ @Min(0)
+ @Max(100)
+ private Integer intensity = 0;
+
+ /**
+ * 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 setRoomId(Long roomId) {
+ this.roomId = roomId;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getRoomId() {
+ return roomId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Integer getIntensity() {
+ return intensity;
+ }
+
+ public void setIntensity(Integer intensity) {
+ this.intensity = intensity;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java
new file mode 100644
index 0000000..8ec2671
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java
@@ -0,0 +1,30 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.NotNull;
+
+public class GenericDeviceSaveReguest {
+ /**
+ * 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 setRoomId(Long roomId) {
+ this.roomId = roomId;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getRoomId() {
+ return roomId;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
new file mode 100644
index 0000000..d82c4f0
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
@@ -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 >input type="email"<>
+ *
, 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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java
new file mode 100644
index 0000000..da11bc3
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java
@@ -0,0 +1,36 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.NotNull;
+
+public class JWTRequest {
+ @NotNull private String usernameOrEmail;
+ @NotNull private String password;
+
+ public String getUsernameOrEmail() {
+ return this.usernameOrEmail;
+ }
+
+ public void setUsernameOrEmail(String usernameOrEmail) {
+ this.usernameOrEmail = usernameOrEmail;
+ }
+
+ public String getPassword() {
+ return this.password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ @Override
+ public String toString() {
+ return "JWTRequest{"
+ + "usernameOrEmail='"
+ + usernameOrEmail
+ + '\''
+ + ", password='"
+ + password
+ + '\''
+ + '}';
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java
new file mode 100644
index 0000000..7bc04f2
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java
@@ -0,0 +1,13 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+public class JWTResponse {
+ private final String jwttoken;
+
+ public JWTResponse(String jwttoken) {
+ this.jwttoken = jwttoken;
+ }
+
+ public String getToken() {
+ return this.jwttoken;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java
new file mode 100644
index 0000000..6df303a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java
@@ -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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/OkResponse.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/OkResponse.java
new file mode 100644
index 0000000..e3de94e
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/OkResponse.java
@@ -0,0 +1,6 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+/** A dummy DTO to return when there is no data to return */
+public class OkResponse {
+ private boolean success = true;
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
new file mode 100644
index 0000000..bf5bccf
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
@@ -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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java
new file mode 100644
index 0000000..99211e5
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java
@@ -0,0 +1,52 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.NotNull;
+
+public class RegularLightSaveRequest {
+ /** The state of this switch */
+ private boolean on;
+
+ /** 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 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 boolean isOn() {
+ return on;
+ }
+
+ public void setOn(boolean on) {
+ this.on = on;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java
new file mode 100644
index 0000000..02a0e35
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java
@@ -0,0 +1,56 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room;
+import javax.persistence.Lob;
+import javax.validation.constraints.NotNull;
+
+public class RoomSaveRequest {
+
+ /** Room identifier */
+ private long id;
+
+ @NotNull private Room.Icon icon;
+
+ /**
+ * 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:
+ * https://www.baeldung.com/java-base64-image-string
+ * https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html
+ */
+ @Lob private String image;
+
+ /** The user given name of this room (e.g. 'Master bedroom') */
+ @NotNull private String name;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Room.Icon getIcon() {
+ return icon;
+ }
+
+ public void setIcon(Room.Icon icon) {
+ this.icon = icon;
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public void setImage(String image) {
+ this.image = image;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java
new file mode 100644
index 0000000..62b0b5e
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java
@@ -0,0 +1,74 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
+import com.google.gson.annotations.SerializedName;
+import java.math.BigDecimal;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.validation.constraints.NotNull;
+
+public class SensorSaveRequest {
+
+ /** Type of sensor, i.e. of the thing the sensor measures. */
+ public enum SensorType {
+ /** A sensor that measures temperature in degrees celsius */
+ @SerializedName("TEMPERATURE")
+ TEMPERATURE,
+
+ /** A sensor that measures relative humidity in percentage points */
+ @SerializedName("HUMIDITY")
+ HUMIDITY,
+
+ /** A sensor that measures light in degrees */
+ @SerializedName("LIGHT")
+ LIGHT
+ }
+
+ /** The type of this sensor */
+ @NotNull
+ @Enumerated(value = EnumType.STRING)
+ private Sensor.SensorType sensor;
+
+ @NotNull private BigDecimal value;
+
+ /**
+ * 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 setRoomId(Long roomId) {
+ this.roomId = roomId;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getRoomId() {
+ return roomId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Sensor.SensorType getSensor() {
+ return sensor;
+ }
+
+ public void setSensor(Sensor.SensorType sensor) {
+ this.sensor = sensor;
+ }
+
+ public BigDecimal getValue() {
+ return value;
+ }
+
+ public void setValue(BigDecimal value) {
+ this.value = value;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java
new file mode 100644
index 0000000..6b2f9b5
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java
@@ -0,0 +1,52 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.NotNull;
+
+public class SmartPlugSaveRequest {
+ /** Whether the smart plug is on */
+ @NotNull private boolean on;
+
+ /** 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 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 boolean isOn() {
+ return on;
+ }
+
+ public void setOn(boolean on) {
+ this.on = on;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java
new file mode 100644
index 0000000..3fb552b
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java
@@ -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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java
new file mode 100644
index 0000000..785d408
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java
@@ -0,0 +1,70 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.*;
+
+public class UserRegistrationRequest {
+
+ /** The full name of the user */
+ @NotNull
+ @NotEmpty(message = "Please provide a full name")
+ private String name;
+
+ /** The full name of the user */
+ @NotNull
+ @NotEmpty(message = "Please provide a username")
+ @Pattern(
+ regexp = "[A-Za-z0-9_\\-]+",
+ message = "Username can contain only letters, numbers, '_' and '-'")
+ private String username;
+
+ /** A properly salted way to store the password */
+ @NotNull
+ @NotEmpty(message = "Please provide a password")
+ @Size(
+ min = 6,
+ max = 255,
+ message = "Your password should be at least 6 characters long and up to 255 chars long")
+ private String password;
+
+ /**
+ * The user's email (validated according to criteria used in >input type="email"<>
+ *
, technically not RFC 5322 compliant
+ */
+ @NotNull
+ @NotEmpty(message = "Please provide an email")
+ @Email(message = "Please provide a valid email address")
+ @Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
+ private String email;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserUpdateRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserUpdateRequest.java
new file mode 100644
index 0000000..551b84a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserUpdateRequest.java
@@ -0,0 +1,48 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+public class UserUpdateRequest {
+ /** The full name of the user */
+ @NotEmpty(message = "Please provide a full name")
+ private String name;
+
+ /** A non-salted password */
+ @NotEmpty(message = "Please provide a password")
+ private String password;
+
+ /**
+ * The user's email (validated according to criteria used in >input type="email"<>
+ *
, 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 getName() {
+ return name;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/BadDataException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/BadDataException.java
new file mode 100644
index 0000000..2c6c4d4
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/BadDataException.java
@@ -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);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateRegistrationException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateRegistrationException.java
new file mode 100644
index 0000000..302c07e
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateRegistrationException.java
@@ -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 DuplicateRegistrationException extends Exception {
+ public DuplicateRegistrationException() {
+ super("Email or username already belonging to another user");
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/EmailTokenNotFoundException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/EmailTokenNotFoundException.java
new file mode 100644
index 0000000..b3e38b1
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/EmailTokenNotFoundException.java
@@ -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 EmailTokenNotFoundException extends Exception {
+ public EmailTokenNotFoundException() {
+ super("Email verification token not found in DB");
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java
new file mode 100644
index 0000000..471107f
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java
@@ -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");
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java
new file mode 100644
index 0000000..9176df6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java
@@ -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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UserNotFoundException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UserNotFoundException.java
new file mode 100644
index 0000000..d2e93f6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UserNotFoundException.java
@@ -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");
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java
new file mode 100644
index 0000000..da9af1c
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java
@@ -0,0 +1,32 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.Entity;
+
+/**
+ * Represents a dimmer that can only instruct an increase or decrease of intensity (i.e. like a
+ * dimmer with a '+' and a '-' button)
+ */
+@Entity
+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() {
+ super("buttonDimmer");
+ }
+
+ /** Increases the current intensity level of the dimmable light by DIM_INCREMENT */
+ public void increaseIntensity() {
+ for (DimmableLight dl : getOutputs()) {
+ dl.setIntensity(dl.getIntensity() + DIM_INCREMENT);
+ }
+ }
+
+ /** Decreases the current intensity level of the dimmable light by DIM_INCREMENT */
+ public void decreaseIntensity() {
+ for (DimmableLight dl : getOutputs()) {
+ dl.setIntensity(dl.getIntensity() - DIM_INCREMENT);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmerRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmerRepository.java
new file mode 100644
index 0000000..f39644a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmerRepository.java
@@ -0,0 +1,9 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.transaction.Transactional;
+
+public interface ButtonDimmerRepository extends DeviceRepository {
+
+ @Transactional
+ void deleteAllByRoomId(long roomId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
new file mode 100644
index 0000000..d324724
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
@@ -0,0 +1,86 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.Date;
+import java.util.UUID;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+@Entity
+public class ConfirmationToken {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(name = "id", updatable = false, nullable = false)
+ private Long id;
+
+ @Column(name = "confirmation_token", unique = true)
+ private String confirmationToken;
+
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date createdDate;
+
+ @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
+ @JoinColumn(nullable = false, name = "user_id")
+ private User user;
+
+ @Column(nullable = false)
+ private Boolean resetPassword;
+
+ public ConfirmationToken(User user) {
+ this.user = user;
+ createdDate = new Date();
+ confirmationToken = UUID.randomUUID().toString();
+ resetPassword = false;
+ }
+
+ /** Constructor for hibernate reflective stuff things whatever */
+ public ConfirmationToken() {}
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getConfirmationToken() {
+ return confirmationToken;
+ }
+
+ public Date getCreatedDate() {
+ return createdDate;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public void setConfirmationToken(String confirmationToken) {
+ this.confirmationToken = confirmationToken;
+ }
+
+ public void setCreatedDate(Date createdDate) {
+ this.createdDate = createdDate;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ public Boolean getResetPassword() {
+ return resetPassword;
+ }
+
+ public void setResetPassword(Boolean resetPassword) {
+ this.resetPassword = resetPassword;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
new file mode 100644
index 0000000..40c6a17
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
@@ -0,0 +1,13 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.transaction.Transactional;
+import org.springframework.data.repository.CrudRepository;
+
+public interface ConfirmationTokenRepository extends CrudRepository {
+ ConfirmationToken findByConfirmationToken(String confirmationToken);
+
+ ConfirmationToken findByUser(User user);
+
+ @Transactional
+ void deleteByUserAndResetPassword(User user, boolean resetPassword);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java
new file mode 100644
index 0000000..91b1e88
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java
@@ -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 the input device type
+ * @param the output device type
+ */
+@FunctionalInterface
+public interface Connector {
+
+ /**
+ * 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 the input device type
+ * @param the output device type
+ * @return a Connector implementation for the pair of types J and K
+ */
+ static Connector basic(
+ Function> outputsGetter, BiConsumer 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);
+ };
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java
new file mode 100644
index 0000000..295fe37
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java
@@ -0,0 +1,88 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
+import com.google.gson.annotations.SerializedName;
+import io.swagger.annotations.ApiModelProperty;
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/** Generic abstraction for a smart home device */
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class Device {
+
+ /** Ways a device can behave in the automation flow. For now only input/output */
+ public enum FlowType {
+ @SerializedName("INPUT")
+ INPUT,
+
+ @SerializedName("OUTPUT")
+ OUTPUT
+ }
+
+ /** Device identifier */
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(name = "id", updatable = false, nullable = false, unique = true)
+ @ApiModelProperty(hidden = true)
+ private long id;
+
+ @ManyToOne
+ @JoinColumn(name = "room_id", updatable = false, insertable = false)
+ @GsonExclude
+ private Room room;
+
+ /**
+ * The room this device belongs in, as a foreign key id. To use when updating and inserting from
+ * a REST call.
+ */
+ @Column(name = "room_id", nullable = false)
+ @NotNull
+ private Long roomId;
+
+ /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
+ @NotNull
+ @Column(nullable = false)
+ private String name;
+
+ /**
+ * The name for the category of this particular device (e.g 'dimmer'). Not stored in the
+ * database but set thanks to constructors
+ */
+ @Transient private final String kind;
+
+ /**
+ * The way this device behaves in the automation flow. Not stored in the database but set thanks
+ * to constructors
+ */
+ @Transient private final FlowType flowType;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getRoomId() {
+ return roomId;
+ }
+
+ public void setRoomId(Long roomId) {
+ this.roomId = roomId;
+ }
+
+ public Device(String kind, FlowType flowType) {
+ this.kind = kind;
+ this.flowType = flowType;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java
new file mode 100644
index 0000000..ef57a88
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java
@@ -0,0 +1,48 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
+
+import javax.transaction.Transactional;
+
+/**
+ * DeviceRepository acts as a superclass for the other repositories so to mirror in the database the
+ * class inheritance present among the various devices.
+ */
+public interface DeviceRepository extends CrudRepository {
+ List findByRoomId(@Param("roomId") long roomId);
+
+ /**
+ * Finds devices by their id and a username
+ *
+ * @param id the device id
+ * @param username a User's username
+ * @return an optional device, empty if none found
+ */
+ @Transactional
+ @Query("SELECT d FROM Device d JOIN d.room r JOIN r.user u WHERE d.id = ?1 AND u.username = ?2")
+ Optional findByIdAndUsername(Long id, String username);
+
+ /**
+ * Finds all devices belonging to a user
+ *
+ * @param username the User's username
+ * @return all devices of that user
+ */
+ @Transactional
+ @Query("SELECT d FROM Device d JOIN d.room r JOIN r.user u WHERE u.username = ?1")
+ List findAllByUsername(String username);
+
+ /**
+ * Find the user associated with a device through a room
+ *
+ * @param deviceId the device id
+ * @return a user object
+ */
+ @Transactional
+ @Query("SELECT u FROM Device d JOIN d.room r JOIN r.user u WHERE d.id = ?1")
+ User findUser(Long deviceId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java
new file mode 100644
index 0000000..6b537c6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java
@@ -0,0 +1,89 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/** Represent a dimmable light */
+@Entity
+public class DimmableLight extends Switchable {
+
+ public static final Connector
+ BUTTON_DIMMER_DIMMABLE_LIGHT_CONNECTOR =
+ Connector.basic(ButtonDimmer::getOutputs, DimmableLight::setDimmerId);
+
+ public static final Connector KNOB_DIMMER_DIMMABLE_LIGHT_CONNECTOR =
+ Connector.basic(KnobDimmer::getOutputs, DimmableLight::setDimmerId);
+
+ public DimmableLight() {
+ super("dimmableLight");
+ }
+
+ @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) */
+ @NotNull
+ @Column(nullable = false)
+ @Min(0)
+ @Max(100)
+ private Integer intensity = 0;
+
+ @NotNull
+ @Column(nullable = false)
+ private Integer oldIntensity = 100;
+
+ public Integer getIntensity() {
+ return intensity;
+ }
+
+ /**
+ * Sets the intensity to a certain level. Out of bound values are corrected to the respective
+ * 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;
+ this.oldIntensity = 100;
+ } else {
+ this.intensity = intensity;
+ this.oldIntensity = intensity;
+ }
+ }
+
+ @Override
+ public boolean isOn() {
+ return intensity != 0;
+ }
+
+ @Override
+ public void setOn(boolean on) {
+ intensity = on ? oldIntensity : 0;
+ }
+
+ public void setDimmerId(Long dimmerId) {
+ this.dimmerId = dimmerId;
+ super.setSwitchId(null);
+ }
+
+ @Override
+ public void setSwitchId(Long switchId) {
+ super.setSwitchId(switchId);
+ this.dimmerId = null;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java
new file mode 100644
index 0000000..a32b3c6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java
@@ -0,0 +1,3 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+public interface DimmableLightRepository extends SwitchableRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java
new file mode 100644
index 0000000..d06bece
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java
@@ -0,0 +1,43 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.persistence.Entity;
+import javax.persistence.Inheritance;
+import javax.persistence.InheritanceType;
+import javax.persistence.OneToMany;
+import javax.persistence.PreRemove;
+
+/** Represents a generic dimmer input device */
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class Dimmer extends InputDevice {
+ public Dimmer(String kind) {
+ super(kind);
+ }
+
+ @OneToMany(mappedBy = "dimmer")
+ private Set lights = new HashSet<>();
+
+ /**
+ * Get the lights connected to this dimmer
+ *
+ * @return duh
+ */
+ @Override
+ public Set getOutputs() {
+ return this.lights;
+ }
+
+ /** Add a light to be controller by this dimmer */
+ public void addDimmableLight(DimmableLight dimmableLight) {
+ lights.add(dimmableLight);
+ }
+
+ @PreRemove
+ private void removeLightsFromDimmer() {
+ for (DimmableLight dl : getOutputs()) {
+ dl.setDimmerId(null);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java
new file mode 100644
index 0000000..da45b67
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java
@@ -0,0 +1,22 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.Set;
+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
+ * environment (sensor) or the user (switch / dimmer).
+ */
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class InputDevice extends Device {
+ public InputDevice(String kind) {
+ super(kind, FlowType.INPUT);
+ }
+
+ public Set extends OutputDevice> getOutputs() {
+ return Set.of();
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java
new file mode 100644
index 0000000..06ee415
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/JWTUserDetailsService.java
@@ -0,0 +1,25 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.*;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JWTUserDetailsService implements UserDetailsService {
+ @Autowired private UserRepository repository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User toReturn = repository.findByUsername(username);
+ if (toReturn != null && toReturn.getEnabled()) {
+ return new org.springframework.security.core.userdetails.User(
+ toReturn.getUsername(), toReturn.getPassword(), Set.of());
+ } else {
+ throw new UsernameNotFoundException(username);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java
new file mode 100644
index 0000000..c116165
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java
@@ -0,0 +1,26 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.Entity;
+
+/**
+ * Represents a dimmer able to set absolute intensity values (i.e. knowing the absolute intensity
+ * value, like a knob)
+ */
+@Entity
+public class KnobDimmer extends Dimmer {
+
+ public KnobDimmer() {
+ super("knobDimmer");
+ }
+
+ /**
+ * Sets absolutely the intensity level of all lights connected
+ *
+ * @param intensity the intensity (must be from 0 to 100)
+ */
+ public void setLightIntensity(int intensity) {
+ for (DimmableLight dl : getOutputs()) {
+ dl.setIntensity(intensity);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmerRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmerRepository.java
new file mode 100644
index 0000000..2a4558e
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmerRepository.java
@@ -0,0 +1,9 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.transaction.Transactional;
+
+public interface KnobDimmerRepository extends DeviceRepository {
+
+ @Transactional
+ void deleteAllByRoomId(long roomId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java
new file mode 100644
index 0000000..6c5baf7
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java
@@ -0,0 +1,24 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+
+/** Represents a motion sensor device */
+@Entity
+public class MotionSensor extends InputDevice {
+
+ @Column(nullable = false)
+ private boolean detected;
+
+ public boolean isDetected() {
+ return detected;
+ }
+
+ public void setDetected(boolean detected) {
+ this.detected = detected;
+ }
+
+ public MotionSensor() {
+ super("motionSensor");
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensorRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensorRepository.java
new file mode 100644
index 0000000..aea240d
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensorRepository.java
@@ -0,0 +1,3 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+public interface MotionSensorRepository extends DeviceRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java
new file mode 100644
index 0000000..c5b401f
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java
@@ -0,0 +1,15 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.*;
+
+/**
+ * Represents a generic output device, i.e. something that causes some behaviour (light, smartPlugs,
+ * ...).
+ */
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class OutputDevice extends Device {
+ public OutputDevice(String kind) {
+ super(kind, FlowType.OUTPUT);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java
new file mode 100644
index 0000000..2dcff1b
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java
@@ -0,0 +1,30 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.validation.constraints.NotNull;
+
+/** Represents a standard non-dimmable light */
+@Entity
+public class RegularLight extends Switchable {
+
+ /** Whether the light is on or not */
+ @Column(name = "light_on", nullable = false)
+ @NotNull
+ boolean on;
+
+ public RegularLight() {
+ super("regularLight");
+ this.on = false;
+ }
+
+ @Override
+ public boolean isOn() {
+ return on;
+ }
+
+ @Override
+ public void setOn(boolean on) {
+ this.on = on;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java
new file mode 100644
index 0000000..cad8831
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java
@@ -0,0 +1,3 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+public interface RegularLightRepository extends SwitchableRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java
new file mode 100644
index 0000000..34f3824
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java
@@ -0,0 +1,203 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
+import com.google.gson.annotations.SerializedName;
+import io.swagger.annotations.ApiModelProperty;
+import java.util.HashSet;
+import java.util.Set;
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/** Represents a room in the house owned by the user */
+@Entity
+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
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(name = "id", updatable = false, nullable = false, unique = true)
+ @ApiModelProperty(hidden = true)
+ private Long id;
+
+ /** The room icon, out of a set of Semantic UI icons */
+ @Column private Icon icon;
+
+ /**
+ * 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:
+ * https://www.baeldung.com/java-base64-image-string
+ * https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html
+ */
+ @Column(name = "image", columnDefinition = "TEXT")
+ private String image;
+
+ @ManyToOne
+ @JoinColumn(name = "user_id", updatable = false, insertable = false)
+ @GsonExclude
+ private User user;
+
+ @OneToMany(mappedBy = "room", orphanRemoval = true)
+ @GsonExclude
+ private Set devices = new HashSet<>();
+
+ /**
+ * User that owns the house this room is in as a foreign key id. To use when updating and
+ * inserting from a REST call.
+ */
+ @NotNull
+ @Column(name = "user_id", nullable = false)
+ private Long userId;
+
+ /** The user given name of this room (e.g. 'Master bedroom') */
+ @NotNull
+ @Column(nullable = false)
+ private String name;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Icon getIcon() {
+ return icon;
+ }
+
+ public void setIcon(Icon icon) {
+ this.icon = icon;
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public void setImage(String image) {
+ this.image = image;
+ }
+
+ public Set getDevices() {
+ return devices;
+ }
+
+ @Override
+ public String toString() {
+ return "Room{" + "id=" + id + ", name='" + name + "\'}";
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RoomRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RoomRepository.java
new file mode 100644
index 0000000..b02413d
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RoomRepository.java
@@ -0,0 +1,18 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.Optional;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+
+public interface RoomRepository extends CrudRepository {
+
+ /**
+ * Finds a room by their id and a username
+ *
+ * @param id the room id
+ * @param username a User's username
+ * @return an optional device, empty if none found
+ */
+ @Query("SELECT r FROM Room r JOIN r.user u WHERE r.id = ?1 AND u.username = ?2")
+ Optional findByIdAndUsername(Long id, String username);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java
new file mode 100644
index 0000000..6b5e6b9
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java
@@ -0,0 +1,71 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import com.google.gson.annotations.SerializedName;
+import java.math.BigDecimal;
+import java.util.Map;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.validation.constraints.NotNull;
+
+/** A sensor input device that measures a quantity in a continuous scale (e.g. temperature) */
+@Entity
+public class Sensor extends InputDevice {
+
+ public static final Map TYPICAL_VALUES =
+ Map.of(
+ SensorType.TEMPERATURE, new BigDecimal(17.0),
+ SensorType.HUMIDITY, new BigDecimal(40.0),
+ SensorType.LIGHT, new BigDecimal(1000));
+
+ /** Type of sensor, i.e. of the thing the sensor measures. */
+ public enum SensorType {
+ /** A sensor that measures temperature in degrees celsius */
+ @SerializedName("TEMPERATURE")
+ TEMPERATURE,
+
+ /** A sensor that measures relative humidity in percentage points */
+ @SerializedName("HUMIDITY")
+ HUMIDITY,
+
+ /** A sensor that measures light in degrees */
+ @SerializedName("LIGHT")
+ LIGHT
+ }
+
+ /** The value of this sensor according to its sensor type */
+ @Column(nullable = false, precision = 11, scale = 1)
+ private BigDecimal value;
+
+ /** The type of this sensor */
+ @Column(nullable = false)
+ @NotNull
+ @Enumerated(value = EnumType.STRING)
+ private SensorType sensor;
+
+ public SensorType getSensor() {
+ return sensor;
+ }
+
+ public void setSensor(SensorType sensor) {
+ this.sensor = sensor;
+ }
+
+ public BigDecimal getValue() {
+ return this.value;
+ }
+
+ public void setValue(BigDecimal newValue) {
+ this.value = newValue;
+ }
+
+ public Sensor() {
+ super("sensor");
+ }
+
+ @Override
+ public String toString() {
+ return "Sensor{" + "value=" + value + ", sensor=" + sensor + '}';
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SensorRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SensorRepository.java
new file mode 100644
index 0000000..b796277
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SensorRepository.java
@@ -0,0 +1,3 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+public interface SensorRepository extends DeviceRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java
new file mode 100644
index 0000000..9b07e69
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java
@@ -0,0 +1,47 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.math.BigDecimal;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.validation.constraints.NotNull;
+
+/** A smart plug that can be turned either on or off */
+@Entity
+public class SmartPlug extends Switchable {
+
+ /** The average consumption of an active plug when on in Watt */
+ public static final Double AVERAGE_CONSUMPTION_KW = 200.0;
+
+ /** The total amount of power that the smart plug has consumed represented in W/h */
+ @Column(precision = 13, scale = 3)
+ @NotNull
+ private BigDecimal totalConsumption = BigDecimal.ZERO;
+
+ /** Whether the smart plug is on */
+ @Column(name = "smart_plug_on", nullable = false)
+ @NotNull
+ private boolean on;
+
+ public BigDecimal getTotalConsumption() {
+ return totalConsumption;
+ }
+
+ /** Resets the consuption meter */
+ public void resetTotalConsumption() {
+ totalConsumption = BigDecimal.ZERO;
+ }
+
+ @Override
+ public boolean isOn() {
+ return on;
+ }
+
+ @Override
+ public void setOn(boolean on) {
+ this.on = on;
+ }
+
+ public SmartPlug() {
+ super("smartPlug");
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java
new file mode 100644
index 0000000..8a02243
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java
@@ -0,0 +1,24 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.Collection;
+import javax.transaction.Transactional;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+public interface SmartPlugRepository extends SwitchableRepository {
+ @Transactional
+ Collection findByOn(boolean on);
+
+ /**
+ * Updates total consumption of all activated smart plugs by considering a load of
+ * fakeConsumption W. This query must be executed every second
+ *
+ * @see ch.usi.inf.sa4.sanmarinoes.smarthut.scheduled.UpdateTasks
+ * @param fakeConsumption the fake consumption in watts
+ */
+ @Modifying(clearAutomatically = true)
+ @Transactional
+ @Query(
+ "UPDATE SmartPlug s SET totalConsumption = s.totalConsumption + ?1 / 3600.0 WHERE s.on = true")
+ void updateTotalConsumption(Double fakeConsumption);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java
new file mode 100644
index 0000000..8a8057c
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java
@@ -0,0 +1,62 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.OneToMany;
+import javax.persistence.PreRemove;
+
+/** A switch input device */
+@Entity
+public class Switch extends InputDevice {
+
+ @OneToMany(mappedBy = "switchDevice")
+ private Set switchables = new HashSet<>();
+
+ /** The state of this switch */
+ @Column(nullable = false, name = "switch_on")
+ private boolean on;
+
+ public Switch() {
+ super("switch");
+ }
+
+ /**
+ * Setter method for this Switch
+ *
+ * @param state The state to be set
+ */
+ public void setOn(boolean state) {
+ on = state;
+
+ for (final Switchable s : switchables) {
+ s.setOn(on);
+ }
+ }
+
+ /** Toggle between on and off state */
+ public void toggle() {
+ setOn(!isOn());
+ }
+
+ /**
+ * Getter method for this Switch
+ *
+ * @return This Switch on state
+ */
+ public boolean isOn() {
+ return on;
+ }
+
+ public Set getOutputs() {
+ return switchables;
+ }
+
+ @PreRemove
+ public void removeSwitchable() {
+ for (Switchable s : getOutputs()) {
+ s.setSwitchId(null);
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchRepository.java
new file mode 100644
index 0000000..8650ebe
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchRepository.java
@@ -0,0 +1,9 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.transaction.Transactional;
+
+public interface SwitchRepository extends DeviceRepository {
+
+ @Transactional
+ void deleteAllByRoomId(long roomId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java
new file mode 100644
index 0000000..5ba0702
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java
@@ -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_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;
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableRepository.java
new file mode 100644
index 0000000..589542d
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableRepository.java
@@ -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 extends DeviceRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java
new file mode 100644
index 0000000..60aad17
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java
@@ -0,0 +1,129 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
+import io.swagger.annotations.ApiModelProperty;
+import java.util.Objects;
+import javax.persistence.*;
+
+/** A user of the Smarthut application */
+@Entity(name = "smarthutuser")
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(name = "id", updatable = false, nullable = false, unique = true)
+ @ApiModelProperty(hidden = true)
+ private Long id;
+
+ /** The full name of the user */
+ @Column(nullable = false)
+ private String name;
+
+ /** The full username of the user */
+ @Column(nullable = false, unique = true)
+ private String username;
+
+ /** A properly salted way to store the password */
+ @Column(nullable = false)
+ @GsonExclude
+ private String password;
+
+ /**
+ * The user's email (validated according to criteria used in >input type="email"<>
+ *
, technically not RFC 5322 compliant
+ */
+ @Column(nullable = false, unique = true)
+ private String email;
+
+ @Column(nullable = false)
+ @GsonExclude
+ private Boolean isEnabled = false;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public Boolean getEnabled() {
+ return isEnabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "User{"
+ + "id="
+ + id
+ + ", name='"
+ + name
+ + '\''
+ + ", username='"
+ + username
+ + '\''
+ + ", password='"
+ + password
+ + '\''
+ + ", email='"
+ + email
+ + '\''
+ + ", isEnabled="
+ + isEnabled
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ User user = (User) o;
+ return id.equals(user.id)
+ && name.equals(user.name)
+ && username.equals(user.username)
+ && password.equals(user.password)
+ && email.equals(user.email)
+ && isEnabled.equals(user.isEnabled);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, username, password, email, isEnabled);
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java
new file mode 100644
index 0000000..01fd897
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java
@@ -0,0 +1,10 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.*;
+import org.springframework.data.repository.CrudRepository;
+
+public interface UserRepository extends CrudRepository {
+ User findByUsername(String username);
+
+ User findByEmailIgnoreCase(String email);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java
new file mode 100644
index 0000000..932db5c
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java
@@ -0,0 +1,77 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.scheduled;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.controller.MotionSensorController;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.controller.SensorController;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.StreamSupport;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * Generates fake sensor (and motion sensor) and smart plug consumption updates as required by
+ * milestone one
+ */
+@Component
+public class UpdateTasks {
+
+ @Autowired private SensorRepository sensorRepository;
+
+ @Autowired private MotionSensorRepository motionSensorRepository;
+
+ @Autowired private SmartPlugRepository smartPlugRepository;
+
+ @Autowired private SensorController sensorController;
+
+ @Autowired private MotionSensorController motionSensorController;
+
+ @Autowired private SensorSocketEndpoint sensorSocketEndpoint;
+
+ /** Generates fake sensor updates every two seconds with a +/- 1.25% error */
+ @Scheduled(fixedRate = 2000)
+ public void sensorFakeUpdate() {
+ StreamSupport.stream(sensorRepository.findAll().spliterator(), true)
+ .forEach(
+ sensor ->
+ sensorController.updateValueFromSensor(
+ sensor,
+ Sensor.TYPICAL_VALUES
+ .get(sensor.getSensor())
+ .multiply(
+ new BigDecimal(
+ 0.9875 + Math.random() / 40))));
+ }
+
+ /**
+ * Generate fake motion detections in all motion detectors every 20 seconds for 2 seconds at
+ * most
+ */
+ @Scheduled(fixedDelay = 20000)
+ public void motionSensorFakeUpdate() {
+ StreamSupport.stream(motionSensorRepository.findAll().spliterator(), true)
+ .forEach(
+ sensor -> {
+ motionSensorController.updateDetectionFromMotionSensor(sensor, true);
+ CompletableFuture.delayedExecutor(
+ (long) (Math.random() * 2000), TimeUnit.MILLISECONDS)
+ .execute(
+ () ->
+ motionSensorController
+ .updateDetectionFromMotionSensor(
+ sensor, false));
+ });
+ }
+
+ /** Updates power consumption of all activated smart plugs every second */
+ @Scheduled(fixedDelay = 1000)
+ public void smartPlugConsumptionFakeUpdate() {
+ smartPlugRepository.updateTotalConsumption(SmartPlug.AVERAGE_CONSUMPTION_KW);
+ final Collection c = smartPlugRepository.findByOn(true);
+ c.forEach(s -> sensorSocketEndpoint.broadcast(s, sensorRepository.findUser(s.getId())));
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java
new file mode 100644
index 0000000..89415aa
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java
@@ -0,0 +1,95 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.jsonwebtoken.ExpiredJwtException;
+import java.io.IOException;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/** Generates MessageHandlers for unauthenticated socket sessions */
+@Component
+public class AuthenticationMessageListener {
+
+ private Gson gson = GsonConfig.gson();
+
+ private JWTTokenUtils jwtTokenUtils;
+
+ private UserRepository userRepository;
+
+ @Autowired
+ public AuthenticationMessageListener(
+ JWTTokenUtils jwtTokenUtils, UserRepository userRepository) {
+ this.jwtTokenUtils = jwtTokenUtils;
+ this.userRepository = userRepository;
+ }
+
+ /**
+ * Generates a new message handler to handle socket authentication
+ *
+ * @param session the session to which authentication must be checked
+ * @param authorizedSetter function to call once user is authenticated
+ * @return a new message handler to handle socket authentication
+ */
+ MessageHandler.Whole newHandler(
+ final Session session, BiConsumer authorizedSetter) {
+ return new MessageHandler.Whole<>() {
+ @Override
+ public void onMessage(final String message) {
+ if (message == null) {
+ acknowledge(false);
+ return;
+ }
+
+ String token;
+ String username;
+
+ try {
+ token = gson.fromJson(message, JsonObject.class).get("token").getAsString();
+ username = jwtTokenUtils.getUsernameFromToken(token);
+ } catch (ExpiredJwtException e) {
+ System.err.println(e.getMessage());
+ acknowledge(false);
+ return;
+ } catch (Throwable ignored) {
+ System.out.println("Token format not valid");
+ acknowledge(false);
+ return;
+ }
+
+ final User user = userRepository.findByUsername(username);
+ if (user == null || jwtTokenUtils.isTokenExpired(token)) {
+ System.out.println("Token not valid");
+ acknowledge(false);
+ return;
+ }
+
+ // Here user is authenticated
+ session.removeMessageHandler(this);
+
+ // Add user-session pair in authorized list
+ authorizedSetter.accept(user, session);
+
+ // update client to acknowledge authentication
+ acknowledge(true);
+ }
+
+ private void acknowledge(boolean success) {
+ try {
+ session.getBasicRemote()
+ .sendText(gson.toJson(Map.of("authenticated", success)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
new file mode 100644
index 0000000..503667a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
@@ -0,0 +1,54 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
+
+import javax.websocket.server.ServerEndpointConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+import org.springframework.web.socket.server.standard.ServerEndpointRegistration;
+
+/** Configures the sensor socket and maps it to the /sensor-socket path */
+@Configuration
+public class SensorSocketConfig extends ServerEndpointConfig.Configurator {
+
+ private SensorSocketEndpoint instance;
+
+ @Autowired
+ public SensorSocketConfig(SensorSocketEndpoint instance) {
+ this.instance = instance;
+ }
+
+ /**
+ * Registers the sensor socket endpoint to the url /sensor-socket
+ *
+ * @return an endpoint registration object
+ */
+ @Bean
+ public ServerEndpointRegistration serverEndpointRegistration() {
+ return new ServerEndpointRegistration("/sensor-socket", instance);
+ }
+
+ /**
+ * Returns a new ServerEndpointExporter
+ *
+ * @return a new ServerEndpointExporter
+ */
+ @Bean
+ public ServerEndpointExporter endpointExporter() {
+ return new ServerEndpointExporter();
+ }
+
+ @Override
+ public T getEndpointInstance(Class endpointClass) throws InstantiationException {
+ try {
+ @SuppressWarnings("unchecked")
+ final T instance = (T) this.instance;
+ return instance;
+ } catch (ClassCastException e) {
+ final var e2 =
+ new InstantiationException("Cannot cast SensorSocketEndpoint to desired type");
+ e2.initCause(e);
+ throw e2;
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
new file mode 100644
index 0000000..600bdf4
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
@@ -0,0 +1,92 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
+
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.*;
+import javax.websocket.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/** Endpoint of socket at URL /sensor-socket used to update the client with sensor information */
+@Component
+public class SensorSocketEndpoint extends Endpoint {
+
+ private Gson gson = GsonConfig.gson();
+
+ private AuthenticationMessageListener authenticationMessageListener;
+
+ private Set unauthorizedClients = Collections.synchronizedSet(new HashSet<>());
+
+ private Multimap authorizedClients =
+ Multimaps.synchronizedMultimap(HashMultimap.create());
+
+ @Autowired
+ public SensorSocketEndpoint(AuthenticationMessageListener authenticationMessageListener) {
+ this.authenticationMessageListener = authenticationMessageListener;
+ }
+
+ /**
+ * Returns a synchronized set of socket sessions not yet authorized with a token
+ *
+ * @return a synchronized set of socket sessions not yet authorized with a token
+ */
+ public Set getUnauthorizedClients() {
+ return unauthorizedClients;
+ }
+
+ /**
+ * Returns a synchronized User to Session multimap with authorized sessions
+ *
+ * @return a synchronized User to Session multimap with authorized sessions
+ */
+ public Multimap getAuthorizedClients() {
+ return authorizedClients;
+ }
+
+ /**
+ * Given a message and a user, broadcasts that message in json to all associated clients and
+ * returns the number of successful transfers
+ *
+ * @param message the message to send
+ * @param u the user to which to send the message
+ * @return number of successful transfer
+ */
+ public void broadcast(Object message, User u) {
+ final HashSet sessions = new HashSet<>(authorizedClients.get(u));
+ for (Session s : sessions) {
+ try {
+ if (s.isOpen()) {
+ s.getBasicRemote().sendText(gson.toJson(message));
+ } else {
+ authorizedClients.remove(u, s);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Handles the opening of a socket session with a client
+ *
+ * @param session the newly born session
+ * @param config endpoint configuration
+ */
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ unauthorizedClients.add(session);
+ session.addMessageHandler(
+ authenticationMessageListener.newHandler(
+ session,
+ (u, s) -> {
+ unauthorizedClients.remove(s);
+ authorizedClients.put(u, s);
+ }));
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java
new file mode 100644
index 0000000..99d363b
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java
@@ -0,0 +1,32 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.utils;
+
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/** A class with a bunch of useful static methods */
+public final class Utils {
+ private Utils() {}
+
+ @FunctionalInterface
+ public interface ConsumerWithException {
+ void apply(T input) throws Throwable;
+ }
+
+ public static List toList(Iterable iterable) {
+ return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList());
+ }
+
+ public static Predicate didThrow(ConsumerWithException consumer) {
+ return (t) -> {
+ try {
+ consumer.apply(t);
+ return true;
+ } catch (Throwable e) {
+ System.err.println(e.getMessage());
+ return false;
+ }
+ };
+ }
+}
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
new file mode 100644
index 0000000..3c77362
--- /dev/null
+++ b/src/main/resources/application-dev.properties
@@ -0,0 +1,35 @@
+spring.http.converters.preferred-json-mapper=gson
+spring.datasource.url=jdbc:postgresql://localhost:5432/smarthut
+spring.datasource.username=postgres
+spring.datasource.password=
+
+# Hibernate properties
+spring.jpa.database=POSTGRESQL
+spring.jpa.show-sql=false
+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
+
+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.registrationRedirect=http://localhost:3000
+
+email.resetpasswordSubject=SmartHut.sm password reset
+email.resetpassword=To reset your password, please click here:
+email.resetpasswordPath=http://localhost:3000/password-reset?token=
+email.resetPasswordRedirect=http://localhost:3000/conf-reset-pass
\ No newline at end of file
diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties
new file mode 100644
index 0000000..482fa13
--- /dev/null
+++ b/src/main/resources/application-prod.properties
@@ -0,0 +1,42 @@
+spring.http.converters.preferred-json-mapper=gson
+
+# Database connection properties
+spring.datasource.url=${POSTGRES_JDBC}
+spring.datasource.username=${POSTGRES_USER}
+spring.datasource.password=${POSTGRES_PASS}
+
+# Hibernate properties
+spring.jpa.database=POSTGRESQL
+spring.jpa.show-sql=false
+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
+jwt.secret=${SECRET}
+
+# Mail connection properties
+spring.mail.test-connection=true
+spring.mail.host=${MAIL_HOST}
+spring.mail.port=${MAIL_PORT}
+spring.mail.properties.mail.smtp.starttls.enable=${MAIL_STARTTLS}
+spring.mail.username=${MAIL_USER}
+spring.mail.password=${MAIL_PASS}
+spring.mail.properties.mail.smtp.starttls.required=${MAIL_STARTTLS}
+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
+
+# Registration email properties
+email.registrationSubject=Complete your SmartHut.sm registration
+email.registration=To confirm your registration, please click here:
+email.registrationPath=${BACKEND_URL}/register/confirm-account?token=
+email.registrationSuccess=${FRONTEND_URL}
+
+
+# Password reset email properties
+email.resetpasswordSubject=SmartHut.sm password reset
+email.resetpassword=To reset your password, please click here:
+email.resetpasswordPath=${FRONTEND_URL}/password-reset?token=
+email.resetPasswordSuccess=${FRONTEND_URL}/conf-reset-pass
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..2150e4d
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.profiles.active=${SMARTHUT_THIS_VALUE_IS_PROD_IF_THIS_IS_A_CONTAINER_PIZZOCCHERI:dev}
\ No newline at end of file
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java
new file mode 100644
index 0000000..d13104f
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java
@@ -0,0 +1,239 @@
+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 ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+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;
+
+ @Autowired private UserRepository userRepository;
+
+ @Autowired private ConfirmationTokenRepository tokenRepository;
+
+ 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;
+ }
+
+ @Override
+ protected void setUp() {
+ final ResponseEntity res =
+ this.restTemplate.postForEntity(
+ this.url("/register"), getDisabledUser(), OkResponse.class);
+ assertThat(res.getStatusCode().equals(HttpStatus.OK));
+
+ registerTestUser(restTemplate, userRepository, tokenRepository);
+ }
+
+ @Test
+ public void registrationShouldReturnBadRequestWithIncorrectFields() {
+ final Map 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 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 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 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 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 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 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 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 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 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 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 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 loginShouldReturnTokenWithEnabledUser() {
+ final JWTRequest request = new JWTRequest();
+ request.setUsernameOrEmail("enabled");
+ request.setPassword("password");
+
+ final ResponseEntity res =
+ this.restTemplate.postForEntity(url("/auth/login"), request, JWTResponse.class);
+ assertThat(res.getStatusCode().equals(HttpStatus.OK));
+ assertThat(res.getBody() != null);
+ assertThat(res.getBody().getToken() != null);
+ assertThat(!res.getBody().getToken().isEmpty());
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/ButtonDimmerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/ButtonDimmerTests.java
new file mode 100644
index 0000000..22b3e0c
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/ButtonDimmerTests.java
@@ -0,0 +1,55 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmer;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("A Button Dimmer")
+public class ButtonDimmerTests {
+
+ ButtonDimmer buttonDimmer;
+
+ @BeforeEach
+ public void createNewButtonDimmer() {
+ this.buttonDimmer = new ButtonDimmer();
+ }
+
+ @Nested
+ @DisplayName(" when multiple lights are present")
+ class MultipleLights {
+
+ @BeforeEach
+ public void setLights() {
+ DimmableLight dl;
+ for (int i = 0; i < 3; i++) {
+ dl = new DimmableLight();
+ dl.setIntensity(10);
+ ;
+ buttonDimmer.addDimmableLight(dl);
+ }
+ }
+
+ @Test
+ @DisplayName(" increase the intensity ")
+ public void increase() {
+ buttonDimmer.increaseIntensity();
+ for (DimmableLight dl : buttonDimmer.getOutputs()) {
+ assertTrue(dl.getIntensity() > 10);
+ }
+ }
+
+ @Test
+ @DisplayName(" decrease the intensity ")
+ public void decrease() {
+ buttonDimmer.decreaseIntensity();
+ for (DimmableLight dl : buttonDimmer.getOutputs()) {
+ assertTrue(dl.getIntensity() < 10);
+ }
+ }
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/DimmableLightTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/DimmableLightTests.java
new file mode 100644
index 0000000..f54e754
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/DimmableLightTests.java
@@ -0,0 +1,106 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("A Button Dimmer")
+public class DimmableLightTests {
+
+ DimmableLight dimmableLight;
+
+ @BeforeEach
+ public void createNewButtonDimmer() {
+ this.dimmableLight = new DimmableLight();
+ }
+
+ @Nested
+ @DisplayName(" when on")
+ class WhenOn {
+
+ @BeforeEach
+ public void setLightOn() {
+ dimmableLight.setOn(true);
+ }
+
+ @Test
+ @DisplayName("the isOn method should return true")
+ public void isOn() {
+ assertTrue(dimmableLight.isOn());
+ }
+
+ @Test
+ @DisplayName("the intensity should be 100 when the light is turned on")
+ public void checkIntensityWhenTurnedOn() {
+ assertEquals(100, dimmableLight.getIntensity());
+ }
+
+ @Test
+ @DisplayName("setting the intensity to a number between 1 and 100")
+ public void checkIntensityBetweenLimits() {
+ dimmableLight.setIntensity(50);
+ assertEquals(50, dimmableLight.getIntensity());
+ }
+
+ @Test
+ @DisplayName("setting the intensity to a number > 100")
+ public void checkIntensityMoreThanLimits() {
+ dimmableLight.setIntensity(150);
+ assertEquals(100, dimmableLight.getIntensity());
+ }
+
+ @Test
+ @DisplayName("setting the intensity to a number < 0")
+ public void checkIntensityLessThanLimits() {
+ dimmableLight.setIntensity(-30);
+ assertEquals(0, dimmableLight.getIntensity());
+ }
+
+ @Test
+ @DisplayName("setting the intensity to a number <= 0 should turn the light off")
+ public void checkTurnOn() {
+ dimmableLight.setIntensity(0);
+ assertFalse(dimmableLight.isOn());
+ }
+ }
+
+ @Nested
+ @DisplayName(" when off")
+ class WhenOff {
+
+ @BeforeEach
+ public void setLightOff() {
+ dimmableLight.setOn(false);
+ }
+
+ @Test
+ @DisplayName("the isOn method should return false")
+ public void isOn() {
+ assertFalse(dimmableLight.isOn());
+ }
+
+ @Test
+ @DisplayName("the intensity should be 0 when the light is turned off")
+ public void checkIntensityWhenTurnedOff() {
+ assertEquals(0, dimmableLight.getIntensity());
+ }
+
+ @Test
+ @DisplayName("setting the intensity to a number between 1 and 100")
+ public void checkIntensityBetweenLimits() {
+ dimmableLight.setIntensity(50);
+ assertEquals(50, dimmableLight.getIntensity());
+ }
+
+ @Test
+ @DisplayName("setting the intensity to a number > 0 should turn the light on")
+ public void checkIntensityLessThanLimits() {
+ dimmableLight.setIntensity(47);
+ assertEquals(47, dimmableLight.getIntensity());
+ }
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/KnobDimmerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/KnobDimmerTests.java
new file mode 100644
index 0000000..155242d
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/KnobDimmerTests.java
@@ -0,0 +1,46 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("A Knob Dimmer")
+public class KnobDimmerTests {
+
+ KnobDimmer knobDimmer;
+
+ @BeforeEach
+ public void createNewKnobDimmer() {
+ this.knobDimmer = new KnobDimmer();
+ }
+
+ @Nested
+ @DisplayName(" when multiple lights are present")
+ class MultipleLights {
+
+ @BeforeEach
+ public void setLights() {
+ DimmableLight dl;
+ for (int i = 0; i < 3; i++) {
+ dl = new DimmableLight();
+ dl.setIntensity(10);
+ ;
+ knobDimmer.addDimmableLight(dl);
+ }
+ }
+
+ @Test
+ @DisplayName(" set the intensity ")
+ public void increase() {
+ knobDimmer.setLightIntensity(30);
+ for (DimmableLight dl : knobDimmer.getOutputs()) {
+ assertEquals(30, dl.getIntensity());
+ }
+ }
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/RegularLightTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/RegularLightTests.java
new file mode 100644
index 0000000..c4f4694
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/RegularLightTests.java
@@ -0,0 +1,47 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("A RegularLight")
+public class RegularLightTests {
+
+ RegularLight regularLight;
+
+ @BeforeEach
+ public void createRegularLight() {
+ this.regularLight = new RegularLight();
+ }
+
+ @Test
+ @DisplayName("State when just created")
+ public void beginningState() {
+ assertFalse(regularLight.isOn());
+ }
+
+ @Test
+ @DisplayName("Changing state to on after creating the light")
+ public void createAndSetOn() {
+ regularLight.setOn(true);
+ assertTrue(regularLight.isOn());
+ }
+
+ @Test
+ @DisplayName("Change state of the light to off after creating it")
+ public void createAndSetOff() {
+ regularLight.setOn(false);
+ assertFalse(regularLight.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks whether a turned on light getting turned on is still in the on State")
+ public void setOn() {
+ regularLight.setOn(true);
+ regularLight.setOn(true);
+ assertTrue(regularLight.isOn());
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java
new file mode 100644
index 0000000..f2b737a
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java
@@ -0,0 +1,69 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+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.models.ConfirmationToken;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.reactive.function.client.WebClient;
+
+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() {}
+
+ protected static final UserRegistrationRequest enabledUser = new UserRegistrationRequest();
+
+ static {
+ enabledUser.setName("Enabled User");
+ enabledUser.setEmail("enabled@example.com");
+ enabledUser.setUsername("enabled");
+ enabledUser.setPassword("password");
+ }
+
+ protected void registerTestUser(
+ final TestRestTemplate restTemplate,
+ final UserRepository userRepository,
+ final ConfirmationTokenRepository tokenRepository) {
+ final ResponseEntity res2 =
+ restTemplate.postForEntity(this.url("/register"), enabledUser, OkResponse.class);
+ assertThat(res2.getStatusCode().equals(HttpStatus.OK));
+
+ final User persistedEnabledUser = userRepository.findByUsername("enabled");
+ final ConfirmationToken token = tokenRepository.findByUser(persistedEnabledUser);
+
+ final ResponseEntity res3 =
+ WebClient.create(getBaseURL())
+ .get()
+ .uri("/register/confirm-account?token=" + token.getConfirmationToken())
+ .retrieve()
+ .toBodilessEntity()
+ .block();
+
+ assertThat(res3.getStatusCode().is2xxSuccessful());
+ assertThat(userRepository.findByUsername("enabled").getEnabled());
+ }
+
+ @BeforeEach
+ void setUpHack() {
+ if (!setupDone) {
+ setUp();
+ setupDone = true;
+ }
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java
new file mode 100644
index 0000000..dbd7e21
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java
@@ -0,0 +1,26 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+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;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+@AutoConfigureMockMvc
+public class SmarthutApplicationTests extends SmartHutTest {
+
+ @Autowired private TestRestTemplate restTemplate;
+
+ @Test
+ public void anonymousGreetingShouldNotBeAuthorized() throws Exception {
+ assertThat(
+ this.restTemplate
+ .getForEntity(getBaseURL(), Void.class)
+ .getStatusCode()
+ .equals(HttpStatus.UNAUTHORIZED));
+ }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SwitchTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SwitchTests.java
new file mode 100644
index 0000000..9f937d3
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SwitchTests.java
@@ -0,0 +1,96 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("A switch")
+public class SwitchTests {
+
+ Switch aSwitch;
+
+ @BeforeEach
+ public void createNewSwitch() {
+
+ this.aSwitch = new Switch();
+ RegularLight regularLight = new RegularLight();
+ DimmableLight dimmableLight = new DimmableLight();
+ SmartPlug smartPlug = new SmartPlug();
+ this.aSwitch.getOutputs().add(regularLight);
+ this.aSwitch.getOutputs().add(dimmableLight);
+ this.aSwitch.getOutputs().add(smartPlug);
+ }
+
+ @Test
+ @DisplayName("check state when switch created")
+ public void createdSwitch() {
+ assertFalse(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Check toggle on a switch in its off state")
+ public void offToggle() {
+ aSwitch.toggle();
+ assertTrue(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks whether setting on a off switch works as intended")
+ public void offSetOn() {
+ aSwitch.setOn(true);
+ assertTrue(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks whether setting off a on switch works as intended")
+ public void onSetOff() {
+ aSwitch.toggle();
+ aSwitch.setOn(false);
+ assertFalse(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks that setting off an off switch results in a off state")
+ public void offSetOff() {
+ aSwitch.setOn(false);
+ assertFalse(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks that setting on an on switch results in a on state")
+ public void onSetOn() {
+ aSwitch.toggle();
+ aSwitch.setOn(true);
+ assertTrue(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks wheter toggling a on switch set its state to off")
+ public void onToggle() {
+ aSwitch.setOn(true);
+ aSwitch.toggle();
+ assertFalse(aSwitch.isOn());
+ }
+
+ @Test
+ @DisplayName("Checks that toggling on sets all elements of the Set on as well")
+ public void toggleEffctOnSet() {
+ aSwitch.toggle();
+ for (final Switchable s : aSwitch.getOutputs()) {
+ assertTrue(s.isOn());
+ }
+ }
+
+ @Test
+ @DisplayName("Checks that toggling the switch off also sets all elements of its set off")
+ public void toggleOffEffectOnElementes() {
+ aSwitch.setOn(true);
+ aSwitch.toggle();
+ for (final Switchable s : aSwitch.getOutputs()) {
+ assertFalse(s.isOn());
+ }
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..bdaafc0
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,37 @@
+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.registrationRedirect=http://localhost:3000
+
+email.resetpasswordSubject=SmartHut.sm password reset
+email.resetpassword=To reset your password, please click here:
+email.resetpasswordPath=http://localhost:3000/password-reset?token=
+email.resetPasswordRedirect=http://localhost:3000/conf-reset-pass
\ No newline at end of file