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 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 addLight( + @PathVariable("id") long inputId, @RequestParam("lightId") Long lightId) + throws NotFoundException { + return addOutput(inputId, lightId); + } + + @DeleteMapping("/{id}/lights") + public Set 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 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 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 addLight( + @PathVariable("id") long inputId, @RequestParam("lightId") Long lightId) + throws NotFoundException { + return addOutput(inputId, lightId); + } + + @DeleteMapping("/{id}/lights") + public Set 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 addSwitchable( + @PathVariable("id") long inputId, @RequestParam("switchableId") Long switchableId) + throws NotFoundException { + return addOutput(inputId, switchableId); + } + + @DeleteMapping("/{id}/lights") + public Set 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 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