diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..997894c322fef088a7ff37136b44d788d0aab02e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,29 @@ +stages: +- ".pre" +- build +- test +- deploy +- ".post" +build-job: + stage: build + script: + - echo "Compiling the code..." + - echo "Compile complete." +unit-test-job: + stage: test + script: + - echo "Running unit tests... This will take about 60 seconds." + - sleep 60 + - echo "Code coverage is 90%" +lint-test-job: + stage: test + script: + - echo "Linting code... This will take about 10 seconds." + - sleep 10 + - echo "No lint issues found." +deploy-job: + stage: deploy + environment: production + script: + - echo "Deploying application..." + - echo "Application successfully deployed." diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..5f0536eb74343e6e1e0aa69e3aacbc51e015f97e --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/mvnw b/mvnw new file mode 100644 index 0000000000000000000000000000000000000000..66df2854281f4cb6869e4830dd1a7abd1e946c18 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000000000000000000000000000000000000..95ba6f54ac526de46248af840bab26f33f946b93 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..7427f5eb4c5713ba283696bbec8d7a41e35a9dc0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.2.4</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <groupId>idatt2105.group11</groupId> + <artifactId>idatt2105-project-backend</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>idatt2105-project-backend</name> + <description>Backend for IDATT2105 project</description> + <properties> + <java.version>21</java.version> + </properties> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <!-- Spring Security --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.11.5</version> + </dependency> + + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.11.5</version> + </dependency> + + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.11.5</version> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project> diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/Idatt2105ProjectBackendApplication.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/Idatt2105ProjectBackendApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..8205825790b4a80b7ed1536b977c830baa8b44e4 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/Idatt2105ProjectBackendApplication.java @@ -0,0 +1,13 @@ +package idatt2105.group11.idatt2105projectbackend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Idatt2105ProjectBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(Idatt2105ProjectBackendApplication.class, args); + } + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/QuizCreationAndTest.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/QuizCreationAndTest.java new file mode 100644 index 0000000000000000000000000000000000000000..88c0eb6dce44242e91c01029edfaac99c2c48ccb --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/QuizCreationAndTest.java @@ -0,0 +1,73 @@ +package idatt2105.group11.idatt2105projectbackend; + +import idatt2105.group11.idatt2105projectbackend.model.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +public class QuizCreationAndTest { + + // Initialize the logger for the class + private static final Logger logger = LoggerFactory.getLogger(QuizCreationAndTest.class); + + public static void main(String[] args) { + logger.info("Quiz creation and test started."); + + //Step 0: Create a User + Role admin = new Role("ROLE_ADMIN"); + HashSet<Role> roles = new HashSet<>(Arrays.asList(admin)); + + User JohnDoe = new User("John Doe", "password", roles); + logger.info("Created user: {}", JohnDoe.getName()); + + // Step 1: Create questions with assigned scores + MultipleChoiceQuestion question1 = new MultipleChoiceQuestion(null, + "What is the capital of France?", + 2, + Arrays.asList("1. Paris", "2. London", "3. Berlin", "4. Madrid"), + 0 + ); + logger.info("Created question: {}", question1.getQuestionText()); + + MultipleChoiceQuestion question2 = new MultipleChoiceQuestion(null, + "Which language is primarily spoken in Brazil?", + 3, + Arrays.asList("1. Spanish", "2. Portuguese", "3. French", "4. English"), + 1 + ); + logger.info("Created question: {}", question2.getQuestionText()); + + // Step 2: Create a quiz and add questions + List<Question> questions = new ArrayList<>(); + questions.add(question1); + questions.add(question2); + Quiz quiz = new Quiz("Sample Quiz", questions, JohnDoe, QuizCategory.GEOGRAPHY, QuizDifficulty.EASY); + questions.forEach(question -> question.setQuiz(quiz)); + logger.info("Created quiz: {}", quiz.getTitle()); + + // Step 3: Simulate answers from a user + QuestionAnswer answer1 = new QuestionAnswer(question1, "2"); + QuestionAnswer answer2 = new QuestionAnswer(question2, "2"); + logger.info("Simulated answers for the quiz."); + + // Step 4: Create a QuizResult and add answers + QuizResult quizResult = new QuizResult(quiz, new ArrayList<>(), JohnDoe, "påbegynt", LocalDateTime.now(), null); // Initialize with an empty answers list + quizResult.addQuestionAnswer(answer1); + quizResult.addQuestionAnswer(answer2); + logger.info("Created quiz result for user: {}", JohnDoe.getName()); + + // Output results + quizResult.getAnswers().forEach(answer -> { + logger.info("Question: {} - Your Answer: {} - Correct? {}", answer.getQuestion().getQuestionText(), answer.getGivenAnswer(), answer.isCorrect()); + }); + + logger.info("Quiz completed!"); + int totalPossibleScore = questions.stream().mapToInt(Question::getScore).sum(); + logger.info("Your score: {}/{}", quizResult.getScore(), totalPossibleScore); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/configuration/SecurityConfiguration.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/configuration/SecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..0679b4aee4995f4187d4077db753f15e1a5e867a --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/configuration/SecurityConfiguration.java @@ -0,0 +1,93 @@ +package idatt2105.group11.idatt2105projectbackend.configuration; + + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import idatt2105.group11.idatt2105projectbackend.utils.RSAKeyProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfiguration { + + private final RSAKeyProperties keys; + + public SecurityConfiguration(RSAKeyProperties keys) { + this.keys = keys; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder encoder) { + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); + daoAuthenticationProvider.setUserDetailsService(userDetailsService); + daoAuthenticationProvider.setPasswordEncoder(encoder); + return new ProviderManager(daoAuthenticationProvider); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> { + auth.requestMatchers("/api/").permitAll(); + auth.anyRequest().authenticated(); + + }) + .oauth2ResourceServer((configurer) -> + configurer.jwt( + jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter()) + ) + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .build(); + } + + @Bean + public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey(keys.getPublicKey()).build(); + } + + @Bean + public JwtEncoder jwtEncoder() { + JWK jwk = new RSAKey.Builder(keys.getPublicKey()).privateKey(keys.getPrivateKey()).build(); + JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk)); + return new NimbusJwtEncoder(jwkSource); + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); + + return jwtAuthenticationConverter; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/AuthenticationController.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/AuthenticationController.java new file mode 100644 index 0000000000000000000000000000000000000000..ccd2d1d24d4b3fc9f8506f03bfa59a0606f0e561 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/AuthenticationController.java @@ -0,0 +1,44 @@ +package idatt2105.group11.idatt2105projectbackend.controller; + + +import idatt2105.group11.idatt2105projectbackend.dto.LoginResponseDTO; +import idatt2105.group11.idatt2105projectbackend.dto.TokenDTO; +import idatt2105.group11.idatt2105projectbackend.dto.UserRegistrationDTO; +import idatt2105.group11.idatt2105projectbackend.model.User; +import idatt2105.group11.idatt2105projectbackend.service.AuthenticationService; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/api/auth") +@CrossOrigin(origins = "http://localhost:5173") +public class AuthenticationController { + + private final AuthenticationService authService; + + public AuthenticationController(AuthenticationService authService) { + this.authService = authService; + } + + @PostMapping("/register") + public User registerUser(@RequestBody UserRegistrationDTO registrationDTO) { + //TODO: Remove return of password, use DTO + return authService.registerUser(registrationDTO.getName(), registrationDTO.getPassword()); + } + + @PostMapping("/login") + public LoginResponseDTO loginUser(@RequestBody UserRegistrationDTO registrationDTO) { + return authService.loginUser(registrationDTO.getName(), registrationDTO.getPassword()); + } + + @PostMapping("/refresh") + public TokenDTO refreshJWT(@RequestBody TokenDTO existingToken) throws InterruptedException { + Thread.sleep(500); + System.out.println("OLD TOKEN1: " + existingToken); + String token = authService.refreshJWT(existingToken.getToken()); + System.out.println("OLD TOKEN2: " + existingToken); + System.out.println("NEW TOKEN MAYBE: " + token); + + return new TokenDTO(token); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuestionAnswerController.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuestionAnswerController.java new file mode 100644 index 0000000000000000000000000000000000000000..2ba6e67ab3ee5fedffa956a9a36859af55ddb0d2 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuestionAnswerController.java @@ -0,0 +1,37 @@ +package idatt2105.group11.idatt2105projectbackend.controller; + +import idatt2105.group11.idatt2105projectbackend.dto.QuestionAnswerDTO; +import idatt2105.group11.idatt2105projectbackend.model.QuestionAnswer; +import idatt2105.group11.idatt2105projectbackend.service.QuestionAnswerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/question-answers") +public class QuestionAnswerController { + + private final QuestionAnswerService questionAnswerService; + + @Autowired + public QuestionAnswerController(QuestionAnswerService questionAnswerService) { + this.questionAnswerService = questionAnswerService; + } + + @PostMapping("/save") + public QuestionAnswerDTO saveAnswer(@RequestParam Integer quizResultId, + @RequestBody QuestionAnswerDTO answerDTO) { + QuestionAnswer savedAnswer = questionAnswerService.saveAnswer(quizResultId, answerDTO); + return convertToQuestionAnswerDTO(savedAnswer); + } + + private QuestionAnswerDTO convertToQuestionAnswerDTO(QuestionAnswer questionAnswer) { + QuestionAnswerDTO dto = new QuestionAnswerDTO(); + dto.setId(questionAnswer.getId()); + dto.setQuestionId(questionAnswer.getQuestion().getId()); + dto.setGivenAnswer(questionAnswer.getGivenAnswer()); + dto.setCorrect(questionAnswer.isCorrect()); + return dto; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuestionController.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuestionController.java new file mode 100644 index 0000000000000000000000000000000000000000..96580e013fc370959124d5f4c683addd42223b27 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuestionController.java @@ -0,0 +1,57 @@ +package idatt2105.group11.idatt2105projectbackend.controller; + +import idatt2105.group11.idatt2105projectbackend.dto.QuestionDTO; +import idatt2105.group11.idatt2105projectbackend.model.MultipleChoiceQuestion; +import idatt2105.group11.idatt2105projectbackend.model.Question; +import idatt2105.group11.idatt2105projectbackend.service.QuestionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/questions") +public class QuestionController { + + private final QuestionService questionService; + + @Autowired + public QuestionController(QuestionService questionService) { + this.questionService = questionService; + } + + @PostMapping + public QuestionDTO createMCQuestion(@RequestBody QuestionDTO questionDTO) { + MultipleChoiceQuestion question = questionService.createMCQuestion(questionDTO); + return convertToDTO(question); + } + + @PostMapping("/update") + public QuestionDTO updateQuestion(@RequestBody QuestionDTO questionDTO) { + return questionService.updateQuestion(questionDTO); + } + + @PostMapping("/delete") + public void deleteQuestion(@RequestBody Map<String, Integer> payload) { + questionService.deleteQuestion(payload.get("id")); + } + + @GetMapping("/list") + public List<QuestionDTO> getQuestionsByQuizId(@RequestParam Integer quizId) { + return questionService.findAllQuestionsByQuizId(quizId).stream() + .map((Question question) -> convertToDTO((MultipleChoiceQuestion) question)) + .collect(Collectors.toList()); + } + + public QuestionDTO convertToDTO(MultipleChoiceQuestion question) { + QuestionDTO questionDTO = new QuestionDTO(); + questionDTO.setId(question.getId()); + questionDTO.setQuestionText(question.getQuestionText()); + questionDTO.setScore(question.getScore()); + questionDTO.setAnswerOptions(question.getAnswerOptions()); + questionDTO.setCorrectAnswerIndex(question.getCorrectAnswerIndex()); + return questionDTO; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuizController.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuizController.java new file mode 100644 index 0000000000000000000000000000000000000000..efb1ecd213e77191a5a30cd05d930b4a5416f021 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuizController.java @@ -0,0 +1,90 @@ +package idatt2105.group11.idatt2105projectbackend.controller; + +import idatt2105.group11.idatt2105projectbackend.dto.*; +import idatt2105.group11.idatt2105projectbackend.exception.UserNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.*; +import idatt2105.group11.idatt2105projectbackend.service.QuizService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +@RestController +@RequestMapping("/api/quiz") +public class QuizController { + + private final QuizService quizService; + + @Autowired + public QuizController(QuizService quizService) { + this.quizService = quizService; + } + + @PostMapping("/create") + public QuizDTO createQuiz(@RequestBody QuizDTO quizDTO) { + Quiz quiz = quizService.createQuiz(quizDTO); + return convertToDTO(quiz); + } + + @PostMapping("/update") + public QuizDTO updateQuiz(@RequestBody QuizDTO quizDTO) { + return quizService.updateQuiz(quizDTO); + } + + @PostMapping("/delete") + public void deleteQuiz(@RequestBody Map<String, Integer> payload) { + quizService.deleteQuiz(payload.get("id")); + } + + @GetMapping("/quiz") + public QuizDTO getQuizById(@RequestParam Integer quizId) { + return convertToDTO(quizService.findQuizById(quizId)); + } + + + @GetMapping("/") + public List<QuizDTO> getQuizzes() { + return quizService.findAllQuizzes().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @GetMapping("/category") + public List<QuizDTO> getQuizzesByCategory(@RequestParam String category) { + return quizService.findAllQuizzesByCategory(QuizCategory.valueOf(category)).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @GetMapping("/difficulty") + public List<QuizDTO> getQuizzesByDifficulty(@RequestParam String difficulty) { + return quizService.findAllQuizzesByDifficulty(QuizDifficulty.valueOf(difficulty)).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @GetMapping("/creator") + public List<QuizDTO> getQuizzesByCreatorId(@RequestParam Integer creatorId) { + return quizService.findAllQuizzesByCreatorId(creatorId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + public QuizDTO convertToDTO(Quiz quiz) { + QuizDTO quizDTO = new QuizDTO(); + quizDTO.setId(quiz.getId()); + quizDTO.setTitle(quiz.getTitle()); + quizDTO.setCategory(quiz.getCategory()); + List<Integer> questionIds = quiz.getQuestions().stream() + .map(Question::getId) + .collect(Collectors.toList()); + quizDTO.setQuestionIds(questionIds); + return quizDTO; + } + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuizResultController.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuizResultController.java new file mode 100644 index 0000000000000000000000000000000000000000..e301efabc8abaf1aa8d33d2a882e4ba72fb36c5e --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/QuizResultController.java @@ -0,0 +1,61 @@ +package idatt2105.group11.idatt2105projectbackend.controller; + +import idatt2105.group11.idatt2105projectbackend.dto.QuizResultDTO; +import idatt2105.group11.idatt2105projectbackend.model.QuizResult; +import idatt2105.group11.idatt2105projectbackend.service.QuizResultService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/results") +public class QuizResultController { + + private final QuizResultService quizResultService; + + @Autowired + public QuizResultController(QuizResultService quizResultService) { + this.quizResultService = quizResultService; + } + + @PostMapping("/create") + public QuizResultDTO startQuizResult(@RequestBody QuizResultDTO quizResultDTO) { + return convertToQuizResultDTO(quizResultService.startQuizResult(quizResultDTO)); + } + + @PostMapping("/complete/") + public QuizResultDTO completeQuiz(@RequestParam Integer quizResultId) { + return quizResultService.completeQuiz(quizResultId); + } + + @GetMapping("/latest-result") + public QuizResultDTO getLatestQuizResultForUser(@RequestParam Integer userId) { + return quizResultService.findLatestQuizResultForUser(userId); + } + + + @GetMapping("/results") + public List<QuizResultDTO> getQuizResultsForUser(@RequestParam("userId") Integer userId) { + List<QuizResult> quizResults = quizResultService.findAllResultsForUserId(userId); + return quizResults.stream() + .map(this::convertToQuizResultDTO) + .collect(Collectors.toList()); + } + + public QuizResultDTO convertToQuizResultDTO(QuizResult quizResult) { + QuizResultDTO quizResultDTO = new QuizResultDTO(); + quizResultDTO.setQuizId(quizResult.getQuiz().getId()); + quizResultDTO.setUserId(quizResult.getUser().getId()); + quizResultDTO.setScore(quizResult.getScore()); + quizResultDTO.setStatus(quizResult.getStatus()); + quizResultDTO.setStartedAt(quizResult.getStartedAt()); + quizResultDTO.setCompletedAt(quizResult.getCompletedAt()); + + return quizResultDTO; + } + + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/UserController.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..8aed1a6f88950a2b4ce9e05db96843d80f541830 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/controller/UserController.java @@ -0,0 +1,27 @@ +package idatt2105.group11.idatt2105projectbackend.controller; + + +import idatt2105.group11.idatt2105projectbackend.model.User; +import idatt2105.group11.idatt2105projectbackend.service.UserService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping("/api/user") +@CrossOrigin(origins = "http://localhost:5173") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + + @GetMapping("/") + public List<User> getAppUsers() { + return userService.findAllAppUsers(); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/LoginResponseDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/LoginResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..6abc9a4b701dd234a38844cbe35e8c2337ee5e0e --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/LoginResponseDTO.java @@ -0,0 +1,53 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +import idatt2105.group11.idatt2105projectbackend.model.User; + +public class LoginResponseDTO { + + private String name; + private String password; + private String jwt; + + public LoginResponseDTO() { + super(); + } + + public LoginResponseDTO(User user, String jwt) { + this.name = user.getName(); + this.password = user.getPassword(); + this.jwt = jwt; + } + + public String getName() { + return name; + } + + public void setName(String username) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getJwt() { + return jwt; + } + + public void setJwt(String jwt) { + this.jwt = jwt; + } + + @Override + public String toString() { + return "LoginResponseDTO{" + + "username='" + name + '\'' + + ", password='" + password + '\'' + + ", jwt='" + jwt + '\'' + + '}'; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuestionAnswerDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuestionAnswerDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..e8e9a930e10bc83d7cdb0dd3578dfea5f671cbcd --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuestionAnswerDTO.java @@ -0,0 +1,60 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +public class QuestionAnswerDTO { + private Integer id; + private Integer questionId; + private String givenAnswer; + private boolean correct; + + public QuestionAnswerDTO() { + } + + public QuestionAnswerDTO(Integer id, Integer questionId, String givenAnswer, boolean correct) { + this.id = id; + this.questionId = questionId; + this.givenAnswer = givenAnswer; + this.correct = correct; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getQuestionId() { + return questionId; + } + + public void setQuestionId(Integer questionId) { + this.questionId = questionId; + } + + public String getGivenAnswer() { + return givenAnswer; + } + + public void setGivenAnswer(String givenAnswer) { + this.givenAnswer = givenAnswer; + } + + public boolean isCorrect() { + return correct; + } + + public void setCorrect(boolean correct) { + this.correct = correct; + } + + @Override + public String toString() { + return "QuestionAnswerDTO{" + + "id=" + id + + ", questionId=" + questionId + + ", givenAnswer='" + givenAnswer + '\'' + + ", correct=" + correct + + '}'; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuestionDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuestionDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..43511a249edf0abc2b601cfaab074b53c3c30fe2 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuestionDTO.java @@ -0,0 +1,64 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +import java.util.List; + +public class QuestionDTO { + private Integer id; + private String questionText; + private int score; + private List<String> answerOptions; + private int correctAnswerIndex; + + + public QuestionDTO() { + } + + public QuestionDTO(Integer id, String questionText, int score) { + this.id = id; + this.questionText = questionText; + this.score = score; + } + + // Getters and setters + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getQuestionText() { + return questionText; + } + + public void setQuestionText(String questionText) { + this.questionText = questionText; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + public List<String> getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(List<String> answerOptions) { + this.answerOptions = answerOptions; + } + + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + public void setCorrectAnswerIndex(int correctAnswerIndex) { + this.correctAnswerIndex = correctAnswerIndex; + } + + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuizDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuizDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..8ca7f073d1435bc0aa745b52cee9b1d7a8e14357 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuizDTO.java @@ -0,0 +1,76 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +import idatt2105.group11.idatt2105projectbackend.model.Quiz; +import idatt2105.group11.idatt2105projectbackend.model.QuizCategory; +import idatt2105.group11.idatt2105projectbackend.model.QuizDifficulty; + +import java.util.List; + +public class QuizDTO { + private Integer id; + private String title; + private List<Integer> questionIds; + private Integer creatorId; + private QuizCategory category; + private QuizDifficulty difficulty; + + public QuizDTO() { + } + + public QuizDTO(Integer id, String title, List<Integer> questionIds, Integer creatorId, QuizCategory category, QuizDifficulty difficulty) { + this.title = title; + this.questionIds = questionIds; + this.creatorId = creatorId; + this.category = category; + this.difficulty = difficulty; + } + + // Getters and setters + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Iterable<Integer> getQuestionIds() { + return questionIds; + } + + public void setQuestionIds(List<Integer> questionIds) { + this.questionIds = questionIds; + } + + public Integer getCreatorId() { + return creatorId; + } + + public void setCreatorId(Integer creatorId) { + this.creatorId = creatorId; + } + + public QuizCategory getCategory() { + return category; + } + + public void setCategory(QuizCategory category) { + this.category = category; + } + + public QuizDifficulty getDifficulty() { + return difficulty; + } + + public void setDifficulty(QuizDifficulty difficulty) { + this.difficulty = difficulty; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuizResultDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuizResultDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ca90fa8f7b96ca0c3936fcf2d418a93c0d03c495 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/QuizResultDTO.java @@ -0,0 +1,105 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public class QuizResultDTO { + private Integer id; + private Integer quizId; + private Integer userId; + private List<QuestionAnswerDTO> answers; + private int score; + private String status; + private LocalDateTime startedAt; + private LocalDateTime completedAt; + + + public QuizResultDTO() { + } + + public QuizResultDTO(Integer id, Integer quizId, Integer userId, List<QuestionAnswerDTO> answers, int score, String status, LocalDateTime startedAt, LocalDateTime completedAt) { + this.id = id; + this.quizId = quizId; + this.userId = userId; + this.answers = answers; + this.score = score; + this.status = status; + this.startedAt = startedAt; + this.completedAt = completedAt; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getQuizId() { + return quizId; + } + + public void setQuizId(Integer quizId) { + this.quizId = quizId; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public List<QuestionAnswerDTO> getAnswers() { + return answers; + } + + public void setAnswers(List<QuestionAnswerDTO> answers) { + this.answers = answers; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getStartedAt() { + return startedAt; + } + + public void setStartedAt(LocalDateTime startedAt) { + this.startedAt = startedAt; + } + + public LocalDateTime getCompletedAt() { + return completedAt; + } + + public void setCompletedAt(LocalDateTime completedAt) { + this.completedAt = completedAt; + } + + @Override + public String toString() { + return "QuizResultDTO{" + + "id=" + id + + ", quizId=" + quizId + + ", userId=" + userId + + ", answers=" + answers + + ", score=" + score + + '}'; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/TokenDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/TokenDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..3663df5be9b4c3deabd5a7cce812d4254d00beb4 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/TokenDTO.java @@ -0,0 +1,30 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + + +public class TokenDTO { + private String token; + + public TokenDTO() { + super(); + } + + public TokenDTO(String token) { + super(); + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public String toString() { + return "TokenDTO{" + + "token='" + token + '\'' + + '}'; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/UserDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/UserDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..e849b91953f0686bb7389f93007d430739a986c3 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/UserDTO.java @@ -0,0 +1,27 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +import java.util.List; + +public class UserDTO extends UserRegistrationDTO { + private Integer id; + + // Assuming you might need more fields here, such as role, etc. + + public UserDTO() { + super(); + } + + public UserDTO(Integer id, String name, String password) { + super(name, password); + this.id = id; + } + + // Getters and setters for the 'id' field + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/UserRegistrationDTO.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/UserRegistrationDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..586b8fa8399d034e48f4f2ad2f23f71b0a8ac719 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/dto/UserRegistrationDTO.java @@ -0,0 +1,43 @@ +package idatt2105.group11.idatt2105projectbackend.dto; + +public class UserRegistrationDTO { + + private String name; + private String password; + + + public UserRegistrationDTO() { + super(); + } + + public UserRegistrationDTO(String name, String password) { + this.name = name; + this.password = password; + } + + // Getters and setters + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "UserRegistrationDTO{" + + "username='" + name + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/QuestionNotFoundException.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/QuestionNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..bde8a6a8374eead1e2a033a2b43d8de32c48b73e --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/QuestionNotFoundException.java @@ -0,0 +1,7 @@ +package idatt2105.group11.idatt2105projectbackend.exception; + +public class QuestionNotFoundException extends RuntimeException { + public QuestionNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/QuizNotFoundException.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/QuizNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..d49ab7e6cb8c2da6393b5c6ab6f58845b54184f6 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/QuizNotFoundException.java @@ -0,0 +1,8 @@ +package idatt2105.group11.idatt2105projectbackend.exception; + + +public class QuizNotFoundException extends RuntimeException { + public QuizNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/UserNotFoundException.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/UserNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..5a0b429d45e440089073b74444c5539f7b2abc23 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/exception/UserNotFoundException.java @@ -0,0 +1,8 @@ +package idatt2105.group11.idatt2105projectbackend.exception; + + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/MultipleChoiceQuestion.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/MultipleChoiceQuestion.java new file mode 100644 index 0000000000000000000000000000000000000000..3860211ee4d98725a7e770c95051d6382432b626 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/MultipleChoiceQuestion.java @@ -0,0 +1,51 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +import jakarta.persistence.*; + +import java.util.List; + +@Entity +public class MultipleChoiceQuestion extends Question { + @ElementCollection + @CollectionTable(name = "answer_options", joinColumns = @JoinColumn(name = "question_id")) + @Column(name = "option") + private List<String> answerOptions; + + @Column(nullable = false) + private int correctAnswerIndex; + + public MultipleChoiceQuestion() { + } + + public MultipleChoiceQuestion(Quiz quiz, String questionText, int score, List<String> answerOptions, int correctAnswerIndex) { + super(quiz, questionText, score); + this.answerOptions = answerOptions; + this.correctAnswerIndex = correctAnswerIndex; + } + + public List<String> getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(List<String> answerOptions) { + this.answerOptions = answerOptions; + } + + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + public void setCorrectAnswerIndex(int correctAnswerIndex) { + this.correctAnswerIndex = correctAnswerIndex; + } + + @Override + public boolean checkAnswer(String answer) { + try { + int answerIndex = Integer.parseInt(answer); + return answerIndex == correctAnswerIndex + 1; + } catch (NumberFormatException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Question.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Question.java new file mode 100644 index 0000000000000000000000000000000000000000..43551670e3194a45aacb9bf419191d70c5e209e2 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Question.java @@ -0,0 +1,62 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +import jakarta.persistence.*; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +public abstract class Question { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne + @JoinColumn(name = "quiz_id") + private Quiz quiz; + + @Column(nullable = false) + private String questionText; + + @Column(nullable = false) + private int score; + + public Question() { + } + + public Question(Quiz quiz, String questionText, int score) { + this.questionText = questionText; + this.score = score; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getQuestionText() { + return questionText; + } + + public void setQuestionText(String questionText) { + this.questionText = questionText; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + public abstract boolean checkAnswer(String answer); + + public Quiz getQuiz() { + return quiz; + } + public void setQuiz(Quiz quiz) { + this.quiz = quiz; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuestionAnswer.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuestionAnswer.java new file mode 100644 index 0000000000000000000000000000000000000000..615243610fea4b81b934c89181007e13ea28b28c --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuestionAnswer.java @@ -0,0 +1,83 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +import jakarta.persistence.*; + +@Entity +public class QuestionAnswer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "question_id") + private Question question; + + @Column(nullable = false) + private String givenAnswer; + + @Column(nullable = false) + private boolean correct; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "quiz_result_id") + private QuizResult quizResult; + + public QuestionAnswer() { + // JPA requires a no-arg constructor + } + + public QuestionAnswer(Question question, String givenAnswer) { + this.question = question; + this.givenAnswer = givenAnswer; + this.correct = validateAnswer(givenAnswer); + } + + // Validation method + private boolean validateAnswer(String givenAnswer) { + return question.checkAnswer(givenAnswer); + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Question getQuestion() { + return question; + } + + public void setQuestion(Question question) { + this.question = question; + // Update 'correct' whenever the question changes + this.correct = validateAnswer(this.givenAnswer); + } + + public String getGivenAnswer() { + return givenAnswer; + } + + public void setGivenAnswer(String givenAnswer) { + this.givenAnswer = givenAnswer; + // Update 'correct' whenever the given answer changes + this.correct = validateAnswer(givenAnswer); + } + + public boolean isCorrect() { + return correct; + } + + public void setCorrect(boolean correct) { + this.correct = correct; + } + + + public QuizResult getQuizResult() { + return quizResult; + } + public void setQuizResult(QuizResult quizResult) { + this.quizResult = quizResult; + } +} \ No newline at end of file diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Quiz.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Quiz.java new file mode 100644 index 0000000000000000000000000000000000000000..a0bb5a153a199663d97dad16dd8219eec3f76989 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Quiz.java @@ -0,0 +1,90 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +import jakarta.persistence.*; + +import java.util.List; + + +@Entity +public class Quiz { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private String title; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "quiz_id") + private List<Question> questions; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id") + private User creator; + + @Enumerated(EnumType.STRING) + private QuizCategory category; + + @Enumerated(EnumType.STRING) + private QuizDifficulty difficulty; + + public Quiz() { + } + + public Quiz(String title, List<Question> questions, User creator, QuizCategory category, QuizDifficulty difficulty) { + this.title = title; + this.questions = questions; + this.creator = creator; + this.category = category; + this.difficulty = difficulty; + + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List<Question> getQuestions() { + return questions; + } + + public void setQuestions(List<Question> questions) { + this.questions = questions; + } + + public User getCreator() { + return creator; + } + + public QuizCategory getCategory() { + return category; + } + public void setCategory(QuizCategory category) { + this.category = category; + } + + public void setCreator(User creator) { + this.creator = creator; + } + + public QuizDifficulty getDifficulty() { + return difficulty; + } + + public void setDifficulty(QuizDifficulty difficulty) { + this.difficulty = difficulty; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizCategory.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizCategory.java new file mode 100644 index 0000000000000000000000000000000000000000..d3b61a168d77f5c535ec36cb7f4cfac60316d3ca --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizCategory.java @@ -0,0 +1,21 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +public enum QuizCategory { + BUSINESS, + ART, + COMPUTER_SCIENCE, + CULTURE_AND_TRADITIONS, + FINANCE, + GENERAL_KNOWLEDGE, + GEOGRAPHY, + HISTORY, + LANGUAGES, + LAW, + MATH, + MUSIC, + SCIENCE, + SEASONAL, + SOCIAL_EMOTIONAL_LEARNING, + SOCIAL_STUDIES, + TRIVIA +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizDifficulty.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizDifficulty.java new file mode 100644 index 0000000000000000000000000000000000000000..6d3d97cf3a95b16ebca1f9096ffd66bac652217a --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizDifficulty.java @@ -0,0 +1,7 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +public enum QuizDifficulty { + EASY, + MEDIUM, + HARD +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizResult.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizResult.java new file mode 100644 index 0000000000000000000000000000000000000000..c4acb5445e1ddf478ca20023e04169d5886bdb53 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/QuizResult.java @@ -0,0 +1,138 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "quiz_results") +public class QuizResult { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne + @JoinColumn(name = "quiz_id", nullable = false) + private Quiz quiz; + + @OneToMany(mappedBy = "quizResult", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private List<QuestionAnswer> answers = new ArrayList<>(); + + @Column(nullable = false) + private int score; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + + @Column(nullable = false) + private String status; + + @Column(nullable = false) + private LocalDateTime startedAt; + + @Column + private LocalDateTime completedAt; + + + + + public QuizResult() { + // JPA requires a no-arg constructor + } + + // Constructor for initializing with a quiz and optionally with answers. + public QuizResult(Quiz quiz, List<QuestionAnswer> answers, User user, String status, LocalDateTime startedAt, LocalDateTime completedAt) { + this.quiz = quiz; + this.setAnswers(answers); + this.user = user; + this.status = status; + this.startedAt = startedAt; + this.completedAt = completedAt; + } + + // Getter and Setter methods + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Quiz getQuiz() { + return quiz; + } + + public void setQuiz(Quiz quiz) { + this.quiz = quiz; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public List<QuestionAnswer> getAnswers() { + return answers; + } + + // When setting answers, also recalculate the score based on the correctness and question score. + public void setAnswers(List<QuestionAnswer> answers) { + this.answers = answers; + this.answers.forEach(answer -> answer.setQuizResult(this)); + calculateScore(); // Recalculate the score based on the new set of answers + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getStartedAt() { + return startedAt; + } + + public void setStartedAt(LocalDateTime startedAt) { + this.startedAt = startedAt; + } + + public LocalDateTime getCompletedAt() { + return completedAt; + } + + public void setCompletedAt(LocalDateTime completedAt) { + this.completedAt = completedAt; + } + + public void addQuestionAnswer(QuestionAnswer questionAnswer) { + this.answers.add(questionAnswer); + questionAnswer.setQuizResult(this); + calculateScore(); + } + + private void calculateScore() { + this.score = this.answers.stream() + .filter(QuestionAnswer::isCorrect) + .mapToInt(answer -> answer.getQuestion().getScore()) + .sum(); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Role.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..02cfab748a4a8fc3f0455b25be2e210c815396c7 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/Role.java @@ -0,0 +1,62 @@ +package idatt2105.group11.idatt2105projectbackend.model; + + +import jakarta.persistence.Id; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; + +import org.springframework.security.core.GrantedAuthority; + +@Entity +@Table(name = "role") +public class Role implements GrantedAuthority { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "role_id") + private Integer roleId; + + @Column(nullable = false) + private String authority; + + public Role() { + super(); + } + + public Role(String authority) { + this.authority = authority; + } + + public Role(Integer roleId, String authority) { + this.roleId = roleId; + this.authority = authority; + } + + public Integer getRoleId() { + return roleId; + } + + public void setRoleId(Integer roleId) { + this.roleId = roleId; + } + + @Override + public String getAuthority() { + return this.authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + @Override + public String toString() { + return "Role{" + + "roleId=" + roleId + + ", authority='" + authority + '\'' + + '}'; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/model/User.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/User.java new file mode 100644 index 0000000000000000000000000000000000000000..38ac0b49dd27d1cdf18aa72a79988d21268a4ab6 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/model/User.java @@ -0,0 +1,132 @@ +package idatt2105.group11.idatt2105projectbackend.model; + +import idatt2105.group11.idatt2105projectbackend.model.Quiz; +import idatt2105.group11.idatt2105projectbackend.model.QuizResult; +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.*; + + +@Entity +@Table(name = "users") +public class User implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "app_user_id", unique = true) + private Integer id; + + @Column(nullable = false, unique = true) + private String name; + + @Column(nullable = false) + private String password; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "app_user_role", + joinColumns = {@JoinColumn(name = "app_user_id")}, + inverseJoinColumns = {@JoinColumn(name = "role_id")} + ) + private Set<Role> authorities; + + // Quizzes created by the user + @OneToMany(mappedBy = "creator", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List<Quiz> createdQuizzes = new ArrayList<>(); + + // Quiz attempts by the user + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List<QuizResult> quizResults = new ArrayList<>(); + + + public User() { + this.authorities = new HashSet<>(); + } + + public User(String name, String password, Set<Role> authorities) { + this.name = name; + this.password = password; + this.authorities = authorities; + } + + public User(Integer id, String name, String password, Set<Role> authorities) { + this.id = id; + this.name = name; + this.password = password; + this.authorities = authorities; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public List<Quiz> getCreatedQuizzes() { + return createdQuizzes; + } + + public void setCreatedQuizzes(List<Quiz> createdQuizzes) { + this.createdQuizzes = createdQuizzes; + } + + public List<QuizResult> getQuizResults() { + return quizResults; + } + + public void setQuizResults(List<QuizResult> quizResults) { + this.quizResults = quizResults; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return null; + } + + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setAuthorities(Set<Role> authorities) { + this.authorities = authorities; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuestionAnswerRepository.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuestionAnswerRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..8379417a02f96aa55e05fb23b12773656dd65455 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuestionAnswerRepository.java @@ -0,0 +1,15 @@ +package idatt2105.group11.idatt2105projectbackend.repository; + +import idatt2105.group11.idatt2105projectbackend.model.QuestionAnswer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface QuestionAnswerRepository extends JpaRepository<QuestionAnswer, Integer> { + + List<QuestionAnswer> findByQuestionId(Integer questionId); + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuestionRepository.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuestionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..5ee965157540a1c033c6ca56ba5cddd4dd9ec5f7 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuestionRepository.java @@ -0,0 +1,16 @@ +package idatt2105.group11.idatt2105projectbackend.repository; + +import idatt2105.group11.idatt2105projectbackend.model.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface QuestionRepository extends JpaRepository<Question, Integer> { + + Optional<Question> findById(Integer id); + + List<Question> findAllByQuizId(Integer quizId); +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuizRepository.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuizRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..b507f9251a6f1824efd10a72a8a8275bcf87492b --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuizRepository.java @@ -0,0 +1,21 @@ +package idatt2105.group11.idatt2105projectbackend.repository; + +import idatt2105.group11.idatt2105projectbackend.model.QuizCategory; +import idatt2105.group11.idatt2105projectbackend.model.Quiz; +import idatt2105.group11.idatt2105projectbackend.model.QuizDifficulty; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface QuizRepository extends JpaRepository<Quiz, Integer> { + + Optional<Quiz> findById(Integer id); + + List<Quiz> findAllByCategory(QuizCategory category); + List<Quiz> findAllByDifficulty(QuizDifficulty difficulty); + List<Quiz> findAllByCreatorId(Integer creatorId); + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuizResultRepository.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuizResultRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..a20076e27484240c3366eea68f9cfaa1c81d9e15 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/QuizResultRepository.java @@ -0,0 +1,17 @@ +package idatt2105.group11.idatt2105projectbackend.repository; + +import idatt2105.group11.idatt2105projectbackend.model.QuizResult; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface QuizResultRepository extends JpaRepository<QuizResult, Integer> { + List<QuizResult> findByUserId(Integer userId); + + Optional<QuizResult> findFirstByUserIdOrderByCompletedAtDesc(Integer userId); + + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/RoleRepository.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/RoleRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..3a758ba07c72563e7816e3702cae124744cd9a74 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package idatt2105.group11.idatt2105projectbackend.repository; + +import idatt2105.group11.idatt2105projectbackend.model.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository<Role, Integer> { + Optional<Role> findByAuthority(String authority); +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/UserRepository.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..a8a93475d75592888618834657756ec06e80d998 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/repository/UserRepository.java @@ -0,0 +1,20 @@ +package idatt2105.group11.idatt2105projectbackend.repository; + + +import idatt2105.group11.idatt2105projectbackend.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository<User, Integer> { + + Optional<User> findByName(String Name); + + Optional<User> findById(Integer id); + + Optional<User> findByPassword(String password); +} + + diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/AuthenticationService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/AuthenticationService.java new file mode 100644 index 0000000000000000000000000000000000000000..cf05fd22658da70d7828fd5b29ec74c5dd00e5c7 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/AuthenticationService.java @@ -0,0 +1,86 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import idatt2105.group11.idatt2105projectbackend.dto.LoginResponseDTO; +import idatt2105.group11.idatt2105projectbackend.exception.UserNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.Role; +import idatt2105.group11.idatt2105projectbackend.model.User; +import idatt2105.group11.idatt2105projectbackend.repository.RoleRepository; +import idatt2105.group11.idatt2105projectbackend.repository.UserRepository; +import jakarta.transaction.Transactional; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +@Service +@Transactional +public class AuthenticationService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder encoder; + private final AuthenticationManager authManager; + private final TokenService tokenService; + private final UserService userService; + + public AuthenticationService(UserRepository userRepository, RoleRepository roleRepository, + PasswordEncoder encoder, AuthenticationManager authManager, + TokenService tokenService, UserService userService) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.encoder = encoder; + this.authManager = authManager; + this.tokenService = tokenService; + this.userService = userService; + } + + public User registerUser(String username, String password) { + String encodedPassword = encoder.encode(password); + Role userRole = null; + if (roleRepository.findByAuthority("USER").isPresent()) userRole = roleRepository.findByAuthority("USER").get(); + + Set<Role> authorities = new HashSet<>(); + authorities.add(userRole); + + return userRepository.save(new User(username, encodedPassword, authorities)); + } + + public LoginResponseDTO loginUser(String username, String password) { + try { + Authentication auth = authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + String token = tokenService.generateJwt(auth); + System.out.println("first token: " + token); + + Optional<User> optionalUser = userRepository.findByName(username); + + if (optionalUser.isPresent()) { + return new LoginResponseDTO(optionalUser.get(), token); + } else { + throw new UsernameNotFoundException("User not found."); + } + + } catch (AuthenticationException e) { + throw new UserNotFoundException("Invalid username or password."); + } + } + + public String refreshJWT(String existingToken) { + tokenService.verifyToken(existingToken); + + String username = tokenService.getNameFromToken(existingToken); + + UserDetails userDetails = userService.loadUserByUsername(username); + + Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + return tokenService.generateJwt(auth); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuestionAnswerService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuestionAnswerService.java new file mode 100644 index 0000000000000000000000000000000000000000..a9f20e6fa52ef6068b582195ad2ece7cb850e2f3 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuestionAnswerService.java @@ -0,0 +1,48 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import idatt2105.group11.idatt2105projectbackend.dto.QuestionAnswerDTO; +import idatt2105.group11.idatt2105projectbackend.exception.QuizNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.Question; +import idatt2105.group11.idatt2105projectbackend.model.QuestionAnswer; +import idatt2105.group11.idatt2105projectbackend.model.QuizResult; +import idatt2105.group11.idatt2105projectbackend.repository.QuestionAnswerRepository; +import idatt2105.group11.idatt2105projectbackend.repository.QuestionRepository; +import idatt2105.group11.idatt2105projectbackend.repository.QuizResultRepository; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class QuestionAnswerService { + + private final QuestionAnswerRepository questionAnswerRepository; + private final QuizResultRepository quizResultRepository; + private final QuestionRepository questionRepository; + + @Autowired + public QuestionAnswerService(QuestionAnswerRepository questionAnswerRepository, + QuizResultRepository quizResultRepository, + QuestionRepository questionRepository) { + this.questionAnswerRepository = questionAnswerRepository; + this.quizResultRepository = quizResultRepository; + this.questionRepository = questionRepository; + } + + public QuestionAnswer saveAnswer(Integer quizResultId, QuestionAnswerDTO answerDTO) { + QuizResult quizResult = quizResultRepository.findById(quizResultId) + .orElseThrow(() -> new EntityNotFoundException("QuizResult not found with id: " + quizResultId)); + + Question question = questionRepository.findById(answerDTO.getQuestionId()) + .orElseThrow(() -> new QuizNotFoundException("Question not found with id: " + answerDTO.getQuestionId())); + + QuestionAnswer answer = new QuestionAnswer(); + answer.setQuestion(question); + answer.setGivenAnswer(answerDTO.getGivenAnswer()); + answer.setCorrect(question.checkAnswer(answerDTO.getGivenAnswer())); + answer.setQuizResult(quizResult); + + quizResult.addQuestionAnswer(answer); + + return questionAnswerRepository.save(answer); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuestionService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuestionService.java new file mode 100644 index 0000000000000000000000000000000000000000..fa81452fa0ff11c89ab45cccbc71d377c155e454 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuestionService.java @@ -0,0 +1,68 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import idatt2105.group11.idatt2105projectbackend.dto.QuestionDTO; +import idatt2105.group11.idatt2105projectbackend.exception.QuestionNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.MultipleChoiceQuestion; +import idatt2105.group11.idatt2105projectbackend.model.Question; +import idatt2105.group11.idatt2105projectbackend.repository.QuestionRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class QuestionService { + + private final QuestionRepository questionRepository; + + @Autowired + public QuestionService(QuestionRepository questionRepository) { + this.questionRepository = questionRepository; + } + + public QuestionDTO updateQuestion(QuestionDTO questionDTO) { + MultipleChoiceQuestion question = (MultipleChoiceQuestion) questionRepository.findById(questionDTO.getId()) + .orElseThrow(() -> new QuestionNotFoundException("Question not found with id " + questionDTO.getId())); + + question.setQuestionText(questionDTO.getQuestionText()); + question.setScore(questionDTO.getScore()); + question.setAnswerOptions(questionDTO.getAnswerOptions()); + question.setCorrectAnswerIndex(questionDTO.getCorrectAnswerIndex()); + + MultipleChoiceQuestion updatedQuestion = questionRepository.save(question); + return convertToQuestionDTO(updatedQuestion); + } + + + public void deleteQuestion(Integer id) { + questionRepository.deleteById(id); + } + + public List<Question> findAllQuestionsByQuizId(Integer quizId) { + return questionRepository.findAllByQuizId(quizId); + } + + @Transactional + public MultipleChoiceQuestion createMCQuestion(QuestionDTO questionDTO) { + MultipleChoiceQuestion question = new MultipleChoiceQuestion(); + question.setQuestionText(questionDTO.getQuestionText()); + question.setScore(questionDTO.getScore()); + question.setAnswerOptions(questionDTO.getAnswerOptions()); + question.setCorrectAnswerIndex(questionDTO.getCorrectAnswerIndex()); + return questionRepository.save(question); + } + + private QuestionDTO convertToQuestionDTO(MultipleChoiceQuestion question) { + QuestionDTO questionDTO = new QuestionDTO(); + questionDTO.setId(question.getId()); + questionDTO.setQuestionText(question.getQuestionText()); + questionDTO.setScore(question.getScore()); + questionDTO.setAnswerOptions(question.getAnswerOptions()); + questionDTO.setCorrectAnswerIndex(question.getCorrectAnswerIndex()); + + return questionDTO; + } + + +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuizResultService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuizResultService.java new file mode 100644 index 0000000000000000000000000000000000000000..e325ce932252a0befe528a6e75067fa0f68f2d7f --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuizResultService.java @@ -0,0 +1,129 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import idatt2105.group11.idatt2105projectbackend.dto.QuestionAnswerDTO; +import idatt2105.group11.idatt2105projectbackend.dto.QuizResultDTO; +import idatt2105.group11.idatt2105projectbackend.exception.QuestionNotFoundException; +import idatt2105.group11.idatt2105projectbackend.exception.QuizNotFoundException; +import idatt2105.group11.idatt2105projectbackend.exception.UserNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.*; +import idatt2105.group11.idatt2105projectbackend.repository.QuestionRepository; +import idatt2105.group11.idatt2105projectbackend.repository.QuizRepository; +import idatt2105.group11.idatt2105projectbackend.repository.QuizResultRepository; +import idatt2105.group11.idatt2105projectbackend.repository.UserRepository; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.time.LocalDateTime; +import java.util.stream.Collectors; + +@Service +public class QuizResultService { + + private final QuizResultRepository quizResultRepository; + + private final QuizRepository quizRepository; + + private final UserRepository userRepository; + + private final QuestionRepository questionRepository; + + @Autowired + public QuizResultService(QuizResultRepository quizResultRepository, QuizRepository quizRepository, UserRepository userRepository, QuestionRepository questionRepository) { + this.quizResultRepository = quizResultRepository; + this.quizRepository = quizRepository; + this.userRepository = userRepository; + this.questionRepository = questionRepository; + } + + + public List<QuizResult> findAllResultsForUserId(Integer userId) { + return quizResultRepository.findByUserId(userId); + } + + public QuizResult startQuizResult(QuizResultDTO creationDTO) { + Quiz quiz = quizRepository.findById(creationDTO.getQuizId()) + .orElseThrow(() -> new QuizNotFoundException("Quiz not found with id: " + creationDTO.getQuizId())); + User user = userRepository.findById(creationDTO.getUserId()) + .orElseThrow(() -> new UserNotFoundException("User not found with id: " + creationDTO.getUserId())); + + QuizResult quizResult = new QuizResult(); + quizResult.setQuiz(quiz); + quizResult.setUser(user); + quizResult.setStatus("Påbegynt"); + quizResult.setStartedAt(LocalDateTime.now()); + quizResult.setCompletedAt(creationDTO.getCompletedAt()); + return quizResultRepository.save(quizResult); + } + + public QuizResultDTO completeQuiz(Integer quizResultId) { + QuizResult quizResult = quizResultRepository.findById(quizResultId) + .orElseThrow(() -> new EntityNotFoundException("QuizResult not found with id: " + quizResultId)); + + quizResult.setCompletedAt(LocalDateTime.now()); + quizResult.setStatus("Fullført"); + QuizResult updatedQuizResult = quizResultRepository.save(quizResult); + + return convertToQuizResultDTO(updatedQuizResult); + } + + + public QuizResultDTO findLatestQuizResultForUser(Integer userId) { + QuizResult latestQuizResult = quizResultRepository.findFirstByUserIdOrderByCompletedAtDesc(userId) + .orElseThrow(() -> new QuizNotFoundException("No quiz results found for user with id: " + userId)); + return convertToQuizResultDTO(latestQuizResult); + } + + private QuizResultDTO convertToQuizResultDTO(QuizResult quizResult) { + QuizResultDTO quizResultDTO = new QuizResultDTO(); + quizResultDTO.setId(quizResult.getId()); + quizResultDTO.setQuizId(quizResult.getQuiz().getId()); + quizResultDTO.setUserId(quizResult.getUser().getId()); + quizResultDTO.setScore(quizResult.getScore()); + quizResultDTO.setStatus(quizResult.getStatus()); + quizResultDTO.setStartedAt(quizResult.getStartedAt()); + quizResultDTO.setCompletedAt(quizResult.getCompletedAt()); + + // Anta at vi også trenger å konvertere listen av QuestionAnswer til QuestionAnswerDTO + // Dette krever at vi har en metode for å gjøre denne konverteringen: + List<QuestionAnswerDTO> answerDTOs = quizResult.getAnswers().stream() + .map(this::convertToQuestionAnswerDTO) + .collect(Collectors.toList()); + quizResultDTO.setAnswers(answerDTOs); + + return quizResultDTO; + } + + private QuestionAnswerDTO convertToQuestionAnswerDTO(QuestionAnswer questionAnswer) { + QuestionAnswerDTO questionAnswerDTO = new QuestionAnswerDTO(); + questionAnswerDTO.setId(questionAnswer.getId()); + questionAnswerDTO.setQuestionId(questionAnswer.getQuestion().getId()); + questionAnswerDTO.setGivenAnswer(questionAnswer.getGivenAnswer()); + questionAnswerDTO.setCorrect(questionAnswer.isCorrect()); + + return questionAnswerDTO; + } + + + + + + private List<QuestionAnswer> convertToQuestionAnswers(List<QuestionAnswerDTO> answerDTOs, QuizResult quizResult) { + List<QuestionAnswer> answers = new ArrayList<>(); + for (QuestionAnswerDTO answerDTO : answerDTOs) { + QuestionAnswer answer = new QuestionAnswer(); + + MultipleChoiceQuestion question = (MultipleChoiceQuestion) questionRepository.findById(answerDTO.getQuestionId()) + .orElseThrow(() -> new QuestionNotFoundException("Question not found with id: " + answerDTO.getQuestionId())); + answer.setQuestion(question); + answer.setGivenAnswer(answerDTO.getGivenAnswer()); + answer.setCorrect(question.checkAnswer(answerDTO.getGivenAnswer())); + answer.setQuizResult(quizResult); + + answers.add(answer); + } + return answers; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuizService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuizService.java new file mode 100644 index 0000000000000000000000000000000000000000..703a19ea61eb67d51b715d95d39538c020463f71 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/QuizService.java @@ -0,0 +1,96 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import idatt2105.group11.idatt2105projectbackend.dto.QuizDTO; +import idatt2105.group11.idatt2105projectbackend.exception.QuizNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.*; +import idatt2105.group11.idatt2105projectbackend.repository.QuestionRepository; +import idatt2105.group11.idatt2105projectbackend.repository.QuizRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class QuizService { + @Autowired + private QuizRepository quizRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + public QuizService(QuizRepository quizRepository, QuestionRepository questionRepository) { + this.quizRepository = quizRepository; + this.questionRepository = questionRepository; + } + + @Transactional + public Quiz createQuiz(QuizDTO quizDTO) { + Quiz quiz = new Quiz(); + quiz.setTitle(quizDTO.getTitle()); + quiz.setCategory(quizDTO.getCategory()); + + List<Question> questions = questionRepository.findAllById(quizDTO.getQuestionIds()); + quiz.setQuestions(questions); + + return quizRepository.save(quiz); + } + + public QuizDTO updateQuiz(QuizDTO quizDTO) { + Quiz quiz = quizRepository.findById(quizDTO.getId()) + .orElseThrow(() -> new QuizNotFoundException("Quiz not found with id " + quizDTO.getId())); + + quiz.setTitle(quizDTO.getTitle()); + quiz.setCategory(quizDTO.getCategory()); + + Quiz updatedQuiz = quizRepository.save(quiz); + return convertToQuizDTO(updatedQuiz); + } + + public void deleteQuiz(Integer id) { + Quiz quiz = quizRepository.findById(id) + .orElseThrow(() -> new QuizNotFoundException("Quiz not found with id " + id)); + + questionRepository.deleteAll(quiz.getQuestions()); + + quizRepository.delete(quiz); + } + + public List<Quiz> findAllQuizzes() { + return quizRepository.findAll(); + } + + public List<Quiz> findAllQuizzesByCategory(QuizCategory category) { + return quizRepository.findAllByCategory(category); + } + public List<Quiz> findAllQuizzesByDifficulty(QuizDifficulty difficulty) { + return quizRepository.findAllByDifficulty(difficulty); + } + public List<Quiz> findAllQuizzesByCreatorId(Integer creatorId) { + return quizRepository.findAllByCreatorId(creatorId); + } + + public Quiz findQuizById(Integer id) { + return quizRepository.findById(id) + .orElseThrow(() -> new QuizNotFoundException("Quiz not found with id " + id)); + } + + public QuizDTO convertToQuizDTO(Quiz quiz) { + QuizDTO quizDTO = new QuizDTO(); + quizDTO.setId(quiz.getId()); + quizDTO.setTitle(quiz.getTitle()); + quizDTO.setCategory(quiz.getCategory()); + + List<Integer> questionIds = quiz.getQuestions().stream() + .map(Question::getId) + .collect(Collectors.toList()); + quizDTO.setQuestionIds(questionIds); + + return quizDTO; + } + + +} + diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/TokenService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/TokenService.java new file mode 100644 index 0000000000000000000000000000000000000000..c6fd4f8d0286a48cfa26a5350f414655dc71b6db --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/TokenService.java @@ -0,0 +1,54 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.*; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.stream.Collectors; + +@Service +public class TokenService { + + private final JwtEncoder jwtEncoder; + private final JwtDecoder jwtDecoder; + + public TokenService(JwtEncoder jwtEncoder, JwtDecoder jwtDecoder) { + this.jwtEncoder = jwtEncoder; + this.jwtDecoder = jwtDecoder; + } + + public String generateJwt(Authentication auth) { + Instant now = Instant.now(); + Instant expiresAt = now.plusSeconds(300); + + String scope = auth.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(" ")); + + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuer("self") + .issuedAt(now) + .subject(auth.getName()) + .claim("roles", scope) + .expiresAt(expiresAt) + .build(); + + return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + } + + public void verifyToken(String token) { + try { + jwtDecoder.decode(token); + } catch (JwtException e) { + throw new IllegalArgumentException("Invalid token, could not decode. Message:", e); + } + } + + public String getNameFromToken(String token) { + Jwt jwt = jwtDecoder.decode(token); + return jwt.getSubject(); + } +} + diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/service/UserService.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..185bfc55bb4ceb68002462e48d07460a10634cbb --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/service/UserService.java @@ -0,0 +1,48 @@ +package idatt2105.group11.idatt2105projectbackend.service; + +import idatt2105.group11.idatt2105projectbackend.exception.UserNotFoundException; +import idatt2105.group11.idatt2105projectbackend.model.User; +import idatt2105.group11.idatt2105projectbackend.repository.UserRepository; +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.Service; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; +import java.util.Optional; + +@Service +public class UserService implements UserDetailsService { + + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public List<User> findAllAppUsers() { + return userRepository.findAll(); + } + + public void addAppUser(@RequestBody User user) { + userRepository.save(user); + } + + public Boolean verifyAppUser(User user) { + return userRepository.findByName(user.getName()).isPresent() && + userRepository.findByPassword(user.getPassword()).isPresent(); + } + + public User findAppUserByName(String name) { + Optional<User> userOptional = userRepository.findByName(name); + + return userOptional.orElse(null); + } + + @Override + public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { + return userRepository.findByName(name) + .orElseThrow(() -> new UsernameNotFoundException("User '" + name + "' not found.")); + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/utils/KeyGeneratorUtility.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/utils/KeyGeneratorUtility.java new file mode 100644 index 0000000000000000000000000000000000000000..56a6d94fe8f7d75295dab8b03be1425829dbd30c --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/utils/KeyGeneratorUtility.java @@ -0,0 +1,22 @@ +package idatt2105.group11.idatt2105projectbackend.utils; + + +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +public class KeyGeneratorUtility { + + public static KeyPair generateRSAKey() { + KeyPair keyPair; + + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } catch (Exception e) { + throw new IllegalStateException(); + } + + return keyPair; + } +} diff --git a/src/main/java/idatt2105/group11/idatt2105projectbackend/utils/RSAKeyProperties.java b/src/main/java/idatt2105/group11/idatt2105projectbackend/utils/RSAKeyProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..763b6fc58d1237d834eabafe8212e025c917a942 --- /dev/null +++ b/src/main/java/idatt2105/group11/idatt2105projectbackend/utils/RSAKeyProperties.java @@ -0,0 +1,36 @@ +package idatt2105.group11.idatt2105projectbackend.utils; + +import org.springframework.stereotype.Component; + +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +@Component +public class RSAKeyProperties { + + private RSAPublicKey publicKey; + private RSAPrivateKey privateKey; + + public RSAKeyProperties() { + KeyPair keyPair = KeyGeneratorUtility.generateRSAKey(); + publicKey = (RSAPublicKey) keyPair.getPublic(); + privateKey = (RSAPrivateKey) keyPair.getPrivate(); + } + + public RSAPublicKey getPublicKey() { + return publicKey; + } + + public void setPublicKey(RSAPublicKey publicKey) { + this.publicKey = publicKey; + } + + public RSAPrivateKey getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(RSAPrivateKey privateKey) { + this.privateKey = privateKey; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..76e1cf573705f3c659270f849edc2a27cb1b9176 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=idatt2105-project-backend diff --git a/src/test/java/idatt2105/group11/idatt2105projectbackend/Idatt2105ProjectBackendApplicationTests.java b/src/test/java/idatt2105/group11/idatt2105projectbackend/Idatt2105ProjectBackendApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..c3296332859b522ae82ced6f7faa27092f3e45d5 --- /dev/null +++ b/src/test/java/idatt2105/group11/idatt2105projectbackend/Idatt2105ProjectBackendApplicationTests.java @@ -0,0 +1,13 @@ +package idatt2105.group11.idatt2105projectbackend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Idatt2105ProjectBackendApplicationTests { + + @Test + void contextLoads() { + } + +}