diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..29805753e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/Sprint 2/prototype2/target
+*.log
+*.iml
+/Sprint 2/prototype2/docker-compose.yml
+*.gz
+Sprint 2/prototype2/qodana.yaml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 000000000..6f97dc024
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml
new file mode 100644
index 000000000..e5b74ffd6
--- /dev/null
+++ b/.idea/dataSources.local.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ #@
+ `
+
+
+ master_key
+ root
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 000000000..451913f86
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ mysql.8
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://localhost:3306
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 000000000..e13a3d47c
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 000000000..1ed94d38f
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 000000000..712ab9d98
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml
new file mode 100644
index 000000000..f9eb689a3
--- /dev/null
+++ b/.idea/jsLibraryMappings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..2cea27280
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml
new file mode 100644
index 000000000..ea8af431b
--- /dev/null
+++ b/.idea/runConfigurations/All_Tests.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index b2588bc51..573ed80cc 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
My groupmembers are:
-- XXXX
-- XXXX
-- XXXX
-- XXXX
-
+- Yassine Bihi
+- Diego Rivera
+- Christian Jackson
+- Jad El Masri
+- Ozi Jawad
------------------ Fill in some information about your project under this ------------------
diff --git a/Sprint 1/JydocTestCases.pdf b/Sprint 1/JydocTestCases.pdf
new file mode 100644
index 000000000..dd678e5f2
Binary files /dev/null and b/Sprint 1/JydocTestCases.pdf differ
diff --git a/Sprint 1/TestCasesPlaceholder.txt b/Sprint 1/TestCasesPlaceholder.txt
index e69de29bb..927bb8564 100644
--- a/Sprint 1/TestCasesPlaceholder.txt
+++ b/Sprint 1/TestCasesPlaceholder.txt
@@ -0,0 +1 @@
+//check JydocTestCases.pdf
diff --git a/Sprint 1/codePlaceHolder.txt b/Sprint 1/codePlaceHolder.txt
index e69de29bb..8fc10861b 100644
--- a/Sprint 1/codePlaceHolder.txt
+++ b/Sprint 1/codePlaceHolder.txt
@@ -0,0 +1 @@
+//Check deliverable3 folder
diff --git a/Sprint 1/deliverable3/HELP.md b/Sprint 1/deliverable3/HELP.md
new file mode 100644
index 000000000..6bed04940
--- /dev/null
+++ b/Sprint 1/deliverable3/HELP.md
@@ -0,0 +1,29 @@
+# Getting Started
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
+* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.4/maven-plugin)
+* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.4/maven-plugin/build-image.html)
+* [Spring Web](https://docs.spring.io/spring-boot/3.4.4/reference/web/servlet.html)
+* [Thymeleaf](https://docs.spring.io/spring-boot/3.4.4/reference/web/servlet.html#web.servlet.spring-mvc.template-engines)
+* [htmx](https://github.com/wimdeblauwe/htmx-spring-boot)
+
+### Guides
+The following guides illustrate how to use some features concretely:
+
+* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
+* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
+* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
+* [Handling Form Submission](https://spring.io/guides/gs/handling-form-submission/)
+* [htmx](https://www.youtube.com/watch?v=j-rfPoXe5aE)
+* [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/)
+
+### Maven Parent overrides
+
+Due to Maven's design, elements are inherited from the parent POM to the project POM.
+While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent.
+To prevent this, the project POM contains empty overrides for these elements.
+If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
+
diff --git a/Sprint 1/deliverable3/mvnw b/Sprint 1/deliverable3/mvnw
new file mode 100644
index 000000000..b9a45d76e
--- /dev/null
+++ b/Sprint 1/deliverable3/mvnw
@@ -0,0 +1,259 @@
+#!/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
+#
+# http://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.3.2
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ 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"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/Sprint 1/deliverable3/mvnw.cmd b/Sprint 1/deliverable3/mvnw.cmd
new file mode 100644
index 000000000..b150b91ed
--- /dev/null
+++ b/Sprint 1/deliverable3/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@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 http://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.3.2
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/Sprint 1/deliverable3/pom.xml b/Sprint 1/deliverable3/pom.xml
new file mode 100644
index 000000000..d8a68814f
--- /dev/null
+++ b/Sprint 1/deliverable3/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.4
+
+
+ com.jydoc
+ deliverable3
+ 0.0.1-SNAPSHOT
+ deliverable3
+ Demo project for Spring Boot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ io.github.wimdeblauwe
+ htmx-spring-boot-thymeleaf
+ 4.0.1
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.modelmapper
+ modelmapper
+ 3.2.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
diff --git a/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/Controller/indexController.java b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/Controller/indexController.java
new file mode 100644
index 000000000..e3bfd2dae
--- /dev/null
+++ b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/Controller/indexController.java
@@ -0,0 +1,101 @@
+//This controller "listens" for user responses.
+//TODO: We need to figure out how to set the user input into a DTO, then convert to Model and add into database
+
+package com.jydoc.deliverable3.Controller;
+import com.jydoc.deliverable3.DTO.UserDTO;
+import com.jydoc.deliverable3.Model.UserModel;
+import com.jydoc.deliverable3.Repository.UserRepository;
+import com.jydoc.deliverable3.Service.UserService;
+import jakarta.validation.Valid;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+
+
+import java.time.LocalDate;
+
+
+
+
+@Controller
+public class indexController {
+
+ private final UserRepository userRepository;
+ private final UserService userService;
+
+ public indexController(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ this.userService = new UserService();
+ }
+
+ // This method maps to the root URL ("/")
+ @GetMapping("/")
+ public String index(Model model) {
+
+ // Add the current date to the model
+ model.addAttribute("currentDate", LocalDate.now());
+
+ // Return the name of the Thymeleaf template ("index")
+ return "index";
+ }
+
+ @GetMapping("/login")
+ public String login(Model model) {
+ if (!model.containsAttribute("userDTO")) {
+ model.addAttribute("userDTO", new UserDTO());
+ }
+ return "login"; // Make sure the login page is returned
+ }
+
+ @PostMapping("/login")
+ public String handleLogin(@Valid @ModelAttribute("userDTO") UserDTO userDTO, BindingResult result, Model model) {
+
+ if (result.hasErrors()) {
+ return "login";
+ }
+
+ boolean isAuthenticated = userService.authenticate(userDTO.getEmail(), userDTO.getPassword());
+
+ if(!isAuthenticated) {
+ model.addAttribute("error", "Invalid email or password");
+ return "login"; // Return to the login page and show an error message
+ }
+ return "redirect:/";
+
+
+
+ }
+
+ @GetMapping("/register")
+ public String showRegistrationForm(Model model) {
+ model.addAttribute("user", new UserDTO());
+ return "register";
+ }
+
+ @PostMapping("/register")
+ public String registerUser(@Valid @ModelAttribute("user") UserDTO userDTO, BindingResult result, Model model) {
+ //TODO: Implement registration system
+
+ if (result.hasErrors()) {
+
+ return "register"; // TODO: Implement register error to bring popup then refresh
+ }
+ else {
+ UserModel user = new UserModel();
+ user.setEmail(userDTO.getEmail());
+ user.setPassword(userDTO.getPassword());
+ user.setFirstName(userDTO.getFirstName()); //TODO: Transfer to Service package
+ user.setLastName(userDTO.getLastName());
+ userRepository.save(user);
+ return "redirect:/";
+ }
+
+
+
+
+ }
+}
diff --git a/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/DTO/UserDTO.java b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/DTO/UserDTO.java
new file mode 100644
index 000000000..dfbdda002
--- /dev/null
+++ b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/DTO/UserDTO.java
@@ -0,0 +1,38 @@
+// This DTO accepts user input to fill variables, then is converted by
+// UserService into a UserModel.
+// Also used for business logic.
+//test
+package com.jydoc.deliverable3.DTO;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+//TODO: Authentication needs to be fixed here
+@Data
+public class UserDTO { //@Data applies Getters, Setters, NoArgsConstructor, and AllArgsConstructor
+
+ private int id;
+ private boolean admin;
+
+ @NotBlank(message = "Email cannot be empty")
+ @Email(message = "Invalid email format")
+ private String email;
+
+ @NotBlank(message = "Password cannot be empty")
+ @Size(min = 6, message = "Password must be at least 6 characters")
+ @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).{6,}$",
+ message = "Password must contain at least one letter and one number")
+ private String password;
+
+ @NotBlank(message = "First name cannot be empty")
+ @Pattern(regexp = "^[\\p{L}'-]+$", message = "First name can only contain letters, hyphens, and apostrophes")
+ @Size(min = 2, max = 30, message = "First name must be between 2 and 30 characters")
+ private String firstName;
+
+ @NotBlank(message = "Last name cannot be empty")
+ @Pattern(regexp = "^[\\p{L}'-]+$", message = "Last name can only contain letters, hyphens, and apostrophes")
+ @Size(min = 2, max = 30, message = "Last name must be between 2 and 30 characters")
+ private String lastName;
+}
diff --git a/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/Deliverable3Application.java b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/Deliverable3Application.java
new file mode 100644
index 000000000..64ce3b1c7
--- /dev/null
+++ b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/Deliverable3Application.java
@@ -0,0 +1,13 @@
+package com.jydoc.deliverable3;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Deliverable3Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Deliverable3Application.class, args);
+ }
+
+}
diff --git a/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/GlobalExceptionHandler.java b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/GlobalExceptionHandler.java
new file mode 100644
index 000000000..cc52e6d56
--- /dev/null
+++ b/Sprint 1/deliverable3/src/main/java/com/jydoc/deliverable3/GlobalExceptionHandler.java
@@ -0,0 +1,48 @@
+package com.jydoc.deliverable3;
+
+import jakarta.validation.ConstraintViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.validation.FieldError;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ // Handle validation errors (MethodArgumentNotValidException)
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity
+ *
+ *
Security Note: All error messages are sanitized to prevent
+ * exposure of sensitive information before being displayed to users.
+ */
+@Controller
+public class CustomErrorController implements ErrorController {
+
+ /**
+ * Default error message displayed when no specific message is available.
+ */
+ private static final String DEFAULT_ERROR_MESSAGE = "An unexpected error occurred";
+
+ /**
+ * Specific error message for database validation failures.
+ */
+ private static final String DB_VALIDATION_ERROR =
+ "Database error: Required role configuration is missing. Please contact support.";
+
+ /**
+ * Handles all error requests and prepares error information for display.
+ *
+ * This method:
+ *
+ *
Resolves the HTTP status code from the request
+ *
Extracts any associated exception
+ *
Sanitizes error messages for security
+ *
Populates the model with error details for the view
+ *
Returns the error view template
+ *
+ *
+ *
+ * @param request The HTTP request containing error attributes. Must not be null.
+ * @param model The Spring MVC model to populate with error details. Automatically
+ * provided by Spring MVC.
+ * @return The logical view name "error" which resolves to the error template
+ *
+ * @see jakarta.servlet.RequestDispatcher#ERROR_STATUS_CODE
+ * @see jakarta.servlet.RequestDispatcher#ERROR_EXCEPTION
+ * @see jakarta.servlet.RequestDispatcher#ERROR_REQUEST_URI
+ */
+ @RequestMapping("/error")
+ public String handleError(HttpServletRequest request, Model model) {
+ HttpStatus httpStatus = resolveHttpStatus(request);
+ Throwable exception = resolveException(request);
+
+ model.addAttribute("status", httpStatus.value());
+ model.addAttribute("error", httpStatus.getReasonPhrase());
+ model.addAttribute("message", getSafeErrorMessage(exception));
+ model.addAttribute("path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI));
+ model.addAttribute("timestamp", Instant.now());
+
+ return "error";
+ }
+
+ /**
+ * Resolves the HTTP status code from the request attributes.
+ *
+ * Attempts to extract the status code from request attributes, falling back to
+ * HTTP 500 (Internal Server Error) if not available.
+ *
+ *
+ * @param request The HTTP request containing error attributes
+ * @return The resolved HttpStatus, never null
+ * @throws NumberFormatException if the status code attribute contains
+ * a non-numeric value (should not occur in normal operation)
+ */
+ private HttpStatus resolveHttpStatus(HttpServletRequest request) {
+ return Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE))
+ .map(status -> HttpStatus.valueOf(Integer.parseInt(status.toString())))
+ .orElse(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ /**
+ * Resolves the exception from the request attributes.
+ *
+ * Extracts any exception associated with the error request, if available.
+ *
+ *
+ * @param request The HTTP request containing error attributes
+ * @return The associated Throwable, or null if no exception is present
+ */
+ private Throwable resolveException(HttpServletRequest request) {
+ return Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_EXCEPTION))
+ .map(Throwable.class::cast)
+ .orElse(null);
+ }
+
+ /**
+ * Provides a safe error message for display.
+ *
+ * This method ensures:
+ *
+ *
Null exceptions return a default message
+ *
Specific database errors return a standardized message
+ *
All other messages are sanitized before display
+ *
+ *
+ *
+ * @param exception The exception to derive the message from, may be null
+ * @return A safe, user-appropriate error message, never null
+ */
+ private String getSafeErrorMessage(Throwable exception) {
+ if (exception == null) {
+ return DEFAULT_ERROR_MESSAGE;
+ }
+
+ String message = exception.getMessage();
+ if (message == null) {
+ return DEFAULT_ERROR_MESSAGE;
+ }
+
+ return message.contains("Field 'authority' doesn't have a default value")
+ ? DB_VALIDATION_ERROR
+ : sanitizeMessage(message);
+ }
+
+ /**
+ * Sanitizes error messages to prevent exposing sensitive information.
+ *
+ * Replaces sensitive patterns (like passwords, tokens, etc.) with [REDACTED].
+ * Extend this method to include additional sensitive patterns as needed.
+ *
+ *
+ * @param message The raw error message to sanitize
+ * @return A sanitized version of the message safe for user display
+ */
+ private String sanitizeMessage(String message) {
+ // Basic sanitization - extend this for your specific security requirements
+ return message.replaceAll("(?i)password|secret|key|token", "[REDACTED]");
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/controllers/UserController.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/controllers/UserController.java
new file mode 100644
index 000000000..39e7280b5
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/controllers/UserController.java
@@ -0,0 +1,510 @@
+package com.jydoc.deliverable4.controllers;
+
+import com.jydoc.deliverable4.dtos.userdtos.DashboardDTO;
+import com.jydoc.deliverable4.dtos.MedicationDTO;
+import com.jydoc.deliverable4.dtos.userdtos.UserDTO;
+import com.jydoc.deliverable4.security.Exceptions.PasswordMismatchException;
+import com.jydoc.deliverable4.security.Exceptions.WeakPasswordException;
+import com.jydoc.deliverable4.services.userservices.DashboardService;
+import com.jydoc.deliverable4.services.medicationservices.MedicationService;
+import com.jydoc.deliverable4.services.userservices.UserService;
+import jakarta.validation.constraints.NotBlank;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import javax.validation.Valid;
+
+/**
+ * Controller handling all user-related operations including profile management,
+ * medication tracking, and dashboard display.
+ *
+ *
This controller serves as the main interface between the user-facing views
+ * and backend services for user-specific operations.
+ *
+ *
All endpoints are prefixed with "/user" and require authentication.
+ */
+@Controller
+@RequestMapping("/user")
+public class UserController {
+
+ private static final Logger logger = LoggerFactory.getLogger(UserController.class);
+
+ private final DashboardService dashboardService;
+ private final MedicationService medicationService;
+ private final UserService userService;
+
+ /**
+ * Constructs a new UserController with required services.
+ *
+ * @param dashboardService Service for dashboard-related operations
+ * @param medicationService Service for medication management
+ * @param userService Service for user profile operations
+ */
+ public UserController(DashboardService dashboardService,
+ MedicationService medicationService,
+ UserService userService) {
+ this.dashboardService = dashboardService;
+ this.medicationService = medicationService;
+ this.userService = userService;
+ logger.info("UserController initialized with all required services");
+ }
+
+ /**
+ * Displays the user dashboard with summary information.
+ *
+ * @param userDetails Authenticated user details
+ * @param model Spring MVC model for view data
+ * @return The dashboard view template
+ */
+ @GetMapping("/dashboard")
+ public String showDashboard(@AuthenticationPrincipal UserDetails userDetails, Model model) {
+ if (userDetails == null || userDetails.getUsername() == null) {
+ logger.error("Unauthenticated access attempt to dashboard");
+ return "redirect:/login";
+ }
+
+ String username = userDetails.getUsername();
+ logger.debug("Loading dashboard for user: {}", username);
+
+ try {
+ DashboardDTO dashboard = dashboardService.getUserDashboardData(userDetails);
+ boolean hasMedications = dashboard.isHasMedications();
+
+ logger.debug("Dashboard data retrieved successfully for user: {}", username);
+ model.addAttribute("dashboard", dashboard);
+ model.addAttribute("hasMedications", hasMedications);
+
+ return "user/dashboard";
+ } catch (IllegalArgumentException e) {
+ logger.warn("Invalid request for dashboard: {}", e.getMessage());
+ model.addAttribute("error", "Invalid request: " + e.getMessage());
+ return "user/dashboard";
+ } catch (Exception e) {
+ logger.error("Failed to load dashboard for user {}: {}", username, e.getMessage(), e);
+ model.addAttribute("error", "Failed to load dashboard data. Please try again later.");
+ return "user/dashboard";
+ }
+ }
+
+ /**
+ * Displays the user's profile information.
+ *
+ * @param userDetails Authenticated user details
+ * @param model Spring MVC model for view data
+ * @return The profile view template
+ */
+ @GetMapping("/profile")
+ public String showProfile(@AuthenticationPrincipal UserDetails userDetails, Model model) {
+ logger.debug("Loading profile for user: {}", userDetails.getUsername());
+ try {
+ UserDTO user = userService.getUserByUsername(userDetails.getUsername());
+ model.addAttribute("user", user);
+ logger.info("Profile data loaded successfully for user: {}", userDetails.getUsername());
+ return "user/profile";
+ } catch (Exception e) {
+ logger.error("Failed to load profile for user {}: {}", userDetails.getUsername(), e.getMessage(), e);
+ model.addAttribute("error", "Failed to load profile data");
+ return "user/profile";
+ }
+ }
+
+ /**
+ * Handles profile updates for the authenticated user.
+ *
+ * @param userDTO Data transfer object containing updated profile information
+ * @param result Binding result for validation errors
+ * @param currentPassword Current password for verification
+ * @param userDetails Authenticated user details
+ * @param redirectAttributes Attributes for redirect scenarios
+ * @return Redirect to profile page with success/error message
+ */
+ @PostMapping("/profile/update")
+ public String updateProfile(
+ @ModelAttribute("user") UserDTO userDTO,
+ BindingResult result,
+ @RequestParam("currentPassword") String currentPassword,
+ @AuthenticationPrincipal UserDetails userDetails,
+ RedirectAttributes redirectAttributes) {
+
+ logger.info("Attempting profile update for user: {}", userDetails.getUsername());
+
+ // Manually validate only the fields we want to update
+ if (userDTO.getEmail() == null || userDTO.getEmail().trim().isEmpty()) {
+ logger.warn("Email validation failed for user: {}", userDetails.getUsername());
+ result.rejectValue("email", "NotBlank", "Email is required");
+ }
+ if (userDTO.getFirstName() == null || userDTO.getFirstName().trim().isEmpty()) {
+ logger.warn("First name validation failed for user: {}", userDetails.getUsername());
+ result.rejectValue("firstName", "NotBlank", "First name is required");
+ }
+ if (userDTO.getLastName() == null || userDTO.getLastName().trim().isEmpty()) {
+ logger.warn("Last name validation failed for user: {}", userDetails.getUsername());
+ result.rejectValue("lastName", "NotBlank", "Last name is required");
+ }
+
+ if (result.hasErrors()) {
+ logger.warn("Profile update validation failed with {} errors for user: {}",
+ result.getErrorCount(), userDetails.getUsername());
+ return "user/profile";
+ }
+
+ try {
+ logger.debug("Verifying current password for user: {}", userDetails.getUsername());
+ if (!userService.verifyCurrentPassword(userDetails.getUsername(), currentPassword)) {
+ logger.warn("Current password verification failed for user: {}", userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("error", "Current password is incorrect");
+ return "redirect:/user/profile";
+ }
+
+ // Create a clean DTO with only updatable fields
+ UserDTO updateDto = new UserDTO();
+ updateDto.setEmail(userDTO.getEmail());
+ updateDto.setFirstName(userDTO.getFirstName());
+ updateDto.setLastName(userDTO.getLastName());
+
+ logger.debug("Updating profile for user: {}", userDetails.getUsername());
+ userService.updateUserProfile(userDetails.getUsername(), updateDto);
+
+ logger.info("Profile updated successfully for user: {}", userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("success", "Profile updated successfully");
+ } catch (Exception e) {
+ logger.error("Profile update failed for user {}: {}", userDetails.getUsername(), e.getMessage(), e);
+ redirectAttributes.addFlashAttribute("error", "Failed to update profile");
+ }
+
+ return "redirect:/user/profile";
+ }
+
+ /**
+ * Handles password change requests for authenticated users.
+ *
+ * @param currentPassword User's current password for verification
+ * @param newPassword New password to set
+ * @param confirmPassword Confirmation of new password
+ * @param userDetails Authenticated user details
+ * @param redirectAttributes Attributes for redirect scenarios
+ * @return Redirect to profile page with success/error message
+ */
+ @PostMapping("/profile/change-password")
+ public String changePassword(
+ @RequestParam("currentPassword") @NotBlank String currentPassword,
+ @RequestParam("newPassword") @NotBlank String newPassword,
+ @RequestParam("confirmPassword") @NotBlank String confirmPassword,
+ @AuthenticationPrincipal UserDetails userDetails,
+ RedirectAttributes redirectAttributes) {
+
+ logger.info("Password change request received for user: {}", userDetails.getUsername());
+
+ // Validate password match first
+ if (!newPassword.equals(confirmPassword)) {
+ logger.warn("Password mismatch during change attempt for user: {}", userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("error", "New passwords do not match");
+ return "redirect:/user/profile";
+ }
+
+ try {
+ logger.debug("Attempting password change for user: {}", userDetails.getUsername());
+ boolean success = userService.changePassword(
+ userDetails.getUsername(),
+ currentPassword,
+ newPassword
+ );
+
+ if (!success) {
+ logger.warn("Password change failed - current password incorrect for user: {}",
+ userDetails.getUsername());
+ throw new PasswordMismatchException("Current password is incorrect");
+ }
+
+ logger.info("Password changed successfully for user: {}", userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("success", "Password changed successfully");
+ } catch (PasswordMismatchException e) {
+ logger.warn("Password verification failed for user {}: {}",
+ userDetails.getUsername(), e.getMessage());
+ redirectAttributes.addFlashAttribute("error", "Current password is incorrect");
+ } catch (IllegalArgumentException e) {
+ logger.warn("Invalid password change request for user {}: {}",
+ userDetails.getUsername(), e.getMessage());
+ redirectAttributes.addFlashAttribute("error", e.getMessage());
+ } catch (Exception e) {
+ logger.error("Password change failed unexpectedly for user {}: {}",
+ userDetails.getUsername(), e.getMessage(), e);
+ redirectAttributes.addFlashAttribute("error", "Failed to change password");
+ }
+ return "redirect:/user/profile";
+ }
+
+ /**
+ * Handles account deletion requests.
+ *
+ * @param password Current password for verification
+ * @param confirmDelete User confirmation flag
+ * @param userDetails Authenticated user details
+ * @param redirectAttributes Attributes for redirect scenarios
+ * @return Redirect to logout on success, profile page on failure
+ */
+ @PostMapping("/profile/delete")
+ public String deleteAccount(
+ @RequestParam("deletePassword") String password,
+ @RequestParam("confirmDelete") boolean confirmDelete,
+ @AuthenticationPrincipal UserDetails userDetails,
+ RedirectAttributes redirectAttributes) {
+
+ logger.info("Account deletion request received for user: {}", userDetails.getUsername());
+
+ if (!confirmDelete) {
+ logger.warn("Account deletion not confirmed for user: {}", userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("error", "Please confirm account deletion");
+ return "redirect:/user/profile";
+ }
+
+ try {
+ logger.debug("Attempting account deletion for user: {}", userDetails.getUsername());
+ boolean deleted = userService.deleteAccount(userDetails.getUsername(), password);
+
+ if (deleted) {
+ logger.info("Account deleted successfully for user: {}", userDetails.getUsername());
+ return "redirect:/auth/logout";
+ } else {
+ logger.warn("Incorrect password provided for account deletion by user: {}",
+ userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("error", "Incorrect password");
+ }
+ } catch (Exception e) {
+ logger.error("Account deletion failed for user {}: {}",
+ userDetails.getUsername(), e.getMessage(), e);
+ redirectAttributes.addFlashAttribute("error", "Failed to delete account");
+ }
+ return "redirect:/user/profile";
+ }
+
+ /**
+ * Displays the user's medication list.
+ *
+ * @param userDetails Authenticated user details
+ * @param model Spring MVC model for view data
+ * @return The medication list view template
+ */
+ @GetMapping("/medication")
+ public String showMedications(@AuthenticationPrincipal UserDetails userDetails, Model model) {
+ logger.debug("Loading medications for user: {}", userDetails.getUsername());
+ try {
+ model.addAttribute("medications",
+ medicationService.getUserMedications(userDetails.getUsername()));
+ // Add the username to the model
+ model.addAttribute("username", userDetails.getUsername());
+ logger.info("Medications loaded successfully for user: {}", userDetails.getUsername());
+ return "user/medication/list";
+ } catch (Exception e) {
+ logger.error("Failed to load medications for user {}: {}",
+ userDetails.getUsername(), e.getMessage(), e);
+ model.addAttribute("error", "Failed to load medications");
+ return "user/medication/list";
+ }
+ }
+
+ /**
+ * Alternative endpoint for displaying medication list.
+ *
+ * @param userDetails Authenticated user details
+ * @param model Spring MVC model for view data
+ * @return The medication list view template
+ */
+ @GetMapping("/medication/list")
+ public String showMedicationList(@AuthenticationPrincipal UserDetails userDetails, Model model) {
+ logger.debug("Loading medications via list endpoint for user: {}", userDetails.getUsername());
+ try {
+ model.addAttribute("medications",
+ medicationService.getUserMedications(userDetails.getUsername()));
+
+ // Add the username to the model
+ model.addAttribute("username", userDetails.getUsername());
+ return "user/medication/list";
+ } catch (Exception e) {
+ logger.error("Failed to load medications via list endpoint for user {}: {}",
+ userDetails.getUsername(), e.getMessage(), e);
+ model.addAttribute("error", "Failed to load medications");
+ return "user/medication/list";
+ }
+ }
+
+ /**
+ * Displays the form for adding new medications.
+ *
+ * @param model Spring MVC model for view data
+ * @return The add medication form view
+ */
+ @GetMapping("/medication/add")
+ public String showAddMedicationForm(@AuthenticationPrincipal UserDetails userDetails, Model model) {
+ logger.debug("Displaying add medication form");
+ model.addAttribute("medicationDTO", new MedicationDTO());
+ model.addAttribute("username", userDetails.getUsername());
+ return "user/medication/add";
+ }
+
+ /**
+ * Handles submission of new medication information.
+ *
+ * @param medicationDTO Data transfer object containing medication details
+ * @param result Binding result for validation errors
+ * @param userDetails Authenticated user details
+ * @return Redirect to medication list on success, form view on failure
+ */
+ @PostMapping("/medication")
+ public String addMedication(
+ @Valid @ModelAttribute("medicationDTO") MedicationDTO medicationDTO,
+ BindingResult result,
+ @AuthenticationPrincipal UserDetails userDetails,
+ RedirectAttributes redirectAttributes) {
+
+ logger.info("Attempting to add new medication for user: {}", userDetails.getUsername());
+
+ // Additional validation for days of week
+ if (medicationDTO.getDaysOfWeek() == null || medicationDTO.getDaysOfWeek().isEmpty()) {
+ result.rejectValue("daysOfWeek", "NotEmpty", "Please select at least one day");
+ }
+
+ // Additional validation for intake times
+ if (medicationDTO.getIntakeTimes() == null || medicationDTO.getIntakeTimes().isEmpty()) {
+ result.rejectValue("intakeTimes", "NotEmpty", "Please add at least one intake time");
+ }
+
+ if (result.hasErrors()) {
+ logger.warn("Medication validation failed with {} errors for user: {}",
+ result.getErrorCount(), userDetails.getUsername());
+ return "user/medication/add";
+ }
+
+ try {
+ // Set default active status if not provided
+ if (medicationDTO.getActive() == null) {
+ medicationDTO.setActive(true);
+ }
+
+ medicationService.createMedication(medicationDTO, userDetails.getUsername());
+ logger.info("Medication added successfully for user: {}", userDetails.getUsername());
+ redirectAttributes.addFlashAttribute("success", "Medication added successfully");
+ return "redirect:/user/medication";
+ } catch (Exception e) {
+ logger.error("Failed to add medication for user {}: {}",
+ userDetails.getUsername(), e.getMessage(), e);
+ redirectAttributes.addFlashAttribute("error", "Failed to add medication: " + e.getMessage());
+ return "user/medication/add";
+ }
+ }
+
+ /**
+ * Displays the form for editing existing medications.
+ *
+ * @param id ID of the medication to edit
+ * @param model Spring MVC model for view data
+ * @return The edit medication form view
+ */
+ @GetMapping("/medication/{id}/edit")
+ public String showEditMedicationForm(@PathVariable Long id, Model model) {
+ logger.debug("Displaying edit form for medication ID: {}", id);
+ try {
+ MedicationDTO medicationDTO = medicationService.getMedicationById(id);
+ model.addAttribute("medicationDTO", medicationDTO);
+ return "user/medication/edit";
+ } catch (Exception e) {
+ logger.error("Failed to load medication for editing (ID: {}): {}", id, e.getMessage(), e);
+ model.addAttribute("error", "Failed to load medication data");
+ return "user/medication/edit";
+ }
+ }
+
+ /**
+ * Handles submission of updated medication information.
+ *
+ * @param id ID of the medication being updated
+ * @param medicationDTO Updated medication details
+ * @param result Binding result for validation errors
+ * @param model Spring MVC model for view data
+ * @return Redirect to medication list on success, form view on failure
+ */
+ @PostMapping("/medication/{id}")
+ public String updateMedication(
+ @PathVariable Long id,
+ @Valid @ModelAttribute("medicationDTO") MedicationDTO medicationDTO,
+ BindingResult result,
+ Model model) {
+
+ logger.info("Attempting to update medication ID: {}", id);
+
+ if (result.hasErrors()) {
+ logger.warn("Medication update validation failed with {} errors for ID: {}",
+ result.getErrorCount(), id);
+ return "user/medication/edit";
+ }
+
+ try {
+ medicationService.updateMedication(id, medicationDTO);
+ logger.info("Medication updated successfully (ID: {})", id);
+ return "redirect:/user/medication?updated";
+ } catch (Exception e) {
+ logger.error("Failed to update medication (ID: {}): {}", id, e.getMessage(), e);
+ model.addAttribute("error", "Error updating medication: " + e.getMessage());
+ return "user/medication/edit";
+ }
+ }
+
+ /**
+ * Handles medication deletion requests.
+ *
+ * @param id ID of the medication to delete
+ * @return Redirect to medication list with status parameter
+ */
+ @PostMapping("/medication/{id}/delete")
+ public String deleteMedication(@PathVariable Long id) {
+ logger.info("Attempting to delete medication ID: {}", id);
+ try {
+ medicationService.deleteMedication(id);
+ logger.info("Medication deleted successfully (ID: {})", id);
+ return "redirect:/user/medication?deleted";
+ } catch (Exception e) {
+ logger.error("Failed to delete medication (ID: {}): {}", id, e.getMessage(), e);
+ return "redirect:/user/medication?error";
+ }
+ }
+
+ /**
+ * Displays upcoming medication refills.
+ *
+ * @param userDetails Authenticated user details
+ * @param model Spring MVC model for view data
+ * @return The refills view template
+ */
+ @GetMapping("/refills")
+ public String showRefills(@AuthenticationPrincipal UserDetails userDetails, Model model) {
+ logger.debug("Loading upcoming refills for user: {}", userDetails.getUsername());
+ try {
+ model.addAttribute("refills",
+ medicationService.getUpcomingRefills(userDetails.getUsername()));
+ return "user/refills";
+ } catch (Exception e) {
+ logger.error("Failed to load refills for user {}: {}",
+ userDetails.getUsername(), e.getMessage(), e);
+ model.addAttribute("error", "Failed to load refill data");
+ return "user/refills";
+ }
+ }
+
+ /**
+ * Displays health metrics information.
+ *
+ * @return The health metrics view template
+ */
+ @GetMapping("/health")
+ public String showHealthMetrics() {
+ logger.debug("Displaying health metrics view");
+ return "user/health";
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/MedicationDTO.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/MedicationDTO.java
new file mode 100644
index 000000000..155b345df
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/MedicationDTO.java
@@ -0,0 +1,166 @@
+package com.jydoc.deliverable4.dtos;
+
+import lombok.*;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.time.LocalTime;
+import java.util.Set;
+
+/**
+ * Data Transfer Object (DTO) representing medication information in the system.
+ * This class serves as the primary data structure for transferring medication-related data
+ * between different layers of the application while enforcing validation rules.
+ *
+ *
The class includes comprehensive validation annotations to ensure data integrity
+ * and provides utility methods for common medication-related operations.
+ *
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MedicationDTO {
+
+ /**
+ * Unique identifier for the medication record.
+ * This field is automatically generated by the persistence layer.
+ */
+ private Long id;
+
+ /**
+ * Identifier of the user associated with this medication.
+ * Must not be null.
+ */
+ @NotNull(message = "User ID cannot be null")
+ private Long userId;
+
+ /**
+ * Name of the medication. Must not be blank.
+ *
Example: "Amoxicillin", "Lisinopril"
+ */
+ @NotBlank(message = "Medication name cannot be blank")
+ private String medicationName;
+
+ /**
+ * Urgency level of the medication. Must not be null.
+ * See {@link MedicationUrgency} for possible values.
+ */
+ @NotNull(message = "Urgency level must be specified")
+ private MedicationUrgency urgency;
+
+ /**
+ * Set of times when the medication should be taken.
+ * Must not be null, but can be empty for PRN medications.
+ */
+ @NotNull(message = "Intake times must be specified")
+ private Set intakeTimes;
+
+ /**
+ * Days of the week when the medication should be taken.
+ * Must not be null, but can be empty for as-needed medications.
+ * See {@link DayOfWeek} for possible values.
+ */
+ @NotNull(message = "Days of week must be specified")
+ private Set daysOfWeek;
+
+ /**
+ * Dosage information for the medication.
+ *
Example: "200mg", "1 tablet"
+ */
+ private String dosage;
+
+ /**
+ * Special instructions for taking the medication.
+ *
Example: "Take with food", "Avoid alcohol"
+ */
+ private String instructions;
+
+ /**
+ * Flag indicating whether the medication is currently active.
+ * Null values are treated as inactive.
+ */
+ private Boolean active;
+
+ /**
+ * Enumeration representing the urgency level of a medication.
+ *
+ *
Possible Values:
+ *
+ *
URGENT - Requires immediate attention
+ *
NONURGENT - Important but not time-critical
+ *
ROUTINE - Standard scheduled medication
+ *
+ */
+ public enum MedicationUrgency {
+ URGENT,
+ NONURGENT,
+ ROUTINE;
+
+ /**
+ * Converts a string value to the corresponding MedicationUrgency enum.
+ *
+ * @param value The string value to convert (case-insensitive)
+ * @return Corresponding MedicationUrgency enum
+ * @throws IllegalArgumentException if the value doesn't match any enum constant
+ * @apiNote Returns ROUTINE as default if input is null
+ */
+ public static MedicationUrgency fromString(String value) {
+ if (value == null) {
+ return ROUTINE; // Default value
+ }
+ try {
+ return MedicationUrgency.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Invalid urgency value. Must be 'URGENT', 'NONURGENT' or 'ROUTINE'");
+ }
+ }
+ }
+
+ /**
+ * Enumeration representing days of the week with display names.
+ * Each enum constant includes a user-friendly display name.
+ */
+ @Getter
+ public enum DayOfWeek {
+ MONDAY("Monday"),
+ TUESDAY("Tuesday"),
+ WEDNESDAY("Wednesday"),
+ THURSDAY("Thursday"),
+ FRIDAY("Friday"),
+ SATURDAY("Saturday"),
+ SUNDAY("Sunday");
+
+ private final String displayName;
+
+ /**
+ * Constructs a DayOfWeek enum constant with the specified display name.
+ *
+ * @param displayName The user-friendly name of the day
+ */
+ DayOfWeek(String displayName) {
+ this.displayName = displayName;
+ }
+ }
+
+ /**
+ * Determines if the medication is currently active.
+ *
+ * @return true if the medication is explicitly marked as active,
+ * false otherwise (including null active status)
+ */
+ public boolean isActive() {
+ return active != null && active;
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/MedicationScheduleDTO.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/MedicationScheduleDTO.java
new file mode 100644
index 000000000..c48fb7945
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/MedicationScheduleDTO.java
@@ -0,0 +1,58 @@
+package com.jydoc.deliverable4.dtos;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalTime;
+import java.util.Set;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MedicationScheduleDTO {
+
+ public enum MedicationUrgency {
+ URGENT, NONURGENT, ROUTINE
+ }
+
+ // Days of the week enum
+ public enum DayOfWeek {
+ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
+ }
+
+ private Long medicationId;
+ private String medicationName;
+ private String dosage;
+ private LocalTime scheduleTime;
+ private boolean isTaken;
+ private String instructions;
+ private String status; // "UPCOMING", "MISSED", "TAKEN", etc.
+ private MedicationUrgency urgency;
+ private Set daysOfWeek; // Days when medication should be taken
+
+ public String getFormattedTime() {
+ return scheduleTime != null ? scheduleTime.toString() : "";
+ }
+
+ public String getStatusBadgeClass() {
+ return switch (status) {
+ case "TAKEN" -> "badge bg-success";
+ case "MISSED" -> "badge bg-danger";
+ default -> "badge bg-warning text-dark";
+ };
+ }
+
+ // Helper method to get days as comma-separated string
+ public String getDaysAsString() {
+ if (daysOfWeek == null || daysOfWeek.isEmpty()) {
+ return "Everyday";
+ }
+ return daysOfWeek.stream()
+ .map(Enum::name)
+ .reduce((a, b) -> a + ", " + b)
+ .orElse("");
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/RefillReminderDTO.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/RefillReminderDTO.java
new file mode 100644
index 000000000..b7ec5ef31
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/RefillReminderDTO.java
@@ -0,0 +1,35 @@
+package com.jydoc.deliverable4.dtos;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RefillReminderDTO {
+ private Long medicationId;
+ private String medicationName;
+ private int remainingDoses;
+ private LocalDate refillByDate;
+ private String pharmacyInfo;
+ private String urgency; // "CRITICAL", "WARNING", "INFO"
+
+ public String getUrgencyBadgeClass() {
+ return switch (urgency) {
+ case "CRITICAL" -> "badge bg-danger";
+ case "WARNING" -> "badge bg-warning text-dark";
+ default -> "badge bg-info text-dark";
+ };
+ }
+
+ public String getDaysUntilRefill() {
+ if (refillByDate == null) return "N/A";
+ long days = LocalDate.now().datesUntil(refillByDate).count();
+ return days + " day" + (days != 1 ? "s" : "");
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/UpcomingMedicationDto.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/UpcomingMedicationDto.java
new file mode 100644
index 000000000..4e5652f0d
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/UpcomingMedicationDto.java
@@ -0,0 +1,42 @@
+package com.jydoc.deliverable4.dtos;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * DTO representing an upcoming medication for the dashboard view.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpcomingMedicationDto {
+ private String name;
+ private String dosage;
+ private String nextDoseTime;
+ private boolean taken;
+
+ // Optional additional fields that might be useful
+ private String instructions;
+ private String status; // "UPCOMING", "MISSED", "TAKEN"
+ private String urgency; // "URGENT", "NONURGENT", "ROUTINE"
+
+ /**
+ * Helper method to get a CSS class for status display
+ */
+ public String getStatusBadgeClass() {
+ if (taken) {
+ return "badge bg-success";
+ }
+ return "badge bg-warning text-dark";
+ }
+
+ /**
+ * Helper method to get status text
+ */
+ public String getStatusText() {
+ return taken ? "Taken" : "Pending";
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/DashboardDTO.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/DashboardDTO.java
new file mode 100644
index 000000000..df0c65d26
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/DashboardDTO.java
@@ -0,0 +1,40 @@
+package com.jydoc.deliverable4.dtos.userdtos;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+public class DashboardDTO {
+
+
+ private String username;
+ private List healthConditions;
+ private int activeMedicationsCount;
+ private int todaysDosesCount;
+ private int healthMetricsCount;
+ private List alerts;
+ private List upcomingMedications;
+ private boolean hasMedications;
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class MedicationAlertDto {
+ private String type;
+ private String message;
+ private String medicationName;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class UpcomingMedicationDto {
+ private String name;
+ private String dosage;
+ private String nextDoseTime;
+ private boolean taken;
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/LoginDTO.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/LoginDTO.java
new file mode 100644
index 000000000..24492c4b9
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/LoginDTO.java
@@ -0,0 +1,52 @@
+package com.jydoc.deliverable4.dtos.userdtos;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * Data Transfer Object (DTO) for user login requests.
+ *
+ * Validates credentials format and provides utility methods for credential processing.
+ *
+ * @param username The username or email for authentication (case-insensitive)
+ * @param password The password for authentication
+ */
+public record LoginDTO(
+ @NotBlank(message = "Username or email cannot be blank")
+ String username,
+
+ @NotBlank(message = "Password cannot be blank")
+ @Size(min = 8, message = "Password must be at least 8 characters long")
+ String password
+) implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates an empty LoginDTO instance.
+ * @return A LoginDTO with empty strings for both fields
+ */
+ public static LoginDTO empty() {
+ return new LoginDTO("", "");
+ }
+
+ /**
+ * Returns a normalized version of the username (trimmed and lowercase).
+ * @return The processed username ready for comparison
+ */
+ public String getNormalizedUsername() {
+ return username.trim().toLowerCase();
+ }
+
+ /**
+ * Checks if this LoginDTO represents an empty credential set.
+ * @return true if both username and password are blank
+ */
+ public boolean isEmpty() {
+ return username.isBlank() && password.isBlank();
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/UserDTO.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/UserDTO.java
new file mode 100644
index 000000000..77e4581d1
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/dtos/userdtos/UserDTO.java
@@ -0,0 +1,108 @@
+package com.jydoc.deliverable4.dtos.userdtos;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Data Transfer Object (DTO) for user-related operations.
+ * This class represents the user data that is transferred between layers,
+ * particularly between the controller and service layers.
+ *
+ *
Includes validation constraints for user input fields to ensure data integrity
+ * before processing. Uses Lombok annotations to reduce boilerplate code for getters/setters.
+ *
+ *
+ * @version 1.0
+ * @see com.jydoc.deliverable4.model.UserModel
+ * @since 1.0
+ */
+@Setter
+@Getter
+@Data
+public class UserDTO {
+
+ /**
+ * The username for authentication. Must be unique and between 3-20 characters.
+ *
+ * @NotBlank Ensures the username is not null or empty
+ * @Size Constrains the length between 3-20 characters
+ */
+ @NotBlank(message = "Username is required")
+ @Size(min = 3, max = 20, message = "Username must be 3-20 characters")
+ private String username;
+
+ /**
+ * The password for authentication. Must meet complexity requirements.
+ *
+ * @NotBlank Ensures the password is not null or empty
+ * @Size Requires minimum 6 characters
+ * @Pattern Enforces at least one uppercase, one lowercase letter and one number
+ */
+ @NotBlank(message = "Password is required")
+ @Size(min = 6, message = "Password must be at least 6 characters")
+ @Pattern(
+ regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
+ message = "Password must contain at least one uppercase, lowercase letter and number"
+ )
+ private String password;
+
+ /**
+ * The user's email address. Must be in valid format.
+ *
+ * @NotBlank Ensures the email is not null or empty
+ * @Pattern Validates the email format using regex pattern
+ */
+ @NotBlank(message = "Email is required")
+ @Pattern(regexp = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" +
+ "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$",
+ message = "Invalid email format")
+ private String email;
+
+ /**
+ * The user's first name. Cannot be blank.
+ */
+ @NotBlank(message = "First name is required")
+ private String firstName;
+
+ /**
+ * The user's last name. Cannot be blank.
+ */
+ @NotBlank(message = "Last name is required")
+ private String lastName;
+
+ /**
+ * The authority/role assigned to the user. Defaults to "ROLE_USER".
+ */
+ private String authority = "ROLE_USER";
+
+ /**
+ * Default constructor.
+ */
+ public UserDTO() {
+ }
+
+ /**
+ * Constructs a UserDTO with specified parameters.
+ *
+ * @param username the username
+ * @param password the password
+ * @param email the email address
+ * @param firstName the first name
+ * @param lastName the last name
+ */
+ public UserDTO(String username, String password, String email, String firstName, String lastName) {
+ this.username = username;
+ this.password = password;
+ this.email = email;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+
+
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/initializers/AuthorityInitializer.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/initializers/AuthorityInitializer.java
new file mode 100644
index 000000000..1228b3775
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/initializers/AuthorityInitializer.java
@@ -0,0 +1,86 @@
+package com.jydoc.deliverable4.initializers;
+
+import com.jydoc.deliverable4.model.auth.AuthorityModel;
+import com.jydoc.deliverable4.repositories.userrepositories.AuthorityRepository;
+import jakarta.annotation.PostConstruct;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * Component responsible for initializing default system authorities during application startup.
+ *
+ *
This initializer ensures that essential role-based authorities exist in the system
+ * before the application becomes fully operational. The initialization occurs automatically
+ * after dependency injection is complete.
+ *
+ *
Key features:
+ *
+ *
Transactional initialization to maintain data consistency
+ */
+@Component
+@RequiredArgsConstructor
+public class AuthorityInitializer {
+
+ /**
+ * Default system authorities that will be created if they don't exist.
+ *
+ *
Contains the standard role-based authorities used throughout the application:
+ *
+ *
ROLE_USER - Basic authenticated user privileges
+ *
ROLE_ADMIN - Full administrative privileges
+ *
ROLE_MODERATOR - Content moderation privileges
+ *
+ */
+ private static final Set DEFAULT_AUTHORITIES = Set.of(
+ "ROLE_USER",
+ "ROLE_ADMIN",
+ "ROLE_MODERATOR"
+ );
+
+ private final AuthorityRepository authorityRepository;
+
+ /**
+ * Initializes default authorities during application startup.
+ *
+ *
This method runs automatically after the bean is constructed and performs:
+ *
+ *
Iteration through all default authorities
+ *
Conditional creation of each authority if it doesn't exist
+ *
+ *
+ *
The operation is transactional, ensuring all authorities are created atomically.
+ */
+ @PostConstruct
+ @Transactional
+ public void initializeDefaultAuthorities() {
+ DEFAULT_AUTHORITIES.forEach(this::createAuthorityIfNotExists);
+ }
+
+ /**
+ * Creates an authority in the system if it doesn't already exist.
+ *
+ *
This method implements an idempotent create operation that:
+ *
+ *
Checks for authority existence
+ *
Only creates new authority if no matching record exists
+ *
Returns the existing authority if found
+ *
+ *
+ * @param authorityName the name of the authority to create (e.g., "ROLE_ADMIN")
+ */
+ private void createAuthorityIfNotExists(String authorityName) {
+ authorityRepository.findByAuthority(authorityName)
+ .orElseGet(() -> {
+ AuthorityModel authority = new AuthorityModel();
+ authority.setAuthority(authorityName);
+ return authorityRepository.save(authority);
+ });
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/MedicationIntakeTime.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/MedicationIntakeTime.java
new file mode 100644
index 000000000..523ac5c9a
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/MedicationIntakeTime.java
@@ -0,0 +1,176 @@
+package com.jydoc.deliverable4.model;
+
+import jakarta.persistence.*;
+import lombok.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalTime;
+import java.util.Objects;
+
+/**
+ * Represents a scheduled medication intake time associated with a specific medication.
+ * This entity maps to the 'medication_intake_times' table in the database.
+ */
+@Entity
+@Table(name = "medication_intake_times")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MedicationIntakeTime {
+ private static final Logger logger = LoggerFactory.getLogger(MedicationIntakeTime.class);
+
+ /**
+ * Unique identifier for the medication intake time record.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ /**
+ * The medication associated with this intake time.
+ * This is a many-to-one relationship with MedicationModel.
+ */
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "medication_id", nullable = false)
+ private MedicationModel medication;
+
+ /**
+ * The scheduled time for medication intake.
+ * This field cannot be null.
+ */
+ @Column(nullable = false)
+ private LocalTime intakeTime;
+
+ /**
+ * Constructs a new MedicationIntakeTime with the specified medication and intake time.
+ *
+ * @param medication The medication associated with this intake time (cannot be null)
+ * @param intakeTime The scheduled time for medication intake (cannot be null)
+ * @throws IllegalArgumentException if intakeTime is null
+ * @throws NullPointerException if medication is null
+ */
+ public MedicationIntakeTime(MedicationModel medication, LocalTime intakeTime) {
+ logger.debug("Constructing new MedicationIntakeTime with medication: {} and intake time: {}",
+ medication, intakeTime);
+
+ if (intakeTime == null) {
+ logger.error("Attempted to create MedicationIntakeTime with null intakeTime");
+ throw new IllegalArgumentException("Intake time cannot be null");
+ }
+
+ this.medication = Objects.requireNonNull(medication, "Medication cannot be null");
+ this.intakeTime = Objects.requireNonNull(intakeTime, "Intake time cannot be null");
+
+ logger.info("Created new MedicationIntakeTime for medication ID: {} at time: {}",
+ medication.getId(), intakeTime);
+ }
+
+ /**
+ * Sets the medication associated with this intake time.
+ * Maintains bidirectional relationship by updating both sides of the association.
+ *
+ * @param medication The medication to associate with this intake time (cannot be null)
+ * @throws NullPointerException if medication is null
+ */
+ public void setMedication(MedicationModel medication) {
+ logger.debug("Setting medication for intake time ID: {}. New medication ID: {}",
+ this.id, medication != null ? medication.getId() : "null");
+
+ Objects.requireNonNull(medication, "Medication cannot be null");
+
+ // Remove this intake time from the previous medication's list
+ if (this.medication != null) {
+ logger.trace("Removing intake time from previous medication's list");
+ this.medication.getIntakeTimes().remove(this);
+ }
+
+ this.medication = medication;
+
+ // Add this intake time to the new medication's list if not already present
+ if (!medication.getIntakeTimes().contains(this)) {
+ logger.trace("Adding intake time to new medication's list");
+ medication.getIntakeTimes().add(this);
+ }
+
+ logger.info("Updated medication association for intake time ID: {}. New medication ID: {}",
+ this.id, medication.getId());
+ }
+
+ /**
+ * Sets the scheduled time for medication intake.
+ *
+ * @param intakeTime The time for medication intake (cannot be null)
+ * @throws NullPointerException if intakeTime is null
+ */
+ public void setIntakeTime(LocalTime intakeTime) {
+ logger.debug("Setting intake time for ID: {}. New time: {}", this.id, intakeTime);
+
+ if (intakeTime == null) {
+ logger.error("Attempted to set null intake time for ID: {}", this.id);
+ throw new NullPointerException("Intake time cannot be null");
+ }
+
+ this.intakeTime = Objects.requireNonNull(intakeTime, "Intake time cannot be null");
+ logger.info("Updated intake time for ID: {}. New time: {}", this.id, intakeTime);
+ }
+
+ /**
+ * Compares this MedicationIntakeTime with another object for equality.
+ * Two MedicationIntakeTimes are considered equal if they have the same
+ * medication and intake time.
+ *
+ * @param o The object to compare with
+ * @return true if the objects are equal, false otherwise
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ logger.trace("Equals comparison with same instance");
+ return true;
+ }
+
+ if (!(o instanceof MedicationIntakeTime)) {
+ logger.trace("Equals comparison with different type");
+ return false;
+ }
+
+ MedicationIntakeTime that = (MedicationIntakeTime) o;
+ boolean isEqual = getIntakeTime().equals(that.getIntakeTime()) &&
+ getMedication().equals(that.getMedication());
+
+ logger.debug("Equals comparison result: {} for IDs: {} and {}",
+ isEqual, this.id, that.id);
+
+ return isEqual;
+ }
+
+ /**
+ * Generates a hash code for this MedicationIntakeTime based on
+ * its medication and intake time.
+ *
+ * @return The computed hash code
+ */
+ @Override
+ public int hashCode() {
+ int hash = Objects.hash(getIntakeTime(), getMedication());
+ logger.trace("Generated hash code: {} for ID: {}", hash, this.id);
+ return hash;
+ }
+
+ /**
+ * Returns a string representation of this MedicationIntakeTime.
+ *
+ * @return String representation of the object
+ */
+ @Override
+ public String toString() {
+ return "MedicationIntakeTime{" +
+ "id=" + id +
+ ", medicationId=" + (medication != null ? medication.getId() : "null") +
+ ", intakeTime=" + intakeTime +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/MedicationModel.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/MedicationModel.java
new file mode 100644
index 000000000..6b2ffcdb8
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/MedicationModel.java
@@ -0,0 +1,230 @@
+package com.jydoc.deliverable4.model;
+
+import com.jydoc.deliverable4.dtos.MedicationDTO;
+import jakarta.persistence.*;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalTime;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Entity class representing a medication in the system.
+ *
+ * This class models a medication prescribed to a user, including its properties,
+ * intake times, urgency level, days of week, and other relevant information.
+ *
+ */
+@Entity
+@Table(name = "medications")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(of = "id")
+public class MedicationModel {
+ private static final Logger logger = LoggerFactory.getLogger(MedicationModel.class);
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ @NotNull
+ private UserModel user;
+
+ @Column(nullable = false)
+ @NotBlank
+ private String name;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ @NotNull
+ private MedicationUrgency urgency;
+
+ @ElementCollection(targetClass = DayOfWeek.class, fetch = FetchType.EAGER)
+ @CollectionTable(name = "medication_days",
+ joinColumns = @JoinColumn(name = "medication_id"))
+ @Enumerated(EnumType.STRING)
+ @Column(name = "day_of_week")
+ @Builder.Default
+ private Set daysOfWeek = new HashSet<>();
+
+ @OneToMany(mappedBy = "medication", cascade = CascadeType.ALL, orphanRemoval = true)
+ @Builder.Default
+ private Set intakeTimes = new HashSet<>();
+
+ private String dosage;
+ private String instructions;
+
+ @Column(name = "is_active", nullable = false)
+ @Builder.Default
+ private Boolean isActive = true;
+
+ /**
+ * Enum representing the urgency level of a medication.
+ */
+ public enum MedicationUrgency {
+ /** Medication requires immediate attention */
+ URGENT,
+ /** Medication is important but not immediately critical */
+ NONURGENT,
+ /** Regular medication without special urgency */
+ ROUTINE
+ }
+
+ /**
+ * Enum representing days of the week when medication should be taken.
+ */
+ public enum DayOfWeek {
+ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
+ }
+
+ /**
+ * Adds an intake time for this medication.
+ *
+ *
+ * @param time The LocalTime to add as an intake time (must not be null)
+ * @throws NullPointerException if the time parameter is null
+ */
+ public void addIntakeTime(LocalTime time) {
+ logger.debug("Attempting to add intake time: {} for medication ID: {}", time, id);
+ Objects.requireNonNull(time, "Intake time cannot be null");
+
+ if (getIntakeTimesAsLocalTimes().contains(time)) {
+ logger.info("Intake time {} already exists for medication ID: {}, skipping addition", time, id);
+ return;
+ }
+
+ MedicationIntakeTime newIntake = new MedicationIntakeTime(this, time);
+ intakeTimes.add(newIntake);
+ logger.info("Added new intake time: {} for medication ID: {}", time, id);
+ }
+
+ /**
+ * Removes an intake time from this medication.
+ *
+ * Handles the bidirectional relationship cleanup.
+ *
+ *
+ * @param time The LocalTime to remove (must not be null)
+ * @throws NullPointerException if the time parameter is null
+ */
+ public void removeIntakeTime(LocalTime time) {
+ logger.debug("Attempting to remove intake time: {} from medication ID: {}", time, id);
+ Objects.requireNonNull(time, "Intake time cannot be null");
+
+ int initialSize = intakeTimes.size();
+ intakeTimes.removeIf(intake -> time.equals(intake.getIntakeTime()));
+
+ if (intakeTimes.size() < initialSize) {
+ logger.info("Removed intake time: {} from medication ID: {}", time, id);
+ } else {
+ logger.debug("No matching intake time found for removal: {} in medication ID: {}", time, id);
+ }
+ }
+
+ /**
+ * Adds a day when this medication should be taken.
+ *
+ * @param day The DayOfWeek to add (must not be null)
+ * @throws NullPointerException if the day parameter is null
+ */
+ public void addDay(DayOfWeek day) {
+ logger.debug("Attempting to add day: {} for medication ID: {}", day, id);
+ Objects.requireNonNull(day, "Day cannot be null");
+ daysOfWeek.add(day);
+ logger.info("Added day {} to medication ID: {}", day, id);
+ }
+
+ /**
+ * Removes a day when this medication should be taken.
+ *
+ * @param day The DayOfWeek to remove (must not be null)
+ * @throws NullPointerException if the day parameter is null
+ */
+ public void removeDay(DayOfWeek day) {
+ logger.debug("Attempting to remove day: {} from medication ID: {}", day, id);
+ Objects.requireNonNull(day, "Day cannot be null");
+ daysOfWeek.remove(day);
+ logger.info("Removed day {} from medication ID: {}", day, id);
+ }
+
+ /**
+ * Checks if medication should be taken on a specific day.
+ *
+ * @param day The DayOfWeek to check
+ * @return true if medication should be taken on this day
+ */
+ public boolean isTakenOnDay(DayOfWeek day) {
+ return daysOfWeek.contains(day);
+ }
+
+ /**
+ * Gets all intake times as LocalTime objects.
+ *
+ * @return A Set of LocalTime representing all intake times for this medication
+ */
+ public Set getIntakeTimesAsLocalTimes() {
+ logger.debug("Retrieving intake times as LocalTime for medication ID: {}", id);
+ return intakeTimes.stream()
+ .map(MedicationIntakeTime::getIntakeTime)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Converts this MedicationModel to a MedicationDTO.
+ *
+ * Includes all relevant fields including days of week and intake times.
+ *
+ *
+ * @return A MedicationDTO representation of this model
+ */
+ public MedicationDTO toDto() {
+ logger.debug("Converting MedicationModel to DTO for medication ID: {}", id);
+ return MedicationDTO.builder()
+ .id(this.id)
+ .userId(this.user.getId())
+ .medicationName(this.name)
+ .urgency(this.urgency != null ?
+ MedicationDTO.MedicationUrgency.valueOf(this.urgency.name()) : null)
+ .intakeTimes(this.getIntakeTimesAsLocalTimes())
+ .daysOfWeek(this.daysOfWeek.stream()
+ .map(day -> MedicationDTO.DayOfWeek.valueOf(day.name()))
+ .collect(Collectors.toSet()))
+ .dosage(this.dosage)
+ .instructions(this.instructions)
+ .build();
+ }
+
+ /**
+ * Sets the intake times for this medication.
+ *
+ * Ensures bidirectional relationship consistency and prevents null values.
+ * This is a private method as direct replacement of the set should be controlled.
+ *
+ *
+ * @param intakeTimes The set of MedicationIntakeTime objects to set
+ */
+ private void setIntakeTimes(Set intakeTimes) {
+ logger.debug("Setting intake times for medication ID: {}", id);
+ this.intakeTimes = intakeTimes != null ? intakeTimes : new HashSet<>();
+
+ // Ensure bidirectional consistency
+ this.intakeTimes.forEach(it -> {
+ it.setMedication(this);
+ logger.trace("Ensured bidirectional consistency for intake time: {} in medication ID: {}",
+ it.getIntakeTime(), id);
+ });
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/UserModel.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/UserModel.java
new file mode 100644
index 000000000..7b915847f
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/UserModel.java
@@ -0,0 +1,210 @@
+package com.jydoc.deliverable4.model;
+
+import com.jydoc.deliverable4.model.auth.AuthorityModel;
+import jakarta.persistence.*;
+import lombok.*;
+import org.hibernate.annotations.Fetch;
+import org.hibernate.annotations.FetchMode;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Entity class representing a user in the system.
+ *
+ *
This class maps to the "users" database table and includes:
+ *
+ *
Core user credentials (username, password)
+ *
Personal information (name, email)
+ *
Account status flags
+ *
Role-based authorities
+ *
+ *
+ *
Security Features:
+ *
+ *
Immutable authority collections to prevent unintended modifications
+ */
+ @Column(name = "last_name", nullable = false, length = 50)
+ private String lastName;
+
+ /**
+ * Flag indicating if the account is enabled.
+ *
+ *
Default: true (accounts are enabled by default)
+ */
+ @Builder.Default
+ private boolean enabled = true;
+
+ /**
+ * Flag indicating if the account is non-expired.
+ *
+ *
Default: true (accounts don't expire by default)
+ */
+ @Builder.Default
+ private boolean accountNonExpired = true;
+
+ /**
+ * Flag indicating if credentials are non-expired.
+ *
+ *
Default: true (credentials don't expire by default)
+ */
+ @Builder.Default
+ private boolean credentialsNonExpired = true;
+
+ /**
+ * Flag indicating if the account is non-locked.
+ *
+ *
Default: true (accounts aren't locked by default)
+ */
+ @Builder.Default
+ private boolean accountNonLocked = true;
+
+ /**
+ * Set of authorities/roles granted to the user.
+ *
+ *
Relationship Details:
+ *
+ *
Many-to-many with AuthorityModel
+ *
Eager fetching for immediate availability
+ *
Cascade persist and merge operations
+ *
Mapped via join table "user_authorities"
+ *
+ */
+ @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
+ @JoinTable(
+ name = "user_authorities",
+ joinColumns = @JoinColumn(name = "user_id"),
+ inverseJoinColumns = @JoinColumn(name = "authority_id")
+ )
+ @Fetch(FetchMode.JOIN)
+ @Builder.Default
+ private Set authorities = new HashSet<>();
+
+ /**
+ * Returns an unmodifiable view of the user's authorities.
+ *
+ * @return immutable set of authorities
+ */
+ public Set getAuthorities() {
+ return Collections.unmodifiableSet(new HashSet<>(this.authorities));
+ }
+
+ /**
+ * Replaces all authorities with the given collection.
+ *
+ * @param authorities new collection of authorities (null creates empty set)
+ */
+ public void setAuthorities(Collection authorities) {
+ this.authorities = authorities != null ? new HashSet<>(authorities) : new HashSet<>();
+ }
+
+ /**
+ * Adds a single authority to the user.
+ *
+ * @param authority the authority to add
+ */
+ public void addAuthority(AuthorityModel authority) {
+ this.authorities.add(authority);
+ authority.getUsers().add(this);
+ }
+
+ /**
+ * Removes a single authority from the user.
+ *
+ * @param authority the authority to remove
+ */
+ public void removeAuthority(AuthorityModel authority) {
+ this.authorities.remove(authority);
+ authority.getUsers().remove(this);
+ }
+
+
+ @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
+ private Set medications;
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/AuthorityModel.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/AuthorityModel.java
new file mode 100644
index 000000000..9612e1369
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/AuthorityModel.java
@@ -0,0 +1,62 @@
+package com.jydoc.deliverable4.model.auth;
+
+import com.jydoc.deliverable4.model.UserModel;
+import jakarta.persistence.*;
+import lombok.*;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents an authority/role in the system that can be assigned to users.
+ * Authorities define permissions or access levels for users.
+ */
+@Entity
+@Table(name = "authorities")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder(toBuilder = true)
+public class AuthorityModel {
+
+ /**
+ * Unique identifier for the authority.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ /**
+ * Name of the authority (e.g., "ROLE_ADMIN", "ROLE_USER").
+ * Must be unique and cannot be null.
+ */
+ @Column(nullable = false, unique = true, length = 50)
+ private String authority;
+
+ /**
+ * Set of users who have been granted this authority.
+ * Represents the many-to-many relationship with UserModel.
+ */
+ @ManyToMany(mappedBy = "authorities")
+ @Builder.Default
+ private Set users = new HashSet<>();
+
+ /**
+ * Constructs an AuthorityModel with the given authority name.
+ *
+ * @param authority the name of the authority
+ */
+ public AuthorityModel(String authority) {
+ this.authority = authority;
+ }
+
+ /**
+ * Custom builder class for AuthorityModel that initializes the users set.
+ */
+ public static class AuthorityModelBuilder {
+ /**
+ * The set of users for this authority, initialized as empty HashSet.
+ */
+ private Set users = new HashSet<>();
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/UserAuthority.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/UserAuthority.java
new file mode 100644
index 000000000..5205320cc
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/UserAuthority.java
@@ -0,0 +1,40 @@
+package com.jydoc.deliverable4.model.auth;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Represents the many-to-many relationship between users and authorities.
+ * This entity serves as a join table that maps users to their assigned authorities/roles.
+ *
+ * Uses a composite primary key represented by {@link UserAuthorityId} consisting of
+ * {@code userId} and {@code authorityId}.
+ *
+ * @see UserAuthorityId
+ */
+@Entity
+@Getter
+@Setter
+@Table(name = "user_authorities")
+@IdClass(UserAuthorityId.class)
+public class UserAuthority {
+
+ /**
+ * The ID of the user associated with this authority mapping.
+ * Part of the composite primary key.
+ * Cannot be null.
+ */
+ @Id
+ @Column(name = "user_id", nullable = false)
+ private Long userId;
+
+ /**
+ * The ID of the authority associated with this user mapping.
+ * Part of the composite primary key.
+ * Cannot be null.
+ */
+ @Id
+ @Column(name = "authority_id", nullable = false)
+ private Long authorityId;
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/UserAuthorityId.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/UserAuthorityId.java
new file mode 100644
index 000000000..3a3420ab2
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/model/auth/UserAuthorityId.java
@@ -0,0 +1,59 @@
+package com.jydoc.deliverable4.model.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Composite primary key class for {@link UserAuthority} entity.
+ *
+ * Represents the compound key consisting of user ID and authority ID
+ * used in the user-authority many-to-many relationship mapping.
+ *
+ * @see UserAuthority
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserAuthorityId implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The ID of the user in the relationship.
+ * Part of the composite primary key.
+ */
+ private Long userId;
+
+ /**
+ * The ID of the authority in the relationship.
+ * Part of the composite primary key.
+ */
+ private Long authorityId;
+
+ /**
+ * Compares this composite key with another object for equality.
+ * @param o the object to compare with
+ * @return true if the objects are equal
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UserAuthorityId that = (UserAuthorityId) o;
+ return Objects.equals(userId, that.userId) &&
+ Objects.equals(authorityId, that.authorityId);
+ }
+
+ /**
+ * Generates a hash code for this composite key.
+ * @return the hash code
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(userId, authorityId);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/medicationrepositories/MedicationIntakeTimeRepository.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/medicationrepositories/MedicationIntakeTimeRepository.java
new file mode 100644
index 000000000..1618a719d
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/medicationrepositories/MedicationIntakeTimeRepository.java
@@ -0,0 +1,41 @@
+package com.jydoc.deliverable4.repositories.medicationrepositories;
+
+import com.jydoc.deliverable4.model.MedicationIntakeTime;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalTime;
+import java.util.List;
+
+@Repository
+public interface MedicationIntakeTimeRepository extends JpaRepository {
+
+ // Find all intake times for a specific medication
+ List findByMedicationId(Long medicationId);
+
+ // Delete all intake times for a specific medication
+ @Modifying
+ @Query("DELETE FROM MedicationIntakeTime mit WHERE mit.medication.id = :medicationId")
+ int deleteByMedicationId(@Param("medicationId") Long medicationId);
+
+ @Modifying
+ @Query("DELETE FROM MedicationIntakeTime mit WHERE mit.medication.id = :medicationId AND mit.intakeTime = :intakeTime")
+ void deleteByMedicationIdAndIntakeTime(@Param("medicationId") Long medicationId, @Param("intakeTime") LocalTime intakeTime);
+
+ // Check if a specific intake time exists for a medication
+ boolean existsByMedicationIdAndIntakeTime(Long medicationId, LocalTime intakeTime);
+
+ // Find intake times by time range for a specific medication
+ @Query("SELECT mit FROM MedicationIntakeTime mit WHERE mit.medication.id = :medicationId " +
+ "AND mit.intakeTime BETWEEN :startTime AND :endTime")
+ List findByMedicationAndTimeRange(
+ @Param("medicationId") Long medicationId,
+ @Param("startTime") LocalTime startTime,
+ @Param("endTime") LocalTime endTime);
+
+
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/medicationrepositories/MedicationRepository.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/medicationrepositories/MedicationRepository.java
new file mode 100644
index 000000000..ae67bcd89
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/medicationrepositories/MedicationRepository.java
@@ -0,0 +1,26 @@
+package com.jydoc.deliverable4.repositories.medicationrepositories;
+
+import com.jydoc.deliverable4.model.MedicationModel;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface MedicationRepository extends JpaRepository {
+
+ // Basic user medication query
+ List findByUserUsername(String username);
+
+ // In MedicationRepository.java
+ @Query("SELECT DISTINCT m FROM MedicationModel m LEFT JOIN FETCH m.intakeTimes WHERE m.user.username = :username")
+ List findByUserUsernameWithIntakeTimes(@Param("username") String username);
+
+
+ @Query("SELECT DISTINCT m FROM MedicationModel m " +
+ "LEFT JOIN FETCH m.intakeTimes " +
+ "LEFT JOIN FETCH m.daysOfWeek " +
+ "WHERE m.user.username = :username")
+ List findByUserUsernameWithMedicationDetails(@Param("username") String username);
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/AuthorityRepository.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/AuthorityRepository.java
new file mode 100644
index 000000000..86782faf7
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/AuthorityRepository.java
@@ -0,0 +1,33 @@
+package com.jydoc.deliverable4.repositories.userrepositories;
+
+import com.jydoc.deliverable4.model.auth.AuthorityModel;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Repository for managing {@link AuthorityModel} entities.
+ * Provides methods to query authority data from the database.
+ */
+public interface AuthorityRepository extends JpaRepository {
+
+ /**
+ * Finds all authority models associated with a specific user ID.
+ *
+ * @param userId the ID of the user to search for
+ * @return a list of authority models associated with the user
+ */
+ @Query("SELECT a FROM AuthorityModel a JOIN a.users u WHERE u.id = :userId")
+ List findAllByUserId(@Param("userId") Long userId);
+
+ /**
+ * Finds an authority model by its authority string.
+ *
+ * @param authority the authority string to search for
+ * @return an Optional containing the authority model if found
+ */
+ Optional findByAuthority(String authority);
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/UserAuthorityRepository.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/UserAuthorityRepository.java
new file mode 100644
index 000000000..841234fe7
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/UserAuthorityRepository.java
@@ -0,0 +1,34 @@
+package com.jydoc.deliverable4.repositories.userrepositories;
+
+import com.jydoc.deliverable4.model.auth.UserAuthority;
+import com.jydoc.deliverable4.model.auth.UserAuthorityId;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+/**
+ * Repository for managing {@link UserAuthority} entities.
+ * Provides methods to query user-authority relationships from the database.
+ */
+public interface UserAuthorityRepository extends JpaRepository {
+
+ /**
+ * Finds all user-authority relationships for a given user ID using JPQL.
+ *
+ * @param userId the ID of the user to search for
+ * @return list of user-authority relationships
+ */
+ @Query("SELECT ua FROM UserAuthority ua WHERE ua.userId = :userId")
+ List findAllByUserId(@Param("userId") Long userId);
+
+ /**
+ * Finds all user-authority relationships for a given user ID using native SQL.
+ *
+ * @param userId the ID of the user to search for
+ * @return list of user-authority relationships
+ */
+ @Query(value = "SELECT * FROM user_authorities WHERE user_id = :userId", nativeQuery = true)
+ List findAllByUserIdNative(@Param("userId") Long userId);
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/UserRepository.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/UserRepository.java
new file mode 100644
index 000000000..0b362e50a
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/repositories/userrepositories/UserRepository.java
@@ -0,0 +1,73 @@
+package com.jydoc.deliverable4.repositories.userrepositories;
+
+import com.jydoc.deliverable4.model.UserModel;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Repository for {@link UserModel} entities providing user lookup operations.
+ * Includes methods for finding users with different loading strategies for authorities.
+ */
+@Repository
+public interface UserRepository extends JpaRepository {
+
+ // Basic user lookups
+ Optional findByUsername(String username);
+ boolean existsByUsername(String username);
+ boolean existsByEmail(String email);
+
+ /**
+ * Finds a user by username with authorities eagerly loaded.
+ * @param username the username to search for
+ * @return user with authorities loaded
+ */
+ @Query("SELECT DISTINCT u FROM UserModel u LEFT JOIN FETCH u.authorities WHERE u.username = :username")
+ Optional findByUsernameWithAuthorities(@Param("username") String username);
+
+ /**
+ * Finds a user by username or email (case-insensitive).
+ * @param credential username or email to search for
+ * @return matching user if found
+ */
+ @Query("SELECT u FROM UserModel u WHERE LOWER(u.username) = LOWER(:credential) OR LOWER(u.email) = LOWER(:credential)")
+ Optional findByUsernameOrEmail(@Param("credential") String credential);
+
+ /**
+ * Finds a user by username or email with authorities eagerly loaded.
+ * @param credential username or email to search for
+ * @return matching user with authorities if found
+ */
+ @Query("SELECT DISTINCT u FROM UserModel u LEFT JOIN FETCH u.authorities " +
+ "WHERE u.username = :credential OR u.email = :credential")
+ Optional findByUsernameOrEmailWithAuthorities(@Param("credential") String credential);
+
+// /** TODO: Implement this
+// * Finds the most recent users ordered by creation date.
+// * @param limit maximum number of users to return
+// * @return list of recent users
+// */
+// @Query("SELECT u FROM UserModel u ORDER BY u.createdDate DESC LIMIT :limit")
+// List findTopNByOrderByCreatedDateDesc(@Param("limit") int limit);
+
+ /**
+ * Finds all users with their authorities eagerly loaded.
+ *
+ * @return list of all users with authorities
+ */
+ @Query("SELECT DISTINCT u FROM UserModel u LEFT JOIN FETCH u.authorities")
+ List findAllWithAuthorities();
+
+ /**
+ * Checks if a user exists by ID.
+ *
+ * @param id the user ID to check
+ * @return true if user exists, false otherwise
+ */
+ boolean existsById(Long id);
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/EmailExistsException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/EmailExistsException.java
new file mode 100644
index 000000000..90c1d9935
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/EmailExistsException.java
@@ -0,0 +1,52 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Exception thrown when attempting to register with an email address
+ * that already exists in the system.
+ *
+ *
This exception should be thrown during user registration validation
+ * when a duplicate email address is detected.
+ */
+public class EmailExistsException extends RuntimeException {
+ private static final Logger logger = LogManager.getLogger(EmailExistsException.class);
+
+ /**
+ * Constructs a new exception with a standardized message format.
+ *
+ * @param email the duplicate email address that caused the exception
+ */
+ public EmailExistsException(String email) {
+ super(String.format("The email address '%s' is already registered", email));
+ logger.warn("Registration attempt with existing email: {}", email);
+ }
+
+ /**
+ * Constructs a new exception with custom message and cause.
+ *
+ * @param message the detail message
+ * @param cause the underlying cause
+ */
+ public EmailExistsException(String message, Throwable cause) {
+ super(message, cause);
+ logger.warn("Email conflict detected: {}", message, cause);
+ }
+
+ /**
+ * Gets the duplicate email that caused this exception.
+ *
+ * @return the duplicate email address
+ */
+ public String getEmail() {
+ return extractEmailFromMessage(getMessage());
+ }
+
+ private String extractEmailFromMessage(String message) {
+ if (message != null && message.contains("'")) {
+ return message.substring(message.indexOf("'") + 1, message.lastIndexOf("'"));
+ }
+ return "unknown";
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationCreationException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationCreationException.java
new file mode 100644
index 000000000..3970ffaa8
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationCreationException.java
@@ -0,0 +1,11 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+public class MedicationCreationException extends RuntimeException {
+ public MedicationCreationException(String message) {
+ super(message);
+ }
+
+ public MedicationCreationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationNotFoundException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationNotFoundException.java
new file mode 100644
index 000000000..c3e9f26cd
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationNotFoundException.java
@@ -0,0 +1,7 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+public class MedicationNotFoundException extends RuntimeException {
+ public MedicationNotFoundException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationScheduleException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationScheduleException.java
new file mode 100644
index 000000000..edcb9e0b2
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/MedicationScheduleException.java
@@ -0,0 +1,7 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+public class MedicationScheduleException extends RuntimeException {
+ public MedicationScheduleException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/PasswordMismatchException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/PasswordMismatchException.java
new file mode 100644
index 000000000..797a72c70
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/PasswordMismatchException.java
@@ -0,0 +1,7 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+public class PasswordMismatchException extends RuntimeException {
+ public PasswordMismatchException(String message) {
+ super(message);
+ }
+}
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/UserNotFoundException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/UserNotFoundException.java
new file mode 100644
index 000000000..58a854677
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/UserNotFoundException.java
@@ -0,0 +1,26 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+/**
+ * Exception thrown when a requested user cannot be found in the system.
+ *
+ *
This exception typically occurs when attempting to retrieve, update, or delete
+ * a user with a specific ID that doesn't exist in the database.
+ *
+ *
The exception includes the ID that was searched for in its error message,
+ * making it easier to diagnose the issue during debugging.
+ */
+public class UserNotFoundException extends RuntimeException {
+
+ /**
+ * Constructs a new UserNotFoundException with a detailed message containing
+ * the ID that couldn't be found.
+ *
+ * @param id The user ID that could not be found in the system. The ID will
+ * be included in the exception's detail message.
+ * @throws NullPointerException if the provided id is null (though primitive
+ * long can't be null, this note is included for documentation completeness)
+ */
+ public UserNotFoundException(Long id) {
+ super("Could not find user with id: " + id);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/UsernameExistsException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/UsernameExistsException.java
new file mode 100644
index 000000000..7db42464b
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/UsernameExistsException.java
@@ -0,0 +1,31 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+/**
+ * Custom exception thrown when attempting to create or update a user with a username
+ * that already exists in the system.
+ *
+ *
This exception should be used during user registration or profile updates
+ * to enforce unique username constraints.
+ */
+public class UsernameExistsException extends RuntimeException {
+
+ /**
+ * Constructs a new UsernameExistsException with the specified detail message.
+ *
+ * @param message the detail message that explains which username already exists.
+ * The message is saved for later retrieval by the {@link #getMessage()} method.
+ */
+ public UsernameExistsException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new UsernameExistsException with the specified detail message and cause.
+ *
+ * @param message the detail message that explains which username already exists
+ * @param cause the underlying cause of this exception
+ */
+ public UsernameExistsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/WeakPasswordException.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/WeakPasswordException.java
new file mode 100644
index 000000000..7747a46f0
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/Exceptions/WeakPasswordException.java
@@ -0,0 +1,7 @@
+package com.jydoc.deliverable4.security.Exceptions;
+
+public class WeakPasswordException extends RuntimeException {
+ public WeakPasswordException(String message) {
+ super(message);
+ }
+}
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/auth/CustomUserDetails.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/auth/CustomUserDetails.java
new file mode 100644
index 000000000..bd81f954b
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/auth/CustomUserDetails.java
@@ -0,0 +1,199 @@
+package com.jydoc.deliverable4.security.auth;
+
+import lombok.Getter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.io.Serial;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * Custom implementation of Spring Security's {@link UserDetails} interface.
+ * This class represents an authenticated user's details and is used throughout
+ * the security context of the application.
+ *
+ *
It extends Spring Security's core user details with additional user ID field
+ * while implementing all required authentication and authorization contract methods.
+ *
+ *
The class is immutable and thread-safe by design, with all fields being final.
+ *
+ * @author Your Name
+ * @version 1.0
+ * @see org.springframework.security.core.userdetails.UserDetails
+ * @since 1.0
+ */
+public class CustomUserDetails implements UserDetails {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The unique identifier of the user in the system
+ * -- GETTER --
+ * Returns the unique identifier of the user.
+ *
+ * @return the user ID
+
+ */
+ @Getter
+ private final Long userId;
+
+ /**
+ * The username used to authenticate the user
+ */
+ private final String username;
+
+ /**
+ * The encrypted password of the user
+ */
+ private final String password;
+
+ /**
+ * Flag indicating whether the user is enabled
+ */
+ private final boolean enabled;
+
+ /**
+ * Flag indicating whether the user's account is non-expired
+ */
+ private final boolean accountNonExpired;
+
+ /**
+ * Flag indicating whether the user's account is non-locked
+ */
+ private final boolean accountNonLocked;
+
+ /**
+ * Flag indicating whether the user's credentials are non-expired
+ */
+ private final boolean credentialsNonExpired;
+
+ /**
+ * Collection of authorities (roles/permissions) granted to the user
+ */
+ private final Collection extends GrantedAuthority> authorities;
+
+ /**
+ * Constructs a new CustomUserDetails with the specified parameters.
+ *
+ * @param userId the unique identifier of the user (cannot be null)
+ * @param username the username used for authentication (cannot be null)
+ * @param password the encrypted password (cannot be null)
+ * @param enabled whether the user is enabled
+ * @param accountNonExpired whether the account is non-expired
+ * @param accountNonLocked whether the account is non-locked
+ * @param credentialsNonExpired whether the credentials are non-expired
+ * @param authorities the collection of granted authorities (cannot be null)
+ * @throws NullPointerException if any of the non-null parameters are null
+ */
+ public CustomUserDetails(Long userId, String username, String password,
+ boolean enabled, boolean accountNonExpired,
+ boolean accountNonLocked, boolean credentialsNonExpired,
+ Collection extends GrantedAuthority> authorities) {
+ this.userId = Objects.requireNonNull(userId);
+ this.username = Objects.requireNonNull(username);
+ this.password = Objects.requireNonNull(password);
+ this.enabled = enabled;
+ this.accountNonExpired = accountNonExpired;
+ this.accountNonLocked = accountNonLocked;
+ this.credentialsNonExpired = credentialsNonExpired;
+ this.authorities = Objects.requireNonNull(authorities);
+ }
+
+ /**
+ * Returns the authorities granted to the user.
+ *
+ * @return a collection of granted authorities
+ */
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return Collections.unmodifiableCollection(authorities); // ✅ Safe
+ }
+
+ /**
+ * Returns the password used to authenticate the user.
+ *
+ * @return the password
+ */
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Returns the username used to authenticate the user.
+ *
+ * @return the username
+ */
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Indicates whether the user's account has expired.
+ *
+ * @return true if the account is non-expired, false otherwise
+ */
+ @Override
+ public boolean isAccountNonExpired() {
+ return accountNonExpired;
+ }
+
+ /**
+ * Indicates whether the user is locked or unlocked.
+ *
+ * @return true if the account is non-locked, false otherwise
+ */
+ @Override
+ public boolean isAccountNonLocked() {
+ return accountNonLocked;
+ }
+
+ /**
+ * Indicates whether the user's credentials (password) have expired.
+ *
+ * @return true if the credentials are non-expired, false otherwise
+ */
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return credentialsNonExpired;
+ }
+
+ /**
+ * Indicates whether the user is enabled or disabled.
+ *
+ * @return true if the user is enabled, false otherwise
+ */
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Compares this CustomUserDetails with another object for equality.
+ * Two CustomUserDetails are considered equal if they have the same userId and username.
+ *
+ * @param o the object to compare with
+ * @return true if the objects are equal, false otherwise
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CustomUserDetails that = (CustomUserDetails) o;
+ return Objects.equals(userId, that.userId) &&
+ Objects.equals(username, that.username);
+ }
+
+ /**
+ * Returns a hash code value for this CustomUserDetails.
+ *
+ * @return a hash code value based on userId and username
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(userId, username);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/auth/CustomUserDetailsService.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/auth/CustomUserDetailsService.java
new file mode 100644
index 000000000..6f46c3f2e
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/auth/CustomUserDetailsService.java
@@ -0,0 +1,85 @@
+package com.jydoc.deliverable4.security.auth;
+
+import com.jydoc.deliverable4.model.UserModel;
+import com.jydoc.deliverable4.repositories.userrepositories.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+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.transaction.annotation.Transactional;
+
+import java.util.stream.Collectors;
+
+/**
+ * Custom implementation of Spring Security's {@link UserDetailsService}.
+ *
+ * This service is responsible for loading user-specific data during authentication.
+ * It bridges the application's {@link UserModel} with Spring Security's authentication framework.
+ *
+ *
Key responsibilities include:
+ *
+ *
Loading user details by username or email
+ *
Converting application roles to Spring Security authorities
+ *
Handling user not found scenarios
+ *
Providing transactional access to user data
+ *
+ *
+ * @Service Marks this class as a Spring service component
+ * @RequiredArgsConstructor Generates constructor for final fields (Dependency Injection)
+ * @see UserDetailsService
+ * @see UserDetails
+ * @see UserModel
+ */
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ /**
+ * Repository for accessing user data.
+ * Injected automatically by Spring via constructor.
+ */
+ private final UserRepository userRepository;
+
+ /**
+ * Loads user details by username or email.
+ *
+ * This is the core method of the UserDetailsService interface. It:
+ *
+ *
Attempts to find the user by username or email
+ *
Throws UsernameNotFoundException if user not found
+ *
Converts the user's authorities to Spring Security GrantedAuthority objects
+ *
Constructs a CustomUserDetails object with all required authentication information
+ *
+ *
+ * @param usernameOrEmail the username or email address to search for
+ * @return UserDetails implementation containing the user's authentication information
+ * @throws UsernameNotFoundException if no user is found with the given username/email
+ * @implNote The method is transactional with readOnly=true since it only reads data
+ * @see CustomUserDetails
+ * @see Transactional
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
+ // Find user with their authorities in a single query
+ UserModel user = userRepository.findByUsernameOrEmailWithAuthorities(usernameOrEmail)
+ .orElseThrow(() -> new UsernameNotFoundException(
+ "User not found with username or email: " + usernameOrEmail));
+
+ // Convert UserModel to Spring Security's UserDetails implementation
+ return new CustomUserDetails(
+ user.getId(),
+ user.getUsername(),
+ user.getPassword(),
+ user.isEnabled(),
+ user.isAccountNonExpired(),
+ user.isAccountNonLocked(),
+ user.isCredentialsNonExpired(),
+ user.getAuthorities().stream()
+ .map(auth -> new SimpleGrantedAuthority(auth.getAuthority()))
+ .collect(Collectors.toSet())
+ );
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/handlers/CustomAuthenticationSuccessHandler.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/handlers/CustomAuthenticationSuccessHandler.java
new file mode 100644
index 000000000..56883ee7b
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/security/handlers/CustomAuthenticationSuccessHandler.java
@@ -0,0 +1,95 @@
+package com.jydoc.deliverable4.security.handlers;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+
+import java.io.IOException;
+
+/**
+ * Custom authentication success handler that determines the redirect target URL
+ * based on the user's authorities after successful login.
+ *
+ *
This handler extends Spring Security's {@link SimpleUrlAuthenticationSuccessHandler}
+ * to provide role-based redirection logic:
+ *
+ *
Administrators are redirected to the admin dashboard
+ *
Regular users are redirected to the standard dashboard
+ *
+ *
+ *
Security Considerations:
+ *
+ *
Ensures proper redirection based on verified authorities
+ *
Handles already-committed responses gracefully
+ *
Follows Spring Security's authentication flow
+ *
+ */
+public class CustomAuthenticationSuccessHandler
+ extends SimpleUrlAuthenticationSuccessHandler
+ implements AuthenticationSuccessHandler {
+
+ /**
+ * Path for admin dashboard redirect
+ */
+ private static final String ADMIN_DASHBOARD_URL = "/admin/dashboard";
+
+ /**
+ * Path for regular user dashboard redirect
+ */
+ private static final String USER_DASHBOARD_URL = "/dashboard";
+
+ /**
+ * Handles successful authentication by redirecting to the appropriate target URL.
+ *
+ * @param request the HTTP request
+ * @param response the HTTP response
+ * @param authentication the authentication object containing user authorities
+ * @throws IOException if a redirect error occurs
+ */
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request,
+ HttpServletResponse response,
+ Authentication authentication) throws IOException {
+
+ String targetUrl = determineTargetUrl(authentication);
+
+ if (response.isCommitted()) {
+ logger.debug("Response already committed - unable to redirect to " + targetUrl);
+ return;
+ }
+
+ getRedirectStrategy().sendRedirect(request, response, targetUrl);
+ }
+
+ /**
+ * Determines the target URL based on the user's authorities.
+ *
+ *
The logic checks for the presence of ROLE_ADMIN authority:
+ *
+ *
If present: redirects to admin dashboard
+ *
Otherwise: redirects to regular dashboard
+ *
+ *
+ * @param authentication the authentication object containing user authorities
+ * @return the appropriate target URL
+ */
+ protected String determineTargetUrl(Authentication authentication) {
+ boolean isAdmin = authentication.getAuthorities().stream()
+ .anyMatch(this::isAdminAuthority);
+
+ return isAdmin ? ADMIN_DASHBOARD_URL : USER_DASHBOARD_URL;
+ }
+
+ /**
+ * Checks if a given authority represents an admin role.
+ *
+ * @param authority the granted authority to check
+ * @return true if the authority is ROLE_ADMIN, false otherwise
+ */
+ private boolean isAdminAuthority(GrantedAuthority authority) {
+ return authority.getAuthority().equals("ROLE_ADMIN");
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/authservices/AuthService.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/authservices/AuthService.java
new file mode 100644
index 000000000..3571648af
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/authservices/AuthService.java
@@ -0,0 +1,350 @@
+package com.jydoc.deliverable4.services.authservices;
+
+import com.jydoc.deliverable4.dtos.userdtos.LoginDTO;
+import com.jydoc.deliverable4.dtos.userdtos.UserDTO;
+import com.jydoc.deliverable4.model.auth.AuthorityModel;
+import com.jydoc.deliverable4.model.UserModel;
+import com.jydoc.deliverable4.repositories.userrepositories.AuthorityRepository;
+import com.jydoc.deliverable4.repositories.userrepositories.UserRepository;
+import com.jydoc.deliverable4.services.userservices.UserValidationHelper;
+import lombok.RequiredArgsConstructor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashSet;
+
+/**
+ * Service handling all authentication and authorization operations including:
+ * - User registration and credential management
+ * - Login authentication and validation
+ * - Account status checks
+ * - Role assignment and management
+ *
+ *
This service integrates with Spring Security's authentication mechanisms
+ * while providing additional business logic for user management.
+ *
+ *
All methods perform comprehensive validation and throw appropriate exceptions
+ * for error conditions.
+ */
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+ private static final Logger logger = LogManager.getLogger(AuthService.class);
+ private static final String DEFAULT_ROLE = "ROLE_USER";
+
+ // Dependencies injected via constructor (using Lombok @RequiredArgsConstructor)
+ private final UserRepository userRepository;
+ private final AuthorityRepository authorityRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final AuthenticationManager authenticationManager;
+ private final UserValidationHelper validationHelper;
+
+ /* ====================== Public API Methods ====================== */
+
+ /**
+ * Registers a new user in the system with comprehensive validation.
+ *
+ *
Performs the following operations:
+ *
+ *
Validates all required user fields
+ *
Checks for duplicate username/email
+ *
Encodes the password
+ *
Assigns default role (ROLE_USER)
+ *
Persists the user entity
+ *
+ *
+ * @param userDto Data Transfer Object containing user registration information
+ * @throws IllegalArgumentException if any required field is missing or invalid
+ * @throws UsernameExistsException if the username is already taken
+ * @throws EmailExistsException if the email is already registered
+ */
+ @Transactional
+ public void registerNewUser(UserDTO userDto) {
+ validationHelper.validateUserRegistration(userDto);
+ validateUserDto(userDto);
+ checkForExistingCredentials(userDto);
+
+ UserModel user = createUserFromDto(userDto);
+ assignDefaultRole(user);
+ userRepository.save(user);
+ logger.info("Registered new user: {}", user.getUsername());
+ }
+
+ /**
+ * Authenticates a user using Spring Security's authentication manager.
+ *
+ *
This method:
+ *
+ *
Delegates authentication to Spring Security
+ *
Handles various authentication failure scenarios
+ *
Returns the authenticated user entity on success
+ *
+ *
+ * @param loginDto Data Transfer Object containing login credentials
+ * @return Authenticated UserModel entity
+ * @throws IllegalArgumentException if loginDto is null
+ * @throws AuthenticationException for authentication failures (bad credentials,
+ * disabled account, locked account, etc.)
+ */
+ @Transactional(readOnly = true)
+ public UserModel authenticate(LoginDTO loginDto) {
+ if (loginDto == null) {
+ throw new IllegalArgumentException("Login credentials cannot be null");
+ }
+ return authenticateUser(loginDto.username(), loginDto.password());
+ }
+
+ /**
+ * Validates user credentials directly against the database.
+ *
+ *
This method provides an alternative to Spring Security authentication
+ * with more direct control over the validation process.
+ *
+ * @param loginDto Data Transfer Object containing login credentials
+ * @return Validated UserModel entity
+ * @throws IllegalArgumentException if loginDto is null
+ * @throws AuthenticationException for invalid credentials or account issues
+ */
+ @Transactional(readOnly = true)
+ public UserModel validateLogin(LoginDTO loginDto) {
+ if (loginDto == null) {
+ throw new IllegalArgumentException("Login credentials cannot be null");
+ }
+
+ String credential = loginDto.username().trim().toLowerCase();
+ String rawPassword = loginDto.password();
+
+ logger.debug("Login attempt for: {}", credential);
+ UserModel user = findUserByCredential(credential);
+ validateUserPassword(user, rawPassword);
+ checkAccountEnabled(user);
+
+ logger.info("Login successful for: {}", user.getUsername());
+ return user;
+ }
+
+ /* ====================== Core Business Logic ====================== */
+
+ /**
+ * Authenticates a user with Spring Security's authentication manager.
+ *
+ * @param username The username to authenticate
+ * @param password The raw (unencoded) password
+ * @return Authenticated UserModel entity
+ * @throws AuthenticationException wrapping various Spring Security exceptions
+ */
+ private UserModel authenticateUser(String username, String password) {
+ try {
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(username, password)
+ );
+ return findAuthenticatedUser(authentication.getName());
+ } catch (BadCredentialsException e) {
+ handleAuthenticationFailure("Bad credentials for user: {}", username, "Invalid username or password");
+ } catch (DisabledException e) {
+ handleAuthenticationFailure("Disabled account: {}", username, "Account is disabled");
+ } catch (LockedException e) {
+ handleAuthenticationFailure("Locked account: {}", username, "Account is locked");
+ }
+ return null; // Unreachable due to exception handling
+ }
+
+ /**
+ * Creates a new UserModel entity from registration DTO.
+ *
+ * @param userDto Source data for user creation
+ * @return New UserModel with encoded password and trimmed fields
+ */
+ private UserModel createUserFromDto(UserDTO userDto) {
+ return UserModel.builder()
+ .username(userDto.getUsername().trim())
+ .password(passwordEncoder.encode(userDto.getPassword()))
+ .email(userDto.getEmail().toLowerCase().trim())
+ .firstName(userDto.getFirstName().trim())
+ .lastName(userDto.getLastName().trim())
+ .enabled(true)
+ .accountNonExpired(true)
+ .credentialsNonExpired(true)
+ .accountNonLocked(true)
+ .build();
+ }
+
+ /**
+ * Assigns the default role (ROLE_USER) to a new user.
+ *
+ *
If the default role doesn't exist in the database, it will be created.
+ *
+ * @param user The user to receive the default role
+ */
+ @Transactional(propagation = Propagation.MANDATORY)
+ protected void assignDefaultRole(UserModel user) {
+ AuthorityModel authority = authorityRepository.findByAuthority(DEFAULT_ROLE)
+ .orElseGet(this::createAndSaveNewAuthority);
+ user.addAuthority(authority);
+ }
+
+ /* ====================== Validation Helpers ====================== */
+
+ /**
+ * Validates that all required fields in UserDTO are present and non-empty.
+ *
+ * @param userDto The DTO to validate
+ * @throws IllegalArgumentException if any required field is missing or empty
+ */
+ private void validateUserDto(UserDTO userDto) {
+ if (userDto == null) throw new IllegalArgumentException("UserDTO cannot be null");
+ if (userDto.getUsername() == null || userDto.getUsername().trim().isEmpty())
+ throw new IllegalArgumentException("Username cannot be empty");
+ if (userDto.getPassword() == null || userDto.getPassword().trim().isEmpty())
+ throw new IllegalArgumentException("Password cannot be empty");
+ if (userDto.getEmail() == null || userDto.getEmail().trim().isEmpty())
+ throw new IllegalArgumentException("Email cannot be empty");
+ if (userDto.getFirstName() == null || userDto.getFirstName().trim().isEmpty())
+ throw new IllegalArgumentException("First name cannot be empty");
+ if (userDto.getLastName() == null || userDto.getLastName().trim().isEmpty())
+ throw new IllegalArgumentException("Last name cannot be empty");
+ }
+
+ /**
+ * Checks if username or email already exist in the system.
+ *
+ * @param userDto The DTO containing credentials to check
+ * @throws UsernameExistsException if username is taken
+ * @throws EmailExistsException if email is registered
+ */
+ private void checkForExistingCredentials(UserDTO userDto) {
+ if (userRepository.existsByUsername(userDto.getUsername())) {
+ throw new UsernameExistsException(userDto.getUsername());
+ }
+ if (userRepository.existsByEmail(userDto.getEmail())) {
+ throw new EmailExistsException(userDto.getEmail());
+ }
+ }
+
+ /**
+ * Validates that the provided raw password matches the user's encoded password.
+ *
+ * @param user The user to validate
+ * @param rawPassword The raw (unencoded) password to check
+ * @throws AuthenticationException if passwords don't match
+ */
+ private void validateUserPassword(UserModel user, String rawPassword) {
+ if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
+ logger.warn("Password mismatch for user: {}", user.getUsername());
+ throw new AuthenticationException("Invalid credentials");
+ }
+ }
+
+ /**
+ * Verifies that the user account is enabled.
+ *
+ * @param user The user to check
+ * @throws AuthenticationException if account is disabled
+ */
+ private void checkAccountEnabled(UserModel user) {
+ if (!user.isEnabled()) {
+ logger.warn("Disabled account login attempt: {}", user.getUsername());
+ throw new AuthenticationException("Account is disabled");
+ }
+ }
+
+ /* ====================== Repository Helpers ====================== */
+
+ /**
+ * Finds a user by either username or email (case-insensitive).
+ *
+ * @param credential Username or email to search for
+ * @return Found UserModel entity
+ * @throws AuthenticationException if no user found
+ */
+ private UserModel findUserByCredential(String credential) {
+ return userRepository.findByUsernameOrEmail(credential)
+ .orElseThrow(() -> {
+ logger.warn("User not found: {}", credential);
+ return new AuthenticationException("Invalid credentials");
+ });
+ }
+
+ /**
+ * Finds a user by username after successful authentication.
+ *
+ * @param username The authenticated username
+ * @return Found UserModel entity
+ * @throws AuthenticationException if user not found (unexpected after auth)
+ */
+ private UserModel findAuthenticatedUser(String username) {
+ return userRepository.findByUsername(username)
+ .orElseThrow(() -> {
+ logger.error("Authenticated user not found: {}", username);
+ return new AuthenticationException("User account error");
+ });
+ }
+
+ /**
+ * Creates and persists a new authority with the default role.
+ *
+ * @return The newly created AuthorityModel
+ */
+ private AuthorityModel createAndSaveNewAuthority() {
+ AuthorityModel newRole = new AuthorityModel(DEFAULT_ROLE);
+ newRole.setUsers(new HashSet<>());
+ return authorityRepository.save(newRole);
+ }
+
+ /* ====================== Exception Handling ====================== */
+
+ /**
+ * Handles authentication failures consistently with logging and exception throwing.
+ *
+ * @param logMessage The log message template
+ * @param username The username that failed authentication
+ * @param exceptionMessage The exception message for clients
+ * @throws AuthenticationException always
+ */
+ private void handleAuthenticationFailure(String logMessage, String username, String exceptionMessage) {
+ logger.warn(logMessage, username);
+ throw new AuthenticationException(exceptionMessage);
+ }
+
+ /* ====================== Custom Exceptions ====================== */
+
+ /**
+ * Exception indicating authentication failure.
+ */
+ public static class AuthenticationException extends RuntimeException {
+ public AuthenticationException(String message) {
+ super(message);
+ logger.error("Authentication failed: {}", message);
+ }
+ }
+
+ /**
+ * Exception indicating duplicate username during registration.
+ */
+ public static class UsernameExistsException extends RuntimeException {
+ public UsernameExistsException(String username) {
+ super(String.format("Username '%s' already exists", username));
+ logger.warn("Duplicate username: {}", username);
+ }
+ }
+
+ /**
+ * Exception indicating duplicate email during registration.
+ */
+ public static class EmailExistsException extends RuntimeException {
+ public EmailExistsException(String email) {
+ super(String.format("Email '%s' already registered", email));
+ logger.warn("Duplicate email: {}", email);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/impl/DashboardServiceImpl.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/impl/DashboardServiceImpl.java
new file mode 100644
index 000000000..f84b500b7
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/impl/DashboardServiceImpl.java
@@ -0,0 +1,233 @@
+package com.jydoc.deliverable4.services.impl;
+
+import com.jydoc.deliverable4.dtos.MedicationScheduleDTO;
+import com.jydoc.deliverable4.dtos.userdtos.DashboardDTO;
+import com.jydoc.deliverable4.model.MedicationModel;
+import com.jydoc.deliverable4.repositories.medicationrepositories.MedicationRepository;
+import com.jydoc.deliverable4.services.userservices.DashboardService;
+import com.jydoc.deliverable4.services.medicationservices.MedicationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of the DashboardService interface that provides methods for retrieving
+ * and processing user dashboard data including medication schedules, health metrics,
+ * and alerts.
+ */
+@Service
+public class DashboardServiceImpl implements DashboardService {
+ private static final Logger logger = LoggerFactory.getLogger(DashboardServiceImpl.class);
+
+ private final MedicationService medicationService;
+ private final MedicationRepository medicationRepository;
+
+ /**
+ * Constructs a new DashboardServiceImpl with the required MedicationService dependency.
+ *
+ * @param medicationService The medication service used to retrieve medication data
+ */
+ public DashboardServiceImpl(MedicationService medicationService, MedicationRepository medicationRepository) {
+ this.medicationService = medicationService;
+ this.medicationRepository = medicationRepository;
+ logger.debug("DashboardServiceImpl initialized with MedicationService");
+ }
+
+ /**
+ * Retrieves and processes all dashboard data for the specified user.
+ * This includes medication schedules, health conditions, metrics, and alerts.
+ *
+ * @param userDetails The authenticated user details
+ * @return DashboardDTO containing all dashboard data for the user
+ * @throws IllegalArgumentException if userDetails is null
+ * @throws RuntimeException if there's an error processing dashboard data
+ */
+ @Override
+ public DashboardDTO getUserDashboardData(UserDetails userDetails) {
+ logger.debug("Entering getUserDashboardData for user: {}",
+ userDetails != null ? userDetails.getUsername() : "null");
+
+ if (userDetails == null) {
+ logger.error("UserDetails parameter cannot be null");
+ throw new IllegalArgumentException("UserDetails cannot be null");
+ }
+
+ String username = userDetails.getUsername();
+ logger.info("Building dashboard data for user: {}", username);
+
+ DashboardDTO dashboard = new DashboardDTO();
+ dashboard.setUsername(username);
+
+ try {
+ // Retrieve and process medication schedule
+ logger.debug("Retrieving medication schedule for user: {}", username);
+ List schedule = medicationService.getMedicationSchedule(username);
+ logger.debug("Retrieved {} medication schedule entries for user: {}",
+ schedule.size(), username);
+
+ List upcomingMeds = processMedicationSchedule(schedule);
+ logger.debug("Processed {} upcoming medications for user: {}",
+ upcomingMeds.size(), username);
+
+ // Set all dashboard metrics
+ logger.debug("Setting dashboard metrics for user: {}", username);
+ dashboard.setActiveMedicationsCount(countActiveMedications(schedule));
+ dashboard.setTodaysDosesCount(upcomingMeds.size());
+ dashboard.setHealthMetricsCount(0); // Placeholder - would come from health service
+ dashboard.setHasMedications(hasMedications(userDetails));
+ dashboard.setUpcomingMedications(upcomingMeds);
+
+ // Generate medication alerts
+ logger.debug("Generating medication alerts for user: {}", username);
+ dashboard.setAlerts(generateMedicationAlerts(schedule));
+
+ logger.info("Successfully built dashboard for user: {}", username);
+ } catch (Exception e) {
+ logger.error("Error building dashboard for user {}: {}", username, e.getMessage(), e);
+ throw new RuntimeException("Failed to build dashboard data", e);
+ }
+
+ return dashboard;
+ }
+
+ /**
+ * Processes the medication schedule to extract upcoming doses.
+ * Filters medications with future schedule times and sorts them chronologically.
+ *
+ * @param schedule The list of medication schedule DTOs
+ * @return List of upcoming medications sorted by schedule time
+ */
+ private List processMedicationSchedule(List schedule) {
+ logger.debug("Processing medication schedule with {} entries",
+ schedule != null ? schedule.size() : "null");
+
+ if (schedule == null || schedule.isEmpty()) {
+ logger.debug("Empty or null schedule provided, returning empty list");
+ return Collections.emptyList();
+ }
+
+ LocalTime now = LocalTime.now();
+ logger.debug("Current time for schedule filtering: {}", now);
+
+ List result = schedule.stream()
+ .filter(med -> {
+ boolean isValid = med.getScheduleTime() != null && med.getScheduleTime().isAfter(now);
+ if (!isValid) {
+ logger.trace("Filtered out medication {} with schedule time {}",
+ med.getMedicationName(), med.getScheduleTime());
+ }
+ return isValid;
+ })
+ .sorted(Comparator.comparing(MedicationScheduleDTO::getScheduleTime))
+ .map(this::convertToUpcomingMedicationDto)
+ .collect(Collectors.toList());
+
+ logger.debug("Processed {} upcoming medications from schedule", result.size());
+ return result;
+ }
+
+ /**
+ * Converts a MedicationScheduleDTO to an UpcomingMedicationDto for dashboard display.
+ *
+ * @param scheduleDto The medication schedule DTO to convert
+ * @return UpcomingMedicationDto with relevant medication details
+ */
+ private DashboardDTO.UpcomingMedicationDto convertToUpcomingMedicationDto(MedicationScheduleDTO scheduleDto) {
+ logger.debug("Converting MedicationScheduleDTO to UpcomingMedicationDto for medication: {}",
+ scheduleDto.getMedicationName());
+
+ DashboardDTO.UpcomingMedicationDto dto = new DashboardDTO.UpcomingMedicationDto();
+ dto.setName(scheduleDto.getMedicationName());
+ dto.setDosage(scheduleDto.getDosage());
+ dto.setNextDoseTime(scheduleDto.getScheduleTime().toString());
+ dto.setTaken(scheduleDto.isTaken());
+
+
+ logger.trace("Converted medication details: name={}, dosage={}, time={}, taken={}",
+ dto.getName(), dto.getDosage(), dto.getNextDoseTime(), dto.isTaken());
+
+ return dto;
+ }
+
+ /**
+ * Counts the number of distinct active medications in the schedule.
+ *
+ * @param schedule The list of medication schedule DTOs
+ * @return Count of distinct active medications
+ */
+ private int countActiveMedications(List schedule) {
+ logger.debug("Counting active medications in schedule with {} entries",
+ schedule != null ? schedule.size() : "null");
+
+ if (schedule == null) {
+ logger.debug("Null schedule provided, returning 0 active medications");
+ return 0;
+ }
+
+ int count = (int) schedule.stream()
+ .map(MedicationScheduleDTO::getMedicationId)
+ .distinct()
+ .count();
+
+ logger.debug("Found {} distinct active medications", count);
+ return count;
+ }
+
+ /**
+ * Generates placeholder medication alerts for the dashboard.
+ * In a production environment, this would check for actual refill needs,
+ * interactions, and other medication-related alerts.
+ *
+ * @param schedule The list of medication schedule DTOs
+ * @return List of generated medication alerts
+ */
+ private List generateMedicationAlerts(List schedule) {
+ logger.debug("Generating medication alerts for schedule with {} entries",
+ schedule != null ? schedule.size() : "null");
+
+ List alerts = new ArrayList<>();
+
+ // Sample refill alert (would check actual refill status in production)
+ if (!schedule.isEmpty()) {
+ logger.trace("Adding placeholder refill alert");
+ alerts.add(new DashboardDTO.MedicationAlertDto(
+ "Placeholder",
+ "Placeholder",
+ schedule.get(0).getMedicationName()
+ ));
+ }
+
+ // Sample interaction alert (would check actual interactions in production)
+ logger.trace("Adding placeholder interaction alert");
+ alerts.add(new DashboardDTO.MedicationAlertDto(
+ "Placeholder",
+ "Placeholder",
+ "Placeholder"
+ ));
+
+ logger.debug("Generated {} medication alerts", alerts.size());
+ return alerts;
+ }
+
+ /**
+ * Checks if the user has any medications in their schedule.
+ *
+ * @param userDetails The authenticated user details
+ * @return true if the user has medications, false otherwise
+ * @throws IllegalArgumentException if userDetails is null
+ */
+ @Override
+ public boolean hasMedications(UserDetails userDetails) {
+ if (userDetails == null) {
+ throw new IllegalArgumentException("UserDetails cannot be null");
+ }
+ List user = medicationRepository.findByUserUsernameWithMedicationDetails(userDetails.getUsername());
+ return user != null && !user.isEmpty();
+ }
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/impl/MedicationServiceImpl.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/impl/MedicationServiceImpl.java
new file mode 100644
index 000000000..40e980ab6
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/impl/MedicationServiceImpl.java
@@ -0,0 +1,723 @@
+package com.jydoc.deliverable4.services.impl;
+
+import com.jydoc.deliverable4.dtos.MedicationDTO;
+import com.jydoc.deliverable4.dtos.MedicationScheduleDTO;
+import com.jydoc.deliverable4.dtos.RefillReminderDTO;
+import com.jydoc.deliverable4.model.*;
+import com.jydoc.deliverable4.repositories.medicationrepositories.MedicationIntakeTimeRepository;
+import com.jydoc.deliverable4.repositories.medicationrepositories.MedicationRepository;
+import com.jydoc.deliverable4.repositories.userrepositories.UserRepository;
+import com.jydoc.deliverable4.security.Exceptions.MedicationCreationException;
+import com.jydoc.deliverable4.security.Exceptions.MedicationNotFoundException;
+import com.jydoc.deliverable4.security.Exceptions.MedicationScheduleException;
+import com.jydoc.deliverable4.security.Exceptions.UserNotFoundException;
+import com.jydoc.deliverable4.services.medicationservices.MedicationService;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Implementation of the MedicationService interface providing medication management functionality.
+ * This service handles CRUD operations for medications, schedule generation, and refill reminders.
+ */
+@Service
+@RequiredArgsConstructor
+public class MedicationServiceImpl implements MedicationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(MedicationServiceImpl.class);
+
+ private final MedicationRepository medicationRepository;
+ private final UserRepository userRepository;
+ private final MedicationIntakeTimeRepository intakeTimeRepository;
+
+ /**
+ * Retrieves all medications for a specific user.
+ *
+ * @param username The username of the user whose medications to retrieve
+ * @return List of MedicationDTO objects representing the user's medications
+ * @throws UserNotFoundException if the specified user doesn't exist
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public List getUserMedications(String username) {
+ logger.debug("Fetching medications for user: {}", username);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ validateUserExists(username);
+
+ List medications = medicationRepository.findByUserUsername(username);
+ logger.debug("Found {} medications in database query", medications.size());
+
+ List result = medications.stream()
+ .map(this::convertToDto)
+ .collect(Collectors.toList());
+
+ logOperationSuccess("retrieved", result.size(), username, startTime);
+ return result;
+ } catch (Exception e) {
+ logOperationError("fetching medications for user", username, e);
+ throw e;
+ }
+ }
+
+ /**
+ * Retrieves a single medication by its ID.
+ *
+ * @param id The ID of the medication to retrieve
+ * @return MedicationDTO representing the requested medication
+ * @throws MedicationNotFoundException if no medication exists with the specified ID
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public MedicationDTO getMedicationById(Long id) {
+ logger.debug("Fetching medication by ID: {}", id);
+
+ try {
+ MedicationModel medication = medicationRepository.findById(id)
+ .orElseThrow(() -> {
+ logger.error("Medication not found with ID: {}", id);
+ return new MedicationNotFoundException("Medication not found with ID: " + id);
+ });
+
+ MedicationDTO result = convertToDto(medication);
+ logger.info("Successfully retrieved medication ID: {}", id);
+ return result;
+ } catch (Exception e) {
+ logOperationError("fetching medication by ID", id.toString(), e);
+ throw e;
+ }
+ }
+
+ /**
+ * Creates a new medication for a user.
+ *
+ * @param medicationDTO DTO containing medication details
+ * @param username The username of the user who will own this medication
+ * @return MedicationDTO representing the created medication
+ * @throws UserNotFoundException if the specified user doesn't exist
+ */
+ @Override
+ @Transactional
+ public MedicationDTO createMedication(MedicationDTO medicationDTO, String username) {
+ logger.info("Creating new medication for user: {}", username);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ // Validate and get user
+ UserModel user = validateAndGetUser(username);
+
+ // Set default values if not provided
+ if (medicationDTO.getActive() == null) {
+ medicationDTO.setActive(true);
+ }
+
+ // Validate days of week
+ if (medicationDTO.getDaysOfWeek() == null || medicationDTO.getDaysOfWeek().isEmpty()) {
+ throw new IllegalArgumentException("At least one day of week must be selected");
+ }
+
+ // Build and convert the medication
+ MedicationModel medication = convertToEntity(medicationDTO, user);
+
+ // Process intake times
+ processIntakeTimes(medicationDTO, medication);
+
+ // Process days of week
+ processDaysOfWeek(medicationDTO, medication);
+
+ // Save the medication
+ MedicationModel savedMedication = saveMedication(medication);
+
+ // Convert to DTO and return
+ MedicationDTO result = savedMedication.toDto();
+ logOperationSuccess("created", savedMedication.getId(), username, startTime);
+ return result;
+ } catch (Exception e) {
+ logOperationError("creating medication", username, e);
+ throw new MedicationCreationException("Failed to create medication: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Retrieves the days of medication intake for a specific medication.
+ *
+ * @param medicationId The ID of the medication to retrieve intake days for
+ * @return Set of days of the week when the medication should be taken
+ * @throws MedicationNotFoundException if no medication exists with the specified ID
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public Set getMedicationIntakeDays(Long medicationId) {
+ logger.debug("Fetching intake days for medication ID: {}", medicationId);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ MedicationModel medication = medicationRepository.findById(medicationId)
+ .orElseThrow(() -> {
+ logger.error("Medication not found with ID: {}", medicationId);
+ return new MedicationNotFoundException("Medication not found with ID: " + medicationId);
+ });
+
+ Set days = medication.getDaysOfWeek().stream()
+ .map(day -> MedicationDTO.DayOfWeek.valueOf(day.name()))
+ .collect(Collectors.toSet());
+
+ logger.info("Successfully retrieved {} intake days for medication ID {} in {} ms",
+ days.size(), medicationId, System.currentTimeMillis() - startTime);
+ return days;
+ } catch (Exception e) {
+ logOperationError("fetching intake days for medication", medicationId.toString(), e);
+ throw new MedicationNotFoundException("Failed to retrieve intake days for medication");
+ }
+ }
+
+
+ /**
+ * Updates an existing medication.
+ *
+ * @param id The ID of the medication to update
+ * @param medicationDTO DTO containing updated medication details
+ * @return MedicationDTO representing the updated medication
+ * @throws MedicationNotFoundException if no medication exists with the specified ID
+ */
+ @Override
+ @Transactional
+ public MedicationDTO updateMedication(Long id, MedicationDTO medicationDTO) {
+ logger.info("Updating medication ID: {}", id);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ MedicationModel existing = getExistingMedication(id);
+ updateMedicationFields(existing, medicationDTO);
+
+ if (medicationDTO.getIntakeTimes() != null) {
+ logger.debug("Processing {} intake times for update", medicationDTO.getIntakeTimes().size());
+ updateIntakeTimes(existing, medicationDTO.getIntakeTimes());
+ }
+
+ MedicationModel updatedMedication = medicationRepository.save(existing);
+ MedicationDTO result = convertToDto(updatedMedication);
+
+ logOperationSuccess("updated", id, "medication", startTime);
+ return result;
+ } catch (Exception e) {
+ logOperationError("updating medication", id.toString(), e);
+ throw new RuntimeException("Failed to update medication", e);
+ }
+ }
+
+ /**
+ * Deletes a medication.
+ *
+ * @param id The ID of the medication to delete
+ * @throws MedicationNotFoundException if no medication exists with the specified ID
+ */
+ @Override
+ @Transactional
+ public void deleteMedication(Long id) {
+ logger.info("Deleting medication ID: {}", id);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ validateMedicationExists(id);
+ deleteIntakeTimes(id);
+ medicationRepository.deleteById(id);
+
+ logOperationSuccess("deleted", id, "medication", startTime);
+ } catch (Exception e) {
+ logOperationError("deleting medication", id.toString(), e);
+ throw new RuntimeException("Failed to delete medication", e);
+ }
+ }
+
+ /**
+ * Generates a medication schedule for a user.
+ *
+ * @param username The username of the user whose schedule to generate
+ * @return List of MedicationScheduleDTO objects representing the schedule
+ * @throws MedicationScheduleException if there's an error generating the schedule
+ */
+
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getMedicationSchedule(String username) {
+ logger.debug("Fetching medication schedule for user: {}", username);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ // 1. Fetch medications with all necessary relationships
+ List medications = medicationRepository.findByUserUsernameWithMedicationDetails(username);
+ logger.debug("Found {} medications for user {}", medications.size(), username);
+
+ if (medications.isEmpty()) {
+ logger.info("No medications found for user {}", username);
+ return Collections.emptyList();
+ }
+
+ // 2. Get current day of week
+ DayOfWeek currentDay = LocalDate.now().getDayOfWeek();
+ logger.debug("Current day of week: {}", currentDay);
+
+ // 3. Convert to your model's DayOfWeek enum
+ MedicationModel.DayOfWeek currentMedDay;
+ try {
+ currentMedDay = MedicationModel.DayOfWeek.valueOf(currentDay.name());
+ logger.debug("Converted to model day: {}", currentMedDay);
+ } catch (IllegalArgumentException e) {
+ logger.error("Day of week conversion failed for {}", currentDay.name(), e);
+ throw new MedicationScheduleException("Day of week conversion failed", e);
+ }
+
+ // 4. Process medications
+ List schedule = medications.stream()
+ .filter(Objects::nonNull)
+ .peek(med -> logger.trace("Processing medication: {}", med.getId()))
+ .filter(medication -> {
+ // Skip if no intake times
+ if (medication.getIntakeTimes() == null || medication.getIntakeTimes().isEmpty()) {
+ logger.debug("Medication {} skipped - no intake times", medication.getId());
+ return false;
+ }
+ return true;
+ })
+ .filter(medication -> {
+ // Include if no days specified OR current day matches
+ if (medication.getDaysOfWeek() == null || medication.getDaysOfWeek().isEmpty()) {
+ logger.trace("Medication {} included - no day restrictions", medication.getId());
+ return true;
+ }
+
+ boolean matchesDay = medication.getDaysOfWeek().contains(currentMedDay);
+ logger.trace("Medication {} day check: {}", medication.getId(), matchesDay);
+ return matchesDay;
+ })
+ .flatMap(this::processMedicationForSchedule)
+ .collect(Collectors.toList());
+
+ logger.info("Generated schedule with {} entries for user {} in {} ms",
+ schedule.size(), username, System.currentTimeMillis() - startTime);
+
+ return schedule;
+ } catch (Exception e) {
+ logger.error("Error generating schedule for user {}: {}", username, e.getMessage(), e);
+ throw new MedicationScheduleException("Failed to generate medication schedule", e);
+ }
+ }
+
+ /**
+ * Retrieves upcoming medication refill reminders for a user.
+ *
+ * @param username The username of the user whose reminders to generate
+ * @return List of RefillReminderDTO objects representing the reminders
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public List getUpcomingRefills(String username) {
+ logger.debug("Fetching upcoming refills for user: {}", username);
+ long startTime = System.currentTimeMillis();
+
+ try {
+ List medications = medicationRepository.findByUserUsername(username);
+ logger.debug("Found {} medications for refill reminders", medications.size());
+
+ List reminders = medications.stream()
+ .map(this::mapToRefillReminderDTO)
+ .collect(Collectors.toList());
+
+ logger.info("Generated {} refill reminders for user {} in {} ms",
+ reminders.size(), username, System.currentTimeMillis() - startTime);
+ return reminders;
+ } catch (Exception e) {
+ logger.error("Error generating refill reminders for user {}: {}", username, e.getMessage(), e);
+ throw e;
+ }
+ }
+
+ // ==================== PRIVATE HELPER METHODS ====================
+
+ /**
+ * Validates that a user exists in the system.
+ *
+ * @param username The username to validate
+ * @throws UserNotFoundException if the user doesn't exist
+ */
+ private void validateUserExists(String username) {
+ if (!userRepository.existsByUsername(username)) {
+ logger.error("User not found: {}", username);
+ throw new UserNotFoundException(1L);
+ }
+ }
+
+ /**
+ * Validates and retrieves a user entity.
+ *
+ * @param username The username of the user to retrieve
+ * @return UserModel entity
+ * @throws UserNotFoundException if the user doesn't exist
+ */
+ private UserModel validateAndGetUser(String username) {
+ return userRepository.findByUsername(username)
+ .orElseThrow(() -> {
+ logger.error("User not found: {}", username);
+ return new UserNotFoundException(1L);
+ });
+ }
+
+ /**
+ * Builds a MedicationDTO with user association.
+ *
+ * @param medicationDTO Source DTO with medication details
+ * @param user The user who will own the medication
+ * @return Prepared MedicationDTO
+ */
+ private MedicationDTO buildMedicationDTO(MedicationDTO medicationDTO, UserModel user) {
+ return MedicationDTO.builder()
+ .medicationName(medicationDTO.getMedicationName())
+ .userId(user.getId())
+ .urgency(medicationDTO.getUrgency())
+ .dosage(medicationDTO.getDosage())
+ .instructions(medicationDTO.getInstructions())
+ .intakeTimes(medicationDTO.getIntakeTimes() != null ?
+ new HashSet<>(medicationDTO.getIntakeTimes()) : new HashSet<>())
+ .daysOfWeek(medicationDTO.getDaysOfWeek() != null ?
+ new HashSet<>(medicationDTO.getDaysOfWeek()) : new HashSet<>())
+ .active(medicationDTO.getActive() != null ?
+ medicationDTO.getActive() : true) // Default to true if null
+ .build();
+ }
+
+ /**
+ * Processes intake times for a medication during creation.
+ *
+ * @param medicationDTO DTO containing intake times
+ * @param medication The medication entity to associate with intake times
+ */
+ private void processIntakeTimes(MedicationDTO medicationDTO, MedicationModel medication) {
+ if (medicationDTO.getIntakeTimes() != null && !medicationDTO.getIntakeTimes().isEmpty()) {
+ logger.debug("Processing {} intake times", medicationDTO.getIntakeTimes().size());
+ medicationDTO.getIntakeTimes().forEach(time -> {
+ MedicationIntakeTime mit = MedicationIntakeTime.builder()
+ .medication(medication)
+ .intakeTime(time)
+ .build();
+ medication.addIntakeTime(mit.getIntakeTime());
+ });
+ }
+ }
+
+ /**
+ * Saves a medication entity and validates the result.
+ *
+ * @param medication The medication to save
+ * @return The saved medication entity
+ * @throws RuntimeException if the save operation fails
+ */
+ private MedicationModel saveMedication(MedicationModel medication) {
+ MedicationModel savedMedication = medicationRepository.saveAndFlush(medication);
+ if (savedMedication.getId() == null) {
+ logger.error("Saved medication has null ID!");
+ throw new RuntimeException("Medication ID is null after save");
+ }
+ return savedMedication;
+ }
+
+ /**
+ * Retrieves an existing medication entity.
+ *
+ * @param id The ID of the medication to retrieve
+ * @return The medication entity
+ * @throws MedicationNotFoundException if the medication doesn't exist
+ */
+ private MedicationModel getExistingMedication(Long id) {
+ return medicationRepository.findById(id)
+ .orElseThrow(() -> {
+ logger.error("Medication not found: {}", id);
+ return new MedicationNotFoundException("Medication not found with ID: " + id);
+ });
+ }
+
+ /**
+ * Validates that a medication exists.
+ *
+ * @param id The ID of the medication to validate
+ * @throws MedicationNotFoundException if the medication doesn't exist
+ */
+ private void validateMedicationExists(Long id) {
+ if (!medicationRepository.existsById(id)) {
+ logger.error("Medication not found: {}", id);
+ throw new MedicationNotFoundException("Medication not found with ID: " + id);
+ }
+ }
+
+ /**
+ * Deletes intake times associated with a medication.
+ *
+ * @param medicationId The ID of the medication whose intake times to delete
+ */
+ private void deleteIntakeTimes(Long medicationId) {
+ int deletedIntakeTimes = intakeTimeRepository.deleteByMedicationId(medicationId);
+ logger.debug("Deleted {} intake times", deletedIntakeTimes);
+ }
+
+ /**
+ * Processes a medication for schedule generation.
+ *
+ * @param medication The medication to process
+ * @return Stream of MedicationScheduleDTO objects
+ */
+ private Stream processMedicationForSchedule(MedicationModel medication) {
+ logger.trace("Processing medication ID: {}", medication.getId());
+ return medication.getIntakeTimes().stream()
+ .filter(Objects::nonNull)
+ .map(intakeTimeEntity -> createScheduleDTO(medication, intakeTimeEntity));
+ }
+
+ /**
+ * Creates a schedule DTO from medication and intake time.
+ *
+ * @param medication The medication entity
+ * @param intakeTimeEntity The intake time entity
+ * @return MedicationScheduleDTO
+ */
+ private MedicationScheduleDTO createScheduleDTO(MedicationModel medication, MedicationIntakeTime intakeTimeEntity) {
+ if (intakeTimeEntity == null) {
+ throw new IllegalArgumentException("Intake time entity cannot be null");
+ }
+ return createScheduleDTO(medication, intakeTimeEntity.getIntakeTime());
+ }
+
+ /**
+ * Creates a schedule DTO from medication and time.
+ *
+ * @param medication The medication entity
+ * @param intakeTime The intake time
+ * @return MedicationScheduleDTO
+ */
+ private MedicationScheduleDTO createScheduleDTO(MedicationModel medication, LocalTime intakeTime) {
+ if (intakeTime == null) {
+ logger.warn("Null intake time encountered for medication ID: {}", medication.getId());
+ intakeTime = LocalTime.MIDNIGHT;
+ }
+
+ return MedicationScheduleDTO.builder()
+ .medicationId(medication.getId())
+ .medicationName(medication.getName())
+ .dosage(medication.getDosage())
+ .scheduleTime(intakeTime)
+ .isTaken(false)
+ .instructions(medication.getInstructions())
+ .status("UPCOMING")
+ .urgency(convertToDtoUrgency(medication.getUrgency()))
+ .build();
+ }
+
+ /**
+ * Converts medication urgency to DTO format.
+ *
+ * @param modelUrgency The urgency from the model
+ * @return Converted urgency for DTO
+ */
+ private MedicationScheduleDTO.MedicationUrgency convertToDtoUrgency(MedicationModel.MedicationUrgency modelUrgency) {
+ if (modelUrgency == null) {
+ logger.debug("Null urgency encountered, defaulting to ROUTINE");
+ return MedicationScheduleDTO.MedicationUrgency.ROUTINE;
+ }
+ try {
+ return MedicationScheduleDTO.MedicationUrgency.valueOf(modelUrgency.name());
+ } catch (IllegalArgumentException e) {
+ logger.warn("Unknown urgency value: {}, defaulting to ROUTINE", modelUrgency);
+ return MedicationScheduleDTO.MedicationUrgency.ROUTINE;
+ }
+ }
+
+ /**
+ * Updates intake times for a medication.
+ *
+ * @param medication The medication to update
+ * @param newIntakeTimes The new intake times to set
+ */
+ private void updateIntakeTimes(MedicationModel medication, Set newIntakeTimes) {
+ Set currentTimes = medication.getIntakeTimesAsLocalTimes();
+
+ // Remove times that are no longer present
+ currentTimes.stream()
+ .filter(time -> !newIntakeTimes.contains(time))
+ .forEach(medication::removeIntakeTime);
+
+ // Add new times that aren't already present
+ newIntakeTimes.stream()
+ .filter(time -> !currentTimes.contains(time))
+ .forEach(medication::addIntakeTime);
+
+ logger.trace("Intake times updated for medication ID: {}. Total times now: {}",
+ medication.getId(), medication.getIntakeTimes().size());
+ }
+
+ /**
+ * Updates medication fields from DTO.
+ *
+ * @param medication The medication to update
+ * @param dto The DTO containing new values
+ */
+ private void updateMedicationFields(MedicationModel medication, MedicationDTO dto) {
+ logger.trace("Updating fields for medication ID: {}", medication.getId());
+
+ if (dto.getMedicationName() != null) {
+ medication.setName(dto.getMedicationName());
+ } else {
+ logger.warn("Medication name is null in DTO for medication ID: {}", medication.getId());
+ }
+
+ if (dto.getUrgency() != null) {
+ try {
+ medication.setUrgency(MedicationModel.MedicationUrgency.valueOf(dto.getUrgency().name()));
+ } catch (IllegalArgumentException e) {
+ logger.error("Invalid urgency value in DTO: {}. Keeping existing value for medication ID: {}",
+ dto.getUrgency(), medication.getId());
+ }
+ } else {
+ logger.warn("Urgency is null in DTO for medication ID: {}", medication.getId());
+ }
+
+ medication.setDosage(dto.getDosage());
+ medication.setInstructions(dto.getInstructions());
+ }
+
+ /**
+ * Converts a medication entity to DTO.
+ *
+ * @param medication The medication to convert
+ * @return MedicationDTO
+ */
+ private MedicationDTO convertToDto(MedicationModel medication) {
+ if (medication == null) {
+ logger.warn("Attempted to convert null MedicationModel to DTO");
+ return null;
+ }
+
+ logger.trace("Converting medication ID {} to DTO", medication.getId());
+ Set intakeTimes = medication.getIntakeTimesAsLocalTimes();
+
+ try {
+ // Convert days of week if they exist
+ Set daysOfWeek = null;
+ if (medication.getDaysOfWeek() != null && !medication.getDaysOfWeek().isEmpty()) {
+ daysOfWeek = medication.getDaysOfWeek().stream()
+ .map(day -> MedicationDTO.DayOfWeek.valueOf(day.name()))
+ .collect(Collectors.toSet());
+ }
+
+ return MedicationDTO.builder()
+ .id(medication.getId())
+ .userId(medication.getUser() != null ? medication.getUser().getId() : null)
+ .medicationName(medication.getName())
+ .urgency(medication.getUrgency() != null ?
+ MedicationDTO.MedicationUrgency.valueOf(medication.getUrgency().name()) : null)
+ .dosage(medication.getDosage())
+ .instructions(medication.getInstructions())
+ .intakeTimes(intakeTimes)
+ .daysOfWeek(daysOfWeek) // Add this line
+ .build();
+ } catch (IllegalArgumentException e) {
+ logger.error("Failed to convert urgency {} to DTO enum for medication ID: {}",
+ medication.getUrgency(), medication.getId(), e);
+ throw new RuntimeException("Invalid urgency value during DTO conversion", e);
+ }
+ }
+
+ /**
+ * Converts a medication DTO to entity.
+ *
+ * @param dto The DTO to convert
+ * @param user The user who will own the medication
+ * @return MedicationModel
+ */
+ private MedicationModel convertToEntity(MedicationDTO dto, UserModel user) {
+ logger.trace("Converting DTO to medication entity for user ID: {}", user.getId());
+
+ return MedicationModel.builder()
+ .user(user)
+ .name(dto.getMedicationName())
+ .urgency(MedicationModel.MedicationUrgency.valueOf(dto.getUrgency().name()))
+ .dosage(dto.getDosage())
+ .instructions(dto.getInstructions())
+ .build();
+ }
+
+ /**
+ * Maps a medication to a refill reminder DTO.
+ *
+ * @param medication The medication to map
+ * @return RefillReminderDTO
+ */
+ private RefillReminderDTO mapToRefillReminderDTO(MedicationModel medication) {
+ logger.trace("Mapping medication ID {} to refill reminder", medication.getId());
+
+ int remainingDoses = calculateRemainingDoses(medication);
+ logger.trace("Calculated {} remaining doses", remainingDoses);
+
+ return RefillReminderDTO.builder()
+ .medicationId(medication.getId())
+ .medicationName(medication.getName())
+ .remainingDoses(remainingDoses)
+ .refillByDate(LocalDate.now().plusDays(7))
+ .urgency(medication.getUrgency().name())
+ .build();
+ }
+
+ /**
+ * Calculates remaining doses for a medication.
+ *
+ * @param medication The medication to calculate for
+ * @return Estimated number of remaining doses
+ */
+ private int calculateRemainingDoses(MedicationModel medication) {
+ int count = medication.getIntakeTimes().size() * 7; // Assume 1 week supply
+ logger.trace("Calculated remaining doses: {} ({} intake times × 7 days)",
+ count, medication.getIntakeTimes().size());
+ return count;
+ }
+
+ /**
+ * Logs a successful operation.
+ *
+ * @param operation Description of the operation performed
+ * @param identifier Identifier of the affected entity
+ * @param entityType Type of entity affected
+ * @param startTime Start time of the operation (for duration calculation)
+ */
+ private void logOperationSuccess(String operation, Object identifier, String entityType, long startTime) {
+ logger.info("Successfully {} {} {} in {} ms",
+ operation, entityType, identifier, System.currentTimeMillis() - startTime);
+ }
+
+ /**
+ * Logs an operation error.
+ *
+ * @param operation Description of the operation attempted
+ * @param identifier Identifier of the affected entity
+ * @param exception The exception that occurred
+ */
+ private void logOperationError(String operation, String identifier, Exception exception) {
+ logger.error("Error {} {}: {}", operation, identifier, exception.getMessage(), exception);
+ }
+
+
+
+ private void processDaysOfWeek(MedicationDTO medicationDTO, MedicationModel medication) {
+ medicationDTO.getDaysOfWeek().forEach(day ->
+ medication.addDay(MedicationModel.DayOfWeek.valueOf(day.name())));
+ }
+
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/medicationservices/MedicationService.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/medicationservices/MedicationService.java
new file mode 100644
index 000000000..d7dcc21b8
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/medicationservices/MedicationService.java
@@ -0,0 +1,101 @@
+package com.jydoc.deliverable4.services.medicationservices;
+
+import com.jydoc.deliverable4.dtos.MedicationDTO;
+import com.jydoc.deliverable4.dtos.MedicationScheduleDTO;
+import com.jydoc.deliverable4.dtos.RefillReminderDTO;
+import com.jydoc.deliverable4.security.Exceptions.MedicationNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Service interface for medication-related operations.
+ *
+ * Defines the contract for services that manage medication data including CRUD operations,
+ * scheduling, and refill reminders. This interface serves as the main API for all
+ * medication management functionality in the system.
+ *
+ */
+@Service
+public interface MedicationService {
+
+ /**
+ * Retrieves all medications associated with a specific user.
+ *
+ * @param username The username of the patient whose medications to retrieve. Must not be null or empty.
+ * @return A list of {@link MedicationDTO} objects representing the user's medications.
+ * Returns empty list if no medications found.
+ * @throws IllegalArgumentException if username is null or empty
+ */
+ List getUserMedications(String username);
+
+ /**
+ * Retrieves a specific medication by its unique identifier.
+ *
+ * @param id The unique identifier of the medication to retrieve. Must not be null.
+ * @return The {@link MedicationDTO} representing the requested medication
+ * @throws IllegalArgumentException if id is null
+ * @throws com.jydoc.deliverable4.security.Exceptions.MedicationNotFoundException if medication with specified id doesn't exist
+ */
+ MedicationDTO getMedicationById(Long id);
+
+ /**
+ * Creates a new medication record for the specified user.
+ *
+ * @param medicationDTO The medication data to create. Must not be null and must contain valid data.
+ * @param username The username of the patient who will own this medication. Must not be null or empty.
+ * @return The created {@link MedicationDTO} with generated fields populated (e.g., id)
+ * @throws IllegalArgumentException if either parameter is null or contains invalid data
+ */
+ MedicationDTO createMedication(MedicationDTO medicationDTO, String username);
+
+ /**
+ * Updates an existing medication record.
+ *
+ * @param id The unique identifier of the medication to update. Must not be null.
+ * @param medicationDTO The updated medication data. Must not be null and must contain valid data.
+ * @return The updated {@link MedicationDTO}
+ * @throws IllegalArgumentException if either parameter is null or contains invalid data
+ * @throws com.jydoc.deliverable4.security.Exceptions.MedicationNotFoundException if medication with specified id doesn't exist
+ */
+ MedicationDTO updateMedication(Long id, MedicationDTO medicationDTO);
+
+ /**
+ * Deletes a medication record.
+ *
+ * @param id The unique identifier of the medication to delete. Must not be null.
+ * @throws IllegalArgumentException if id is null
+ * @throws com.jydoc.deliverable4.security.Exceptions.MedicationNotFoundException if medication with specified id doesn't exist
+ */
+ void deleteMedication(Long id);
+
+ /**
+ * Retrieves the days of medication intake for a specific medication.
+ *
+ * @param medicationId The ID of the medication to retrieve intake days for
+ * @return Set of days of the week when the medication should be taken
+ * @throws MedicationNotFoundException if no medication exists with the specified ID
+ */
+ Set getMedicationIntakeDays(Long medicationId);
+
+ /**
+ * Retrieves the medication schedule for a specific user.
+ *
+ * @param username The username of the patient whose schedule to retrieve. Must not be null or empty.
+ * @return A list of {@link MedicationScheduleDTO} objects representing the user's medication schedule.
+ * Returns empty list if no scheduled medications found.
+ * @throws IllegalArgumentException if username is null or empty
+ */
+ List getMedicationSchedule(String username);
+
+ /**
+ * Retrieves upcoming medication refill reminders for a specific user.
+ *
+ * @param username The username of the patient whose refill reminders to retrieve. Must not be null or empty.
+ * @return A list of {@link RefillReminderDTO} objects representing upcoming refills.
+ * Returns empty list if no upcoming refills found.
+ * @throws IllegalArgumentException if username is null or empty
+ */
+ List getUpcomingRefills(String username);
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/DashboardService.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/DashboardService.java
new file mode 100644
index 000000000..4b1d52dee
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/DashboardService.java
@@ -0,0 +1,48 @@
+package com.jydoc.deliverable4.services.userservices;
+
+import com.jydoc.deliverable4.dtos.userdtos.DashboardDTO;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * Service interface for dashboard-related operations.
+ *
+ * Defines the contract for services that provide dashboard data and functionality
+ * to the presentation layer. Implementations should handle the retrieval and
+ * processing of user-specific dashboard information.
+ *
+ */
+public interface DashboardService {
+
+ /**
+ * Retrieves comprehensive dashboard data for the authenticated user.
+ *
+ * The implementation should gather all necessary information to populate
+ * the user's dashboard view, including:
+ *
+ *
User information and profile data
+ *
Health conditions and status
+ *
Medication information and alerts
+ *
Upcoming medication schedules
+ *
+ *
+ *
+ * @param userDetails The authenticated user's details, containing security
+ * and identification information. Must not be null.
+ * @return A fully populated {@link DashboardDTO} containing all dashboard data
+ * @throws IllegalArgumentException if userDetails parameter is null
+ */
+ DashboardDTO getUserDashboardData(UserDetails userDetails);
+
+ /**
+ * Checks whether the user has any medications associated with their account.
+ *
+ * This information is typically used to determine whether to display
+ * medication-related UI components or reminders to add medications.
+ *
+ *
+ * @param userDetails The authenticated user's details. Must not be null.
+ * @return true if the user has one or more medications, false otherwise
+ * @throws IllegalArgumentException if userDetails parameter is null
+ */
+ boolean hasMedications(UserDetails userDetails);
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/UserService.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/UserService.java
new file mode 100644
index 000000000..7f62cd65f
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/UserService.java
@@ -0,0 +1,380 @@
+package com.jydoc.deliverable4.services.userservices;
+
+import com.jydoc.deliverable4.dtos.userdtos.UserDTO;
+import com.jydoc.deliverable4.model.UserModel;
+import com.jydoc.deliverable4.repositories.userrepositories.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Service handling comprehensive user management operations including:
+ * - User retrieval by various criteria (ID, username, email)
+ * - User existence verification and counting
+ * - User profile updates and account deletions
+ * - Password management and verification
+ * - Conversion between entity and DTO representations
+ *
+ *
All operations are transactional with appropriate read-only or read-write semantics.
+ * Security-sensitive operations include proper validation and password handling.
+ *
+ *
This service provides a clean API for user-related operations while abstracting
+ * repository access and ensuring proper transaction management and audit logging.
+ *
+ *
Logging is implemented at different levels:
+ * - TRACE: Detailed flow tracing
+ * - DEBUG: Important variable states and minor events
+ * - INFO: Significant business operations
+ * - WARN: Unexpected but handled situations
+ * - ERROR: Critical failures
+ */
+@Service
+@RequiredArgsConstructor
+public class UserService {
+ private static final Logger logger = LogManager.getLogger(UserService.class);
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ /* ====================== User Retrieval Methods ====================== */
+
+ /**
+ * Finds an active user by username or email.
+ *
+ * @param usernameOrEmail The username or email to search for (case-insensitive)
+ * @return Optional containing the user if found and active, empty otherwise
+ * @throws IllegalArgumentException if input is null or empty after trimming
+ */
+ @Transactional(readOnly = true)
+ public Optional findActiveUser(String usernameOrEmail) {
+ logger.trace("Entering findActiveUser with parameter: {}", usernameOrEmail);
+
+ if (usernameOrEmail == null || usernameOrEmail.trim().isEmpty()) {
+ logger.warn("Attempted to find user with null/empty usernameOrEmail");
+ throw new IllegalArgumentException("Username or email cannot be null or empty");
+ }
+
+ String normalizedInput = usernameOrEmail.trim().toLowerCase();
+ logger.debug("Searching for active user with normalized input: {}", normalizedInput);
+
+ Optional user = userRepository.findByUsernameOrEmail(normalizedInput)
+ .filter(UserModel::isEnabled);
+
+ if (user.isPresent()) {
+ logger.debug("Found active user: {}", user.get().getUsername());
+ } else {
+ logger.debug("No active user found for: {}", normalizedInput);
+ }
+
+ return user;
+ }
+
+ /**
+ * Finds a user by username (exact match, case-sensitive).
+ *
+ * @param username The username to search for
+ * @return The found user entity
+ * @throws IllegalArgumentException if user is not found
+ */
+ @Transactional(readOnly = true)
+ public UserModel findByUsername(String username) {
+ logger.trace("Entering findByUsername with parameter: {}", username);
+
+ if (username == null || username.trim().isEmpty()) {
+ logger.warn("Attempted to find user with null/empty username");
+ throw new IllegalArgumentException("Username cannot be null or empty");
+ }
+
+ logger.debug("Searching for user by username: {}", username);
+ return userRepository.findByUsername(username)
+ .orElseThrow(() -> {
+ logger.warn("User not found with username: {}", username);
+ return new IllegalArgumentException("User not found");
+ });
+ }
+
+ /**
+ * Retrieves a user by their unique ID.
+ *
+ * @param id The user ID to search for
+ * @return Optional containing the user if found, empty otherwise
+ */
+ @Transactional(readOnly = true)
+ public Optional getUserById(Long id) {
+ logger.trace("Entering getUserById with parameter: {}", id);
+
+ if (id == null || id <= 0) {
+ logger.warn("Attempted to get user with invalid ID: {}", id);
+ return Optional.empty();
+ }
+
+ logger.debug("Retrieving user by ID: {}", id);
+ return userRepository.findById(id);
+ }
+
+ /**
+ * Retrieves all users in the system.
+ *
+ * @return List of all user entities
+ */
+ @Transactional(readOnly = true)
+ public List getAllUsers() {
+ logger.trace("Entering getAllUsers");
+ logger.debug("Retrieving all users from repository");
+
+ List users = userRepository.findAll();
+ logger.info("Retrieved {} users from database", users.size());
+
+ return users;
+ }
+
+ /**
+ * Verifies if the provided password matches the user's current password.
+ *
+ * @param username The username of the user to verify
+ * @param currentPassword The password to verify
+ * @return true if passwords match, false otherwise
+ * @throws IllegalArgumentException if user is not found
+ */
+ @Transactional(readOnly = true)
+ public boolean verifyCurrentPassword(String username, String currentPassword) {
+ logger.trace("Entering verifyCurrentPassword for user: {}", username);
+
+ if (currentPassword == null || currentPassword.isEmpty()) {
+ logger.warn("Attempted password verification with empty password for user: {}", username);
+ return false;
+ }
+
+ UserModel user = findByUsername(username);
+ boolean matches = passwordEncoder.matches(currentPassword, user.getPassword());
+
+ logger.debug("Password verification {} for user: {}",
+ matches ? "succeeded" : "failed", username);
+
+ return matches;
+ }
+
+ /* ====================== User Status Methods ====================== */
+
+ /**
+ * Checks if a user exists with the given ID.
+ *
+ * @param id The user ID to check
+ * @return true if user exists, false otherwise
+ */
+ @Transactional(readOnly = true)
+ public boolean existsById(Long id) {
+ logger.trace("Entering existsById with parameter: {}", id);
+
+ if (id == null || id <= 0) {
+ logger.debug("Exists check for invalid ID: {}", id);
+ return false;
+ }
+
+ boolean exists = userRepository.existsById(id);
+ logger.debug("User existence check for ID {}: {}", id, exists);
+
+ return exists;
+ }
+
+ /**
+ * Gets the total count of users in the system.
+ *
+ * @return The number of users
+ */
+ @Transactional(readOnly = true)
+ public long getUserCount() {
+ logger.trace("Entering getUserCount");
+
+ long count = userRepository.count();
+ logger.info("System currently contains {} users", count);
+
+ return count;
+ }
+
+ /* ====================== User Modification Methods ====================== */
+
+ /**
+ * Updates a user entity in the database.
+ *
+ * @param user The user entity to update
+ * @throws IllegalArgumentException if user is null
+ */
+ @Transactional
+ public void updateUser(UserModel user) {
+ logger.trace("Entering updateUser");
+
+ if (user == null) {
+ logger.error("Attempted to update null user");
+ throw new IllegalArgumentException("User cannot be null");
+ }
+
+ logger.debug("Updating user with ID: {}", user.getId());
+ userRepository.save(user);
+ logger.info("Successfully updated user with ID: {}", user.getId());
+ }
+
+ /**
+ * Deletes a user by their ID.
+ *
+ * @param id The ID of the user to delete
+ * @throws IllegalArgumentException if user is not found
+ */
+ @Transactional
+ public void deleteUser(Long id) {
+ logger.trace("Entering deleteUser with parameter: {}", id);
+
+ if (!userRepository.existsById(id)) {
+ logger.error("Attempted to delete non-existent user with ID: {}", id);
+ throw new IllegalArgumentException("User not found with ID: " + id);
+ }
+
+ logger.debug("Deleting user with ID: {}", id);
+ userRepository.deleteById(id);
+ logger.info("Successfully deleted user with ID: {}", id);
+ }
+
+ /* ====================== Profile Management Methods ====================== */
+
+ /**
+ * Retrieves a user's profile data as a DTO by username.
+ *
+ * @param username The username to search for
+ * @return UserDTO containing profile information
+ * @throws IllegalArgumentException if user is not found
+ */
+ @Transactional(readOnly = true)
+ public UserDTO getUserByUsername(String username) {
+ logger.trace("Entering getUserByUsername with parameter: {}", username);
+
+ UserModel user = findByUsername(username);
+ UserDTO dto = convertToDTO(user);
+
+ logger.debug("Returning DTO for user: {}", username);
+ return dto;
+ }
+
+ /**
+ * Updates a user's profile information.
+ *
+ * @param username The username of the user to update
+ * @param userDTO The DTO containing updated profile data
+ * @return The updated user DTO
+ * @throws IllegalArgumentException if DTO is null or email is already in use
+ */
+ @Transactional
+ public UserDTO updateUserProfile(String username, UserDTO userDTO) {
+ logger.trace("Entering updateUserProfile for user: {}", username);
+
+ Objects.requireNonNull(userDTO, "UserDTO cannot be null");
+ logger.debug("Updating profile for user: {} with data: {}", username, userDTO);
+
+ UserModel user = findByUsername(username);
+
+ // Verify email uniqueness if changing email
+ if (!user.getEmail().equalsIgnoreCase(userDTO.getEmail())) {
+ logger.debug("Email change detected for user: {}", username);
+ if (userRepository.existsByEmail(userDTO.getEmail())) {
+ logger.warn("Email already in use: {}", userDTO.getEmail());
+ throw new IllegalArgumentException("Email already in use");
+ }
+ }
+
+ // Update allowed fields
+ user.setFirstName(userDTO.getFirstName().trim());
+ user.setLastName(userDTO.getLastName().trim());
+ user.setEmail(userDTO.getEmail().trim().toLowerCase());
+
+ UserModel updatedUser = userRepository.save(user);
+ logger.info("Successfully updated profile for user: {}", username);
+
+ return convertToDTO(updatedUser);
+ }
+
+ /**
+ * Changes a user's password after verifying the current password.
+ *
+ * @param username The username of the user
+ * @param currentPassword The current password for verification
+ * @param newPassword The new password to set
+ * @return true if password was changed successfully, false if verification failed
+ */
+ @Transactional
+ public boolean changePassword(String username, String currentPassword, String newPassword) {
+ logger.trace("Entering changePassword for user: {}", username);
+
+ UserModel user = findByUsername(username);
+
+ // Verify current password
+ if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
+ logger.warn("Password change failed for user: {} - current password mismatch", username);
+ return false;
+ }
+
+ // Set new password
+ user.setPassword(passwordEncoder.encode(newPassword));
+ userRepository.save(user);
+ logger.info("Password successfully changed for user: {}", username);
+
+ return true;
+ }
+
+ /**
+ * Deletes a user account after password verification.
+ *
+ * @param username The username of the account to delete
+ * @param password The password for verification
+ * @return true if account was deleted, false if verification failed
+ */
+ @Transactional
+ public boolean deleteAccount(String username, String password) {
+ logger.trace("Entering deleteAccount for user: {}", username);
+
+ UserModel user = findByUsername(username);
+
+ // Verify password before deletion
+ if (!passwordEncoder.matches(password, user.getPassword())) {
+ logger.warn("Account deletion failed for user: {} - password verification failed", username);
+ return false;
+ }
+
+ userRepository.delete(user);
+ logger.info("Account successfully deleted for user: {}", username);
+
+ return true;
+ }
+
+ /* ====================== Helper Methods ====================== */
+
+ /**
+ * Converts a UserModel entity to a UserDTO.
+ *
+ * @param user The user entity to convert
+ * @return The converted DTO
+ * @throws IllegalArgumentException if user is null
+ */
+ private UserDTO convertToDTO(UserModel user) {
+ logger.trace("Entering convertToDTO for user: {}", user != null ? user.getUsername() : "null");
+
+ if (user == null) {
+ logger.error("Attempted to convert null user to DTO");
+ throw new IllegalArgumentException("User cannot be null");
+ }
+
+ UserDTO dto = new UserDTO();
+ dto.setUsername(user.getUsername());
+ dto.setEmail(user.getEmail());
+ dto.setFirstName(user.getFirstName());
+ dto.setLastName(user.getLastName());
+
+ logger.debug("Converted user {} to DTO", user.getUsername());
+ return dto;
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/UserValidationHelper.java b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/UserValidationHelper.java
new file mode 100644
index 000000000..f7e47589f
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/java/com/jydoc/deliverable4/services/userservices/UserValidationHelper.java
@@ -0,0 +1,90 @@
+package com.jydoc.deliverable4.services.userservices;
+
+import com.jydoc.deliverable4.dtos.userdtos.UserDTO;
+import com.jydoc.deliverable4.security.Exceptions.EmailExistsException;
+import com.jydoc.deliverable4.security.Exceptions.UsernameExistsException;
+import com.jydoc.deliverable4.repositories.userrepositories.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Service component responsible for validating user registration data.
+ *
+ * Performs checks for duplicate usernames and email addresses before user registration.
+ * All validation methods are transactional and read-only to ensure data consistency.
+ */
+@Service
+@RequiredArgsConstructor
+public class UserValidationHelper {
+
+ private final UserRepository userRepository;
+
+ /**
+ * Validates user registration data for potential conflicts.
+ *
+ * @param userDto the user data transfer object containing registration information
+ * @throws IllegalArgumentException if the provided user data is null
+ * @throws UsernameExistsException if the username is already registered
+ * @throws EmailExistsException if the email address is already registered
+ */
+ @Transactional(readOnly = true)
+ public void validateUserRegistration(UserDTO userDto) {
+ if (userDto == null) {
+ throw new IllegalArgumentException("User data cannot be null");
+ }
+
+ validateUsername(userDto.getUsername());
+ validateEmail(userDto.getEmail());
+ }
+
+ /**
+ * Checks if a username already exists in the system.
+ *
+ * @param username the username to check (will be trimmed before checking)
+ * @throws UsernameExistsException if the username is already taken
+ */
+ @Transactional(readOnly = true)
+ public void validateUsername(String username) {
+ String normalizedUsername = username.trim();
+ if (existsByUsername(normalizedUsername)) {
+ throw new UsernameExistsException(normalizedUsername);
+ }
+ }
+
+ /**
+ * Checks if an email address already exists in the system.
+ *
+ * @param email the email to check (will be normalized to lowercase and trimmed)
+ * @throws EmailExistsException if the email is already registered
+ */
+ @Transactional(readOnly = true)
+ public void validateEmail(String email) {
+ String normalizedEmail = email.trim().toLowerCase();
+ if (existsByEmail(normalizedEmail)) {
+ throw new EmailExistsException(normalizedEmail);
+ }
+ }
+
+ /**
+ * Checks if a username exists in the repository.
+ *
+ * @param username the username to check
+ * @return true if the username exists, false otherwise
+ */
+ @Transactional(readOnly = true)
+ public boolean existsByUsername(String username) {
+ return userRepository.existsByUsername(username);
+ }
+
+ /**
+ * Checks if an email exists in the repository.
+ *
+ * @param email the email to check
+ * @return true if the email exists, false otherwise
+ */
+ @Transactional(readOnly = true)
+ public boolean existsByEmail(String email) {
+ return userRepository.existsByEmail(email);
+ }
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/resources/application-test.properties b/Sprint 2/prototype2/src/main/resources/application-test.properties
new file mode 100644
index 000000000..50a306302
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/resources/application-test.properties
@@ -0,0 +1,55 @@
+# ======================
+# Test Metadata
+# ======================
+spring.application.name=prototype2-test
+spring.main.allow-bean-definition-overriding=true
+# ======================
+# Security Test Configuration
+# ======================
+# Disable security auto-configuration for tests
+spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
+
+# ======================
+# Test Database Configuration (H2)
+# ======================
+spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MYSQL
+spring.datasource.username=sa
+spring.datasource.password=
+spring.datasource.driver-class-name=org.h2.Driver
+
+# Hikari connection pool for tests
+spring.datasource.hikari.maximum-pool-size=5
+spring.datasource.hikari.connection-timeout=20000
+
+# ======================
+# Test JPA/Hibernate Settings
+# ======================
+spring.jpa.hibernate.ddl-auto=create-drop
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
+spring.jpa.defer-datasource-initialization=true
+
+# ======================
+# Test Session Management
+# ======================
+spring.session.jdbc.initialize-schema=always
+spring.session.jdbc.table-name=SPRING_SESSION
+server.servlet.session.timeout=1m
+
+# ======================
+# Test Logging Configuration
+# ======================
+logging.level.com.jydoc=DEBUG
+logging.level.org.springframework.security=DEBUG
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
+logging.level.org.springframework.jdbc.core=DEBUG
+
+# Disable banner for cleaner test output
+spring.main.banner-mode=off
+# Enable H2 console for test debugging
+spring.h2.console.enabled=true
+spring.h2.console.path=/h2-console
+
+
diff --git a/Sprint 2/prototype2/src/main/resources/application.properties b/Sprint 2/prototype2/src/main/resources/application.properties
new file mode 100644
index 000000000..062a3d939
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/resources/application.properties
@@ -0,0 +1,55 @@
+# ======================
+# Application Metadata
+# ======================
+spring.application.name=prototype2
+
+# ======================
+# Database Configuration
+# ======================
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
+spring.datasource.url=${SPRING_DATASOURCE_URL}
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+
+# Connection pool settings
+spring.datasource.hikari.maximum-pool-size=10
+spring.datasource.hikari.connection-timeout=30000
+
+# ======================
+# JPA/Hibernate Settings
+# ======================
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
+
+
+
+# ======================
+# Logging Configuration
+# ======================
+
+logging.level.org.springframework.security=INFO
+logging.level.root=DEBUG
+logging.level.org.hibernate.SQL=INFO
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=INFO
+logging.level.org.springframework.jdbc.core=INFO
+# Thymeleaf
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+# Additional logging
+logging.level.com.jydoc.deliverable4.services.impl.=TRACE
+logging.level.com.jydoc.deliverable4.controllers=INFO
+logging.level.org.springframework.web=INFO
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+logging.file.name=application.log
+logging.logback.rollingpolicy.max-file-size=10MB
+logging.logback.rollingpolicy.max-history=2
+spring.sql.init.mode=always
+spring.jpa.hibernate.ddl-auto=update
+
+spring.thymeleaf.cache=false
+
+logging.level.org.thymeleaf=INFO
+spring.datasource.hikari.transaction-isolation=TRANSACTION_READ_COMMITTED
+
+spring.thymeleaf.check-template-location=true
diff --git a/Sprint 2/prototype2/src/main/resources/static/css/styles.css b/Sprint 2/prototype2/src/main/resources/static/css/styles.css
new file mode 100644
index 000000000..ff30abe6f
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/resources/static/css/styles.css
@@ -0,0 +1,107 @@
+body {
+ font-family: Arial, sans-serif;
+ line-height: 1.6;
+ margin: 0;
+ padding: 20px;
+}
+
+.error {
+ color: red;
+}
+
+.success {
+ color: green;
+}
+
+form {
+ max-width: 400px;
+ margin: 20px 0;
+}
+
+form div {
+ margin-bottom: 10px;
+}
+
+label {
+ display: inline-block;
+ width: 100px;
+}
+
+/* Logout Page Styles */
+.logout-container {
+ max-width: 500px;
+ margin: 2rem auto;
+ padding: 2rem;
+ text-align: center;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+}
+
+.btn-logout {
+ background-color: #dc3545;
+ color: white;
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-right: 1rem;
+}
+
+.btn-cancel {
+ padding: 0.5rem 1rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ text-decoration: none;
+ color: #333;
+}
+
+.btn-logout:hover {
+ background-color: #c82333;
+}
+
+.btn-cancel:hover {
+ background-color: #f8f9fa;
+}
+
+/* Custom styles */
+body {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+main {
+ flex: 1;
+}
+
+.btn-lg {
+ min-width: 180px;
+}
+
+.shadow-sm {
+ box-shadow: 0 .125rem .25rem rgba(0,0,0,.075) !important;
+}
+
+/* Animation for buttons */
+.btn {
+ transition: all 0.3s ease;
+}
+
+.btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+}
+
+/* Footer styling */
+footer {
+ box-shadow: 0 -2px 5px rgba(0,0,0,0.05);
+}
+
+footer a {
+ transition: color 0.2s ease;
+}
+
+footer a:hover {
+ color: #0d6efd !important;
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/resources/static/js/script.js b/Sprint 2/prototype2/src/main/resources/static/js/script.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/Sprint 2/prototype2/src/main/resources/templates/access-denied.html b/Sprint 2/prototype2/src/main/resources/templates/access-denied.html
new file mode 100644
index 000000000..244c4d386
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/resources/templates/access-denied.html
@@ -0,0 +1,43 @@
+
+
+
Everything you need to manage your medications effectively and improve your health outcomes.
+
+
+
+
+
+
+
+
+
Medication Tracking
+
Keep track of all your medications, dosages, and schedules in one convenient place.
+
+
+
+
+
+
+
+
Dose Reminders
+
Never miss a dose with customizable reminders via email, SMS, or push notifications.
+
+
+
+
+
+
+
+
Health Metrics
+
Track vital signs, symptoms, and other health metrics alongside your medication regimen.
+
+
+
+
+
+
+
+
Interaction Alerts
+
Get warnings about potential drug interactions and side effects.
+
+
+
+
+
+
+
+
Refill Management
+
Receive alerts when it's time to refill prescriptions and track pharmacy information.
+
+
+
+
+
+
+
+
Progress Reports
+
Generate detailed reports to share with your healthcare providers.
+
+
+
+
+
+
+
+
+
+
+
How HealthTrack Works
+
Simple steps to better medication management.
+
+
+
+
+
+
+ 1
+
+
Create Account
+
Sign up for free in just a few seconds.
+
+
+
+
+
+ 2
+
+
Add Medications
+
Enter your medications and dosages.
+
+
+
+
+
+ 3
+
+
Set Reminders
+
Configure your preferred notification methods.
+
+
+
+
+
+ 4
+
+
Track Progress
+
Monitor your adherence and health metrics.
+
+
+
+
+
+
+
+
+
+
+
What Our Users Say
+
Hear from people who have transformed their medication management.
+
+
+
+
+
+
+
Sarah J.
+
+
+
+
+
+
+
+
"HealthTrack has completely changed how I manage my medications. The reminders ensure I never miss a dose, and the interaction alerts have been lifesavers!"
+
+
+
+
+
+
Michael T.
+
+
+
+
+
+
+
+
"As someone with multiple chronic conditions, keeping track of my medications was overwhelming. HealthTrack simplified everything and gave me peace of mind."
+
+
+
+
+
+
Patricia L.
+
+
+
+
+
+
+
+
"I use HealthTrack to manage my elderly mother's medications. The caregiver features make it easy to stay on top of her complex regimen from anywhere."
+
+
+
+
+
+
+
+
+
+
Ready to Take Control of Your Health?
+
Join thousands of users who are managing their medications more effectively with HealthTrack.
Find answers to common questions about HealthTrack.
+
+
+
+
+
+
+
+
+ Is HealthTrack free to use?
+
+
+
+
+
Yes! HealthTrack offers a free plan with all the essential features for medication management. We also offer premium plans with additional features for those who need them.
+
+
+
+
+
+
+ How does HealthTrack protect my health data?
+
+
+
+
+
We take your privacy and security very seriously. All data is encrypted both in transit and at rest. We comply with healthcare data protection regulations and never share your information without your consent.
+
+
+
+
+
+
+ Can I share my medication information with my doctor?
+
+
+
+
+
Absolutely! HealthTrack allows you to generate comprehensive reports that you can share with your healthcare providers. You can also grant temporary access to specific providers if needed.
+
+
+
+
+
+
+ What if I need help setting up my medications?
+
+
+
+
+
Our support team is available 24/7 to help you get started. We also have detailed guides and video tutorials to walk you through the process step by step.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Sprint 2/prototype2/src/main/resources/templates/user/dashboard.html b/Sprint 2/prototype2/src/main/resources/templates/user/dashboard.html
new file mode 100644
index 000000000..762b57adf
--- /dev/null
+++ b/Sprint 2/prototype2/src/main/resources/templates/user/dashboard.html
@@ -0,0 +1,449 @@
+
+
+
+
+
+ HealthTrack - Medication Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Medication Setup Required
+
+
+
+
+
+
+
+
Let's Get Started!
+
You haven't added any medications yet. Setting up your medication profile will enable: