From e1686fcac43051f2bc88b8b4a9492153d636117c Mon Sep 17 00:00:00 2001 From: Andrii Solianyk Date: Mon, 27 Apr 2026 15:15:21 +0200 Subject: [PATCH] initial --- .idea/.gitignore | 10 + .idea/compiler.xml | 19 ++ .idea/dataSources.xml | 17 ++ .idea/encodings.xml | 6 + .idea/jarRepositories.xml | 20 ++ .idea/misc.xml | 14 + .idea/vcs.xml | 6 + .mvn/wrapper/maven-wrapper.properties | 19 ++ listhub_backend.iml | 8 + mvnw | 259 ++++++++++++++++++ pom.xml | 136 +++++++++ .../ArtisanConnectBackendApplication.java | 27 ++ .../config/AppConfig.java | 15 + .../config/CustomErrorController.java | 35 +++ .../config/SecurityConfig.java | 61 +++++ .../controller/AuthController.java | 109 ++++++++ .../controller/ClientController.java | 63 +++++ .../controller/ImageController.java | 95 +++++++ .../controller/NoticeController.java | 106 +++++++ .../controller/VariablesController.java | 33 +++ .../controller/WishlistController.java | 62 +++++ .../ClientAlreadyExistsException.java | 7 + .../WrongLoginPasswordException.java | 7 + .../dto/AttributeDto.java | 11 + .../dto/AuthRequestDTO.java | 10 + .../dto/AuthResponseDTO.java | 12 + .../dto/CategoriesDTO.java | 16 ++ .../artisanconnectbackend/dto/ClientDTO.java | 24 ++ .../dto/ClientRegistrationDTO.java | 16 ++ .../dto/GoogleAuthRequestDTO.java | 9 + .../dto/NoticeAdditionDTO.java | 19 ++ .../dto/NoticeBoostDTO.java | 9 + .../dto/NoticeRequestDTO.java | 27 ++ .../dto/NoticeResponseDTO.java | 33 +++ .../dto/RequestResponseDTO.java | 17 ++ .../dto/WishlistDTO.java | 12 + .../entities/AttributeValues.java | 20 ++ .../entities/Attributes.java | 21 ++ .../entities/AttributesNotice.java | 20 ++ .../entities/Client.java | 43 +++ .../artisanconnectbackend/entities/Image.java | 23 ++ .../entities/Notice.java | 40 +++ .../artisanconnectbackend/entities/Role.java | 19 ++ .../entities/Wishlist.java | 24 ++ .../repository/AttributeValuesRepository.java | 14 + .../AttributesNoticeRepository.java | 9 + .../repository/AttributesRepository.java | 12 + .../repository/ClientRepository.java | 8 + .../repository/ImageRepository.java | 14 + .../repository/NoticeRepository.java | 9 + .../repository/RolesRepository.java | 10 + .../repository/WishlistRepository.java | 16 ++ .../security/JwtRequestFilter.java | 80 ++++++ .../security/JwtUtil.java | 97 +++++++ .../service/AuthService.java | 116 ++++++++ .../service/ClientService.java | 128 +++++++++ .../service/ImageService.java | 94 +++++++ .../service/NoticeService.java | 204 ++++++++++++++ .../service/WishlistService.java | 66 +++++ .../artisanconnectbackend/utils/Enums.java | 46 ++++ .../artisanconnectbackend/utils/Tools.java | 24 ++ src/main/resources/application.properties | 22 ++ .../resources/application.properties.prod | 20 ++ src/main/resources/sql/data.sql | 61 +++++ 64 files changed, 2609 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/dataSources.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 listhub_backend.iml create mode 100755 mvnw create mode 100644 pom.xml create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/ArtisanConnectBackendApplication.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/controller/ClientController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/controller/WishlistController.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/ClientAlreadyExistsException.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/WrongLoginPasswordException.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/AttributeDto.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/CategoriesDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/GoogleAuthRequestDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeAdditionDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeBoostDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeRequestDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeResponseDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/dto/WishlistDTO.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributeValues.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/Attributes.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributesNotice.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/entities/Wishlist.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributeValuesRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesNoticeRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/NoticeRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/repository/WishlistRepository.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/service/AuthService.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/service/WishlistService.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/utils/Enums.java create mode 100644 src/main/java/_11/asktpk/artisanconnectbackend/utils/Tools.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.properties.prod create mode 100644 src/main/resources/sql/data.sql diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..7e8a669 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..081f4c6 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,17 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7a42f04 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/listhub_backend.iml b/listhub_backend.iml new file mode 100644 index 0000000..9a5cfce --- /dev/null +++ b/listhub_backend.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/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/pom.xml b/pom.xml new file mode 100644 index 0000000..3d94143 --- /dev/null +++ b/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.1.0-RC1 + + + _11.asktpk + ListHubBackend + 1.0.0 + ListHubBackend + ListHubBackend + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + com.squareup.okhttp3 + okhttp + 5.3.2 + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains + annotations + 13.0 + + + jakarta.validation + jakarta.validation-api + 3.1.0 + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + runtime + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + -Xshare:off + + + + + + + diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/ArtisanConnectBackendApplication.java b/src/main/java/_11/asktpk/artisanconnectbackend/ArtisanConnectBackendApplication.java new file mode 100644 index 0000000..7be767e --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/ArtisanConnectBackendApplication.java @@ -0,0 +1,27 @@ +package _11.asktpk.artisanconnectbackend; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; + +@SpringBootApplication +public class ArtisanConnectBackendApplication { + + private final Environment environment; + + public ArtisanConnectBackendApplication(Environment environment) { + this.environment = environment; + } + + static void main(String[] args) { + SpringApplication.run(ArtisanConnectBackendApplication.class, args); + } + + @PostConstruct + public void logDataSourceUrl() { + System.out.println("Datasource URL: " + environment.getProperty("spring.datasource.url")); + } +} + + diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java b/src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java new file mode 100644 index 0000000..9636a39 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java @@ -0,0 +1,15 @@ +package _11.asktpk.artisanconnectbackend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class AppConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java b/src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java new file mode 100644 index 0000000..09feb13 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java @@ -0,0 +1,35 @@ +package _11.asktpk.artisanconnectbackend.config; + +import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO; +import org.springframework.boot.webmvc.error.ErrorController; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +@Controller +public class CustomErrorController implements ErrorController { + + @RequestMapping("/error") + public ResponseEntity handleError(HttpServletRequest request) { + Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + + if (status != null) { + int statusCode = Integer.parseInt(status.toString()); + + if (statusCode == HttpStatus.NOT_FOUND.value()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new RequestResponseDTO("Nie znaleziono zasobu. Sprawdź URL i spróbuj ponownie.")); + } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RequestResponseDTO("Wystąpił wewnętrzny błąd serwera. Spróbuj ponownie później.")); + } + } + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RequestResponseDTO("Wystąpił nieoczekiwany błąd.")); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java b/src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java new file mode 100644 index 0000000..19a2ad9 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java @@ -0,0 +1,61 @@ +package _11.asktpk.artisanconnectbackend.config; + +import _11.asktpk.artisanconnectbackend.security.JwtRequestFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.Collections; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtRequestFilter jwtRequestFilter; + + public SecurityConfig(JwtRequestFilter jwtRequestFilter) { + this.jwtRequestFilter = jwtRequestFilter; + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(Arrays.asList( + "http://localhost:5173", + "http://127.0.0.1:5173" + )); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + config.setAllowedHeaders(Collections.singletonList("*")); + config.setAllowCredentials(true); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/**", "/api/v1/payments/notification").permitAll() + .anyRequest().authenticated()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java new file mode 100644 index 0000000..0b5ba48 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java @@ -0,0 +1,109 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.customExceptions.ClientAlreadyExistsException; +import _11.asktpk.artisanconnectbackend.customExceptions.WrongLoginPasswordException; +import _11.asktpk.artisanconnectbackend.dto.*; +import _11.asktpk.artisanconnectbackend.security.JwtUtil; +import _11.asktpk.artisanconnectbackend.service.AuthService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.HttpClientErrorException; + +@Slf4j +@RestController +@RequestMapping("/api/v1/auth") +public class AuthController { + + private final AuthService authService; + private final JwtUtil jwtUtil; + public AuthController(AuthService authService, JwtUtil jwtUtil) { + this.authService = authService; + this.jwtUtil = jwtUtil; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthRequestDTO authRequestDTO) { + if (authRequestDTO.getEmail() == null || authRequestDTO.getPassword() == null + || authRequestDTO.getEmail().isEmpty() || authRequestDTO.getPassword().isEmpty()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Przekazano puste login lub hasło")); + } + + authRequestDTO.setEmail(authRequestDTO.getEmail().toLowerCase()); + + try { + AuthResponseDTO responseDTO = authService.login(authRequestDTO.getEmail(), authRequestDTO.getPassword()); + + return ResponseEntity.status(HttpStatus.OK) + .body(responseDTO); + + } catch (WrongLoginPasswordException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new RequestResponseDTO(e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO(e.getMessage())); + } + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody ClientRegistrationDTO clientRegistrationDTO) { + if (clientRegistrationDTO.getEmail() == null || clientRegistrationDTO.getPassword() == null + || clientRegistrationDTO.getEmail().isEmpty() || clientRegistrationDTO.getPassword().isEmpty()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Przekazano puste login lub hasło")); + } + + clientRegistrationDTO.setEmail(clientRegistrationDTO.getEmail().toLowerCase()); + + try { + AuthResponseDTO registrationData = authService.register(clientRegistrationDTO.getEmail(), clientRegistrationDTO.getPassword(), clientRegistrationDTO.getFirstName(), clientRegistrationDTO.getLastName()); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(registrationData); + } catch (ClientAlreadyExistsException clientAlreadyExistsException) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(new RequestResponseDTO(clientAlreadyExistsException.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO(e.getMessage())); + } + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + authService.logout(token); + return ResponseEntity.ok(new RequestResponseDTO("Successfully logged out")); + } + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Invalid token")); + } + + @PostMapping("/google") + public ResponseEntity authenticateWithGoogle(@RequestBody GoogleAuthRequestDTO dto) { + if(dto.getGoogleToken() == null){ + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Invalid or empty token")); + } + + try { + AuthResponseDTO response = authService.googleLogin(dto.getGoogleToken()); + return ResponseEntity.status(HttpStatus.OK).body(response); + } catch (HttpClientErrorException httpClientErrorException) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Google access token is invalid or expired")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new RequestResponseDTO("Authentication Error (Google): " + e.getMessage())); + } + } + + @GetMapping("/me") + public ResponseEntity getMe(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + return ResponseEntity.status(HttpStatus.OK).body(new AuthResponseDTO(jwtUtil.extractUserId(token), jwtUtil.extractRole(token), token)); + } + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Invalid or empty token")); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/ClientController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/ClientController.java new file mode 100644 index 0000000..f5ba9a5 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/ClientController.java @@ -0,0 +1,63 @@ +package _11.asktpk.artisanconnectbackend.controller; + + +import _11.asktpk.artisanconnectbackend.service.ClientService; +import _11.asktpk.artisanconnectbackend.dto.ClientDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/clients") +public class ClientController { + private final ClientService clientService; + + public ClientController(ClientService clientService) { + this.clientService = clientService; + } + + @GetMapping("/get/all") + public List getAllClients() { + return clientService.getAllClients(); + } + + @GetMapping("/get/{id}") + public ResponseEntity getClientById(@PathVariable long id) { + if(clientService.getClientById(id) != null) { + return new ResponseEntity<>(clientService.getClientByIdDTO(id), HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @PostMapping("/add") + public ResponseEntity addClient(@RequestBody ClientDTO clientDTO) { + if(clientService.clientExists(clientDTO.getId())) { + return new ResponseEntity<>(HttpStatus.CONFLICT); + } else { + return new ResponseEntity<>(clientService.addClient(clientDTO), HttpStatus.CREATED); + } + } + + // TODO: do zrobienia walidacja danych + @PutMapping("/edit/{id}") + public ResponseEntity updateClient(@PathVariable("id") long id, @RequestBody ClientDTO clientDTO) { + if(clientService.clientExists(id)) { + return new ResponseEntity<>(clientService.updateClient(id, clientDTO),HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @DeleteMapping("/delete/{id}") + public ResponseEntity deleteClient(@PathVariable("id") long id) { + if(clientService.clientExists(id)) { + clientService.deleteClient(id); + return new ResponseEntity<>(HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java new file mode 100644 index 0000000..5f75d6c --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java @@ -0,0 +1,95 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO; +import _11.asktpk.artisanconnectbackend.service.ImageService; +import _11.asktpk.artisanconnectbackend.service.NoticeService; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.data.repository.query.Param; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.*; + +@RestController +@RequestMapping("/api/v1/images") +public class ImageController { + + private final ImageService imageService; + private final NoticeService noticeService; + ImageController(ImageService imageService, NoticeService noticeService) { + this.imageService = imageService; + this.noticeService = noticeService; + } + + @Value("${file.upload-dir}") + private String uploadDir; + + @PostMapping("/upload/{id}") + public ResponseEntity uploadImage(@RequestParam("file") MultipartFile file, @PathVariable("id") Long noticeId, @Param("isMainImage") Boolean isMainImage) { + try { + if(file.isEmpty()) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("File is empty")); + } + + if(!Objects.equals(file.getContentType(), "image/jpeg") && !Objects.equals(file.getContentType(), "image/png")) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("File must be a JPEG or PNG image.")); + } + + if(noticeId == null || !noticeService.noticeExists(noticeId)) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("Notice ID is invalid or does not exist.")); + } + + String newImageName = imageService.saveImageToStorage(uploadDir, file); + imageService.addImageNameToDB(newImageName, noticeId, isMainImage); + + return ResponseEntity.ok(new RequestResponseDTO("Image uploaded successfully with new name: " + newImageName)); + } catch (Exception e) { + System.out.println(e.getMessage()); + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(new RequestResponseDTO(e.getMessage())); + } + } + + @GetMapping("/get/{filename}") + public ResponseEntity getImage(@PathVariable String filename) { + try { + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .body(imageService.getImage(uploadDir, filename)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @GetMapping("/list/{id}") + public ResponseEntity getImagesNamesList(@PathVariable("id") Long noticeId) { + List result; + try { + noticeService.getNoticeById(noticeId); + result = imageService.getImagesList(noticeId); + return ResponseEntity.ok(result); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO(e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new RequestResponseDTO(e.getMessage())); + } + } + + @DeleteMapping("/delete/{filename}") + public ResponseEntity deleteImage(@PathVariable("filename") String filename) { + if(filename == null) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Filename is empty.")); + } + + try { + imageService.deleteImage(uploadDir, filename); + return ResponseEntity.status(HttpStatus.OK).body(new RequestResponseDTO("Image deleted successfully.")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestResponseDTO(e.getMessage())); + } + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java new file mode 100644 index 0000000..d2c8cfc --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java @@ -0,0 +1,106 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.dto.*; +import _11.asktpk.artisanconnectbackend.service.ClientService; +import _11.asktpk.artisanconnectbackend.service.NoticeService; +import _11.asktpk.artisanconnectbackend.utils.Enums; +import _11.asktpk.artisanconnectbackend.utils.Tools; +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RequestMapping("/api/v1/notices") +@RestController +public class NoticeController { + private final NoticeService noticeService; + private final ClientService clientService; + private final Tools tools; + + public NoticeController(NoticeService noticeService, ClientService clientService, Tools tools) { + this.noticeService = noticeService; + this.clientService = clientService; + this.tools = tools; + } + + @GetMapping("/get/all") + public List getAllNotices() { + return noticeService.getAllNotices(); + } + + @GetMapping("/get/{id}") + public ResponseEntity getNoticeById(@PathVariable long id) { + if (noticeService.noticeExists(id)) { + return ResponseEntity.ok(noticeService.getNoticeById(id)); + } else { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping("/add") + public ResponseEntity addNotice(@RequestBody NoticeRequestDTO dto, HttpServletRequest request) { + Long clientId = tools.getClientIdFromRequest(request); + if (!clientService.clientExists(clientId)) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new NoticeAdditionDTO("Nie znaleziono klienta o ID: " + clientId)); + } + + dto.setClientId(clientId); + + if (dto.getCategory() == null || !Arrays.asList(Enums.Category.values()).contains(dto.getCategory())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new NoticeAdditionDTO("Nie ma takiej kategorii")); + } + + Long newNoticeId = noticeService.addNotice(dto); + + return ResponseEntity.status(HttpStatus.CREATED).body(new NoticeAdditionDTO(newNoticeId ,"Dodano ogłoszenie.")); + } + + @PutMapping("/edit/{id}") + public ResponseEntity editNotice(@PathVariable("id") long id, @RequestBody NoticeRequestDTO dto, HttpServletRequest request) { + Long clientIdFromToken = tools.getClientIdFromRequest(request); + if (noticeService.noticeExists(id)) { + if (!noticeService.isNoticeOwnedByClient(id, clientIdFromToken)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponseDTO("Nie masz uprawnień do edycji tego ogłoszenia.")); + } + try { + return ResponseEntity.status(HttpStatus.OK).body(noticeService.updateNotice(id, dto)); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Nie znaleziono ogłoszenia o ID: " + id); + } + } + + @DeleteMapping("/delete/{id}") + public ResponseEntity deleteNotice(@PathVariable("id") long id, HttpServletRequest request) { + Long clientIdFromToken = tools.getClientIdFromRequest(request); + if (noticeService.noticeExists(id)) { + if (!noticeService.isNoticeOwnedByClient(id, clientIdFromToken)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponseDTO("Nie masz uprawnień do usunięcia tego ogłoszenia.")); + } + + noticeService.deleteNotice(id); + return ResponseEntity.status(HttpStatus.OK).body(new RequestResponseDTO("Pomyślnie usunięto ogłoszenie o ID: " + id)); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new RequestResponseDTO("Nie znaleziono ogłoszenia o ID: " + id)); + } + } + + @PostMapping("/boost") + public ResponseEntity boostNotice(@RequestBody NoticeBoostDTO dto, HttpServletRequest request) { + Long clientId = tools.getClientIdFromRequest(request); + if (noticeService.isNoticeOwnedByClient(dto.getNoticeId(), clientId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new RequestResponseDTO("Ogłoszenie nie istnieje lub nie należy do zalogowanego klienta.")); + } + noticeService.boostNotice(dto.getNoticeId()); + + return ResponseEntity.status(HttpStatus.OK).body(new RequestResponseDTO("Ogłoszenie zostało pomyślnie wypromowane.")); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java new file mode 100644 index 0000000..e9767ba --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java @@ -0,0 +1,33 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.dto.CategoriesDTO; +import _11.asktpk.artisanconnectbackend.utils.Enums; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/vars") +public class VariablesController { + @GetMapping("/categories") + public List getAllVariables() { + List categoriesDTOList = new ArrayList<>(); + for (Map.Entry entry : Enums.categoryPL.entrySet()) { + CategoriesDTO categoriesDTO = new CategoriesDTO(); + categoriesDTO.setLabel(entry.getValue()); + categoriesDTO.setValue(entry.getKey().toString()); + categoriesDTOList.add(categoriesDTO); + } + + return categoriesDTOList; + } + + @GetMapping("/statuses") + public List getAllStatuses() { + return List.of(Enums.Status.values()); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/WishlistController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/WishlistController.java new file mode 100644 index 0000000..e5e45fd --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/WishlistController.java @@ -0,0 +1,62 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.dto.NoticeResponseDTO; +import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO; +import _11.asktpk.artisanconnectbackend.service.ClientService; +import _11.asktpk.artisanconnectbackend.service.NoticeService; +import _11.asktpk.artisanconnectbackend.service.WishlistService; +import _11.asktpk.artisanconnectbackend.utils.Tools; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/api/v1/wishlist") +public class WishlistController { + private final WishlistService wishlistService; + private final ClientService clientService; + private final NoticeService noticeService; + private final Tools tools; + + public WishlistController(WishlistService wishlistService, ClientService clientService, NoticeService noticeService, Tools tools) { + this.wishlistService = wishlistService; + this.clientService = clientService; + this.noticeService = noticeService; + this.tools = tools; + } + + @PostMapping("/toggle/{noticeId}") + public ResponseEntity toggleWishlist(@PathVariable Long noticeId, HttpServletRequest request) { + Long clientId = tools.getClientIdFromRequest(request); + NoticeResponseDTO noticeResponseDTO = noticeService.getNoticeById(noticeId); + if (noticeResponseDTO == null) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("Notice not found")); + } + boolean added = wishlistService.toggleWishlist( + clientService.getClientById(clientId), + noticeService.getNoticeByIdEntity(noticeId) + ); + + if (added) { + return ResponseEntity.ok(new RequestResponseDTO("Wishlist entry added")); + } else { + return ResponseEntity.ok(new RequestResponseDTO("Wishlist entry removed")); + } + } + +// @GetMapping("/{clientId}") +// public ResponseEntity> getWishlist(@PathVariable Long clientId) { +// List wishlist = wishlistService.getWishlistForClientId(clientId); +// return ResponseEntity.ok(wishlist); +// } + + @GetMapping("/") + public List getWishlistForClient(HttpServletRequest request) { + Long clientId = tools.getClientIdFromRequest(request); + return wishlistService.getNoticesInWishlist(clientId); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/ClientAlreadyExistsException.java b/src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/ClientAlreadyExistsException.java new file mode 100644 index 0000000..8cb678a --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/ClientAlreadyExistsException.java @@ -0,0 +1,7 @@ +package _11.asktpk.artisanconnectbackend.customExceptions; + +public class ClientAlreadyExistsException extends Exception { + public ClientAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/WrongLoginPasswordException.java b/src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/WrongLoginPasswordException.java new file mode 100644 index 0000000..4b308eb --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/customExceptions/WrongLoginPasswordException.java @@ -0,0 +1,7 @@ +package _11.asktpk.artisanconnectbackend.customExceptions; + +public class WrongLoginPasswordException extends Exception { + public WrongLoginPasswordException(String message) { + super(message); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/AttributeDto.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AttributeDto.java new file mode 100644 index 0000000..dbc69d6 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AttributeDto.java @@ -0,0 +1,11 @@ +package _11.asktpk.artisanconnectbackend.dto; + + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class AttributeDto { + private String name; + private String value; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java new file mode 100644 index 0000000..3c37189 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java @@ -0,0 +1,10 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class AuthRequestDTO { + private String email; + private String password; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java new file mode 100644 index 0000000..5c76d39 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java @@ -0,0 +1,12 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter @AllArgsConstructor +public class AuthResponseDTO { + private Long user_id; + private String user_role; + private String token; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/CategoriesDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/CategoriesDTO.java new file mode 100644 index 0000000..d794617 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/CategoriesDTO.java @@ -0,0 +1,16 @@ +package _11.asktpk.artisanconnectbackend.dto; + +//[ +// { "label": "Meble", "value": "Furniture" }, +// { "label": "Biżuteria", "value": "Jewelry" }, +// { "label": "Ceramika", "value": "Ceramics" } +//] + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class CategoriesDTO { + String label; + String value; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java new file mode 100644 index 0000000..a4c4495 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java @@ -0,0 +1,24 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import jakarta.validation.constraints.Email; + +@Getter @Setter +@AllArgsConstructor +@NoArgsConstructor +public class ClientDTO { + private Long id; + + @Email + @NotBlank + private String email; + private String firstName; + private String lastName; + private String image; + private String role; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java new file mode 100644 index 0000000..4d4cd07 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java @@ -0,0 +1,16 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class ClientRegistrationDTO { + @Email + @NotBlank + private String email; + private String firstName; + private String lastName; + private String password; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/GoogleAuthRequestDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/GoogleAuthRequestDTO.java new file mode 100644 index 0000000..df94256 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/GoogleAuthRequestDTO.java @@ -0,0 +1,9 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class GoogleAuthRequestDTO { + private String googleToken; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeAdditionDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeAdditionDTO.java new file mode 100644 index 0000000..42dd205 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeAdditionDTO.java @@ -0,0 +1,19 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class NoticeAdditionDTO { + public Long noticeId; + public String message; + + public NoticeAdditionDTO(String message) { + this.message = message; + } + + public NoticeAdditionDTO(Long noticeId, String message) { + this.noticeId = noticeId; + this.message = message; + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeBoostDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeBoostDTO.java new file mode 100644 index 0000000..d008dcc --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeBoostDTO.java @@ -0,0 +1,9 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class NoticeBoostDTO { + private Long noticeId; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeRequestDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeRequestDTO.java new file mode 100644 index 0000000..f81e423 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeRequestDTO.java @@ -0,0 +1,27 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import _11.asktpk.artisanconnectbackend.utils.Enums; +import lombok.Getter; +import lombok.Setter; +import java.util.List; + +@Getter @Setter +public class NoticeRequestDTO { + private String title; + + private Long clientId; + + private String description; + + private Double price; + + private Enums.Category category; + + private Enums.Status status; + + private List attributes; + + public NoticeRequestDTO() { + + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeResponseDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeResponseDTO.java new file mode 100644 index 0000000..327cfbd --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeResponseDTO.java @@ -0,0 +1,33 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import _11.asktpk.artisanconnectbackend.utils.Enums; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter @Setter +public class NoticeResponseDTO { + private long noticeId; + + private String title; + + private Long clientId; + + private String description; + + private Double price; + + private Enums.Category category; + + private Enums.Status status; + + private LocalDateTime publishDate; + + private List attributes; + + public NoticeResponseDTO() { + + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java new file mode 100644 index 0000000..845258d --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java @@ -0,0 +1,17 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class RequestResponseDTO { + public String message; + + public RequestResponseDTO(String message) { + this.message = message; + } + + public String toJSON() { + return "{\"message\":\"" + message + "\"}"; + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/WishlistDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/WishlistDTO.java new file mode 100644 index 0000000..669d463 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/WishlistDTO.java @@ -0,0 +1,12 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WishlistDTO { + private Long id; + private Long clientId; + private Long noticeId; +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributeValues.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributeValues.java new file mode 100644 index 0000000..90570ee --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributeValues.java @@ -0,0 +1,20 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "attribute_values") +@Getter @Setter +public class AttributeValues { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "id_attribute") + private Attributes attribute; + + private String value; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Attributes.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Attributes.java new file mode 100644 index 0000000..a6b4c52 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Attributes.java @@ -0,0 +1,21 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import lombok.Setter; +import lombok.Getter; + +import java.util.List; + +@Entity +@Table(name = "attributes") +@Getter @Setter +public class Attributes { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idAttribute; + + private String name; + + @OneToMany(mappedBy = "attribute") + private List attributeValues; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributesNotice.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributesNotice.java new file mode 100644 index 0000000..2558721 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/AttributesNotice.java @@ -0,0 +1,20 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import lombok.Setter; +import lombok.Getter; + +@Entity +@Table(name = "attributes_notice") +@Getter @Setter +public class AttributesNotice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long notice_id; + + @ManyToOne + @JoinColumn(name = "id_value") + private AttributeValues attributeValue; +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java new file mode 100644 index 0000000..124264b --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java @@ -0,0 +1,43 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.util.Date; + +@Entity +@Table(name = "clients") +@Getter @Setter +@NoArgsConstructor +public class Client { + public Client(String email, String password, String firstName, String lastName) { + this.email = email; + this.password = password; + this.firstName = firstName; + this.lastName = lastName; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String email; + + private String password; + + private String firstName; + + private String lastName; + + private String image; + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "role_id", referencedColumnName = "id") + private Role role; + + @CreationTimestamp + private Date createdAt; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java new file mode 100644 index 0000000..b96f120 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java @@ -0,0 +1,23 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import jdk.jfr.BooleanFlag; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "images") +@Getter @Setter +public class Image { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long noticeId; + + private String imageName; + + @BooleanFlag + private boolean isMainImage; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java new file mode 100644 index 0000000..45771ce --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java @@ -0,0 +1,40 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.List; + +import _11.asktpk.artisanconnectbackend.utils.Enums.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "notice") +@Getter @Setter +public class Notice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idNotice; + + private String title; + + @ManyToOne + @JoinColumn(name = "client_id") + private Client client; + + private String description; + + private Double price; + + @Enumerated(EnumType.STRING) + private Category category; + + @Enumerated(EnumType.STRING) + private Status status; + + private LocalDateTime publishDate; + + @OneToMany(mappedBy = "notice_id") + private List attributesNotices; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java new file mode 100644 index 0000000..ec2d8a8 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java @@ -0,0 +1,19 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "roles") +@Getter +@Setter +public class Role { + @Id + private Long id; + @Column(name="rolename") + private String role; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Wishlist.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Wishlist.java new file mode 100644 index 0000000..4def389 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Wishlist.java @@ -0,0 +1,24 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "wishlist") +@Getter +@Setter +public class Wishlist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "client_id", nullable = false) + private Client client; + + @ManyToOne + @JoinColumn(name = "notice_id", nullable = false) + private Notice notice; +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributeValuesRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributeValuesRepository.java new file mode 100644 index 0000000..8e7f330 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributeValuesRepository.java @@ -0,0 +1,14 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.AttributeValues; +import _11.asktpk.artisanconnectbackend.entities.Attributes; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AttributeValuesRepository extends JpaRepository { + + Optional findByAttributeAndValue(Attributes attribute, String value); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesNoticeRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesNoticeRepository.java new file mode 100644 index 0000000..2c9a93d --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesNoticeRepository.java @@ -0,0 +1,9 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.AttributesNotice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AttributesNoticeRepository extends JpaRepository { +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesRepository.java new file mode 100644 index 0000000..2a8bd0e --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/AttributesRepository.java @@ -0,0 +1,12 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.Attributes; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AttributesRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java new file mode 100644 index 0000000..eccd51f --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java @@ -0,0 +1,8 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.Client; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ClientRepository extends JpaRepository { + Client findByEmail(String email); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java new file mode 100644 index 0000000..0cba2cb --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java @@ -0,0 +1,14 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.Image; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ImageRepository extends JpaRepository { + List findByNoticeId(Long noticeId); + + boolean existsImageByImageNameEqualsIgnoreCase(String imageName); + + void deleteByImageNameEquals(String imageName); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/NoticeRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/NoticeRepository.java new file mode 100644 index 0000000..7de86c2 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/NoticeRepository.java @@ -0,0 +1,9 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.Notice; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeRepository extends JpaRepository { + + boolean existsByIdNoticeAndClientId(long noticeId, long clientId); +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java new file mode 100644 index 0000000..a35617b --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java @@ -0,0 +1,10 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import _11.asktpk.artisanconnectbackend.entities.Role; + +@Repository +public interface RolesRepository extends JpaRepository { + Role findRoleByRole(String role); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/WishlistRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/WishlistRepository.java new file mode 100644 index 0000000..1b3fc78 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/WishlistRepository.java @@ -0,0 +1,16 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.Client; +import _11.asktpk.artisanconnectbackend.entities.Notice; +import _11.asktpk.artisanconnectbackend.entities.Wishlist; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface WishlistRepository extends JpaRepository { + + List findAllByClientId(Long clientId); + + Optional findByClientAndNotice(Client client, Notice notice); +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java new file mode 100644 index 0000000..ff66518 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java @@ -0,0 +1,80 @@ +package _11.asktpk.artisanconnectbackend.security; + +import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; + +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + public JwtRequestFilter(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) + throws ServletException, IOException { + + final String authorizationHeader = request.getHeader("Authorization"); + + String email = null; + String jwt = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + + try { + if (jwtUtil.isBlacklisted(jwt) || !jwtUtil.isLatestToken(jwt)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + String jsonResponse = "{\"error\": \"Token is invalid. Please login again.\"}"; + response.getWriter().write(jsonResponse); + return; + } + + + email = jwtUtil.extractEmail(jwt); + } catch (ExpiredJwtException expiredJwtException) { + logger.error(expiredJwtException.getMessage()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write(new RequestResponseDTO("Authentication token is expired. Please login again.").toJSON()); + return; + } catch (Exception e) { + logger.error(e.getMessage()); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write(new RequestResponseDTO(e.getMessage()).toJSON()); + return; + } + } + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + String role = jwtUtil.extractRole(jwt); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + email, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + +// logger.info("Token of user " + jwtUtil.extractEmail(jwt) + (jwtUtil.isTokenExpired(jwt) ? " is expired" : " is not expired")); + + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java new file mode 100644 index 0000000..b65637d --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java @@ -0,0 +1,97 @@ +package _11.asktpk.artisanconnectbackend.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:defaultSecretKeyNeedsToBeAtLeast32BytesLong}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + // sterowanie tokenami wygasnietymi + private final Set blacklistedTokens = ConcurrentHashMap.newKeySet(); + + public void blacklistToken(String token) { + blacklistedTokens.add(token); + } + + public boolean isBlacklisted(String token) { + return blacklistedTokens.contains(token); + } + + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + private final Map userActiveTokens = new ConcurrentHashMap<>(); + + public boolean isLatestToken(String token) { + String email = extractEmail(token); + String tokenId = extractTokenId(token); + String latestTokenId = userActiveTokens.get(email); + + return latestTokenId != null && latestTokenId.equals(tokenId); + } + + public String generateToken(String email, String role, Long userId) { + Map claims = new HashMap<>(); + claims.put("role", role); + claims.put("userId", userId); + claims.put("tokenId", UUID.randomUUID().toString()); + + String token = createToken(claims, email); + + userActiveTokens.put(email, extractTokenId(token)); + + return token; + } + + private String createToken(Map claims, String subject) { + return Jwts.builder().claims(claims).subject(subject).issuedAt(new Date()).expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String extractTokenId(String token) { + return extractAllClaims(token).get("tokenId", String.class); + } + + public String extractEmail(String token) { + return extractClaim(token, Claims::getSubject); + } + + public String extractRole(String token) { + return extractAllClaims(token).get("role", String.class); + } + + public Long extractUserId(String token) { + return extractAllClaims(token).get("userId", Long.class); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/AuthService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/AuthService.java new file mode 100644 index 0000000..ce4891f --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/AuthService.java @@ -0,0 +1,116 @@ +package _11.asktpk.artisanconnectbackend.service; + +import _11.asktpk.artisanconnectbackend.customExceptions.ClientAlreadyExistsException; +import _11.asktpk.artisanconnectbackend.customExceptions.WrongLoginPasswordException; +import _11.asktpk.artisanconnectbackend.dto.AuthResponseDTO; +import _11.asktpk.artisanconnectbackend.dto.ClientDTO; +import _11.asktpk.artisanconnectbackend.entities.Client; +import _11.asktpk.artisanconnectbackend.security.JwtUtil; +import org.springframework.http.*; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Slf4j +@Service +public class AuthService { + + private final ClientService clientService; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + + public AuthService(ClientService clientService, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) { + this.clientService = clientService; + this.jwtUtil = jwtUtil; + this.passwordEncoder = passwordEncoder; + } + + public AuthResponseDTO login(String email, String password) throws Exception { + Client client = clientService.getClientByEmail(email); + if (client == null) { + throw new Exception("Klient o podanym adresie nie istnieje!"); + } + + if (passwordEncoder.matches(password, client.getPassword())) { + String token = jwtUtil.generateToken(client.getEmail(), client.getRole().getRole(), client.getId()); + log.info("User logged in with {}", client.getEmail()); + return new AuthResponseDTO(client.getId(), client.getRole().getRole(), token); + } + throw new WrongLoginPasswordException("Login lub hasło jest niepoprawny!"); + } + + public AuthResponseDTO register(String email, String password, String firstName, String lastName) throws Exception { + if (clientService.getClientByEmail(email) != null) { + throw new ClientAlreadyExistsException("Klient o podanym adresie email już istnieje!"); + } + + Client newClient = new Client(); + newClient.setEmail(email); + newClient.setPassword(passwordEncoder.encode(password)); + newClient.setFirstName(firstName); + newClient.setLastName(lastName); + + ClientDTO savedClient = clientService.registerClient(newClient); + if (savedClient != null) { + log.info("New user registered with {}", savedClient.getEmail()); + String token = jwtUtil.generateToken( + savedClient.getEmail(), + savedClient.getRole(), + savedClient.getId() + ); + + return new AuthResponseDTO(savedClient.getId(), savedClient.getRole(), token); + } else { + throw new Exception("Rejestracja nie powiodła się!"); + } + } + + public void logout(String token) { + jwtUtil.blacklistToken(token); + } + + public AuthResponseDTO googleLogin(String googleAccessToken) throws Exception { + String googleUserInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo"; + + ResponseEntity response; + + + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(googleAccessToken); + RestTemplate restTemplate = new RestTemplate(); + response = restTemplate.exchange( + googleUserInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), Map.class); + + + Map userInfo = response.getBody(); + +// String googleId = (String) userInfo.get("sub"); Potencjalnie możemy używać googlowskiego ID, ale to ma konflikt z naszym generowanym + if (userInfo == null) { + throw new Exception("Pobrany użytkownik jest pusty! Może to być spowodowane niepoprawnym tokenem lub brakiem dostępu do Google API."); + } + String email = (String) userInfo.get("email"); + String name = (String) userInfo.get("name"); + + Client client = clientService.getClientByEmail(email); + if (client == null) { + client = new Client(); + client.setEmail(email); + client.setFirstName(name); + client.setRole(clientService.getUserRole()); // to pobiera po prostu role "USER" z tabeli w bazie + clientService.saveClientToDB(client); + } + + String jwt = jwtUtil.generateToken(client.getEmail(), client.getRole().getRole(), client.getId()); + log.info("User authenticated with google: {}", client.getEmail()); + return new AuthResponseDTO( + client.getId(), + client.getRole().getRole(), + jwt + ); + } + +} + diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java new file mode 100644 index 0000000..722ec6b --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java @@ -0,0 +1,128 @@ +package _11.asktpk.artisanconnectbackend.service; + +import _11.asktpk.artisanconnectbackend.dto.ClientDTO; +import _11.asktpk.artisanconnectbackend.dto.ClientRegistrationDTO; +import _11.asktpk.artisanconnectbackend.entities.Client; +import _11.asktpk.artisanconnectbackend.entities.Role; +import _11.asktpk.artisanconnectbackend.repository.ClientRepository; +import _11.asktpk.artisanconnectbackend.repository.RolesRepository; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ClientService { + private final ClientRepository clientRepository; + private final RolesRepository rolesRepository; + + public ClientService(ClientRepository clientRepository, RolesRepository rolesRepository) { + this.clientRepository = clientRepository; + this.rolesRepository = rolesRepository; + } + + public ClientDTO toDto(Client client) { + if(client == null) { + return null; + } + + ClientDTO dto = new ClientDTO(); + + dto.setId(client.getId()); + dto.setFirstName(client.getFirstName()); + dto.setLastName(client.getLastName()); + dto.setEmail(client.getEmail()); + dto.setRole(client.getRole().getRole()); + dto.setImage(client.getImage()); + + return dto; + } + + public Client fromDto(ClientDTO dto) { + Client client = new Client(); + Role rola; + + if (clientRepository.findById(dto.getId()).isPresent()) { + rola = clientRepository.findById(dto.getId()).get().getRole(); + } else { + rola = new Role(); + rola.setRole("USER"); + } + + client.setId(dto.getId()); + client.setFirstName(dto.getFirstName()); + client.setLastName(dto.getLastName()); + client.setEmail(dto.getEmail()); + client.setRole(rola); + client.setImage(dto.getImage()); + + return client; + } + + private Client fromDto(ClientRegistrationDTO dto) { + Client client = new Client(); + + client.setFirstName(dto.getFirstName()); + client.setLastName(dto.getLastName()); + client.setEmail(dto.getEmail()); + client.setPassword(dto.getPassword()); + return client; + } + + public List getAllClients() { + List clients = clientRepository.findAll(); + return clients.stream().map(this::toDto).toList(); + } + + public Client getClientById(Long id) { + return clientRepository.findById(id).orElse(null); + } + + public ClientDTO getClientByIdDTO(Long id) { + return toDto(clientRepository.findById(id).orElse(null)); + } + + public Client getClientByEmail(String email) { + return clientRepository.findByEmail(email); + } + + public Role getUserRole() { + return rolesRepository.findRoleByRole("USER"); + } + + public boolean clientExists(Long id) { + return clientRepository.existsById(id); + } + + public ClientDTO addClient(ClientDTO clientDTO) { + return toDto(clientRepository.save(fromDto(clientDTO))); + } + + public Client saveClientToDB(Client client) { + return clientRepository.save(client); + } + + public ClientDTO updateClient(long id, ClientDTO clientDTO) { + Client existingClient = clientRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id)); + + Role newRole = rolesRepository.findRoleByRole(clientDTO.getRole()); + + existingClient.setEmail(clientDTO.getEmail()); + existingClient.setFirstName(clientDTO.getFirstName()); + existingClient.setLastName(clientDTO.getLastName()); + existingClient.setImage(clientDTO.getImage()); + existingClient.setRole(newRole); + + return toDto(clientRepository.save(existingClient)); + } + + public void deleteClient(Long id) { + clientRepository.deleteById(id); + } + + public ClientDTO registerClient(Client client) { + client.setRole(getUserRole()); // ID 1 - USER role + return toDto(clientRepository.save(client)); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java new file mode 100644 index 0000000..9a80e32 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java @@ -0,0 +1,94 @@ +package _11.asktpk.artisanconnectbackend.service; + +import _11.asktpk.artisanconnectbackend.entities.Image; +import _11.asktpk.artisanconnectbackend.repository.ImageRepository; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@Transactional +public class ImageService { + private final ImageRepository imageRepository; + + ImageService(ImageRepository imageRepository) { + this.imageRepository = imageRepository; + } + + public String saveImageToStorage(String uploadDirectory, MultipartFile imageFile) throws IOException { + String uniqueFileName = UUID.randomUUID() + imageFile.getOriginalFilename(); + + Path uploadPath = Path.of(uploadDirectory); + Path filePath = uploadPath.resolve(uniqueFileName); + + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + Files.copy(imageFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + return uniqueFileName; + } + + public void addImageNameToDB(String filename, Long noticeId, boolean isMainImage) { + Image image = new Image(); + image.setImageName(filename); + image.setNoticeId(noticeId); + image.setMainImage(isMainImage); + imageRepository.save(image); + } + + public Resource getImage(String imageDirectory, String imageName) throws IOException { + Path filePath = Paths.get(imageDirectory).resolve(imageName); + Resource resource = new UrlResource(filePath.toUri()); + + if(imageName.isEmpty() || imageDirectory.isEmpty()) { + throw new IOException("Filename or folder is empty. Please check your request and try again."); + } + + if (!resource.exists()) { + throw new IOException("File not found"); + } + + return resource; + } + + public void deleteImage(String imageDirectory, String imageName) throws IOException { + Path imagePath = Path.of(imageDirectory, imageName); + + deleteImageRecordFromDB(imageName); + + if (Files.exists(imagePath)) { + Files.delete(imagePath); + } else { + throw new IOException("File not found"); + } + } + + public List getImagesList(Long noticeID) throws Exception { + List images = imageRepository.findByNoticeId(noticeID); + if (images.isEmpty()) { + throw new Exception("There is no images for this notice"); + } + + return images.stream() + .map(Image::getImageName) + .collect(Collectors.toList()); + } + + public void deleteImageRecordFromDB(String imageName) { + if(imageRepository.existsImageByImageNameEqualsIgnoreCase(imageName)) { + imageRepository.deleteByImageNameEquals(imageName); + } + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java new file mode 100644 index 0000000..28c3639 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java @@ -0,0 +1,204 @@ +package _11.asktpk.artisanconnectbackend.service; + +import _11.asktpk.artisanconnectbackend.dto.AttributeDto; +import _11.asktpk.artisanconnectbackend.dto.NoticeRequestDTO; +import _11.asktpk.artisanconnectbackend.entities.*; +import _11.asktpk.artisanconnectbackend.repository.*; +import _11.asktpk.artisanconnectbackend.dto.NoticeResponseDTO; +import jakarta.persistence.EntityNotFoundException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +public class NoticeService { + private static final Logger logger = LogManager.getLogger(NoticeService.class); + + @Value("${file.upload-dir}") + private String uploadDir; + + private final NoticeRepository noticeRepository; + private final ClientRepository clientRepository; + private final ImageService imageService; + private final AttributesRepository attributesRepository; + private final AttributeValuesRepository attributeValuesRepository; + private final AttributesNoticeRepository attributesNoticeRepository; + + public NoticeService(NoticeRepository noticeRepository, + ClientRepository clientRepository, + ImageService imageService, + AttributesRepository attributesRepository, + AttributeValuesRepository attributeValuesRepository, + AttributesNoticeRepository attributesNoticeRepository) { + this.noticeRepository = noticeRepository; + this.clientRepository = clientRepository; + this.imageService = imageService; + this.attributesRepository = attributesRepository; + this.attributeValuesRepository = attributeValuesRepository; + this.attributesNoticeRepository = attributesNoticeRepository; + } + + public Notice fromDTO(NoticeRequestDTO dto) { + Notice notice = new Notice(); + notice.setTitle(dto.getTitle()); + notice.setDescription(dto.getDescription()); + notice.setPrice(dto.getPrice()); + notice.setCategory(dto.getCategory()); + notice.setStatus(dto.getStatus()); + + Client client = clientRepository.findById(dto.getClientId()) + .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono klienta o ID: " + dto.getClientId())); + notice.setClient(client); + + return notice; + } + + private NoticeResponseDTO toDTO(Notice notice) { + NoticeResponseDTO dto = new NoticeResponseDTO(); + dto.setNoticeId(notice.getIdNotice()); + dto.setTitle(notice.getTitle()); + dto.setClientId(notice.getClient().getId()); + dto.setDescription(notice.getDescription()); + dto.setPrice(notice.getPrice()); + dto.setCategory(notice.getCategory()); + dto.setStatus(notice.getStatus()); + dto.setPublishDate(notice.getPublishDate()); + + List attributes = new ArrayList<>(); + if (notice.getAttributesNotices() != null) { + for (AttributesNotice an : notice.getAttributesNotices()) { + AttributeDto attr = new AttributeDto(); + attr.setName(an.getAttributeValue().getAttribute().getName()); + attr.setValue(an.getAttributeValue().getValue()); + attributes.add(attr); + } + } + dto.setAttributes(attributes); + + return dto; + } + + public List getAllNotices() { + List result = new ArrayList<>(); + for (Notice notice : noticeRepository.findAll()) { + result.add(toDTO(notice)); + } + return result; + } + + public NoticeResponseDTO getNoticeById(Long id) { + Notice notice = noticeRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id)); + return toDTO(notice); + } + + public Notice getNoticeByIdEntity(Long id) { + return noticeRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id)); + } + + public Long addNotice(NoticeRequestDTO dto) { + Notice notice = fromDTO(dto); + notice.setPublishDate(LocalDateTime.now()); + Notice savedNotice = noticeRepository.save(notice); + + if (dto.getAttributes() != null && !dto.getAttributes().isEmpty()) { + saveAttributes(savedNotice.getIdNotice(), dto.getAttributes()); + } + + return savedNotice.getIdNotice(); + } + + private void saveAttributes(Long noticeId, List attributeDtos) { + for (AttributeDto attributeDto : attributeDtos) { + Attributes attribute = attributesRepository.findByName(attributeDto.getName()) + .orElseGet(() -> { + Attributes newAttribute = new Attributes(); + newAttribute.setName(attributeDto.getName()); + return attributesRepository.save(newAttribute); + }); + + AttributeValues attributeValue = attributeValuesRepository + .findByAttributeAndValue(attribute, attributeDto.getValue()) + .orElseGet(() -> { + AttributeValues newValue = new AttributeValues(); + newValue.setAttribute(attribute); + newValue.setValue(attributeDto.getValue()); + return attributeValuesRepository.save(newValue); + }); + + AttributesNotice attributesNotice = new AttributesNotice(); + attributesNotice.setNotice_id(noticeId); + attributesNotice.setAttributeValue(attributeValue); + attributesNoticeRepository.save(attributesNotice); + } + } + + public boolean noticeExists(Long id) { + return noticeRepository.existsById(id); + } + + public NoticeResponseDTO updateNotice(Long id, NoticeRequestDTO dto) { + Notice existingNotice = noticeRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id)); + + existingNotice.setTitle(dto.getTitle()); + existingNotice.setDescription(dto.getDescription()); + existingNotice.setPrice(dto.getPrice()); + existingNotice.setCategory(dto.getCategory()); + existingNotice.setStatus(dto.getStatus()); + + if (dto.getClientId() != null && !dto.getClientId().equals(existingNotice.getClient().getId())) { + Client client = clientRepository.findById(dto.getClientId()) + .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono klienta o ID: " + dto.getClientId())); + existingNotice.setClient(client); + } + + return toDTO(noticeRepository.save(existingNotice)); + } + + public void deleteNotice(Long id) { + if (noticeExists(id)) { + noticeRepository.deleteById(id); + + List imagesList = new ArrayList<>(); + + try { + imagesList = imageService.getImagesList(id); + } catch (Exception e) { + logger.info("There weren't any images for notice with ID: " + id + ". Skipping deletion of images. Message: " + e.getMessage()); + } + + try { + for (String imageName : imagesList) { + imageService.deleteImage(uploadDir, imageName); + } + } catch (Exception e) { + logger.info("There were some issues while deleting images for notice with ID: " + id + ". Message: " + e.getMessage()); + } + } else { + throw new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id); + } + } + + public boolean isNoticeOwnedByClient(long noticeId, long clientId) { + return noticeRepository.existsByIdNoticeAndClientId(noticeId, clientId); + } + + public void boostNotice(long noticeId) { + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new EntityNotFoundException("Ogłoszenie o ID " + noticeId + " nie istnieje.")); + + notice.setPublishDate(LocalDateTime.now()); + + noticeRepository.save(notice); + + } + + +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/WishlistService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/WishlistService.java new file mode 100644 index 0000000..4c9659d --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/WishlistService.java @@ -0,0 +1,66 @@ +package _11.asktpk.artisanconnectbackend.service; + +import _11.asktpk.artisanconnectbackend.dto.WishlistDTO; +import _11.asktpk.artisanconnectbackend.dto.NoticeResponseDTO; +import _11.asktpk.artisanconnectbackend.entities.Client; +import _11.asktpk.artisanconnectbackend.entities.Notice; +import _11.asktpk.artisanconnectbackend.entities.Wishlist; +import _11.asktpk.artisanconnectbackend.repository.WishlistRepository; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class WishlistService { + + private final WishlistRepository wishlistRepository; + private final NoticeService noticeService; + + public WishlistService(WishlistRepository wishlistRepository, @Lazy NoticeService noticeService) { + this.wishlistRepository = wishlistRepository; + this.noticeService = noticeService; + } + + public List getWishlistForClientId(Long clientId) { + List wishlistEntities = wishlistRepository.findAllByClientId(clientId); + return wishlistEntities.stream() + .map(this::toDTO) + .toList(); + } + + public boolean toggleWishlist(Client client, Notice notice) { + Optional existingEntry = wishlistRepository.findByClientAndNotice(client, notice); + + if (existingEntry.isPresent()) { + wishlistRepository.delete(existingEntry.get()); + return false; + } else { + Wishlist wishlist = new Wishlist(); + wishlist.setClient(client); + wishlist.setNotice(notice); + wishlistRepository.save(wishlist); + return true; + } + } + + private WishlistDTO toDTO(Wishlist wishlist) { + WishlistDTO dto = new WishlistDTO(); + dto.setId(wishlist.getId()); + dto.setClientId(wishlist.getClient().getId()); + dto.setNoticeId(wishlist.getNotice().getIdNotice()); + + return dto; + } + + public List getNoticesInWishlist(Long clientId) { + List wishlistEntries = wishlistRepository.findAllByClientId(clientId); + + return wishlistEntries.stream() + .map(wishlist -> noticeService.getNoticeById(wishlist.getNotice().getIdNotice())) + .collect(Collectors.toList()); + + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/utils/Enums.java b/src/main/java/_11/asktpk/artisanconnectbackend/utils/Enums.java new file mode 100644 index 0000000..22108e8 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/utils/Enums.java @@ -0,0 +1,46 @@ +package _11.asktpk.artisanconnectbackend.utils; + +import java.util.Map; + +public class Enums { + public enum Role { + ADMIN, USER + } + + public enum Category { + Handmade, Woodworking, Metalworking, Ceramics, + Textiles, Jewelry, Leatherwork, Painting, Sculpture, + Glasswork, Furniture, Restoration, Tailoring, Weaving, + Calligraphy, Pottery, Blacksmithing, Basketry, Embroidery, + Knitting, Carpentry, Other + } + + public static final Map categoryPL = Map.ofEntries( + Map.entry(Category.Handmade, "Rękodzieło"), + Map.entry(Category.Woodworking, "Stolarstwo"), + Map.entry(Category.Metalworking, "Obróbka metalu"), + Map.entry(Category.Ceramics, "Ceramika"), + Map.entry(Category.Textiles, "Tekstylia"), + Map.entry(Category.Jewelry, "Biżuteria"), + Map.entry(Category.Leatherwork, "Wyroby skórzane"), + Map.entry(Category.Painting, "Malarstwo"), + Map.entry(Category.Sculpture, "Rzeźbiarstwo"), + Map.entry(Category.Glasswork, "Szklarstwo"), + Map.entry(Category.Furniture, "Meble"), + Map.entry(Category.Restoration, "Renowacja"), + Map.entry(Category.Tailoring, "Krawiectwo"), + Map.entry(Category.Weaving, "Tkactwo"), + Map.entry(Category.Calligraphy, "Kaligrafia"), + Map.entry(Category.Pottery, "Garncarstwo"), + Map.entry(Category.Blacksmithing, "Kowalstwo"), + Map.entry(Category.Basketry, "Koszykarstwo"), + Map.entry(Category.Embroidery, "Hafciarstwo"), + Map.entry(Category.Knitting, "Dzierganie"), + Map.entry(Category.Carpentry, "Ciesielstwo"), + Map.entry(Category.Other, "Inne") + ); + + public enum Status { + ACTIVE, INACTIVE + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/utils/Tools.java b/src/main/java/_11/asktpk/artisanconnectbackend/utils/Tools.java new file mode 100644 index 0000000..416dc91 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/utils/Tools.java @@ -0,0 +1,24 @@ +package _11.asktpk.artisanconnectbackend.utils; + +import _11.asktpk.artisanconnectbackend.security.JwtUtil; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +@Component +public class Tools { + private final JwtUtil jwtUtil; + + public Tools(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + public Long getClientIdFromRequest(HttpServletRequest request) { + String authorizationHeader = request.getHeader("Authorization"); + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + return jwtUtil.extractUserId(authorizationHeader.substring(7)); + } else { + return -1L; + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..e13f0d3 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,22 @@ +spring.application.name=ListHubBackend + +spring.datasource.url=${DB_URL:jdbc:postgresql://db:5432/postgres} +spring.datasource.username=${DB_USER:postgres} +spring.datasource.password=${DB_PASS:postgres} +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.sql.init.data-locations=classpath:sql/data.sql +spring.sql.init.mode=always +spring.jpa.defer-datasource-initialization=true + +spring.jpa.hibernate.ddl-auto=create-drop + +file.upload-dir=${IMAGES_UPLOAD_DIR:/app/images} +spring.servlet.multipart.max-file-size=${MAX_FILE_SIZE:10MB} +spring.servlet.multipart.max-request-size=${MAX_REQUEST_SIZE:10MB} + +jwt.secret=${JWT_SECRET} +jwt.expiration=1200000 + +logging.file.name=logs/payment-notifications.log +logging.level.TpayLogger=INFO \ No newline at end of file diff --git a/src/main/resources/application.properties.prod b/src/main/resources/application.properties.prod new file mode 100644 index 0000000..610f4e2 --- /dev/null +++ b/src/main/resources/application.properties.prod @@ -0,0 +1,20 @@ +spring.application.name=ArtisanConnectBackend + +## PostgreSQL +spring.datasource.url=jdbc:postgresql://db:5432/postgres +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.username=postgres +spring.datasource.password=postgres + +#initial data for db injection +spring.sql.init.data-locations=classpath:sql/data.sql +spring.sql.init.mode=always +spring.jpa.defer-datasource-initialization=true + +# create and drop table, good for testing, production set to none or comment it +spring.jpa.hibernate.ddl-auto=update + +#file.upload-dir=/Users/andsol/Desktop/uploads +file.upload-dir=/app/images +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB \ No newline at end of file diff --git a/src/main/resources/sql/data.sql b/src/main/resources/sql/data.sql new file mode 100644 index 0000000..bb82361 --- /dev/null +++ b/src/main/resources/sql/data.sql @@ -0,0 +1,61 @@ +INSERT INTO roles (id, rolename) +VALUES + (1, 'USER'), + (2, 'ADMIN'); + +INSERT INTO clients (email, first_name, last_name, password, role_id) +VALUES + ('dignissim.tempor.arcu@aol.ca', 'Diana', 'Harrison', '', 1), + ('john.doe@example.com', 'John', 'Doe', '', 2), + ('jane.smith@example.com', 'Jane', 'Smith', '', 1), + ('michael.brown@example.com', 'Michael', 'Brown', '', 1), + ('emily.jones@example.com', 'Emily', 'Jones', '', 1); + + +INSERT INTO notice (title, description, client_id, price, category, status, publish_date) VALUES + ('Ręcznie robiona biżuteria', 'Unikalna biżuteria wykonana ręcznie z najwyższej jakości materiałów.', 5, 150.00, 'Jewelry', 'ACTIVE', '2023-10-01'), + ('Drewniany stół', 'Solidny stół wykonany z litego drewna dębowego.', 3, 1200.00, 'Furniture', 'ACTIVE', '2023-09-15'), + ('Ceramiczna waza', 'Piękna waza ceramiczna, idealna na prezent.', 2, 300.00, 'Ceramics', 'INACTIVE', '2023-08-20'), + ('Obraz olejny', 'Obraz olejny przedstawiający krajobraz górski.', 4, 800.00, 'Painting', 'ACTIVE', '2023-07-10'), + ('Skórzany portfel', 'Ręcznie wykonany portfel ze skóry naturalnej.', 1, 250.00, 'Leatherwork', 'ACTIVE', '2023-06-05'); + + +insert into attributes (name) values + ('Kolor'), + ('Materiał'); + +-- Kolory +insert into attribute_values (value, id_attribute) values + ('Zielony', 1), + ('Czerwony', 1), + ('Niebieski', 1), + ('Żółty', 1), + ('Biały', 1), + ('Czarny', 1), + ('Różowy', 1), + ('Szary', 1), + ('Fioletowy', 1), + ('Pomarańczowy', 1), + ('Inny', 1); + +-- Materiały +insert into attribute_values (value, id_attribute) values + ('Bawełna', 2), + ('Wełna', 2), + ('Syntetyk', 2), + ('Skóra', 2), + ('Len', 2), + ('Jedwab', 2), + ('Poliester', 2), + ('Akryl', 2), + ('Wiskoza', 2), + ('Nylon', 2), + ('Inny', 2); + +insert into attributes_notice (id_value, notice_id) values + (1, 1), -- Ręcznie robiona biżuteria - Zielony + (22, 1), + (2, 2), -- Drewniany stół - Czerwony + (3, 3), -- Ceramiczna waza - Niebieski + (4, 4), -- Obraz olejny - Żółty + (5, 5); -- Skórzany portfel - Biały