commit c0580118c1e04e73d7a3821b467753d4753fb53a Author: Vitor Hideyoshi Nakazone Batista Date: Mon Sep 5 04:13:18 2022 -0300 Initial commit diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..79c50e4 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7dabba --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +src/main/resources/application-devel.yml diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..c1dd12f Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..22f219d --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..40ecda3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# +# Build stage +# +FROM maven:3.8-jdk-11 AS build +COPY src /home/app/src +COPY pom.xml /home/app +RUN mvn -Dmaven.test.skip -f /home/app/pom.xml clean package + +# +# Package stage +# +FROM openjdk:17-jdk + +COPY --from=build /home/app/target/*.jar app.jar +COPY src/main/resources/* credentials/ +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fad443c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Vitor Hideyoshi Nakazone Batista + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..680343c --- /dev/null +++ b/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.1 + + + com.hideyoshi + backend-template + 0.0.1-SNAPSHOT + backend-template + Backend Template + + 11 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + 2.7.3 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.session + spring-session-core + + + org.springframework.session + spring-session-data-redis + + + + com.auth0 + java-jwt + 4.0.0 + + + org.postgresql + postgresql + 42.5.0 + runtime + + + org.liquibase + liquibase-core + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.data + spring-data-redis + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + junit + junit + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.project-lombok + lombok + + + + + + + + diff --git a/src/main/java/com/hideyoshi/backendportfolio/BackendPortfolioApplication.java b/src/main/java/com/hideyoshi/backendportfolio/BackendPortfolioApplication.java new file mode 100644 index 0000000..4dce649 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/BackendPortfolioApplication.java @@ -0,0 +1,21 @@ +package com.hideyoshi.backendportfolio; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@SpringBootApplication +public class BackendPortfolioApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendPortfolioApplication.class, args); + } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java new file mode 100644 index 0000000..197ea48 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java @@ -0,0 +1,46 @@ +package com.hideyoshi.backendportfolio.base.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.List; + +@Configuration +public class CorsConfig { + + @Value("${com.hideyoshi.frontendPath}") + private String FRONTEND_PATH; + + @Value("${com.hideyoshi.frontendConnectionType}") + private String CONNECTION_TYPE; + + private final String HTTP = "http://"; + + private final String HTTPS = "https://"; + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + + String connectionProtocol = CONNECTION_TYPE.equals("secure") + ? HTTPS + : HTTP; + + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of(connectionProtocol + FRONTEND_PATH)); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token")); + configuration.setAllowCredentials(true); + configuration.setExposedHeaders(List.of("x-auth-token")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java new file mode 100644 index 0000000..2b30817 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java @@ -0,0 +1,54 @@ +package com.hideyoshi.backendportfolio.base.config; + +import com.hideyoshi.backendportfolio.base.user.entity.Role; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.repo.UserRepository; +import com.hideyoshi.backendportfolio.base.user.service.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; + +@Configuration +public class DefaultUserConfig { + + @Value("${com.hideyoshi.defaultUser.fullName}") + private String ADMIN_NAME; + + @Value("${com.hideyoshi.defaultUser.email}") + private String ADMIN_EMAIL; + + @Value("${com.hideyoshi.defaultUser.username}") + private String ADMIN_USERNAME; + + @Value("${com.hideyoshi.defaultUser.password}") + private String ADMIN_PASSWORD; + + @Bean + CommandLineRunner run(UserService userService, UserRepository userRepo) { + return args -> { + UserDTO defaultUser = UserDTO.builder() + .fullname(ADMIN_NAME) + .email(ADMIN_EMAIL) + .username(ADMIN_USERNAME) + .password(ADMIN_PASSWORD) + .roles(new ArrayList<>()) + .build(); + if (!userRepo.findByUsername(defaultUser.getUsername()).isPresent()) { + defaultUser = userService.saveUser(defaultUser); + + userService.addRoleToUser( + defaultUser.getId(), + Role.ADMIN.getDescription() + ); + userService.addRoleToUser( + defaultUser.getId(), + Role.USER.getDescription() + ); + } + }; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/config/RestAuthenticationEntryPointConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/config/RestAuthenticationEntryPointConfig.java new file mode 100644 index 0000000..0cbf0bf --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/config/RestAuthenticationEntryPointConfig.java @@ -0,0 +1,34 @@ +package com.hideyoshi.backendportfolio.base.config; + +import com.hideyoshi.backendportfolio.util.exception.AuthenticationInvalidException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Log4j2 +@Component("restAuthenticationEntryPoint") +public class RestAuthenticationEntryPointConfig implements AuthenticationEntryPoint{ + + @Autowired + @Qualifier("handlerExceptionResolver") + private HandlerExceptionResolver resolver; + + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) { + + resolver.resolveException( + request, + response, + null, + new AuthenticationInvalidException("Authentication Failed. Check your credentials.") + ); + + } +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/config/SessionConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/config/SessionConfig.java new file mode 100644 index 0000000..1185c88 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/config/SessionConfig.java @@ -0,0 +1,22 @@ +package com.hideyoshi.backendportfolio.base.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.session.web.http.CookieSerializer; +import org.springframework.session.web.http.DefaultCookieSerializer; + +public class SessionConfig { + + @Value("${com.hideyoshi.frontEndPath}") + private String frontEndPath; + + @Bean + public CookieSerializer cookieSerializer() { + DefaultCookieSerializer serializer = new DefaultCookieSerializer(); + serializer.setCookieName("SESSION"); + serializer.setCookiePath("/"); + serializer.setDomainNamePattern("(^.+)?(\\.)?(" + frontEndPath + ")((/#!)?(/\\w+)+)?"); + return serializer; + } + +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java new file mode 100644 index 0000000..f02c3de --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java @@ -0,0 +1,72 @@ +package com.hideyoshi.backendportfolio.base.security; + +import com.hideyoshi.backendportfolio.base.config.RestAuthenticationEntryPointConfig; +import com.hideyoshi.backendportfolio.base.security.filter.CustomAuthenticationFilter; +import com.hideyoshi.backendportfolio.base.security.filter.CustomAuthorizationFilter; +import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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 org.springframework.web.servlet.HandlerExceptionResolver; + +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final AuthService authService; + + private final UserDetailsService userDetailsService; + + private final BCryptPasswordEncoder passwordEncoder; + + private final RestAuthenticationEntryPointConfig restAuthenticationEntryPointConfig; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService) + .passwordEncoder(passwordEncoder); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + CustomAuthenticationFilter customAuthenticationFilter = + new CustomAuthenticationFilter(this.authenticationManager(), this.authService, this.restAuthenticationEntryPointConfig); + + customAuthenticationFilter.setFilterProcessesUrl("/user/login"); + + http.cors().and().csrf().disable() + .authorizeRequests().antMatchers("/session/**").permitAll() + .and().authorizeRequests().antMatchers("/user/signup").permitAll() + .and().authorizeRequests().antMatchers("/user/login/refresh").permitAll() + .and().authorizeRequests().antMatchers("/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") + .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and().addFilter(customAuthenticationFilter) + .addFilterBefore(new CustomAuthorizationFilter(this.authService), UsernamePasswordAuthenticationFilter.class); + + } + + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java new file mode 100644 index 0000000..a265181 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java @@ -0,0 +1,74 @@ +package com.hideyoshi.backendportfolio.base.security.filter; + +import com.auth0.jwt.algorithms.Algorithm; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hideyoshi.backendportfolio.base.config.RestAuthenticationEntryPointConfig; +import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import lombok.extern.log4j.Log4j2; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.HashMap; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Log4j2 +public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthService authService; + + private final AuthenticationManager authenticationManager; + + private final RestAuthenticationEntryPointConfig restAuthenticationEntryPointConfig; + + public CustomAuthenticationFilter(AuthenticationManager authenticationManager, AuthService authService, RestAuthenticationEntryPointConfig restAuthenticationEntryPointConfig) { + this.authService = authService; + this.authenticationManager = authenticationManager; + this.restAuthenticationEntryPointConfig = restAuthenticationEntryPointConfig; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + Authentication userAuthentication = null; + try { + userAuthentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password) + ); + } catch (AuthenticationException e) { + restAuthenticationEntryPointConfig.commence(request, response, e); + } + return userAuthentication; + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException { + + UserDTO user = (UserDTO) authentication.getPrincipal(); + Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); + + HashMap tokens = this.authService.generateTokens(user, algorithm, request); + + HttpSession httpSession = request.getSession(); + UserDTO authenticatedUser = user.toResponse(tokens.get("accessToken"), tokens.get("refreshToken")); + httpSession.setAttribute("user", authenticatedUser); + + response.setContentType(APPLICATION_JSON_VALUE); + new ObjectMapper() + .writeValue(response.getOutputStream(), authenticatedUser); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java new file mode 100644 index 0000000..012d5e8 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java @@ -0,0 +1,72 @@ +package com.hideyoshi.backendportfolio.base.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import com.hideyoshi.backendportfolio.util.exception.BadRequestException; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +public class CustomAuthorizationFilter extends OncePerRequestFilter { + + public static String AUTHORIZATION_TYPE_STRING = "Bearer "; + + private final AuthService authService; + + public CustomAuthorizationFilter(AuthService authService) { + this.authService = authService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (request.getServletPath().equals("/user/login")) { + filterChain.doFilter(request, response); + } else { + String authorizationHeader = request.getHeader(AUTHORIZATION); + if (Objects.nonNull(authorizationHeader) && authorizationHeader.startsWith(AUTHORIZATION_TYPE_STRING)) { + try { + + UsernamePasswordAuthenticationToken authenticationToken = + this.authService.verifyAccessToken(authorizationHeader); + + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + filterChain.doFilter(request, response); + + } catch (Exception e) { + response.setHeader("error", e.getMessage()); + + response.setStatus(FORBIDDEN.value()); + + Map error = new HashMap<>(); + error.put("error_message", e.getMessage()); + + response.setContentType(APPLICATION_JSON_VALUE); + new ObjectMapper() + .writeValue(response.getOutputStream(), error); + } + } else { + filterChain.doFilter(request, response); + } + } + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/interceptor/ConfigInterceptor.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/interceptor/ConfigInterceptor.java new file mode 100644 index 0000000..d10783d --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/interceptor/ConfigInterceptor.java @@ -0,0 +1,18 @@ +package com.hideyoshi.backendportfolio.base.security.interceptor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Component +@RequiredArgsConstructor +public class ConfigInterceptor implements WebMvcConfigurer { + + private final UserResourceAccessInterceptor userResourceAccessInterceptor; + + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(userResourceAccessInterceptor); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/interceptor/UserResourceAccessInterceptor.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/interceptor/UserResourceAccessInterceptor.java new file mode 100644 index 0000000..b4a3cbd --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/interceptor/UserResourceAccessInterceptor.java @@ -0,0 +1,45 @@ +package com.hideyoshi.backendportfolio.base.security.interceptor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hideyoshi.backendportfolio.base.user.service.UserService; +import com.hideyoshi.backendportfolio.util.exception.BadRequestException; +import com.hideyoshi.backendportfolio.util.guard.UserResourceGuard; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +@Log4j2 +@Component +@RequiredArgsConstructor +public class UserResourceAccessInterceptor implements HandlerInterceptor { + + private final UserService userService; + + private final ObjectMapper objectMapper; + + public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) { + + if (!(handler instanceof HandlerMethod)) { + return true; + } + + final UserResourceGuard annotation = ((HandlerMethod)handler) + .getMethodAnnotation(UserResourceGuard.class); + + if (Objects.nonNull(annotation)) { + Boolean accessPermission = + annotation.accessType().hasAccess(this.userService, this.objectMapper, request); + if (!accessPermission) { + throw new BadRequestException(annotation.denialMessage()); + } + } + return true; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java new file mode 100644 index 0000000..5194340 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java @@ -0,0 +1,27 @@ +package com.hideyoshi.backendportfolio.base.security.service; + +import com.auth0.jwt.algorithms.Algorithm; +import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.HashMap; + +public interface AuthService { + + TokenDTO generateAccessToken(@Valid UserDTO user, Algorithm algorithm, HttpServletRequest request); + + TokenDTO generateRefreshToken(@Valid UserDTO user, Algorithm algorithm, HttpServletRequest request); + + HashMap generateTokens(@Valid UserDTO user, Algorithm algorithm, HttpServletRequest request); + + UsernamePasswordAuthenticationToken verifyAccessToken(String authorizationHeader); + + UserDTO refreshAccessToken(String refreshToken, HttpServletRequest request, HttpServletResponse response); + + UserDTO signupUser(@Valid UserDTO user, HttpServletRequest request); + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java new file mode 100644 index 0000000..7ce5f27 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java @@ -0,0 +1,175 @@ +package com.hideyoshi.backendportfolio.base.security.service; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.service.UserService; +import com.hideyoshi.backendportfolio.util.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + + @Value("${com.hideyoshi.tokenSecret}") + private String TOKEN_SECRET; + + @Value("${com.hideyoshi.accessTokenDuration}") + private Integer ACCESS_TOKEN_DURATION; + + @Value("${com.hideyoshi.refreshTokenDuration}") + private Integer REFRESH_TOKEN_DURATION; + + private static final String AUTHORIZATION_TYPE_STRING = "Bearer "; + + private final UserService userService; + + @Autowired + @Qualifier("handlerExceptionResolver") + private HandlerExceptionResolver resolver; + + @Override + public TokenDTO generateAccessToken(@Valid UserDTO user, Algorithm algorithm, HttpServletRequest request) { + + Date expirationDate = new Date(System.currentTimeMillis() + ACCESS_TOKEN_DURATION); + + String accessToken = JWT.create() + .withSubject(user.getUsername()) + .withExpiresAt(expirationDate) + .withIssuer(request.getRequestURL().toString()) + .withClaim("roles", user.getAuthorities() + .stream().map(GrantedAuthority::getAuthority) + .collect(Collectors.toList())) + .sign(algorithm); + + return new TokenDTO(accessToken, expirationDate); + } + + @Override + public TokenDTO generateRefreshToken(@Valid UserDTO user, Algorithm algorithm, HttpServletRequest request) { + + Date expirationDate = new Date(System.currentTimeMillis() + REFRESH_TOKEN_DURATION); + + String refreshToken = JWT.create() + .withSubject(user.getUsername()) + .withExpiresAt(expirationDate) + .withIssuer(request.getRequestURL().toString()) + .sign(algorithm); + + return new TokenDTO(refreshToken, expirationDate); + + } + + @Override + public HashMap generateTokens(@Valid UserDTO user, Algorithm algorithm, HttpServletRequest request) { + + TokenDTO accessToken = generateAccessToken(user, algorithm, request); + TokenDTO refreshToken = generateRefreshToken(user, algorithm, request); + + HashMap tokens = new HashMap<>(); + tokens.put("accessToken", accessToken); + tokens.put("refreshToken", refreshToken); + + return tokens; + } + + @Override + public UsernamePasswordAuthenticationToken verifyAccessToken(String authorizationHeader) { + + if (authorizationHeader.startsWith(AUTHORIZATION_TYPE_STRING)) { + + String authorizationToken = authorizationHeader.substring(AUTHORIZATION_TYPE_STRING.length()); + Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET.getBytes()); + + JWTVerifier verifier = JWT.require(algorithm).build(); + DecodedJWT decodedJWT = verifier.verify(authorizationToken); + + String username = decodedJWT.getSubject(); + String[] roles = decodedJWT.getClaim("roles").asArray(String.class); + + Collection authorities = new ArrayList<>(); + stream(roles).forEach(role -> { + authorities.add(new SimpleGrantedAuthority(role)); + }); + return new UsernamePasswordAuthenticationToken(username, null, authorities); + } + return null; + } + + @Override + public UserDTO refreshAccessToken(String refreshToken, HttpServletRequest request, HttpServletResponse response) { + + if (Objects.nonNull(refreshToken)) { + + Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET.getBytes()); + + JWTVerifier verifier = JWT.require(algorithm).build(); + DecodedJWT decodedJWT = verifier.verify(refreshToken); + + UserDTO user = this.userService.getUser(decodedJWT.getSubject()); + + if (Objects.nonNull(user)) { + + HttpSession httpSession = request.getSession(); + UserDTO authenticatedUser = user.toResponse( + this.generateAccessToken(user, algorithm, request), + new TokenDTO( + refreshToken, + decodedJWT.getExpiresAt() + ) + ); + httpSession.setAttribute("user", authenticatedUser); + + return authenticatedUser; + } + + } else { + resolver.resolveException( + request, + response, + null, + new BadRequestException("Invalid Refresh Token. Please authenticate first.") + ); + } + return null; + } + + @Override + public UserDTO signupUser(@Valid UserDTO user, HttpServletRequest request) { + + Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET.getBytes()); + + UserDTO userSaved = this.userService.saveUser(user); + HashMap tokens = this.generateTokens(userSaved, algorithm, request); + + HttpSession httpSession = request.getSession(); + UserDTO userAuthenticated = userSaved.toResponse(tokens.get("accessToken"), tokens.get("refreshToken")); + httpSession.setAttribute("user", userAuthenticated); + + return userAuthenticated; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/session/api/SessionController.java b/src/main/java/com/hideyoshi/backendportfolio/base/session/api/SessionController.java new file mode 100644 index 0000000..b451417 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/session/api/SessionController.java @@ -0,0 +1,35 @@ +package com.hideyoshi.backendportfolio.base.session.api; + +import com.hideyoshi.backendportfolio.base.session.service.SessionManagerService; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpSession; + +@Controller +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/session") +public class SessionController { + + private final SessionManagerService sessionManagerService; + + @GetMapping(path = "/validate") + public ResponseEntity validateCurrentSession(HttpSession session) { + return ResponseEntity.ok(this.sessionManagerService.validateSession(session)); + } + + @PostMapping(path="/destroy") + public ResponseEntity destroyCurrentSession(HttpSession session) { + this.sessionManagerService.destroySession(session); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/session/service/SessionManagerService.java b/src/main/java/com/hideyoshi/backendportfolio/base/session/service/SessionManagerService.java new file mode 100644 index 0000000..6136149 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/session/service/SessionManagerService.java @@ -0,0 +1,13 @@ +package com.hideyoshi.backendportfolio.base.session.service; + +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; + +import javax.servlet.http.HttpSession; + +public interface SessionManagerService { + + UserDTO validateSession(HttpSession session); + + void destroySession(HttpSession session); + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/session/service/SessionManagerServiceImpl.java b/src/main/java/com/hideyoshi/backendportfolio/base/session/service/SessionManagerServiceImpl.java new file mode 100644 index 0000000..36d8542 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/session/service/SessionManagerServiceImpl.java @@ -0,0 +1,35 @@ +package com.hideyoshi.backendportfolio.base.session.service; + +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpSession; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class SessionManagerServiceImpl implements SessionManagerService { + + private final UserService userService; + + @Override + public UserDTO validateSession(HttpSession session) { + + UserDTO sessionObjects = (UserDTO) session.getAttribute("user"); + + if (Objects.nonNull(sessionObjects)) { + return this.userService.getUser(sessionObjects.getUsername()) + .toResponse(sessionObjects.getAccessToken(), sessionObjects.getRefreshToken()); + } + + return null; + } + + @Override + public void destroySession(HttpSession session) { + session.invalidate(); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java new file mode 100644 index 0000000..9abb532 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java @@ -0,0 +1,89 @@ +package com.hideyoshi.backendportfolio.base.user.api; + +import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.service.UserService; +import com.hideyoshi.backendportfolio.util.guard.UserResourceGuard; +import com.hideyoshi.backendportfolio.util.guard.UserResourceGuardEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.net.URI; +import java.util.List; + +@Log4j2 +@Controller +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + private final AuthService authService; + + @GetMapping + @UserResourceGuard(accessType = UserResourceGuardEnum.ADMIN_USER) + public ResponseEntity> getUsers() { + return ResponseEntity.ok(this.userService.getUsers()); + } + + @PostMapping("/signup") + @UserResourceGuard(accessType = UserResourceGuardEnum.OPEN) + public ResponseEntity signupUser(@RequestBody @Valid UserDTO user, HttpServletRequest request) { + URI uri = URI.create( + ServletUriComponentsBuilder + .fromCurrentContextPath() + .path("/user/signup").toUriString() + ); + return ResponseEntity.created(uri).body(this.authService.signupUser(user, request)); + } + + @PostMapping("/delete/{id}") + @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) + public ResponseEntity deleteUser(@PathVariable("id") Long id) { + this.userService.deleteUser(id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +// +// @PostMapping("/alter/{id}") +// @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) +// public ResponseEntity alterUser(@PathVariable("id") Long id, @RequestBody @Valid UserDTO user) { +// this.userService.alterUser(id, user); +// return new ResponseEntity<>(HttpStatus.NO_CONTENT); +// } +// +// @PostMapping("/alter/{id}/role/add") +// @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) +// public ResponseEntity addRoleToUser(@PathVariable("id") Long id, @RequestBody RoleToUserDTO filter) { +// userService.addRoleToUser(id, filter.getRoleName()); +// return ResponseEntity.ok().build(); +// } +// +// @PostMapping("/alter/{id}/role/delete") +// @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) +// public ResponseEntity deleteRoleToUser(@PathVariable("id") Long id, @RequestBody RoleToUserDTO filter) { +// userService.removeRoleFromUser(id, filter.getRoleName()); +// return ResponseEntity.ok().build(); +// } + + @PostMapping("/login/refresh") + @UserResourceGuard(accessType = UserResourceGuardEnum.OPEN) + public ResponseEntity refreshAccessToken( + @RequestBody @Valid TokenDTO refreshToken, + HttpServletRequest request, + HttpServletResponse response) { + return ResponseEntity.ok(this.authService.refreshAccessToken(refreshToken.getToken(), request, response)); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java new file mode 100644 index 0000000..df2cea1 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java @@ -0,0 +1,18 @@ +package com.hideyoshi.backendportfolio.base.user.entity; + +public enum Provider { + + GOOGLE("google"), + + LOCAL("local"); + private String name; + + Provider(String name) { + this.name = name; + } + + public String getName() { + return name; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Role.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Role.java new file mode 100644 index 0000000..a0bd32f --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Role.java @@ -0,0 +1,29 @@ +package com.hideyoshi.backendportfolio.base.user.entity; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Role { + USER("ROLE_USER"), + ADMIN("ROLE_ADMIN"); + + @JsonValue + private final String description; + + Role(String description) { + this.description = description; + } + + public String getDescription() { + return this.description; + } + + public static Role byValue(String description) { + for (Role r : values()) { + if (r.getDescription().equals(description)) { + return r; + } + } + throw new IllegalArgumentException("Argument not valid."); + } + +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java new file mode 100644 index 0000000..4470b66 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java @@ -0,0 +1,77 @@ +package com.hideyoshi.backendportfolio.base.user.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; + +@Data +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "`user`", schema = "auth") +public class User { + + @Id + @SequenceGenerator(name = "seq_user", sequenceName = "auth.user_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_user") + private Long id; + + @Column( + name = "full_name", + nullable = false + ) + private String fullname; + + @Column( + name = "email", + unique = true, + nullable = false + ) + private String email; + + + @Column( + name = "username", + unique = true, + nullable = false + ) + private String username; + + + @Column( + name = "password", + nullable = false + ) + private String password; + + @Column( + name = "roles", + nullable = false + ) + private String roles; + + public void setRoles(List roles) { + this.roles = roles.stream() + .map(role -> role.getDescription()) + .collect(Collectors.joining("&")); + } + + public List getRoles() { + List roles = new ArrayList<>(); + if (Objects.nonNull(this.roles) && !this.roles.isEmpty()) { + roles = stream(this.roles.split("&")) + .map(description -> Role.byValue(description)) + .collect(Collectors.toList()); + } + return roles; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/model/RoleToUserDTO.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/RoleToUserDTO.java new file mode 100644 index 0000000..260d06f --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/RoleToUserDTO.java @@ -0,0 +1,10 @@ +package com.hideyoshi.backendportfolio.base.user.model; + +import lombok.Data; + +@Data +public +class RoleToUserDTO { + private String username; + private String roleName; +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/model/TokenDTO.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/TokenDTO.java new file mode 100644 index 0000000..c293e12 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/TokenDTO.java @@ -0,0 +1,21 @@ +package com.hideyoshi.backendportfolio.base.user.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TokenDTO implements Serializable { + + @NotNull(message = "Invalid AccessToken. Please Authenticate first.") + private String token; + + @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date expirationDate; + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java new file mode 100644 index 0000000..51370eb --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java @@ -0,0 +1,162 @@ +package com.hideyoshi.backendportfolio.base.user.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; +import com.hideyoshi.backendportfolio.base.user.entity.Role; +import com.hideyoshi.backendportfolio.base.user.entity.User; +import com.hideyoshi.backendportfolio.util.validator.email.unique.UniqueEmail; +import com.hideyoshi.backendportfolio.util.validator.email.valid.ValidEmail; +import com.hideyoshi.backendportfolio.util.validator.password.ValidPassword; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class UserDTO implements UserDetails { + + private Long id; + + @NotEmpty + private String fullname; + + @NotEmpty + @ValidEmail + @UniqueEmail + private String email; + + @NotEmpty + private String username; + + @Nullable + @ValidPassword + private String password; + + @Size(min=1) + private List roles; + + private TokenDTO accessToken; + + private TokenDTO refreshToken; + + private Provider provider; + + public UserDTO( + String fullname, + String email, + String username, + String password + ) { + this.fullname = fullname; + this.email = email; + this.username = username; + this.password = password; + this.roles = List.of(Role.USER); + } + + public UserDTO( + String fullname, + String email, + String username, + String password, + List roles + ) { + this.fullname = fullname; + this.email = email; + this.username = username; + this.password = password; + this.roles = roles; + } + + public UserDTO(User entity) { + this.id = entity.getId(); + this.fullname = entity.getFullname(); + this.email = entity.getEmail(); + this.username = entity.getUsername(); + this.password = entity.getPassword(); + this.roles = entity.getRoles(); + } + + public User toEntity() { + return new User( + this.id, + this.fullname, + this.email, + this.username, + this.password, + Objects.nonNull(this.roles) ? this.roles.stream() + .map(role -> role.getDescription()) + .collect(Collectors.joining("&")) : Role.USER.getDescription() + ); + } + + @JsonIgnore + @Override + public Collection getAuthorities() { + return this.roles.stream() + .map(role -> new SimpleGrantedAuthority(role.getDescription())) + .collect(Collectors.toList()); + } + + @JsonIgnore + @Override + public boolean isAccountNonExpired() { + return true; + } + + @JsonIgnore + @Override + public boolean isAccountNonLocked() { + return true; + } + + @JsonIgnore + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @JsonIgnore + @Override + public boolean isEnabled() { + return true; + } + + public UserDTO toResponse() { + return UserDTO.builder() + .fullname(this.fullname) + .email(this.email) + .username(this.username) + .build(); + } + + public UserDTO toResponse(TokenDTO accessToken, TokenDTO refreshToken) { + return UserDTO.builder() + .id(this.id) + .fullname(this.fullname) + .email(this.email) + .username(this.username) + .roles(this.roles) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepository.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepository.java new file mode 100644 index 0000000..455f1bb --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepository.java @@ -0,0 +1,13 @@ +package com.hideyoshi.backendportfolio.base.user.repo; + +import com.hideyoshi.backendportfolio.base.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + Optional findByEmail(String email); +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserService.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserService.java new file mode 100644 index 0000000..5405ed5 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserService.java @@ -0,0 +1,26 @@ +package com.hideyoshi.backendportfolio.base.user.service; + +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import org.springframework.security.core.userdetails.UserDetailsService; + +import javax.validation.Valid; +import java.util.List; + +public interface UserService extends UserDetailsService { + + UserDTO saveUser(@Valid UserDTO user); + + void alterUser(Long id, @Valid UserDTO user); + + void deleteUser(Long id); + + void addRoleToUser(Long id, String roleName); + + void removeRoleFromUser(Long id, String roleName); + + UserDTO getUser(Long id); + + UserDTO getUser(String username); + + List getUsers(); +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java new file mode 100644 index 0000000..3af3254 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java @@ -0,0 +1,146 @@ +package com.hideyoshi.backendportfolio.base.user.service; + +import com.hideyoshi.backendportfolio.base.user.entity.Role; +import com.hideyoshi.backendportfolio.base.user.entity.User; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.repo.UserRepository; +import com.hideyoshi.backendportfolio.util.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import javax.validation.Valid; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Log4j2 +@Service +@Transactional +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepo; + + private final PasswordEncoder passwordEncoder; + + @Override + public UserDTO saveUser(@Valid UserDTO user) { + + this.userRepo.findByUsername(user.getUsername()).ifPresent( userOnDB -> { + throw new BadRequestException(String.format("User %s already exists. Try another UserName.", userOnDB.getUsername())); + }); + + log.info(String.format("Saving to the database user of name: %s", user.getFullname())); + + user.setPassword(passwordEncoder.encode(user.getPassword())); + UserDTO userSaved = new UserDTO(userRepo.save(user.toEntity())); + + if (!userSaved.getRoles().contains(Role.USER)) { + userSaved.getRoles().add(Role.USER); + } + + return userSaved; + } + + @Override + public void alterUser(Long id, @Valid UserDTO user) { + + this.userRepo.findById(id).ifPresentOrElse( userOnDB -> { + User userToSave = user.toEntity(); + userToSave.setId(userOnDB.getId()); + userRepo.save(userToSave); + }, () -> { + throw new BadRequestException(String.format("User {} doesn't exist.", user.getUsername())); + }); + } + + @Override + public void deleteUser(Long id) { + + this.userRepo.findById(id).ifPresentOrElse( userOnDB -> { + this.userRepo.delete(userOnDB); + }, () -> { + throw new BadRequestException("User doesn't exist."); + }); + + } + + @Override + public void addRoleToUser(Long id, String roleName) { + + UserDTO userOnDB = this.getUser(id); + Role newAuthority = Role.byValue(roleName); + + List roles = userOnDB.getRoles(); + if (Objects.nonNull(newAuthority) && !roles.contains(newAuthority)) { + + log.info(String.format("Adding to user %s the role %s", + userOnDB.getUsername(), newAuthority.getDescription())); + + if (roles.add(newAuthority)) { + userOnDB.setRoles(roles); + this.alterUser(userOnDB.getId(), userOnDB); + } + + } + + } + + @Override + public void removeRoleFromUser(Long id, String roleName) { + + UserDTO userOnDB = this.getUser(id); + Role toDeleteAuthority = Role.byValue(roleName); + + List roles = userOnDB.getRoles(); + if (!roles.isEmpty()) { + + log.info(String.format("Removing from user %s the role %s", + userOnDB.getUsername(), toDeleteAuthority.getDescription())); + + roles = roles.stream() + .filter(role -> !role.equals(toDeleteAuthority)) + .collect(Collectors.toList()); + userOnDB.setRoles(roles); + this.alterUser(userOnDB.getId(), userOnDB); + } + } + + @Override + public UserDTO getUser(Long id) { + log.info(String.format("Fetching user with id: %o", id)); + + return new UserDTO( + userRepo.findById(id) + .orElseThrow(() -> new BadRequestException("User Not Found. Please create an Account.")) + ); + } + + @Override + public UserDTO getUser(String username) { + log.info(String.format("Fetching user: %s", username)); + + return new UserDTO( + userRepo.findByUsername(username) + .orElseThrow(() -> new BadRequestException("User Not Found. Please create an Account.")) + ); + } + + @Override + public List getUsers() { + log.info("Fetching all users."); + + return userRepo.findAll().stream() + .map(user -> (new UserDTO(user)).toResponse()) + .collect(Collectors.toList()); + } + + @Override + public UserDetails loadUserByUsername(String username) { + return this.getUser(username); + } +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/exception/AuthenticationInvalidException.java b/src/main/java/com/hideyoshi/backendportfolio/util/exception/AuthenticationInvalidException.java new file mode 100644 index 0000000..02eab84 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/exception/AuthenticationInvalidException.java @@ -0,0 +1,13 @@ +package com.hideyoshi.backendportfolio.util.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.FORBIDDEN) +public class AuthenticationInvalidException extends RuntimeException { + + public AuthenticationInvalidException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/exception/AuthenticationInvalidExceptionDetails.java b/src/main/java/com/hideyoshi/backendportfolio/util/exception/AuthenticationInvalidExceptionDetails.java new file mode 100644 index 0000000..dd9066b --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/exception/AuthenticationInvalidExceptionDetails.java @@ -0,0 +1,15 @@ +package com.hideyoshi.backendportfolio.util.exception; + +import java.time.LocalDateTime; + +public class AuthenticationInvalidExceptionDetails extends ExceptionDetails{ + + public AuthenticationInvalidExceptionDetails( + String title, + Integer status, + String details, + String developerMessage, + LocalDateTime timestamp) { + super(title, status, details, developerMessage, timestamp); + } +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/exception/BadRequestException.java b/src/main/java/com/hideyoshi/backendportfolio/util/exception/BadRequestException.java new file mode 100644 index 0000000..f6830c0 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/exception/BadRequestException.java @@ -0,0 +1,11 @@ +package com.hideyoshi.backendportfolio.util.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class BadRequestException extends RuntimeException{ + public BadRequestException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/exception/BadRequestExceptionDetails.java b/src/main/java/com/hideyoshi/backendportfolio/util/exception/BadRequestExceptionDetails.java new file mode 100644 index 0000000..5e5d89e --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/exception/BadRequestExceptionDetails.java @@ -0,0 +1,13 @@ +package com.hideyoshi.backendportfolio.util.exception; + +import java.time.LocalDateTime; + +public class BadRequestExceptionDetails extends ExceptionDetails { + + public BadRequestExceptionDetails(final String title, final Integer status, + final String details, final String developerMessage, + final LocalDateTime timestamp) { + super(title, status, details, developerMessage, timestamp); + } + +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/exception/ExceptionDetails.java b/src/main/java/com/hideyoshi/backendportfolio/util/exception/ExceptionDetails.java new file mode 100644 index 0000000..d902440 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/exception/ExceptionDetails.java @@ -0,0 +1,30 @@ +package com.hideyoshi.backendportfolio.util.exception; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class ExceptionDetails { + + protected String title; + + protected Integer status; + + protected String details; + + protected String developerMessage; + + protected LocalDateTime timestamp; + + public ExceptionDetails(final String title, final Integer status, final String details, final String developerMessage, final LocalDateTime timestamp) { + this.title = title; + this.status = status; + this.details = details; + this.developerMessage = developerMessage; + this.timestamp = timestamp; + } + +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/exception/ValidationExceptionDetails.java b/src/main/java/com/hideyoshi/backendportfolio/util/exception/ValidationExceptionDetails.java new file mode 100644 index 0000000..e858fc9 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/exception/ValidationExceptionDetails.java @@ -0,0 +1,25 @@ +package com.hideyoshi.backendportfolio.util.exception; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class ValidationExceptionDetails extends ExceptionDetails { + + private final String fields; + + private final String fieldsMessage; + + public ValidationExceptionDetails(final String title, final int status, + final String details, final String developerMessage, + final LocalDateTime timestamp, final String fields, + final String fieldsMessage) { + super(title, status, details, developerMessage, timestamp); + this.fields = fields; + this.fieldsMessage = fieldsMessage; + } + +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceGuard.java b/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceGuard.java new file mode 100644 index 0000000..f3b4098 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceGuard.java @@ -0,0 +1,14 @@ +package com.hideyoshi.backendportfolio.util.guard; + +import java.lang.annotation.*; + +@Target( ElementType.METHOD ) +@Retention( RetentionPolicy.RUNTIME ) +@Documented +public @interface UserResourceGuard { + + String denialMessage() default "Operation not permitted. You don't have access to this Resource."; + + UserResourceGuardEnum accessType(); + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceGuardEnum.java b/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceGuardEnum.java new file mode 100644 index 0000000..1cc77e1 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceGuardEnum.java @@ -0,0 +1,111 @@ +package com.hideyoshi.backendportfolio.util.guard; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hideyoshi.backendportfolio.base.user.entity.Role; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.service.UserService; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.servlet.HandlerMapping; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; + +public enum UserResourceGuardEnum { + + USER("user") { + @Override + public Boolean hasAccess( + UserService userService, + ObjectMapper objectMapper, + HttpServletRequest request) { + return justUser(userService, objectMapper, request); + } + }, + + SAME_USER("same_user") { + @Override + public Boolean hasAccess( + UserService userService, + ObjectMapper objectMapper, + HttpServletRequest request) { + return sameUser(userService, objectMapper, request); + } + }, + + ADMIN_USER("admin_user") { + @Override + public Boolean hasAccess( + UserService userService, + ObjectMapper objectMapper, + HttpServletRequest request) { + return adminUser(userService, objectMapper, request); + } + }, + + OPEN("open") { + @Override + public Boolean hasAccess( + UserService userService, + ObjectMapper objectMapper, + HttpServletRequest request) { + return openAccess(userService, objectMapper, request); + } + }; + + private final String accessType; + + UserResourceGuardEnum(String accessType) { + this.accessType = accessType; + } + + public abstract Boolean hasAccess( + UserService userService, + ObjectMapper objectMapper, + HttpServletRequest request); + + public String getAccessType() { + return this.accessType; + } + + public static UserResourceGuardEnum byValue(String accessType) { + for (UserResourceGuardEnum o : values()) { + if (o.getAccessType().equals(accessType)) { + return o; + } + } + throw new IllegalArgumentException("Argument not valid."); + } + + private static boolean justUser(UserService userService, ObjectMapper objectMapper, HttpServletRequest request) { + + String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + UserDTO userLogged = userService.getUser(username); + + return userLogged.getAuthorities().contains(new SimpleGrantedAuthority(Role.USER.getDescription())); + } + + private static boolean sameUser(UserService userService, ObjectMapper objectMapper, HttpServletRequest request) { + String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + UserDTO userLogged = userService.getUser(username); + + Object requestPathVariable = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + HashMap pathVariable = objectMapper.convertValue(requestPathVariable, HashMap.class); + UserDTO userInfo = userService.getUser(Long.parseLong(pathVariable.get("id"))); + + return userLogged.getUsername().equals(userInfo.getUsername()); + + } + + private static boolean adminUser(UserService userService, ObjectMapper objectMapper, HttpServletRequest request) { + String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + UserDTO userLogged = userService.getUser(username); + + return userLogged.getAuthorities().contains(new SimpleGrantedAuthority(Role.ADMIN.getDescription())); + } + + private static Boolean openAccess(UserService userService, ObjectMapper objectMapper, HttpServletRequest request) { + return true; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceValidator.java b/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceValidator.java new file mode 100644 index 0000000..f76c33d --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/guard/UserResourceValidator.java @@ -0,0 +1,22 @@ +package com.hideyoshi.backendportfolio.util.guard; + +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class UserResourceValidator implements ConstraintValidator { + + @Override + public void initialize(UserResourceGuard constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(UserDTO userDTO, ConstraintValidatorContext constraintValidatorContext) { + System.out.println(SecurityContextHolder.getContext().getAuthentication()); + return false; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/handler/RestExceptionHandler.java b/src/main/java/com/hideyoshi/backendportfolio/util/handler/RestExceptionHandler.java new file mode 100644 index 0000000..2b54adc --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/handler/RestExceptionHandler.java @@ -0,0 +1,84 @@ +package com.hideyoshi.backendportfolio.util.handler; + +import com.hideyoshi.backendportfolio.util.exception.*; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Log4j2 +@ControllerAdvice +public class RestExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequest(final BadRequestException exception) { + return new ResponseEntity<>( + new BadRequestExceptionDetails("Bad Request Exception, Check the Documentation", + HttpStatus.BAD_REQUEST.value(), exception.getMessage(), + exception.getClass().getName(), LocalDateTime.now()), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(AuthenticationInvalidException.class) + public ResponseEntity handleBadRequest(final AuthenticationInvalidException exception) { + return new ResponseEntity<>( + new AuthenticationInvalidExceptionDetails("Authentication Failed. Check your credentials.", + HttpStatus.FORBIDDEN.value(), exception.getMessage(), + exception.getClass().getName(), LocalDateTime.now()), + HttpStatus.FORBIDDEN); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + final MethodArgumentNotValidException exception, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + + final List fieldErrors = exception.getBindingResult().getFieldErrors(); + final String fields = fieldErrors.stream() + .map(FieldError::getField) + .collect(Collectors.joining(", ")); + + final String fieldsMessage = fieldErrors.stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + + return new ResponseEntity<>( + new ValidationExceptionDetails("Bad Request Exception, Invalid Fields", + HttpStatus.BAD_REQUEST.value(), "Check the field(s)", + exception.getClass().getName(), LocalDateTime.now(), + fields, fieldsMessage), + HttpStatus.BAD_REQUEST); + } + + @Override + protected ResponseEntity handleExceptionInternal(final Exception exception, @Nullable final Object body, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + + String errorMessage; + if (Objects.nonNull(exception.getCause())) { + errorMessage = exception.getCause().getMessage(); + } else { + errorMessage = exception.getMessage(); + } + + final ExceptionDetails exceptionDetails = new ExceptionDetails( + errorMessage, + status.value(), + exception.getMessage(), + exception.getClass().getName(), + LocalDateTime.now() + ); + + return new ResponseEntity<>(exceptionDetails, headers, status); + } +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/unique/EmailUnique.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/unique/EmailUnique.java new file mode 100644 index 0000000..869368f --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/unique/EmailUnique.java @@ -0,0 +1,34 @@ +package com.hideyoshi.backendportfolio.util.validator.email.unique; + +import com.hideyoshi.backendportfolio.base.user.repo.UserRepository; +import lombok.RequiredArgsConstructor; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.concurrent.atomic.AtomicReference; + +@RequiredArgsConstructor +public class EmailUnique implements ConstraintValidator { + + private final UserRepository userRepository; + + @Override + public void initialize(UniqueEmail constraintAnnotation) { + } + + @Override + public boolean isValid(String email, ConstraintValidatorContext constraintValidatorContext) { + + AtomicReference emailValid = new AtomicReference(); + this.userRepository.findByEmail(email).ifPresentOrElse( + (value) -> { + emailValid.set(false); + }, + () -> { + emailValid.set(true); + } + ); + + return emailValid.get(); + } +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/unique/UniqueEmail.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/unique/UniqueEmail.java new file mode 100644 index 0000000..9be9559 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/unique/UniqueEmail.java @@ -0,0 +1,24 @@ +package com.hideyoshi.backendportfolio.util.validator.email.unique; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({TYPE, FIELD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Constraint(validatedBy = EmailUnique.class) +@Documented +public @interface UniqueEmail { + + String message() default "Email taken, please choose another"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/valid/EmailValidator.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/valid/EmailValidator.java new file mode 100644 index 0000000..5881814 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/valid/EmailValidator.java @@ -0,0 +1,32 @@ +package com.hideyoshi.backendportfolio.util.validator.email.valid; + +import lombok.RequiredArgsConstructor; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@RequiredArgsConstructor +public class EmailValidator implements ConstraintValidator { + + private Pattern pattern; + private Matcher matcher; + private static final String EMAIL_PATTERN = "^[_A-Za-z\\d-+]+(.[_A-Za-z\\d-]+)*@+[A-Za-z\\d-]+(.[A-Za-z\\d]+)*(.[A-Za-z]{2,})$"; + + @Override + public void initialize(ValidEmail constraintAnnotation) { + } + + @Override + public boolean isValid(String email, ConstraintValidatorContext context){ + return (validateEmail(email)); + } + + private boolean validateEmail(String email) { + pattern = Pattern.compile(EMAIL_PATTERN); + matcher = pattern.matcher(email); + + return matcher.matches(); + } +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/valid/ValidEmail.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/valid/ValidEmail.java new file mode 100644 index 0000000..8d4e920 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/email/valid/ValidEmail.java @@ -0,0 +1,25 @@ +package com.hideyoshi.backendportfolio.util.validator.email.valid; + +import com.hideyoshi.backendportfolio.util.validator.email.valid.EmailValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({TYPE, FIELD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Constraint(validatedBy = EmailValidator.class) +@Documented +public @interface ValidEmail { + + String message() default "Invalid email"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java new file mode 100644 index 0000000..e63d3c1 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java @@ -0,0 +1,25 @@ +package com.hideyoshi.backendportfolio.util.validator.password; + +import lombok.RequiredArgsConstructor; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +@RequiredArgsConstructor +public class PasswordValidator implements ConstraintValidator { + + private final String PASSWORD_PATTERN = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; + + @Override + public void initialize(ValidPassword constraintAnnotation) {} + + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + + return Pattern.compile(PASSWORD_PATTERN) + .matcher(password) + .matches(); + + } +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java new file mode 100644 index 0000000..35cc646 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java @@ -0,0 +1,23 @@ +package com.hideyoshi.backendportfolio.util.validator.password; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({TYPE, FIELD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Constraint(validatedBy = PasswordValidator.class) +@Documented +public @interface ValidPassword { + + String message() default "Invalid password"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ddeab3e --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,42 @@ +com: + hideyoshi: + frontEndPath: ${FRONTEND_PATH}} + frontendConnectionType: ${FRONTEND_CONNECTION_TYPE}} + tokenSecret: ${TOKEN_SECRET} + accessTokenDuration: ${ACCESS_TOKEN_DURATION} + refreshTokenDuration: ${REFRESH_TOKEN_DURATION} + defaultUser: + fullName: ${DEFAULT_USER_FULLNAME} + email: ${DEFAULT_USER_EMAIL} + username: ${DEFAULT_USER_USERNAME} + password: ${DEFAULT_USER_PASSWORD} + + +server: + port: ${PORT} + +spring: + + datasource: + url: jdbc:${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + + session: + store: + type: redis + persistent: true + + redis: + host: ${REDIS_URL} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} + + jpa: + open-in-view: false + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true \ No newline at end of file diff --git a/src/main/resources/db/changelog/client/db.changelog-client.yml b/src/main/resources/db/changelog/client/db.changelog-client.yml new file mode 100644 index 0000000..3ba2038 --- /dev/null +++ b/src/main/resources/db/changelog/client/db.changelog-client.yml @@ -0,0 +1,11 @@ +databaseChangeLog: + + - changeSet: + id: db-table-model-client + author: vitor.h.n.batista@gmail.com + changes: + - sqlFile: + encoding: utf8 + path: sqls/db-table-model-client.sql + relativeToChangelogFile: true + dbms: postgresql \ No newline at end of file diff --git a/src/main/resources/db/changelog/client/sqls/db-table-model-client.sql b/src/main/resources/db/changelog/client/sqls/db-table-model-client.sql new file mode 100644 index 0000000..784ab23 --- /dev/null +++ b/src/main/resources/db/changelog/client/sqls/db-table-model-client.sql @@ -0,0 +1,21 @@ +CREATE SCHEMA IF NOT EXISTS auth; + +CREATE SEQUENCE IF NOT EXISTS auth.user_seq + INCREMENT 1 + MINVALUE 1 + MAXVALUE 9223372036854775807 + START 1 + CACHE 1; + +CREATE TABLE IF NOT EXISTS auth.user ( + id BIGINT NOT NULL DEFAULT NEXTVAL('auth.user_seq'), + full_name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + username VARCHAR(20) NOT NULL, + password VARCHAR(100) NOT NULL, + roles VARCHAR(50) NOT NULL DEFAULT 'ROLE_USER', + + CONSTRAINT client_primary_key PRIMARY KEY (id), + CONSTRAINT client_email_unique UNIQUE (email), + CONSTRAINT client_username_unique UNIQUE (username) +); \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..84f82d3 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,6 @@ + + +databaseChangeLog: + + - include: + file: db/changelog/client/db.changelog-client.yml \ No newline at end of file diff --git a/src/test/java/com/hideyoshi/backendportfolio/BackendPortfolioApplicationTests.java b/src/test/java/com/hideyoshi/backendportfolio/BackendPortfolioApplicationTests.java new file mode 100644 index 0000000..de52fb2 --- /dev/null +++ b/src/test/java/com/hideyoshi/backendportfolio/BackendPortfolioApplicationTests.java @@ -0,0 +1,14 @@ +package com.hideyoshi.backendportfolio; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.SpringBootTest; + +@DataJpaTest +class BackendPortfolioApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java b/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java new file mode 100644 index 0000000..0e526c4 --- /dev/null +++ b/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java @@ -0,0 +1,70 @@ +package com.hideyoshi.backendportfolio.base.user.repo; + +import com.hideyoshi.backendportfolio.base.user.entity.Role; +import com.hideyoshi.backendportfolio.base.user.entity.User; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@Log4j2 +@DataJpaTest +class UserRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private UserRepository underTest; + + @Test + void savesUserToDataBase() { + // Given + User user = this.createEntity(); + // When + User userSaved = this.underTest.save(user); + log.info(userSaved.getUsername()); + // Then + assertThat(userSaved).isNotNull(); + assertThat(userSaved).isEqualTo(user); + } + + @Test + void canFindsUserByUsername() { + // Given + User userSaved = this.entityManager.persist(this.createEntity()); + this.underTest.findAll(); + // When + Optional userOnDB = + this.underTest.findByUsername(userSaved.getUsername()); + // Then + assertThat(userOnDB).isNotEmpty(); + assertThat(userOnDB).hasValue(userSaved); + } + + @Test + void cannotFindUserByUsername() { + // When + Optional userOnDB = this.underTest.findByUsername("Batman"); + // Then + assertThat(userOnDB).isEmpty(); + } + + private User createEntity() { + return new UserDTO( + "Clark Kent", + "superman@gmail.com", + "Superman", + "password", + List.of(Role.USER) + ).toEntity(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java b/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java new file mode 100644 index 0000000..033a427 --- /dev/null +++ b/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java @@ -0,0 +1,363 @@ +package com.hideyoshi.backendportfolio.base.user.service; + +import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import com.hideyoshi.backendportfolio.base.user.entity.Role; +import com.hideyoshi.backendportfolio.base.user.entity.User; +import com.hideyoshi.backendportfolio.base.user.model.UserDTO; +import com.hideyoshi.backendportfolio.base.user.repo.UserRepository; +import com.hideyoshi.backendportfolio.util.exception.BadRequestException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.annotation.DirtiesContext; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +@DataJpaTest +@ExtendWith(MockitoExtension.class) +@DirtiesContext(classMode= DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class UserServiceImplTest { + + @InjectMocks + private UserServiceImpl underTest; + + @Mock + private UserRepository userRepository; + + private PasswordEncoder passwordEncoder; + + private AuthService authService; + + + @BeforeEach + void setUp() { + this.passwordEncoder = new BCryptPasswordEncoder(); + this.underTest = new UserServiceImpl(userRepository,passwordEncoder); + } + + @Test + void canSaveUser() { + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(null)); + + BDDMockito.when(userRepository.save(ArgumentMatchers.any(User.class))) + .thenReturn(createUser().toEntity()); + + // Given + UserDTO user = this.createUser(); + // When + UserDTO userSaved = this.underTest.saveUser(user); + //Then + ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); + + verify(userRepository).save(userArgumentCaptor.capture()); + assertThat(userArgumentCaptor.getValue()).isEqualTo(user.toEntity()); + assertThat(userSaved).isInstanceOf(UserDTO.class); + } + + @Test + void cannotSaveUser() { + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + + // Given + UserDTO user = this.createUser(); + // When + //Then + assertThrows( + BadRequestException.class, + () -> { + this.underTest.saveUser(user); + }, + "Excepts a BadRequestException to be thrown." + ); + } + + @Test + void canAlterUser() { + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + // Given + UserDTO user = this.createUser(); + // When + this.underTest.alterUser(1L, user); + //Then + ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); + + verify(userRepository).save(userArgumentCaptor.capture()); + assertThat(userArgumentCaptor.getValue()).isEqualTo(user.toEntity()); + } + + @Test + void cannotAlterUserDoesntExist() { + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(null)); + + // Given + UserDTO user = this.createUser(); + // When + //Then + assertThrows( + BadRequestException.class, + () -> { + this.underTest.alterUser(1L, user); + }, + "User doesn't exist." + ); + } + + @Test + void canAddRoleToUser() { + UserDTO user = this.createUser(); + + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(user.toEntity())); + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + // Given + UserDTO userSaved = this.underTest.getUser(user.getUsername()); + if (!Objects.nonNull(userSaved)) { + userSaved = this.underTest.saveUser(user); + } + // When + this.underTest.addRoleToUser(userSaved.getId(), Role.USER.getDescription()); + // Then + userSaved = this.underTest.getUser(userSaved.getUsername()); + assertTrue(userSaved.getRoles().stream().anyMatch(e -> Role.USER.equals(e))); + } + + @Test + void cannotAddRoleToUserDoesntExist() { + + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(null)); + + // Given + UserDTO user = this.createUser(); + // When + // Then + UserDTO finalUserSaved = user; + assertThrows( + BadRequestException.class, + () -> { + this.underTest.addRoleToUser(finalUserSaved.getId(), Role.USER.getDescription()); + }, + "User not found. Error while adding role." + ); + } + + @Test + void cannotAddRoleToUserRoleDoesntExist() { + UserDTO user = this.createUser(); + + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(user.toEntity())); + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + // Given + UserDTO userSaved = this.underTest.getUser(user.getUsername()); + if (!Objects.nonNull(userSaved)) { + userSaved = this.underTest.saveUser(user); + } + // When + // Then + UserDTO finalUserSaved = userSaved; + assertThrows( + IllegalArgumentException.class, + () -> { + this.underTest.addRoleToUser(finalUserSaved.getId(), "BANANA"); + }, + "Argument not valid." + ); + } + + @Test + void canRemoveRoleFromUser() { + UserDTO user = this.createUser(); + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(user.toEntity())); + + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(user.toEntity())); + + BDDMockito.when(userRepository.save(ArgumentMatchers.any(User.class))) + .thenReturn(createUser().toEntity()); + + // Given + UserDTO userSaved = this.underTest.getUser(user.getUsername()); + if (!Objects.nonNull(userSaved)) { + userSaved = this.underTest.saveUser(user); + } + this.underTest.addRoleToUser(userSaved.getId(), Role.USER.getDescription()); + // When + this.underTest.removeRoleFromUser(userSaved.getId(), Role.USER.getDescription()); + // Then + ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); + + verify(userRepository).save(userArgumentCaptor.capture()); + + assertThat(userArgumentCaptor.getValue()).hasSameClassAs(user.toEntity()); + assertFalse(userArgumentCaptor.getValue().getRoles().stream().anyMatch(e -> Role.USER.equals(e))); + } + + @Test + void cannotRemoveRoleFromUserDoesntExist() { + UserDTO user = this.createUser(); + + BDDMockito.when(userRepository.findById(ArgumentMatchers.any(Long.class))) + .thenReturn(Optional.ofNullable(user.toEntity())); + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + // Given + UserDTO userSaved = this.underTest.getUser(user.getUsername()); + if (!Objects.nonNull(userSaved)) { + userSaved = this.underTest.saveUser(user); + } + this.underTest.addRoleToUser(userSaved.getId(), Role.USER.getDescription()); + // When + // Then + UserDTO finalUserSaved = userSaved; + assertThrows( + IllegalArgumentException.class, + () -> { + this.underTest.removeRoleFromUser(finalUserSaved.getId(), "BANANA"); + }, + "Argument not valid." + ); + } + + @Test + void cannotRemoveRoleFromUserRoleDoesntExist() { + // Given + UserDTO user = this.createUser(); + // When + // Then + UserDTO finalUserSaved = user; + assertThrows( + BadRequestException.class, + () -> { + this.underTest.removeRoleFromUser(finalUserSaved.getId(), Role.USER.getDescription()); + }, + "User not found. Error while adding role." + ); + } + + @Test + void canGetUser() { + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + // Given + UserDTO user = this.createUser(); + // When + UserDTO userOnDB = this.underTest.getUser(user.getUsername()); + // Then + ArgumentCaptor usernameArgumentCaptor = ArgumentCaptor.forClass(String.class); + verify(userRepository).findByUsername(usernameArgumentCaptor.capture()); + + assertThat(userOnDB).isNotNull(); + assertThat(userOnDB).isInstanceOf(UserDTO.class); + assertThat(user.getUsername()).isEqualTo(usernameArgumentCaptor.getValue()); + } + + @Test + void cannotGetUser() { + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(null)); + + // Given + UserDTO user = this.createUser(); + // When + //Then + assertThrows( + BadRequestException.class, + () -> { + this.underTest.getUser(user.getUsername()); + }, + "Excepts a BadRequestException to be thrown." + ); + } + + @Test + void canGetUsers() { + List users = this.underTest.getUsers(); + assertThat(users).isNotNull(); + } + + @Test + void canLoadUserByUsername() { + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(createUser().toEntity())); + + // Given + UserDTO user = this.createUser(); + // When + UserDTO userOnDB = (UserDTO) this.underTest.loadUserByUsername(user.getUsername()); + // Then + ArgumentCaptor usernameArgumentCaptor = ArgumentCaptor.forClass(String.class); + verify(userRepository).findByUsername(usernameArgumentCaptor.capture()); + + assertThat(userOnDB).isNotNull(); + assertThat(userOnDB).isInstanceOf(UserDetails.class); + assertThat(user.getUsername()).isEqualTo(usernameArgumentCaptor.getValue()); + } + + @Test + void cannotLoadUserByUsername() { + + BDDMockito.when(userRepository.findByUsername(ArgumentMatchers.any(String.class))) + .thenReturn(Optional.ofNullable(null)); + + // Given + UserDTO user = this.createUser(); + // When + //Then + assertThrows( + BadRequestException.class, + () -> { + this.underTest.loadUserByUsername(user.getUsername()); + }, + "User Not Found. Please create an Account." + ); + } + + private UserDTO createUser() { + UserDTO userCreated = new UserDTO( + "Clark Kent", + "superman@gmail.com", + "Superman", + "password", + List.of(Role.USER) + ); + userCreated.setId(1L); + return userCreated; + } + +} \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..e2daa91 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,35 @@ +com: + + hideyoshi: + frontendPath: localhost:4200 + frontendConnectionType: unsecure + tokenSecret: secret + accessTokenDuration: 1800000 + refreshTokenDuration: 1314900000 + + defaultUser: + fullName: "Vitor Hideyoshi" + email: "vitor.h.n.batista@gmail.com" + username: "YoshiUnfriendly" + password: "passwd" + +spring: + + liquibase: + enabled: false + + datasource: + jdbc: + url: jdbc:h2:mem:testdb + user: sa + password: sa + driver_class: org.h2.Driver + + jpa: + open-in-view: false + hibernate: + ddl-auto: update + properties: + hibernate: +# dialect: org.hibernate.dialect.H2Dialect + format_sql: true \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..27e6255 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS auth; \ No newline at end of file