From 69d50cb6c3a80c9e1761c7eab66426a1631a77a9 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:12:55 +0300 Subject: [PATCH 01/85] init server-java/gradle --- packages/server-java/build.gradle | 76 +++++++ packages/server-java/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + packages/server-java/gradlew | 188 ++++++++++++++++++ packages/server-java/gradlew.bat | 100 ++++++++++ packages/server-java/settings.gradle | 6 + 7 files changed, 377 insertions(+) create mode 100644 packages/server-java/build.gradle create mode 100644 packages/server-java/gradle.properties create mode 100644 packages/server-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 packages/server-java/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/server-java/gradlew create mode 100644 packages/server-java/gradlew.bat create mode 100644 packages/server-java/settings.gradle diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle new file mode 100644 index 0000000000..31e293b5a4 --- /dev/null +++ b/packages/server-java/build.gradle @@ -0,0 +1,76 @@ +buildscript { + ext { + springBootVersion = '2.3.1.RELEASE' + } + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +subprojects { + apply plugin: 'idea' + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'org.springframework.boot' + + group = 'com.sysgears' + version = 'release' + + sourceCompatibility = 11 + + jar { + enabled = true + } + + bootJar { + launchScript() + mainClassName = 'com.sysgears.Application' + } + + repositories { + mavenCentral() + jcenter() + maven { url "https://repo.spring.io/milestone" } + maven { url "https://plugins.gradle.org/m2/" } + } + + dependencies { + //Spring Boot + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + compileOnly 'org.springframework.boot:spring-boot-configuration-processor' + + //Lombok + compileOnly "org.projectlombok:lombok:1.18.12" + annotationProcessor "org.projectlombok:lombok:1.18.12" + + //H2 DataBase + implementation 'com.h2database:h2:1.4.200' + + //GraphQL + implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.1.0' + runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:7.1.0' + implementation 'com.graphql-java-kickstart:graphql-java-tools:6.1.0' + + implementation "io.reactivex.rxjava2:rxjava" + + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + } + + test { + useJUnitPlatform() + } +} + +project(':app') { + dependencies { + implementation project(':counter') + implementation project(':user') + } +} \ No newline at end of file diff --git a/packages/server-java/gradle.properties b/packages/server-java/gradle.properties new file mode 100644 index 0000000000..4334c11fef --- /dev/null +++ b/packages/server-java/gradle.properties @@ -0,0 +1 @@ +kotlin.version=1.3.70 \ No newline at end of file diff --git a/packages/server-java/gradle/wrapper/gradle-wrapper.jar b/packages/server-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java dto to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' dto could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a balance-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java dto, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/packages/server-java/gradlew.bat b/packages/server-java/gradlew.bat new file mode 100644 index 0000000000..9618d8d960 --- /dev/null +++ b/packages/server-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem 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, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle new file mode 100644 index 0000000000..63c1b690f2 --- /dev/null +++ b/packages/server-java/settings.gradle @@ -0,0 +1,6 @@ +rootProject.name = 'server-java' +include ':app', ':core', ':counter', ':user' + +project(':core').projectDir = new File('../../modules/core/server-java') +project(':counter').projectDir = new File('../../modules/counter/server-java') +project(':user').projectDir = new File('../../modules/user/server-java') From 82059c216b6664ad9f9a90762b231bf72af9fc92 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:13:51 +0300 Subject: [PATCH 02/85] added .gitignore --- packages/server-java/.gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/server-java/.gitignore diff --git a/packages/server-java/.gitignore b/packages/server-java/.gitignore new file mode 100644 index 0000000000..62b39ddbad --- /dev/null +++ b/packages/server-java/.gitignore @@ -0,0 +1,13 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +/target \ No newline at end of file From 5e6003f5b3008585d0d9f36593dec6572902c7d9 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:19:54 +0300 Subject: [PATCH 03/85] added Dockerfile for java --- Dockerfile-java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Dockerfile-java diff --git a/Dockerfile-java b/Dockerfile-java new file mode 100644 index 0000000000..38fc70760f --- /dev/null +++ b/Dockerfile-java @@ -0,0 +1,12 @@ +FROM openjdk:11-jdk as BUILD_IMAGE +COPY ./packages/server-java ./packages/server-java +COPY ./modules ./modules + +WORKDIR ./packages/server-java +RUN ./gradlew clean bootJar + +FROM openjdk:11-jre-slim +WORKDIR /server-java/ +COPY --from=BUILD_IMAGE /packages/server-java/app/build/libs/app-release.jar . +EXPOSE 8080 +CMD ["java", "-jar", "app-release.jar"] \ No newline at end of file From 049b827ce657137088dc196fac414cfe582f71fc Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:21:16 +0300 Subject: [PATCH 04/85] added docker-compose with react and java --- docker-compose.react-java.yml | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docker-compose.react-java.yml diff --git a/docker-compose.react-java.yml b/docker-compose.react-java.yml new file mode 100644 index 0000000000..1b9ef5c0e9 --- /dev/null +++ b/docker-compose.react-java.yml @@ -0,0 +1,53 @@ +version: '3' +services: + + apollo_java_server: + build: + context: . + dockerfile: Dockerfile-java + environment: + - JAVA_ENV=development + container_name: apollo_java_server + working_dir: ${APP_DIR}/packages/server-java + volumes: + - ./:${APP_DIR} + - ./modules:${APP_DIR}/modules + - ./packages/server-java/gradle/wrapper:${APP_DIR}/packages/server-java/gradle/wrapper + - ./packages/server-java/gradlew:${APP_DIR}/packages/server-java/gradlew + command: > + sh -c './gradlew clean bootRun' + # sh -c './gradlew clean' + ports: + - 8080:8080 + stdin_open: true + tty: true + network_mode: host + + apollo_client: + build: + context: . + dockerfile: Dockerfile + args: + APP_DIR: ${APP_DIR} + depends_on: + - apollo_java_server + environment: + - NODE_ENV=development + - SERVER_HOST=127.0.0.1:8080 + container_name: apollo_client + tty: true + stdin_open: true + volumes: + - ./:${APP_DIR} + - ${APP_DIR}/build + - client_node_modules:${APP_DIR}/node_modules + working_dir: ${APP_DIR} + user: node + command: > + sh -c 'cmp -s yarn.lock node_modules/yarn.lock || yarn install --frozen-lockfile && cp -f yarn.lock node_modules/yarn.lock && yarn watch-client' + ports: + - 3000:3000 + network_mode: host + +volumes: + client_node_modules: From de0d324f153b3d6ff0d11d7ff1fad08f2c9998dd Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:22:24 +0300 Subject: [PATCH 05/85] init spring boot app with default configs --- packages/server-java/app/build.gradle | 0 .../main/java/com/sysgears/Application.java | 11 ++++++ .../app/src/main/resources/application.yml | 35 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 packages/server-java/app/build.gradle create mode 100644 packages/server-java/app/src/main/java/com/sysgears/Application.java create mode 100644 packages/server-java/app/src/main/resources/application.yml diff --git a/packages/server-java/app/build.gradle b/packages/server-java/app/build.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server-java/app/src/main/java/com/sysgears/Application.java b/packages/server-java/app/src/main/java/com/sysgears/Application.java new file mode 100644 index 0000000000..7d7d7a77cf --- /dev/null +++ b/packages/server-java/app/src/main/java/com/sysgears/Application.java @@ -0,0 +1,11 @@ +package com.sysgears; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"com.sysgears"}) +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml new file mode 100644 index 0000000000..252a451ff3 --- /dev/null +++ b/packages/server-java/app/src/main/resources/application.yml @@ -0,0 +1,35 @@ +spring: + jpa: + hibernate: + ddl-auto: update + open-in-view: false + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + show-sql: true + datasource: + driver-class-name: org.h2.Driver + password: '' + url: jdbc:h2:mem:apollo + username: sa + h2: + console: + enabled: true + path: /h2-console +graphql: + servlet: + async-mode-enabled: true + exception-handlers-enabled: true + cors: + allowed-origins: "*" + allow-credentials: true + subscriptions: + apollo: + keep-alive-enabled: false + websocket: + path: /graphql +graphiql: + enabled: true + endpoint: + subscriptions: /graphql + mapping: /graphiql \ No newline at end of file From 2effec0a46af2012f8bc9c52590c3c6d017095ac Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:23:29 +0300 Subject: [PATCH 06/85] added core module --- modules/core/server-java/.gitignore | 3 +++ modules/core/server-java/build.gradle | 0 2 files changed, 3 insertions(+) create mode 100644 modules/core/server-java/.gitignore create mode 100644 modules/core/server-java/build.gradle diff --git a/modules/core/server-java/.gitignore b/modules/core/server-java/.gitignore new file mode 100644 index 0000000000..a42eb305c6 --- /dev/null +++ b/modules/core/server-java/.gitignore @@ -0,0 +1,3 @@ +target/ +/.idea +/build \ No newline at end of file diff --git a/modules/core/server-java/build.gradle b/modules/core/server-java/build.gradle new file mode 100644 index 0000000000..e69de29bb2 From 3acdca559b668f1ab712953a99f6eacb74b253ef Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:24:06 +0300 Subject: [PATCH 07/85] added default graphQL schema --- .../src/main/resources/schema.graphqls | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 modules/core/server-java/src/main/resources/schema.graphqls diff --git a/modules/core/server-java/src/main/resources/schema.graphqls b/modules/core/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..3664fe44c5 --- /dev/null +++ b/modules/core/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,15 @@ +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +type Query { + +} +type Mutation { + +} +type Subscription { + +} \ No newline at end of file From dea5219b1cd0fcbdb40046dadb9b5a6a812999db Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:26:34 +0300 Subject: [PATCH 08/85] added publisher and subscriber for graphQL subscription --- .../subscription/AbstractPubSubService.java | 32 +++++++++++++++++++ .../sysgears/core/subscription/Publisher.java | 5 +++ .../core/subscription/Subscriber.java | 8 +++++ 3 files changed, 45 insertions(+) create mode 100644 modules/core/server-java/src/main/java/com/sysgears/core/subscription/AbstractPubSubService.java create mode 100644 modules/core/server-java/src/main/java/com/sysgears/core/subscription/Publisher.java create mode 100644 modules/core/server-java/src/main/java/com/sysgears/core/subscription/Subscriber.java diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/subscription/AbstractPubSubService.java b/modules/core/server-java/src/main/java/com/sysgears/core/subscription/AbstractPubSubService.java new file mode 100644 index 0000000000..289b2de436 --- /dev/null +++ b/modules/core/server-java/src/main/java/com/sysgears/core/subscription/AbstractPubSubService.java @@ -0,0 +1,32 @@ +package com.sysgears.core.subscription; + +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.functions.Predicate; +import io.reactivex.observables.ConnectableObservable; + +public abstract class AbstractPubSubService implements Publisher, Subscriber { + private final Flowable publisher; + private ObservableEmitter emitter; + + public AbstractPubSubService() { + Observable observable = Observable.create(emitter -> this.emitter = emitter); + + ConnectableObservable connectableObservable = observable.share().publish(); + connectableObservable.connect(); + + publisher = connectableObservable.toFlowable(BackpressureStrategy.BUFFER); + } + + @Override + public Flowable subscribe(Predicate predicate) { + return this.publisher.filter(predicate); + } + + @Override + public void publish(T event) { + this.emitter.onNext(event); + } +} diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/subscription/Publisher.java b/modules/core/server-java/src/main/java/com/sysgears/core/subscription/Publisher.java new file mode 100644 index 0000000000..d8422899c1 --- /dev/null +++ b/modules/core/server-java/src/main/java/com/sysgears/core/subscription/Publisher.java @@ -0,0 +1,5 @@ +package com.sysgears.core.subscription; + +public interface Publisher { + void publish(T event); +} diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/subscription/Subscriber.java b/modules/core/server-java/src/main/java/com/sysgears/core/subscription/Subscriber.java new file mode 100644 index 0000000000..3d4e0e29d7 --- /dev/null +++ b/modules/core/server-java/src/main/java/com/sysgears/core/subscription/Subscriber.java @@ -0,0 +1,8 @@ +package com.sysgears.core.subscription; + +import io.reactivex.Flowable; +import io.reactivex.functions.Predicate; + +public interface Subscriber { + Flowable subscribe(Predicate predicate); +} From 1e618dba6add13c9e64bc73964ebf850a5fb523d Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:29:23 +0300 Subject: [PATCH 09/85] init counter module --- modules/counter/server-java/.gitignore | 1 + modules/counter/server-java/build.gradle | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 modules/counter/server-java/.gitignore create mode 100644 modules/counter/server-java/build.gradle diff --git a/modules/counter/server-java/.gitignore b/modules/counter/server-java/.gitignore new file mode 100644 index 0000000000..ac96206f31 --- /dev/null +++ b/modules/counter/server-java/.gitignore @@ -0,0 +1 @@ +.gradle/ \ No newline at end of file diff --git a/modules/counter/server-java/build.gradle b/modules/counter/server-java/build.gradle new file mode 100644 index 0000000000..378848af7c --- /dev/null +++ b/modules/counter/server-java/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation project(':core') +} \ No newline at end of file From 13af7675adeb1206a4205bd6c12d8d3a66293bff Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:31:18 +0300 Subject: [PATCH 10/85] added counter graphQL schema --- .../server-java/src/main/resources/schema.graphqls | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 modules/counter/server-java/src/main/resources/schema.graphqls diff --git a/modules/counter/server-java/src/main/resources/schema.graphqls b/modules/counter/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..d94a0026e2 --- /dev/null +++ b/modules/counter/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,14 @@ +extend type Query { + serverCounter: Counter! +} + +extend type Mutation { + addServerCounter(amount: Int!): Counter +} + +extend type Subscription { + counterUpdated: Counter +} +type Counter { + amount: Int! +} From 66f41daf450e6b7eb4993ed10ba75140927331f6 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:34:39 +0300 Subject: [PATCH 11/85] added counter model, repository + db initializer --- .../counter/CounterDBInitializer.java | 25 +++++++++++++++ .../counter/constant/CounterConstants.java | 8 +++++ .../com/sysgears/counter/model/Counter.java | 31 +++++++++++++++++++ .../counter/repository/CounterRepository.java | 11 +++++++ 4 files changed, 75 insertions(+) create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/CounterDBInitializer.java create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/model/Counter.java create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/CounterDBInitializer.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/CounterDBInitializer.java new file mode 100644 index 0000000000..276fb10382 --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/CounterDBInitializer.java @@ -0,0 +1,25 @@ +package com.sysgears.counter; + +import com.sysgears.counter.model.Counter; +import com.sysgears.counter.repository.CounterRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CounterDBInitializer { + private final CounterRepository repository; + + @EventListener + public void onApplicationStartedEvent(ApplicationStartedEvent event) { + long count = repository.count(); + if (count == 0) { + repository.save(new Counter(1)); + log.debug("Counter initialized"); + } + } +} \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java new file mode 100644 index 0000000000..4f0d395c8a --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java @@ -0,0 +1,8 @@ +package com.sysgears.counter.constant; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class CounterConstants { + public static final int ID = 1; +} \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/model/Counter.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/model/Counter.java new file mode 100644 index 0000000000..9af016f228 --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/model/Counter.java @@ -0,0 +1,31 @@ +package com.sysgears.counter.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "COUNTER") +public class Counter { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + @Column(name = "ID") + private int id; + + @Column(name = "AMOUNT") + private int amount; + + public Counter(int amount) { + this.amount = amount; + } + + public Counter increaseAmount(int amount) { + this.amount += amount; + return this; + } +} \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java new file mode 100644 index 0000000000..9a94d20a07 --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java @@ -0,0 +1,11 @@ +package com.sysgears.counter.repository; + +import com.sysgears.counter.model.Counter; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.concurrent.CompletableFuture; + +public interface CounterRepository extends JpaRepository { + + CompletableFuture findById(int id); +} \ No newline at end of file From f7022b07672102e5a7295fc82ea414a70309d804 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:35:05 +0300 Subject: [PATCH 12/85] added counter pub/sub service --- .../sysgears/counter/resolvers/CounterPubSubService.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java new file mode 100644 index 0000000000..23d237e584 --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java @@ -0,0 +1,9 @@ +package com.sysgears.counter.resolvers; + +import com.sysgears.core.subscription.AbstractPubSubService; +import com.sysgears.counter.model.Counter; +import org.springframework.stereotype.Component; + +@Component +public class CounterPubSubService extends AbstractPubSubService { +} From b496f6b66aa35b189b56f9ac4ecc4f3be21a6c5a Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:37:39 +0300 Subject: [PATCH 13/85] added counter graphQL resolvers --- .../resolvers/CounterMutationResolver.java | 35 +++++++++++++++++++ .../resolvers/CounterQueryResolver.java | 23 ++++++++++++ .../CounterSubscriptionResolver.java | 21 +++++++++++ 3 files changed, 79 insertions(+) create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java create mode 100644 modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java new file mode 100644 index 0000000000..a1f5a92b70 --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java @@ -0,0 +1,35 @@ +package com.sysgears.counter.resolvers; + +import com.sysgears.core.subscription.Publisher; +import com.sysgears.counter.constant.CounterConstants; +import com.sysgears.counter.model.Counter; +import com.sysgears.counter.repository.CounterRepository; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CounterMutationResolver implements GraphQLMutationResolver { + private final CounterRepository repository; + private final Publisher publisher; + + @Transactional + public CompletableFuture addServerCounter(int amount) { + log.debug("Updating counter"); + return repository.findById(CounterConstants.ID) + .thenApply( + counter -> { + counter.increaseAmount(amount); + repository.save(counter); + publisher.publish(counter); + return counter; + } + ); + } +} \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java new file mode 100644 index 0000000000..ff0066b16c --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java @@ -0,0 +1,23 @@ +package com.sysgears.counter.resolvers; + +import com.sysgears.counter.constant.CounterConstants; +import graphql.kickstart.tools.GraphQLQueryResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.sysgears.counter.model.Counter; +import org.springframework.stereotype.Service; +import com.sysgears.counter.repository.CounterRepository; + +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CounterQueryResolver implements GraphQLQueryResolver { + private final CounterRepository repository; + + public CompletableFuture serverCounter() { + log.debug("Get counter"); + return repository.findById(CounterConstants.ID); + } +} diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java new file mode 100644 index 0000000000..51f535650f --- /dev/null +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java @@ -0,0 +1,21 @@ +package com.sysgears.counter.resolvers; + +import com.sysgears.core.subscription.Subscriber; +import com.sysgears.counter.model.Counter; +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CounterSubscriptionResolver implements GraphQLSubscriptionResolver { + private final Subscriber subscriber; + + public Publisher counterUpdated() { + log.debug("Subscribing counter updates"); + return subscriber.subscribe(o -> true); + } +} From 1ef76c7b465a99b78db33a875c5db36cc4689e24 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:40:18 +0300 Subject: [PATCH 14/85] added user module + graphQL schema --- modules/user/server-java/build.gradle | 4 + .../src/main/resources/schema.graphqls | 160 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 modules/user/server-java/build.gradle create mode 100644 modules/user/server-java/src/main/resources/schema.graphqls diff --git a/modules/user/server-java/build.gradle b/modules/user/server-java/build.gradle new file mode 100644 index 0000000000..4c19fb697c --- /dev/null +++ b/modules/user/server-java/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':core') + implementation 'org.springframework.boot:spring-boot-starter-security' +} \ No newline at end of file diff --git a/modules/user/server-java/src/main/resources/schema.graphqls b/modules/user/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..344aabe264 --- /dev/null +++ b/modules/user/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,160 @@ +extend type Query { + # Get all users ordered by: OrderByUserInput add filtered by: FilterUserInput + users(orderBy: OrderByUserInput, filter: FilterUserInput): [User] + # Get user by id + user(id: Int!): UserPayload + # Get current user + currentUser: User +} + + +extend type Mutation { + # Create new user + addUser(input: AddUserInput!): UserPayload! + # Edit a user + editUser(input: EditUserInput!): UserPayload! + # Delete a user + deleteUser(id: Int!): UserPayload! +} + +extend type Subscription { + # Subscription for users list + usersUpdated(filter: FilterUserInput): UpdateUserPayload +} + +type User { + id: Int! + username: String! + role: String! + isActive: Boolean + email: String! + profile: UserProfile + auth: UserAuth +} + +type UserProfile { + firstName: String + lastName: String + fullName: String +} + +type UserAuth { + certificate: CertificateAuth + facebook: FacebookAuth + google: GoogleAuth + github: GithubAuth + linkedin: LinkedInAuth +} + +type CertificateAuth { + serial: String +} + +type FacebookAuth { + fbId: String + displayName: String +} + +type GoogleAuth { + googleId: String + displayName: String +} + +type GithubAuth { + ghId: String + displayName: String +} + +type LinkedInAuth { + lnId: String + displayName: String +} + +type UserPayload { + user: User +} + +# Input for ordering users +input OrderByUserInput { + # id | username | role | isActive | email + column: String + # asc | desc + order: String +} + +# Input for filtering users +input FilterUserInput { + # search by username or email + searchText: String + # filter by role + role: String + # filter by isActive + isActive: Boolean +} +# +# Additional authentication service info +input AuthInput { + certificate: AuthCertificateInput + facebook: AuthFacebookInput + google: AuthGoogleInput + github: AuthGitHubInput + linkedin: AuthLinkedInInput +} + +input AuthCertificateInput { + serial: String +} + +input AuthFacebookInput { + fbId: String + displayName: String +} + +input AuthGoogleInput { + googleId: String + displayName: String +} + +input AuthGitHubInput { + ghId: String + displayName: String +} + +input AuthLinkedInInput { + lnId: String + displayName: String +} + +# Input for addUser Mutation +input AddUserInput { + username: String! + email: String! + password: String! + role: String! + isActive: Boolean + profile: ProfileInput + auth: AuthInput +} + +# Input for editUser Mutation +input EditUserInput { + id: Int! + username: String! + role: String! + isActive: Boolean + email: String! + password: String + profile: ProfileInput + auth: AuthInput +} + +input ProfileInput { + firstName: String + lastName: String +} + +# Payload for usersUpdated Subscription +type UpdateUserPayload { + mutation: String! + node: User! +} \ No newline at end of file From 1fd5f30841f14e4200dea63ab8f60740575e5491 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:43:56 +0300 Subject: [PATCH 15/85] added entities(models) for user module --- .../java/com/sysgears/user/model/User.java | 55 +++++++++++++++++++ .../com/sysgears/user/model/UserAuth.java | 43 +++++++++++++++ .../com/sysgears/user/model/UserProfile.java | 33 +++++++++++ .../user/model/auth/CertificateAuth.java | 26 +++++++++ .../user/model/auth/FacebookAuth.java | 24 ++++++++ .../sysgears/user/model/auth/GithubAuth.java | 24 ++++++++ .../sysgears/user/model/auth/GoogleAuth.java | 24 ++++++++ .../user/model/auth/LinkedInAuth.java | 24 ++++++++ 8 files changed, 253 insertions(+) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/User.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/UserProfile.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/auth/CertificateAuth.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/auth/FacebookAuth.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GithubAuth.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GoogleAuth.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/model/auth/LinkedInAuth.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java new file mode 100644 index 0000000000..a87ed1b5ae --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java @@ -0,0 +1,55 @@ +package com.sysgears.user.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "USERS") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "USERNAME", nullable = false) + private String username; + + @Column(name = "PASSWORD", nullable = false) + private String password; + + @Column(name = "ROLE", nullable = false) + private String role; + + @Column(name = "IS_ACTIVE") + private Boolean isActive; + + @Column(name = "EMAIL", nullable = false) + private String email; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private UserProfile profile; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private UserAuth auth; + + public User(String username, + String password, + String role, + Boolean isActive, + String email) { + this.username = username; + this.password = password; + this.role = role; + this.isActive = isActive; + this.email = email; + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java new file mode 100644 index 0000000000..321b684843 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java @@ -0,0 +1,43 @@ +package com.sysgears.user.model; + +import com.sysgears.user.model.auth.*; +import lombok.*; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "USER_AUTH") +public class UserAuth { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + @Column(name = "ID") + private int id; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private CertificateAuth certificate; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private FacebookAuth facebook; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private GoogleAuth google; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private GithubAuth github; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Fetch(value = FetchMode.JOIN) + private LinkedInAuth linkedin; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/UserProfile.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/UserProfile.java new file mode 100644 index 0000000000..660366e1bf --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/UserProfile.java @@ -0,0 +1,33 @@ +package com.sysgears.user.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "USER_PROFILE") +public class UserProfile { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "FIRST_NAME") + private String firstName; + + @Column(name = "LAST_NAME") + private String lastName; + + @Column(name = "FULL_NAME") + private String fullName; + + public UserProfile(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + this.fullName = firstName.concat(" ").concat(lastName); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/CertificateAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/CertificateAuth.java new file mode 100644 index 0000000000..66435bfc70 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/CertificateAuth.java @@ -0,0 +1,26 @@ +package com.sysgears.user.model.auth; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "CERTIFICATE_AUTH") +public class CertificateAuth { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + @Column(name = "ID") + private int id; + + @Column(name = "SERIAL") + private String serial; + + public CertificateAuth(String serial) { + this.serial = serial; + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/FacebookAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/FacebookAuth.java new file mode 100644 index 0000000000..b96135f6ea --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/FacebookAuth.java @@ -0,0 +1,24 @@ +package com.sysgears.user.model.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "FACEBOOK_AUTH") +public class FacebookAuth { + @Id + @Column(name = "FACEBOOK_ID", nullable = false) + private String fbId; + + @Column(name = "DISPLAY_NAME") + private String displayName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GithubAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GithubAuth.java new file mode 100644 index 0000000000..fed7545e39 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GithubAuth.java @@ -0,0 +1,24 @@ +package com.sysgears.user.model.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "GITHUB_AUTH") +public class GithubAuth { + @Id + @Column(name = "GITHUB_ID", nullable = false) + private String ghId; + + @Column(name = "DISPLAY_NAME") + private String displayName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GoogleAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GoogleAuth.java new file mode 100644 index 0000000000..78b30be2a6 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/GoogleAuth.java @@ -0,0 +1,24 @@ +package com.sysgears.user.model.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "GOOGLE_AUTH") +public class GoogleAuth { + @Id + @Column(name = "GOOGLE_ID", nullable = false) + private String googleId; + + @Column(name = "DISPLAY_NAME") + private String displayName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/LinkedInAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/LinkedInAuth.java new file mode 100644 index 0000000000..e72af83ecb --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/auth/LinkedInAuth.java @@ -0,0 +1,24 @@ +package com.sysgears.user.model.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "LINKEDIN_AUTH") +public class LinkedInAuth { + @Id + @Column(name = "LINKEDIN_ID", nullable = false) + private String lnId; + + @Column(name = "DISPLAY_NAME") + private String displayName; +} From 0c50adcc757b0e696a8d944c891b1b35a85db592 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:48:41 +0300 Subject: [PATCH 16/85] added DTOs related to graphQL input types --- .../sysgears/user/dto/UpdateUserPayload.java | 10 ++++++++ .../com/sysgears/user/dto/UserPayload.java | 9 ++++++++ .../sysgears/user/dto/input/AddUserInput.java | 22 ++++++++++++++++++ .../user/dto/input/EditUserInput.java | 23 +++++++++++++++++++ .../user/dto/input/FilterUserInput.java | 12 ++++++++++ .../user/dto/input/OrderByUserInput.java | 11 +++++++++ .../sysgears/user/dto/input/ProfileInput.java | 11 +++++++++ .../dto/input/auth/AuthCertificateInput.java | 12 ++++++++++ .../dto/input/auth/AuthFacebookInput.java | 13 +++++++++++ .../user/dto/input/auth/AuthGitHubInput.java | 13 +++++++++++ .../user/dto/input/auth/AuthGoogleInput.java | 13 +++++++++++ .../user/dto/input/auth/AuthInput.java | 14 +++++++++++ .../dto/input/auth/AuthLinkedInInput.java | 13 +++++++++++ 13 files changed, 176 insertions(+) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/UpdateUserPayload.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthCertificateInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthFacebookInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGitHubInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGoogleInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthLinkedInInput.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/UpdateUserPayload.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/UpdateUserPayload.java new file mode 100644 index 0000000000..1720937cc5 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/UpdateUserPayload.java @@ -0,0 +1,10 @@ +package com.sysgears.user.dto; + +import com.sysgears.user.model.User; +import lombok.Data; + +@Data +public class UpdateUserPayload { + private final String mutation; + private final User node; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java new file mode 100644 index 0000000000..5c945ed9ec --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java @@ -0,0 +1,9 @@ +package com.sysgears.user.dto; + +import com.sysgears.user.model.User; +import lombok.Data; + +@Data +public class UserPayload { + private final User user; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java new file mode 100644 index 0000000000..d1e079c3c6 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java @@ -0,0 +1,22 @@ +package com.sysgears.user.dto.input; + +import com.sysgears.user.dto.input.auth.AuthInput; +import lombok.Data; +import org.springframework.lang.NonNull; + +import java.util.Optional; + +@Data +public class AddUserInput { + @NonNull + private final String username; + @NonNull + private final String password; + @NonNull + private final String role; + private final boolean isActive; + @NonNull + private final String email; + private final Optional profile; + private final Optional auth; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java new file mode 100644 index 0000000000..c376ebe651 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java @@ -0,0 +1,23 @@ +package com.sysgears.user.dto.input; + +import com.sysgears.user.dto.input.auth.AuthInput; +import lombok.Data; +import org.springframework.lang.NonNull; + +import java.util.Optional; + +@Data +public class EditUserInput { + @NonNull + private final int id; + @NonNull + private final String username; + @NonNull + private final String role; + private final Optional isActive; + @NonNull + private final String email; + private final Optional password; + private final Optional profile; + private final Optional auth; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java new file mode 100644 index 0000000000..90a8261cb5 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java @@ -0,0 +1,12 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; + +import java.util.Optional; + +@Data +public class FilterUserInput { + private final Optional searchText; + private final Optional role; + private final Optional isActive; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java new file mode 100644 index 0000000000..f264be8c3e --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java @@ -0,0 +1,11 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; + +import java.util.Optional; + +@Data +public class OrderByUserInput { + private final Optional column; + private final Optional order; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java new file mode 100644 index 0000000000..77905b3259 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java @@ -0,0 +1,11 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; + +import java.util.Optional; + +@Data +public class ProfileInput { + private final Optional firstName; + private final Optional lastName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthCertificateInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthCertificateInput.java new file mode 100644 index 0000000000..d77fffcb29 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthCertificateInput.java @@ -0,0 +1,12 @@ +package com.sysgears.user.dto.input.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AuthCertificateInput { + private String serial; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthFacebookInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthFacebookInput.java new file mode 100644 index 0000000000..cdd32e3d8b --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthFacebookInput.java @@ -0,0 +1,13 @@ +package com.sysgears.user.dto.input.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AuthFacebookInput { + private String fbId; + private String displayName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGitHubInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGitHubInput.java new file mode 100644 index 0000000000..3710c81336 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGitHubInput.java @@ -0,0 +1,13 @@ +package com.sysgears.user.dto.input.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AuthGitHubInput { + private String ghId; + private String displayName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGoogleInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGoogleInput.java new file mode 100644 index 0000000000..85f3263e57 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthGoogleInput.java @@ -0,0 +1,13 @@ +package com.sysgears.user.dto.input.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AuthGoogleInput { + private String googleId; + private String displayName; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java new file mode 100644 index 0000000000..52bb73611f --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java @@ -0,0 +1,14 @@ +package com.sysgears.user.dto.input.auth; + +import lombok.Data; + +import java.util.Optional; + +@Data +public class AuthInput { + private final Optional certificate; + private final Optional facebook; + private final Optional google; + private final Optional github; + private final Optional linkedin; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthLinkedInInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthLinkedInInput.java new file mode 100644 index 0000000000..d35ec83415 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthLinkedInInput.java @@ -0,0 +1,13 @@ +package com.sysgears.user.dto.input.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AuthLinkedInInput { + private String lnId; + private String displayName; +} From 51480b8895f002ca9815562a5546cde85ac71ac8 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:49:48 +0300 Subject: [PATCH 17/85] added repositories --- .../repository/DefaultUserRepository.java | 74 +++++++++++++++++++ .../user/repository/JpaUserRepository.java | 13 ++++ .../user/repository/UserRepository.java | 21 ++++++ 3 files changed, 108 insertions(+) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java new file mode 100644 index 0000000000..d92104210f --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java @@ -0,0 +1,74 @@ +package com.sysgears.user.repository; + +import com.sysgears.user.dto.input.FilterUserInput; +import com.sysgears.user.dto.input.OrderByUserInput; +import com.sysgears.user.model.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Repository +@RequiredArgsConstructor +public class DefaultUserRepository implements UserRepository { + private final EntityManager entityManager; + private final JpaUserRepository jpaUserRepository; + + @Override + public Optional findById(int id) { + return jpaUserRepository.findById(id); + } + + @Override + public CompletableFuture findUserById(int id) { + return jpaUserRepository.findUserById(id); + } + + @Override + public User save(User user) { + return jpaUserRepository.save(user); + } + + @Override + public void delete(User user) { + jpaUserRepository.delete(user); + } + + @Override + public CompletableFuture> findByCriteria(Optional orderBy, Optional filter) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(User.class); + + Root user = query.from(User.class); + List predicates = new ArrayList<>(); + + filter.ifPresent(filterUserInput -> { + filterUserInput.getRole().ifPresent(role -> predicates.add(builder.like(user.get("role"), role))); + filterUserInput.getIsActive().ifPresent(isActive -> predicates.add(builder.equal(user.get("isActive"), isActive))); + filterUserInput.getSearchText().ifPresent(searchText -> { + predicates.add(builder.or((builder.like(user.get("username"), searchText)), + builder.like(user.get("email"), searchText))); + }); + }); + + orderBy.ifPresent(orderByUserInput -> { + String orderColumn = orderByUserInput.getColumn().orElse("id"); + if (orderByUserInput.getOrder().isPresent() && orderByUserInput.getOrder().get().toLowerCase().equals("desc")) { + query.orderBy(builder.desc(user.get(orderColumn))); + } else { + query.orderBy(builder.asc(user.get(orderColumn))); + } + }); + query.where(predicates.toArray(new Predicate[0])); + + return CompletableFuture.supplyAsync(() -> entityManager.createQuery(query).getResultList()); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java new file mode 100644 index 0000000000..132fff8e47 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java @@ -0,0 +1,13 @@ +package com.sysgears.user.repository; + +import com.sysgears.user.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public interface JpaUserRepository extends JpaRepository { + CompletableFuture findUserById(int id); + + Optional findById(int id); +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java new file mode 100644 index 0000000000..3e767a4bcf --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java @@ -0,0 +1,21 @@ +package com.sysgears.user.repository; + +import com.sysgears.user.dto.input.FilterUserInput; +import com.sysgears.user.dto.input.OrderByUserInput; +import com.sysgears.user.model.User; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public interface UserRepository { + Optional findById(int id); + + CompletableFuture findUserById(int id); + + User save(User user); + + void delete(User user); + + CompletableFuture> findByCriteria(Optional orderBy, Optional filter); +} From fe1a05d3262c57725b555f4ff5814eca5c4c4c09 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:50:35 +0300 Subject: [PATCH 18/85] added DB initializer when the application starts --- .../com/sysgears/user/UserDBInitializer.java | 45 +++++++++++++++++++ .../sysgears/user/constant/UserConstants.java | 8 ++++ 2 files changed, 53 insertions(+) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java b/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java new file mode 100644 index 0000000000..cd72904c2f --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java @@ -0,0 +1,45 @@ +package com.sysgears.user; + +import com.sysgears.user.model.User; +import com.sysgears.user.model.UserAuth; +import com.sysgears.user.model.UserProfile; +import com.sysgears.user.model.auth.*; +import com.sysgears.user.repository.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserDBInitializer { + private final JpaUserRepository repository; + private final PasswordEncoder passwordEncoder; + + @EventListener + public void onApplicationStartedEvent(ApplicationStartedEvent event) { + long count = repository.count(); + if (count == 0) { + String encodedPassword = passwordEncoder.encode("qwerty"); + log.info("Encoded pass: {}", encodedPassword); + User user = new User("admin", encodedPassword, "ADMIN", true, "example@sysgears.com"); + user.setProfile(new UserProfile("John", "Smith")); + user.setAuth( + UserAuth.builder() + .certificate(new CertificateAuth(UUID.randomUUID().toString())) + .facebook(new FacebookAuth(UUID.randomUUID().toString(), "facebookAuthName")) + .github(new GithubAuth(UUID.randomUUID().toString(), "githubAuthName")) + .google(new GoogleAuth(UUID.randomUUID().toString(), "googleAuthName")) + .linkedin(new LinkedInAuth(UUID.randomUUID().toString(), "linkedInAuthName")) + .build() + ); + repository.save(user); + log.debug("User initialized"); + } + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java b/modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java new file mode 100644 index 0000000000..ed8b9a3590 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java @@ -0,0 +1,8 @@ +package com.sysgears.user.constant; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class UserConstants { + public static final int ID = 1; +} From 05dc37174ead184b5c93b9d4aaaf63cadb629116 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 17:51:03 +0300 Subject: [PATCH 19/85] added implementation of pub/sub service for user --- .../com/sysgears/user/subscription/UserPubSubService.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserPubSubService.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserPubSubService.java b/modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserPubSubService.java new file mode 100644 index 0000000000..f7d8a70255 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserPubSubService.java @@ -0,0 +1,8 @@ +package com.sysgears.user.subscription; + +import com.sysgears.core.subscription.AbstractPubSubService; +import org.springframework.stereotype.Component; + +@Component +public class UserPubSubService extends AbstractPubSubService { +} From 66fced7af84714513c162cb3813309fd47692c09 Mon Sep 17 00:00:00 2001 From: vitalii Date: Mon, 27 Jul 2020 18:04:41 +0300 Subject: [PATCH 20/85] added graphQL resolvers for user --- .../sysgears/user/config/SecurityConfig.java | 23 +++ .../user/exception/UserNotFoundException.java | 18 +++ .../user/resolvers/UserMutationResolver.java | 147 ++++++++++++++++++ .../user/resolvers/UserQueryResolver.java | 41 +++++ .../resolvers/UserSubscriptionResolver.java | 46 ++++++ .../user/subscription/UserUpdatedEvent.java | 24 +++ 6 files changed, 299 insertions(+) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserUpdatedEvent.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java new file mode 100644 index 0000000000..d91ded5d14 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java @@ -0,0 +1,23 @@ +package com.sysgears.user.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + // todo: Security disabled + http.csrf().disable().authorizeRequests().anyRequest().permitAll(); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java new file mode 100644 index 0000000000..bf71dd8aab --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java @@ -0,0 +1,18 @@ +package com.sysgears.user.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException() { + super(); + } + + public UserNotFoundException(String message) { + super(message); + } + public UserNotFoundException(int id) { + super(String.format("User with id %d not found.", id)); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java new file mode 100644 index 0000000000..026c5b5b25 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -0,0 +1,147 @@ +package com.sysgears.user.resolvers; + +import com.sysgears.core.subscription.Publisher; +import com.sysgears.user.dto.UserPayload; +import com.sysgears.user.dto.input.AddUserInput; +import com.sysgears.user.dto.input.EditUserInput; +import com.sysgears.user.dto.input.ProfileInput; +import com.sysgears.user.dto.input.auth.AuthInput; +import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.model.User; +import com.sysgears.user.model.UserAuth; +import com.sysgears.user.model.UserProfile; +import com.sysgears.user.model.auth.*; +import com.sysgears.user.repository.UserRepository; +import com.sysgears.user.subscription.UserUpdatedEvent; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserMutationResolver implements GraphQLMutationResolver { + private final UserRepository repository; + private final Publisher publisher; + private final PasswordEncoder passwordEncoder; + + @Transactional + public CompletableFuture addUser(AddUserInput input) { + User user = new User( + input.getUsername(), + passwordEncoder.encode(input.getPassword()), + input.getRole(), + input.isActive(), + input.getEmail() + ); + input.getProfile().map(profileInput -> + new UserProfile( + profileInput.getFirstName().orElse(""), + profileInput.getLastName().orElse("") + ) + ).ifPresent(user::setProfile); + + input.getAuth() + .map(this::from) + .ifPresent(user::setAuth); + repository.save(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.ADD_USER, user)); + + return CompletableFuture.completedFuture(new UserPayload(user)); + } + + @Transactional + public CompletableFuture editUser(EditUserInput input) { + return repository.findUserById(input.getId()) + .thenApplyAsync(user -> { + if (user == null) throw new UserNotFoundException(input.getId()); + + user.setUsername(input.getUsername()); + user.setRole(input.getRole()); + user.setEmail(input.getEmail()); + + input.getIsActive().ifPresent(user::setIsActive); + input.getPassword().ifPresent(password -> user.setPassword(passwordEncoder.encode(password))); + + if (user.getProfile() != null) { + input.getProfile().ifPresent(profile -> { + profile.getFirstName().ifPresent(fn -> user.getProfile().setFirstName(fn)); + profile.getLastName().ifPresent(ln -> user.getProfile().setLastName(ln)); + + user.getProfile().setFullName( + user.getProfile().getFirstName() + .concat(" ") + .concat(user.getProfile().getLastName()) + ); + }); + } else { + input.getProfile().map(this::from).ifPresent(user::setProfile); + } + + if (user.getAuth() != null) { + input.getAuth().ifPresent(authInput -> { + authInput.getCertificate().ifPresent(cert -> + user.getAuth().setCertificate(new CertificateAuth(cert.getSerial()))); + authInput.getFacebook().ifPresent(fb -> + user.getAuth().setFacebook(new FacebookAuth(fb.getFbId(), fb.getDisplayName()))); + authInput.getLinkedin().ifPresent(li -> + user.getAuth().setLinkedin(new LinkedInAuth(li.getLnId(), li.getDisplayName()))); + authInput.getGoogle().ifPresent(g -> + user.getAuth().setGoogle(new GoogleAuth(g.getGoogleId(), g.getDisplayName()))); + authInput.getGithub().ifPresent(git -> + user.getAuth().setGithub(new GithubAuth(git.getGhId(), git.getDisplayName()))); + }); + } else { + input.getAuth().map(this::from).ifPresent(user::setAuth); + } + + repository.save(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.EDIT_USER, user)); + + return new UserPayload(user); + }); + } + + @Transactional + public CompletableFuture deleteUser(int id) { + return repository.findUserById(id) + .thenApplyAsync(user -> { + if (user == null) throw new UserNotFoundException(id); + + repository.delete(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.DELETE_USER, user)); + + return new UserPayload(user); + }); + } + + private UserAuth from(AuthInput authInput) { + UserAuth.UserAuthBuilder builder = UserAuth.builder(); + authInput.getCertificate() + .ifPresent(cert -> builder.certificate(new CertificateAuth(cert.getSerial()))); + authInput.getFacebook() + .ifPresent(fb -> builder.facebook(new FacebookAuth(fb.getFbId(), fb.getDisplayName()))); + authInput.getGithub() + .ifPresent(git -> builder.github(new GithubAuth(git.getGhId(), git.getDisplayName()))); + authInput.getGoogle() + .ifPresent(g -> builder.google(new GoogleAuth(g.getGoogleId(), g.getDisplayName()))); + authInput.getLinkedin() + .ifPresent(li -> builder.linkedin(new LinkedInAuth(li.getLnId(), li.getDisplayName()))); + return builder.build(); + } + + private UserProfile from(ProfileInput input) { + return new UserProfile( + input.getFirstName().orElse(""), + input.getLastName().orElse("") + ); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java new file mode 100644 index 0000000000..4cdd1c5083 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java @@ -0,0 +1,41 @@ +package com.sysgears.user.resolvers; + +import com.sysgears.user.constant.UserConstants; +import com.sysgears.user.dto.UserPayload; +import com.sysgears.user.dto.input.FilterUserInput; +import com.sysgears.user.dto.input.OrderByUserInput; +import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.model.User; +import com.sysgears.user.repository.UserRepository; +import graphql.kickstart.tools.GraphQLQueryResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserQueryResolver implements GraphQLQueryResolver { + private final UserRepository userRepository; + + public CompletableFuture currentUser() { + return userRepository.findUserById(UserConstants.ID); + } + + public CompletableFuture user(int id) { + return CompletableFuture.supplyAsync(() -> { + User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); + return new UserPayload(user); + }); + } + + @Transactional + public CompletableFuture> users(Optional orderBy, Optional filter) { + return userRepository.findByCriteria(orderBy, filter); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java new file mode 100644 index 0000000000..349e8fd92b --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java @@ -0,0 +1,46 @@ +package com.sysgears.user.resolvers; + +import com.sysgears.core.subscription.Subscriber; +import com.sysgears.user.dto.UpdateUserPayload; +import com.sysgears.user.dto.input.FilterUserInput; +import com.sysgears.user.subscription.UserUpdatedEvent; +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserSubscriptionResolver implements GraphQLSubscriptionResolver { + private final Subscriber subscriber; + + public CompletableFuture> usersUpdated(Optional filter) { + return CompletableFuture.supplyAsync(() -> subscriber.subscribe(event -> { + if (filter.isEmpty()) return true; + + switch (event.getMutation()) { + case DELETE_USER: + return true; + + case ADD_USER: + case EDIT_USER: + return filter.get().getIsActive().map(isActive -> + event.getUser().getIsActive().equals(isActive)).orElse(true) && + filter.get().getRole().map(role -> + event.getUser().getRole().toLowerCase().equals(role.toLowerCase())).orElse(true) && + (filter.get().getSearchText().map(searchText -> + event.getUser().getEmail().toLowerCase().equals(searchText.toLowerCase())).orElse(true) || + filter.get().getSearchText().map(searchText -> + event.getUser().getUsername().toLowerCase().equals(searchText.toLowerCase())).orElse(true)); + + default: + return false; + } + }).map(event -> new UpdateUserPayload(event.getMutation().getOperation(), event.getUser()))); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserUpdatedEvent.java b/modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserUpdatedEvent.java new file mode 100644 index 0000000000..13bff4044b --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/subscription/UserUpdatedEvent.java @@ -0,0 +1,24 @@ +package com.sysgears.user.subscription; + +import com.sysgears.user.model.User; +import lombok.Data; +import lombok.Getter; + +@Data +public class UserUpdatedEvent { + private final Mutation mutation; + private final User user; + + public enum Mutation { + ADD_USER("addUser"), + EDIT_USER("editUser"), + DELETE_USER("deleteUser"); + + @Getter + private final String operation; + + Mutation(String operation) { + this.operation = operation; + } + } +} From e2c9b916ea68d3400d0a7ab7b4d6a3e17631844c Mon Sep 17 00:00:00 2001 From: vitalii Date: Tue, 28 Jul 2020 13:06:58 +0300 Subject: [PATCH 21/85] fixed docker-compose --- docker-compose.react-java.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.react-java.yml b/docker-compose.react-java.yml index 1b9ef5c0e9..8fe305bde5 100644 --- a/docker-compose.react-java.yml +++ b/docker-compose.react-java.yml @@ -15,7 +15,7 @@ services: - ./packages/server-java/gradle/wrapper:${APP_DIR}/packages/server-java/gradle/wrapper - ./packages/server-java/gradlew:${APP_DIR}/packages/server-java/gradlew command: > - sh -c './gradlew clean bootRun' + sh -c 'chmod +x gradlew && ./gradlew clean bootRun' # sh -c './gradlew clean' ports: - 8080:8080 From 8f5c907171bcfcf62c3829325938c15bd0dd62f7 Mon Sep 17 00:00:00 2001 From: vitalii Date: Tue, 28 Jul 2020 13:07:49 +0300 Subject: [PATCH 22/85] added tests for counter module --- .../sysgears/counter/CounterMutationTest.java | 36 +++++++++++++++++++ .../sysgears/counter/CounterQueryTest.java | 27 ++++++++++++++ .../mutation/add-server-counter.graphql | 5 +++ .../resources/query/server-counter.graphql | 5 +++ packages/server-java/build.gradle | 12 +++++-- packages/server-java/gradlew | 0 6 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 modules/counter/server-java/src/test/java/com/sysgears/counter/CounterMutationTest.java create mode 100644 modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java create mode 100644 modules/counter/server-java/src/test/resources/mutation/add-server-counter.graphql create mode 100644 modules/counter/server-java/src/test/resources/query/server-counter.graphql mode change 100644 => 100755 packages/server-java/gradlew diff --git a/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterMutationTest.java b/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterMutationTest.java new file mode 100644 index 0000000000..55a6283a9b --- /dev/null +++ b/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterMutationTest.java @@ -0,0 +1,36 @@ +package com.sysgears.counter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.counter.model.Counter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext +public class CounterMutationTest { + @Autowired + private GraphQLTestTemplate graphQLTestTemplate; + + @Test + void addServerCounter() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("amount", 2); + + GraphQLResponse response = graphQLTestTemplate.perform("mutation/add-server-counter.graphql", node); + + assertTrue(response.isOk()); + assertEquals(3, response.get("$.data.addServerCounter", Counter.class).getAmount()); + } +} diff --git a/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java b/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java new file mode 100644 index 0000000000..0282c2d901 --- /dev/null +++ b/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java @@ -0,0 +1,27 @@ +package com.sysgears.counter; + +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.counter.model.Counter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class CounterQueryTest { + @Autowired + private GraphQLTestTemplate graphQLTestTemplate; + + @Test + void serverCounter() throws IOException { + GraphQLResponse response = graphQLTestTemplate.postForResource("query/server-counter.graphql"); + + assertTrue(response.isOk()); + assertEquals(1, response.get("$.data.serverCounter", Counter.class).getAmount()); + } +} diff --git a/modules/counter/server-java/src/test/resources/mutation/add-server-counter.graphql b/modules/counter/server-java/src/test/resources/mutation/add-server-counter.graphql new file mode 100644 index 0000000000..eab577a3cb --- /dev/null +++ b/modules/counter/server-java/src/test/resources/mutation/add-server-counter.graphql @@ -0,0 +1,5 @@ +mutation AddServerCounter($amount: Int!) { + addServerCounter(amount: $amount) { + amount + } +} diff --git a/modules/counter/server-java/src/test/resources/query/server-counter.graphql b/modules/counter/server-java/src/test/resources/query/server-counter.graphql new file mode 100644 index 0000000000..881281c448 --- /dev/null +++ b/modules/counter/server-java/src/test/resources/query/server-counter.graphql @@ -0,0 +1,5 @@ +query ServerCounter { + serverCounter { + amount + } +} diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index 31e293b5a4..f3c20a7030 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -24,12 +24,11 @@ subprojects { jar { enabled = true } - bootJar { + enabled = false launchScript() mainClassName = 'com.sysgears.Application' } - repositories { mavenCentral() jcenter() @@ -55,16 +54,22 @@ subprojects { implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.1.0' runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:7.1.0' implementation 'com.graphql-java-kickstart:graphql-java-tools:6.1.0' + testImplementation 'com.graphql-java-kickstart:graphql-spring-boot-starter-test:7.1.0' implementation "io.reactivex.rxjava2:rxjava" testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } + testImplementation project(':app') } test { useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + } + outputs.upToDateWhen { false } } } @@ -73,4 +78,7 @@ project(':app') { implementation project(':counter') implementation project(':user') } + bootJar { + enabled = true + } } \ No newline at end of file diff --git a/packages/server-java/gradlew b/packages/server-java/gradlew old mode 100644 new mode 100755 From ed348f2f4ccfb6ab0aab8e5cb6579eefbb3aa8d6 Mon Sep 17 00:00:00 2001 From: vitalii Date: Tue, 28 Jul 2020 16:23:28 +0300 Subject: [PATCH 23/85] added tests for user module --- .../com/sysgears/user/dto/UserPayload.java | 6 +- .../user/resolvers/UserQueryResolver.java | 2 - .../com/sysgears/user/UserMutationTest.java | 147 ++++++++++++++++++ .../java/com/sysgears/user/UserQueryTest.java | 127 +++++++++++++++ .../test/resources/mutation/add-user.graphql | 24 +++ .../resources/mutation/delete-user.graphql | 35 +++++ .../test/resources/mutation/edit-user.graphql | 35 +++++ .../test/resources/query/current-user.graphql | 16 ++ .../src/test/resources/query/user.graphql | 21 +++ .../src/test/resources/query/users.graphql | 35 +++++ 10 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java create mode 100644 modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java create mode 100644 modules/user/server-java/src/test/resources/mutation/add-user.graphql create mode 100644 modules/user/server-java/src/test/resources/mutation/delete-user.graphql create mode 100644 modules/user/server-java/src/test/resources/mutation/edit-user.graphql create mode 100644 modules/user/server-java/src/test/resources/query/current-user.graphql create mode 100644 modules/user/server-java/src/test/resources/query/user.graphql create mode 100644 modules/user/server-java/src/test/resources/query/users.graphql diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java index 5c945ed9ec..5d201fdd22 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/UserPayload.java @@ -1,9 +1,13 @@ package com.sysgears.user.dto; import com.sysgears.user.model.User; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class UserPayload { - private final User user; + private User user; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java index 4cdd1c5083..b3400f3618 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java @@ -11,7 +11,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -34,7 +33,6 @@ public CompletableFuture user(int id) { }); } - @Transactional public CompletableFuture> users(Optional orderBy, Optional filter) { return userRepository.findByCriteria(orderBy, filter); } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java new file mode 100644 index 0000000000..f520749e89 --- /dev/null +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java @@ -0,0 +1,147 @@ +package com.sysgears.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.user.dto.UserPayload; +import com.sysgears.user.model.User; +import com.sysgears.user.model.UserAuth; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class UserMutationTest { + @Autowired + GraphQLTestTemplate template; + @Autowired + UserDBInitializer userDBInitializer; + + @AfterEach + void initUser() { + userDBInitializer.onApplicationStartedEvent(mock(ApplicationStartedEvent.class)); + } + + @Test + void addUser() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + ObjectNode profile = mapper.createObjectNode(); + + node.put("username", "user"); + node.put("password", "supersecret"); + node.put("role", "USER"); + node.put("isActive", true); + node.put("email", "user@sysgears.com"); + + profile.put("firstName", "Edward"); + profile.put("lastName", "Fillmore"); + node.set("profile", profile); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/add-user.graphql", input); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.addUser", UserPayload.class); + User createdUser = payload.getUser(); + + assertEquals("user", createdUser.getUsername()); + assertEquals("USER", createdUser.getRole()); + assertTrue(createdUser.getIsActive()); + assertEquals("user@sysgears.com", createdUser.getEmail()); + assertEquals("Edward Fillmore", createdUser.getProfile().getFullName()); + assertNull(createdUser.getAuth()); + } + + @Test + void editUser() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + ObjectNode profile = mapper.createObjectNode(); + ObjectNode auth = mapper.createObjectNode(); + + node.put("id", 1); + node.put("username", "admin"); + node.put("password", "supersecret"); + node.put("role", "ADMIN"); + node.put("isActive", true); + node.put("email", "admin@sysgears.com"); + + profile.put("firstName", "John"); + profile.put("lastName", "Sinna"); + node.set("profile", profile); + + ObjectNode facebook = mapper.createObjectNode(); + facebook.put("fbId", "fb_id"); + facebook.put("displayName", "some"); + ObjectNode google = mapper.createObjectNode(); + google.put("googleId", "g_id"); + google.put("displayName", "google"); + ObjectNode github = mapper.createObjectNode(); + github.put("ghId", "gh_id"); + github.put("displayName", "github"); + ObjectNode certificate = mapper.createObjectNode(); + certificate.put("serial", "some_unique_id"); + ObjectNode linkedin = mapper.createObjectNode(); + linkedin.put("lnId", "ln_id"); + linkedin.put("displayName", "LinkedIn"); + auth.set("facebook", facebook); + auth.set("google", google); + auth.set("github", github); + auth.set("certificate", certificate); + auth.set("linkedin", linkedin); + node.set("auth", auth); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/edit-user.graphql", input); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.editUser", UserPayload.class); + User createdUser = payload.getUser(); + + assertEquals(1, createdUser.getId()); + assertEquals("admin", createdUser.getUsername()); + assertEquals("ADMIN", createdUser.getRole()); + assertTrue(createdUser.getIsActive()); + assertEquals("admin@sysgears.com", createdUser.getEmail()); + assertEquals("John Sinna", createdUser.getProfile().getFullName()); + + UserAuth userAuth = createdUser.getAuth(); + assertNotNull(userAuth); + assertEquals("some_unique_id", userAuth.getCertificate().getSerial()); + assertEquals("fb_id", userAuth.getFacebook().getFbId()); + assertEquals("some", userAuth.getFacebook().getDisplayName()); + assertEquals("g_id", userAuth.getGoogle().getGoogleId()); + assertEquals("google", userAuth.getGoogle().getDisplayName()); + assertEquals("gh_id", userAuth.getGithub().getGhId()); + assertEquals("github", userAuth.getGithub().getDisplayName()); + assertEquals("ln_id", userAuth.getLinkedin().getLnId()); + assertEquals("LinkedIn", userAuth.getLinkedin().getDisplayName()); + } + + @Test + void deleteUser() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 1); + + GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); + + UserPayload payload = response.get("$.data.deleteUser", UserPayload.class); + User deletedUser = payload.getUser(); + + assertEquals(1, deletedUser.getId()); + } +} diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java new file mode 100644 index 0000000000..aaf946b2a9 --- /dev/null +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java @@ -0,0 +1,127 @@ +package com.sysgears.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.user.constant.UserConstants; +import com.sysgears.user.dto.UserPayload; +import com.sysgears.user.model.User; +import com.sysgears.user.model.UserAuth; +import com.sysgears.user.model.UserProfile; +import com.sysgears.user.model.auth.*; +import com.sysgears.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class UserQueryTest { + @Autowired + GraphQLTestTemplate template; + @MockBean + private UserRepository repository; + + private User user; + private final CertificateAuth certificateAuth = new CertificateAuth(UUID.randomUUID().toString()); + private final FacebookAuth facebookAuth = new FacebookAuth(UUID.randomUUID().toString(), "facebookAuthName"); + private final GithubAuth githubAuth = new GithubAuth(UUID.randomUUID().toString(), "githubAuthName"); + private final GoogleAuth googleAuth = new GoogleAuth(UUID.randomUUID().toString(), "googleAuthName"); + private final LinkedInAuth linkedInAuth = new LinkedInAuth(UUID.randomUUID().toString(), "linkedInAuthName"); + + @BeforeEach + void init() { + user = new User("admin", "pass", "ADMIN", true, "example@sysgears.com"); + user.setProfile(new UserProfile("John", "Smith")); + user.setAuth( + UserAuth.builder() + .certificate(certificateAuth) + .facebook(facebookAuth) + .github(githubAuth) + .google(googleAuth) + .linkedin(linkedInAuth) + .build() + ); + } + + @Test + void users() throws IOException { + when(repository.findByCriteria(Optional.empty(), Optional.empty())) + .thenReturn(CompletableFuture.completedFuture(Collections.singletonList(user))); + + GraphQLResponse response = template.postForResource("query/users.graphql"); + + assertTrue(response.isOk()); + User actualUser = response.get("$.data.users[0]", User.class); + assertThat(actualUser) + .hasFieldOrPropertyWithValue("username", "admin") + .hasFieldOrPropertyWithValue("role", "ADMIN") + .hasFieldOrPropertyWithValue("email", "example@sysgears.com") + .hasFieldOrPropertyWithValue("isActive", true) + .hasFieldOrPropertyWithValue("profile.firstName", "John") + .hasFieldOrPropertyWithValue("profile.lastName", "Smith") + .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); + assertNotNull(actualUser.getAuth()); + assertEquals(certificateAuth, actualUser.getAuth().getCertificate()); + assertEquals(facebookAuth, actualUser.getAuth().getFacebook()); + assertEquals(githubAuth, actualUser.getAuth().getGithub()); + assertEquals(googleAuth, actualUser.getAuth().getGoogle()); + assertEquals(linkedInAuth, actualUser.getAuth().getLinkedin()); + } + + @Test + void user() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 1); + + when(repository.findById(1)) + .thenReturn(Optional.of(user)); + + GraphQLResponse response = template.perform("query/user.graphql", node); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.user", UserPayload.class); + User actualUser = payload.getUser(); + assertThat(actualUser) + .hasFieldOrPropertyWithValue("username", "admin") + .hasFieldOrPropertyWithValue("role", "ADMIN") + .hasFieldOrPropertyWithValue("email", "example@sysgears.com") + .hasFieldOrPropertyWithValue("isActive", true) + .hasFieldOrPropertyWithValue("profile.firstName", "John") + .hasFieldOrPropertyWithValue("profile.lastName", "Smith") + .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); + assertNotNull(actualUser.getAuth()); + assertEquals(certificateAuth, actualUser.getAuth().getCertificate()); + } + + @Test + void currentUser() throws IOException { + when(repository.findUserById(UserConstants.ID)) + .thenReturn(CompletableFuture.completedFuture(user)); + + GraphQLResponse response = template.postForResource("query/current-user.graphql"); + + assertTrue(response.isOk()); + User actualUser = response.get("$.data.currentUser", User.class); + assertThat(actualUser) + .hasFieldOrPropertyWithValue("username", "admin") + .hasFieldOrPropertyWithValue("profile.firstName", "John") + .hasFieldOrPropertyWithValue("profile.lastName", "Smith") + .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); + assertNotNull(actualUser.getAuth()); + assertEquals(facebookAuth, actualUser.getAuth().getFacebook()); + } +} diff --git a/modules/user/server-java/src/test/resources/mutation/add-user.graphql b/modules/user/server-java/src/test/resources/mutation/add-user.graphql new file mode 100644 index 0000000000..60eecc623d --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/add-user.graphql @@ -0,0 +1,24 @@ +mutation AddUser($input: AddUserInput!) { + addUser(input: $input) { + user { + id + username + role + isActive + email + profile{ + fullName + } + auth { + google { + googleId + displayName + } + facebook { + fbId + displayName + } + } + } + } +} \ No newline at end of file diff --git a/modules/user/server-java/src/test/resources/mutation/delete-user.graphql b/modules/user/server-java/src/test/resources/mutation/delete-user.graphql new file mode 100644 index 0000000000..7ec41ad55b --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/delete-user.graphql @@ -0,0 +1,35 @@ +mutation DeleteUser($id: Int!) { + deleteUser(id: $id) { + user { + id + username + role + isActive + email + profile{ + fullName + } + auth { + google { + googleId + displayName + } + facebook { + fbId + displayName + } + github { + ghId + displayName + } + certificate { + serial + } + linkedin { + lnId + displayName + } + } + } + } +} \ No newline at end of file diff --git a/modules/user/server-java/src/test/resources/mutation/edit-user.graphql b/modules/user/server-java/src/test/resources/mutation/edit-user.graphql new file mode 100644 index 0000000000..7f1047f39f --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/edit-user.graphql @@ -0,0 +1,35 @@ +mutation EditUser($input: EditUserInput!) { + editUser(input: $input) { + user { + id + username + role + isActive + email + profile{ + fullName + } + auth { + google { + googleId + displayName + } + facebook { + fbId + displayName + } + github { + ghId + displayName + } + certificate { + serial + } + linkedin { + lnId + displayName + } + } + } + } +} \ No newline at end of file diff --git a/modules/user/server-java/src/test/resources/query/current-user.graphql b/modules/user/server-java/src/test/resources/query/current-user.graphql new file mode 100644 index 0000000000..6d10a906b8 --- /dev/null +++ b/modules/user/server-java/src/test/resources/query/current-user.graphql @@ -0,0 +1,16 @@ +query { + currentUser { + username + profile { + firstName + lastName + fullName + } + auth { + facebook { + displayName + fbId + } + } + } +} \ No newline at end of file diff --git a/modules/user/server-java/src/test/resources/query/user.graphql b/modules/user/server-java/src/test/resources/query/user.graphql new file mode 100644 index 0000000000..eab0dd300a --- /dev/null +++ b/modules/user/server-java/src/test/resources/query/user.graphql @@ -0,0 +1,21 @@ +query GetUser($id: Int!){ + user(id: $id) { + user { + id + username + role + isActive + email + profile { + firstName + lastName + fullName + } + auth{ + certificate { + serial + } + } + } + } +} \ No newline at end of file diff --git a/modules/user/server-java/src/test/resources/query/users.graphql b/modules/user/server-java/src/test/resources/query/users.graphql new file mode 100644 index 0000000000..296cb2ff3d --- /dev/null +++ b/modules/user/server-java/src/test/resources/query/users.graphql @@ -0,0 +1,35 @@ +query { + users { + id + username + role + isActive + email + profile { + firstName + lastName + fullName + } + auth { + google { + googleId + displayName + } + facebook { + fbId + displayName + } + github { + ghId + displayName + } + certificate { + serial + } + linkedin { + lnId + displayName + } + } + } +} \ No newline at end of file From de22da7a75cc1a4362d9c473a22c9d0651efb10a Mon Sep 17 00:00:00 2001 From: vitalii Date: Tue, 28 Jul 2020 18:06:36 +0300 Subject: [PATCH 24/85] format code --- .../com/sysgears/counter/repository/CounterRepository.java | 1 - .../sysgears/counter/resolvers/CounterMutationResolver.java | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java index 9a94d20a07..f21c386692 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java @@ -6,6 +6,5 @@ import java.util.concurrent.CompletableFuture; public interface CounterRepository extends JpaRepository { - CompletableFuture findById(int id); } \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java index a1f5a92b70..7c1c159756 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java @@ -23,8 +23,7 @@ public class CounterMutationResolver implements GraphQLMutationResolver { public CompletableFuture addServerCounter(int amount) { log.debug("Updating counter"); return repository.findById(CounterConstants.ID) - .thenApply( - counter -> { + .thenApply(counter -> { counter.increaseAmount(amount); repository.save(counter); publisher.publish(counter); From 9055d9914a3543ee878504f790027cc21d67e173 Mon Sep 17 00:00:00 2001 From: vitalii Date: Tue, 4 Aug 2020 13:53:30 +0300 Subject: [PATCH 25/85] added graphQL schema for register/login/actions with password added authentication module implemented generating jwt --- .../authentication/server-java/build.gradle | 5 ++ .../authentication/config/AuthConfig.java | 15 +++++ .../authentication/config/JwtConfig.java | 16 +++++ .../model/jwt/JwtUserIdentity.java | 15 +++++ .../service/jwt/JWTGenerator.java | 60 +++++++++++++++++++ .../src/main/resources/jwt/schema.graphqls | 4 ++ .../main/resources/session/schema.graphqls | 4 ++ .../main/resources/password/schema.graphqls | 41 +++++++++++++ .../app/src/main/resources/application.yml | 7 ++- packages/server-java/build.gradle | 4 +- packages/server-java/settings.gradle | 3 +- 11 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 modules/authentication/server-java/build.gradle create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/AuthConfig.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/JwtConfig.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java create mode 100644 modules/authentication/server-java/src/main/resources/jwt/schema.graphqls create mode 100644 modules/authentication/server-java/src/main/resources/session/schema.graphqls create mode 100644 modules/user/server-java/src/main/resources/password/schema.graphqls diff --git a/modules/authentication/server-java/build.gradle b/modules/authentication/server-java/build.gradle new file mode 100644 index 0000000000..7e4e0d4f7e --- /dev/null +++ b/modules/authentication/server-java/build.gradle @@ -0,0 +1,5 @@ +dependencies { + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' +} \ No newline at end of file diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/AuthConfig.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/AuthConfig.java new file mode 100644 index 0000000000..c4bc32017a --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/AuthConfig.java @@ -0,0 +1,15 @@ +package com.sysgears.authentication.config; + +import io.jsonwebtoken.security.Keys; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.security.Key; + +@Configuration +public class AuthConfig { + @Bean + public Key jwtSecretKey(JwtConfig config) { + return Keys.hmacShaKeyFor(config.getSecret().getBytes()); + } +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/JwtConfig.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/JwtConfig.java new file mode 100644 index 0000000000..eb1d3621af --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/config/JwtConfig.java @@ -0,0 +1,16 @@ +package com.sysgears.authentication.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties("jwt") +public class JwtConfig { + private String secret; + private long accessTokenExpirationInSec; + private long refreshTokenExpirationInSec; +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java new file mode 100644 index 0000000000..2b33f306ed --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java @@ -0,0 +1,15 @@ +package com.sysgears.authentication.model.jwt; + +import lombok.Data; + +@Data +public class JwtUserIdentity { + private final int id; + private final String username; + private final String passwordHash; + private final String role; + private final Boolean isActive; + private final String email; + private final String firstName; + private final String lastName; +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java new file mode 100644 index 0000000000..3f05dd3e54 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java @@ -0,0 +1,60 @@ +package com.sysgears.authentication.service.jwt; + +import com.sysgears.authentication.config.JwtConfig; +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JWTGenerator { + private final Key secretKey; + private final JwtConfig jwtConfig; + + //todo: need to separate generation and parsing JWT. + public String generateToken(JwtUserIdentity identity) { + Map claims = new HashMap<>(); + claims.put("identity", identity); + log.debug("Generating new access JWT for user {}", identity.getId()); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(jwtConfig.getAccessTokenExpirationInSec(), ChronoUnit.SECONDS))) + .setHeaderParam("typ", "JWT") + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public String generateRefreshToken(JwtUserIdentity identity) { + Map claims = new HashMap<>(); + claims.put("id", identity.getId()); + log.debug("Generating new refresh JWT for user {}", identity.getId()); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(jwtConfig.getRefreshTokenExpirationInSec(), ChronoUnit.SECONDS))) + .setHeaderParam("typ", "JWT") + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + + public String getAllClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .toString(); + } +} diff --git a/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls b/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls new file mode 100644 index 0000000000..c751942f1b --- /dev/null +++ b/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls @@ -0,0 +1,4 @@ +extend type Mutation { + # Refresh user tokens + refreshTokens(refreshToken: String!): Tokens! +} diff --git a/modules/authentication/server-java/src/main/resources/session/schema.graphqls b/modules/authentication/server-java/src/main/resources/session/schema.graphqls new file mode 100644 index 0000000000..59009da1c5 --- /dev/null +++ b/modules/authentication/server-java/src/main/resources/session/schema.graphqls @@ -0,0 +1,4 @@ +extend type Mutation { + # Logout user + logout: String +} diff --git a/modules/user/server-java/src/main/resources/password/schema.graphqls b/modules/user/server-java/src/main/resources/password/schema.graphqls new file mode 100644 index 0000000000..ee5e64726c --- /dev/null +++ b/modules/user/server-java/src/main/resources/password/schema.graphqls @@ -0,0 +1,41 @@ +input LoginUserInput { + usernameOrEmail: String! + password: String! +} + +type Tokens { + accessToken: String + refreshToken: String +} + +type AuthPayload { + user: User + tokens: Tokens +} + +input RegisterUserInput { + username: String! + email: String! + password: String! +} + +input ForgotPasswordInput { + email: String! +} + +input ResetPasswordInput { + token: String! + password: String! + passwordConfirmation: String! +} + +extend type Mutation { + # Login user + login(input: LoginUserInput!): AuthPayload! + # Forgot password + forgotPassword(input: ForgotPasswordInput!): String + # Reset password + resetPassword(input: ResetPasswordInput!): String + # Register user + register(input: RegisterUserInput!): UserPayload! +} diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 252a451ff3..987e5e892a 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -32,4 +32,9 @@ graphiql: enabled: true endpoint: subscriptions: /graphql - mapping: /graphiql \ No newline at end of file + mapping: /graphiql + +jwt: + secret: "676918AB4D29BFE59CCB943F3C09F5CC8FB3A8511E23E502B67DE95AB9A9D00C" + access-token-expiration-in-sec: 3600 + refresh-token-expiration-in-sec: 604800 \ No newline at end of file diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index f3c20a7030..e6fd6d233a 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -41,7 +41,8 @@ subprojects { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-actuator' - compileOnly 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-aop' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' //Lombok compileOnly "org.projectlombok:lombok:1.18.12" @@ -77,6 +78,7 @@ project(':app') { dependencies { implementation project(':counter') implementation project(':user') + implementation project(':authentication') } bootJar { enabled = true diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index 63c1b690f2..17ad33e330 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user' +include ':app', ':core', ':counter', ':user', ':authentication' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') project(':user').projectDir = new File('../../modules/user/server-java') +project(':authentication').projectDir = new File('../../modules/authentication/server-java') \ No newline at end of file From dd960442d2144cef2ce0f7adcb0664c03a5ede65 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 4 Jan 2021 18:04:38 +0200 Subject: [PATCH 26/85] updated dependency and gradle versions --- packages/server-java/build.gradle | 10 +++++----- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index f3c20a7030..61480d9080 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '2.3.1.RELEASE' + springBootVersion = '2.4.1' } repositories { maven { url "https://plugins.gradle.org/m2/" } @@ -51,10 +51,10 @@ subprojects { implementation 'com.h2database:h2:1.4.200' //GraphQL - implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.1.0' - runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:7.1.0' - implementation 'com.graphql-java-kickstart:graphql-java-tools:6.1.0' - testImplementation 'com.graphql-java-kickstart:graphql-spring-boot-starter-test:7.1.0' + implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:8.1.1' + runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:8.1.1' + implementation 'com.graphql-java-kickstart:graphql-java-tools:6.3.0' + testImplementation 'com.graphql-java-kickstart:graphql-spring-boot-starter-test:8.1.1' implementation "io.reactivex.rxjava2:rxjava" diff --git a/packages/server-java/gradle/wrapper/gradle-wrapper.properties b/packages/server-java/gradle/wrapper/gradle-wrapper.properties index 8b992a0edb..d18ead82e0 100644 --- a/packages/server-java/gradle/wrapper/gradle-wrapper.properties +++ b/packages/server-java/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Thu Jul 23 14:04:29 EEST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists From a1c857cfd235d88138cf385827e5bd984d39baa0 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 4 Jan 2021 18:04:54 +0200 Subject: [PATCH 27/85] updated docker files --- Dockerfile-java | 2 +- docker-compose.react-java.yml | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Dockerfile-java b/Dockerfile-java index 38fc70760f..693d632cd2 100644 --- a/Dockerfile-java +++ b/Dockerfile-java @@ -3,7 +3,7 @@ COPY ./packages/server-java ./packages/server-java COPY ./modules ./modules WORKDIR ./packages/server-java -RUN ./gradlew clean bootJar +RUN ./gradlew clean build FROM openjdk:11-jre-slim WORKDIR /server-java/ diff --git a/docker-compose.react-java.yml b/docker-compose.react-java.yml index 8fe305bde5..6099177e70 100644 --- a/docker-compose.react-java.yml +++ b/docker-compose.react-java.yml @@ -1,6 +1,5 @@ version: '3' services: - apollo_java_server: build: context: . @@ -8,15 +7,11 @@ services: environment: - JAVA_ENV=development container_name: apollo_java_server - working_dir: ${APP_DIR}/packages/server-java volumes: - ./:${APP_DIR} - ./modules:${APP_DIR}/modules - ./packages/server-java/gradle/wrapper:${APP_DIR}/packages/server-java/gradle/wrapper - ./packages/server-java/gradlew:${APP_DIR}/packages/server-java/gradlew - command: > - sh -c 'chmod +x gradlew && ./gradlew clean bootRun' - # sh -c './gradlew clean' ports: - 8080:8080 stdin_open: true @@ -33,7 +28,7 @@ services: - apollo_java_server environment: - NODE_ENV=development - - SERVER_HOST=127.0.0.1:8080 + - SERVER_HOST=apollo_java_server:8080 container_name: apollo_client tty: true stdin_open: true From 0f410aa1952d8e398e02534aa4553bca54dcface Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 4 Jan 2021 18:05:04 +0200 Subject: [PATCH 28/85] added cors configuration --- .../sysgears/user/config/SecurityConfig.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java index d91ded5d14..0a6e97b369 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java @@ -6,6 +6,9 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @@ -18,6 +21,24 @@ public PasswordEncoder passwordEncoder() { @Override protected void configure(HttpSecurity http) throws Exception { // todo: Security disabled - http.csrf().disable().authorizeRequests().anyRequest().permitAll(); + http + .csrf().disable() + .authorizeRequests().anyRequest().permitAll() + .and().cors().configurationSource(securityCorsConfiguration()); + + } + + @Bean + public CorsConfigurationSource securityCorsConfiguration() { + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return source; } } From 21d3e08552ab20d9fd51acfc85048ed9446a0c10 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 4 Jan 2021 18:05:26 +0200 Subject: [PATCH 29/85] refactoring user module --- modules/core/server-java/.gitignore | 3 ++- .../resolvers/CounterQueryResolver.java | 2 ++ modules/user/server-java/.gitignore | 1 + .../com/sysgears/user/UserDBInitializer.java | 4 ++-- .../user/repository/CustomUserRepository.java | 13 +++++++++++ .../user/repository/JpaUserRepository.java | 13 ----------- .../user/repository/UserRepository.java | 14 +++-------- ...epository.java => UserRepositoryImpl.java} | 23 +------------------ .../user/resolvers/UserMutationResolver.java | 1 + .../user/resolvers/UserQueryResolver.java | 2 ++ 10 files changed, 27 insertions(+), 49 deletions(-) create mode 100644 modules/user/server-java/.gitignore create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java delete mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java rename modules/user/server-java/src/main/java/com/sysgears/user/repository/{DefaultUserRepository.java => UserRepositoryImpl.java} (80%) diff --git a/modules/core/server-java/.gitignore b/modules/core/server-java/.gitignore index a42eb305c6..4774d29aad 100644 --- a/modules/core/server-java/.gitignore +++ b/modules/core/server-java/.gitignore @@ -1,3 +1,4 @@ target/ /.idea -/build \ No newline at end of file +/build +/.gradle \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java index ff0066b16c..acf301ae71 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java @@ -7,6 +7,7 @@ import com.sysgears.counter.model.Counter; import org.springframework.stereotype.Service; import com.sysgears.counter.repository.CounterRepository; +import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.CompletableFuture; @@ -16,6 +17,7 @@ public class CounterQueryResolver implements GraphQLQueryResolver { private final CounterRepository repository; + @Transactional(readOnly = true) public CompletableFuture serverCounter() { log.debug("Get counter"); return repository.findById(CounterConstants.ID); diff --git a/modules/user/server-java/.gitignore b/modules/user/server-java/.gitignore new file mode 100644 index 0000000000..6b19dc7f40 --- /dev/null +++ b/modules/user/server-java/.gitignore @@ -0,0 +1 @@ +/.gradle \ No newline at end of file diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java b/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java index cd72904c2f..ef0c7f5ac1 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java @@ -4,7 +4,7 @@ import com.sysgears.user.model.UserAuth; import com.sysgears.user.model.UserProfile; import com.sysgears.user.model.auth.*; -import com.sysgears.user.repository.JpaUserRepository; +import com.sysgears.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationStartedEvent; @@ -18,7 +18,7 @@ @Component @RequiredArgsConstructor public class UserDBInitializer { - private final JpaUserRepository repository; + private final UserRepository repository; private final PasswordEncoder passwordEncoder; @EventListener diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java new file mode 100644 index 0000000000..80a41ed44b --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java @@ -0,0 +1,13 @@ +package com.sysgears.user.repository; + +import com.sysgears.user.dto.input.FilterUserInput; +import com.sysgears.user.dto.input.OrderByUserInput; +import com.sysgears.user.model.User; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public interface CustomUserRepository { + CompletableFuture> findByCriteria(Optional orderBy, Optional filter); +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java deleted file mode 100644 index 132fff8e47..0000000000 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/JpaUserRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sysgears.user.repository; - -import com.sysgears.user.model.User; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -public interface JpaUserRepository extends JpaRepository { - CompletableFuture findUserById(int id); - - Optional findById(int id); -} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java index 3e767a4bcf..259e6f61b1 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java @@ -1,21 +1,13 @@ package com.sysgears.user.repository; -import com.sysgears.user.dto.input.FilterUserInput; -import com.sysgears.user.dto.input.OrderByUserInput; import com.sysgears.user.model.User; +import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; -public interface UserRepository { - Optional findById(int id); - +public interface UserRepository extends JpaRepository, CustomUserRepository { CompletableFuture findUserById(int id); - User save(User user); - - void delete(User user); - - CompletableFuture> findByCriteria(Optional orderBy, Optional filter); + Optional findById(int id); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java similarity index 80% rename from modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java rename to modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java index d92104210f..c940626209 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java @@ -18,29 +18,8 @@ @Repository @RequiredArgsConstructor -public class DefaultUserRepository implements UserRepository { +public class UserRepositoryImpl implements CustomUserRepository { private final EntityManager entityManager; - private final JpaUserRepository jpaUserRepository; - - @Override - public Optional findById(int id) { - return jpaUserRepository.findById(id); - } - - @Override - public CompletableFuture findUserById(int id) { - return jpaUserRepository.findUserById(id); - } - - @Override - public User save(User user) { - return jpaUserRepository.save(user); - } - - @Override - public void delete(User user) { - jpaUserRepository.delete(user); - } @Override public CompletableFuture> findByCriteria(Optional orderBy, Optional filter) { diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index 026c5b5b25..e53d58f6b0 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -11,6 +11,7 @@ import com.sysgears.user.model.UserAuth; import com.sysgears.user.model.UserProfile; import com.sysgears.user.model.auth.*; +import com.sysgears.user.repository.CustomUserRepository; import com.sysgears.user.repository.UserRepository; import com.sysgears.user.subscription.UserUpdatedEvent; import graphql.kickstart.tools.GraphQLMutationResolver; diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java index b3400f3618..4788d7a96b 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -19,6 +20,7 @@ @Slf4j @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class UserQueryResolver implements GraphQLQueryResolver { private final UserRepository userRepository; From b06979a147d6b3b785a6c17e3cc667be6163408a Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 5 Jan 2021 18:48:46 +0200 Subject: [PATCH 30/85] initiate all users when starting app --- .../com/sysgears/user/UserDBInitializer.java | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java b/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java index cd72904c2f..11a3434b71 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/UserDBInitializer.java @@ -1,9 +1,6 @@ package com.sysgears.user; import com.sysgears.user.model.User; -import com.sysgears.user.model.UserAuth; -import com.sysgears.user.model.UserProfile; -import com.sysgears.user.model.auth.*; import com.sysgears.user.repository.JpaUserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,7 +9,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; -import java.util.UUID; +import java.util.List; @Slf4j @Component @@ -25,21 +22,13 @@ public class UserDBInitializer { public void onApplicationStartedEvent(ApplicationStartedEvent event) { long count = repository.count(); if (count == 0) { - String encodedPassword = passwordEncoder.encode("qwerty"); - log.info("Encoded pass: {}", encodedPassword); - User user = new User("admin", encodedPassword, "ADMIN", true, "example@sysgears.com"); - user.setProfile(new UserProfile("John", "Smith")); - user.setAuth( - UserAuth.builder() - .certificate(new CertificateAuth(UUID.randomUUID().toString())) - .facebook(new FacebookAuth(UUID.randomUUID().toString(), "facebookAuthName")) - .github(new GithubAuth(UUID.randomUUID().toString(), "githubAuthName")) - .google(new GoogleAuth(UUID.randomUUID().toString(), "googleAuthName")) - .linkedin(new LinkedInAuth(UUID.randomUUID().toString(), "linkedInAuthName")) - .build() - ); - repository.save(user); - log.debug("User initialized"); + String encodedAdminPassword = passwordEncoder.encode("admin123"); + String encodedUserPassword = passwordEncoder.encode("user1234"); + + User admin = new User("admin", encodedAdminPassword, "admin", true, "admin@example.com"); + User user = new User("user", encodedUserPassword, "user", true, "user@example.com"); + repository.saveAll(List.of(admin, user)); + log.debug("Users initialized"); } } } From b697cd8c991cb14b78676e2fa6749edc3a1f7dba Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 5 Jan 2021 18:48:56 +0200 Subject: [PATCH 31/85] added models for login --- .../sysgears/authentication/model/jwt/Tokens.java | 9 +++++++++ .../java/com/sysgears/user/dto/AuthPayload.java | 11 +++++++++++ .../user/dto/input/ForgotPasswordInput.java | 10 ++++++++++ .../sysgears/user/dto/input/LoginUserInput.java | 12 ++++++++++++ .../sysgears/user/dto/input/RegisterUserInput.java | 14 ++++++++++++++ .../user/dto/input/ResetPasswordInput.java | 14 ++++++++++++++ 6 files changed, 70 insertions(+) create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java new file mode 100644 index 0000000000..df9013e721 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java @@ -0,0 +1,9 @@ +package com.sysgears.authentication.model.jwt; + +import lombok.Data; + +@Data +public class Tokens { + private final String accessToken; + private final String refreshToken; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java new file mode 100644 index 0000000000..0ef1bd7cc2 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java @@ -0,0 +1,11 @@ +package com.sysgears.user.dto; + +import com.sysgears.authentication.model.jwt.Tokens; +import com.sysgears.user.model.User; +import lombok.Data; + +@Data +public class AuthPayload { + private final User user; + private final Tokens tokens; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java new file mode 100644 index 0000000000..7653e7a9d0 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java @@ -0,0 +1,10 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class ForgotPasswordInput { + @NonNull + private final String email; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java new file mode 100644 index 0000000000..83336753bb --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java @@ -0,0 +1,12 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class LoginUserInput { + @NonNull + private final String usernameOrEmail; + @NonNull + private final String password; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java new file mode 100644 index 0000000000..0d224fee60 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java @@ -0,0 +1,14 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class RegisterUserInput { + @NonNull + private final String username; + @NonNull + private final String email; + @NonNull + private final String password; +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java new file mode 100644 index 0000000000..c08e361b14 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java @@ -0,0 +1,14 @@ +package com.sysgears.user.dto.input; + +import lombok.Data; +import lombok.NonNull; + +@Data +public class ResetPasswordInput { + @NonNull + private final String token; + @NonNull + private final String password; + @NonNull + private final String passwordConfirmation; +} From 41f890e122ee9c553c854b984cdd3aec6565860f Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 5 Jan 2021 18:49:04 +0200 Subject: [PATCH 32/85] implemented logout --- .../authentication/server-java/build.gradle | 5 +++++ .../resolvers/SessionMutationResolver.java | 18 ++++++++++++++++++ modules/user/server-java/build.gradle | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java diff --git a/modules/authentication/server-java/build.gradle b/modules/authentication/server-java/build.gradle index 7e4e0d4f7e..f440c19f5b 100644 --- a/modules/authentication/server-java/build.gradle +++ b/modules/authentication/server-java/build.gradle @@ -1,5 +1,10 @@ +plugins { + id 'java-library' +} + dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' + api 'org.springframework.boot:spring-boot-starter-security' } \ No newline at end of file diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java new file mode 100644 index 0000000000..56af688eb5 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java @@ -0,0 +1,18 @@ +package com.sysgears.authentication.resolvers; + +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SessionMutationResolver implements GraphQLMutationResolver { + + public String logout() { + SecurityContextHolder.getContext().setAuthentication(null); + return null; + } +} diff --git a/modules/user/server-java/build.gradle b/modules/user/server-java/build.gradle index 4c19fb697c..c88b3aacb4 100644 --- a/modules/user/server-java/build.gradle +++ b/modules/user/server-java/build.gradle @@ -1,4 +1,4 @@ dependencies { implementation project(':core') - implementation 'org.springframework.boot:spring-boot-starter-security' + implementation project(':authentication') } \ No newline at end of file From 8194b62a675a865f25fea206370f0ee3b4ecf36e Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 5 Jan 2021 18:49:23 +0200 Subject: [PATCH 33/85] implemented jwt + login + refactoring --- .../resolvers/JwtMutationResolver.java | 22 +++++ .../authentication/service/jwt/JwtParser.java | 5 ++ .../service/jwt/JwtService.java | 67 +++++++++++++++ .../config/JWTPreAuthenticationToken.java | 15 ++++ .../com/sysgears/user/config/JwtFilter.java | 44 ++++++++++ .../sysgears/user/config/SecurityConfig.java | 23 +++++- .../exception/PasswordInvalidException.java | 12 +++ .../java/com/sysgears/user/model/User.java | 31 ++++++- .../repository/DefaultUserRepository.java | 40 ++++++--- .../user/repository/UserRepository.java | 4 +- .../user/resolvers/UserMutationResolver.java | 13 ++- .../user/resolvers/UserQueryResolver.java | 16 ++-- .../user/resolvers/password/JwtResolver.java | 82 +++++++++++++++++++ .../sysgears/user/service/UserService.java | 67 +++++++++++++++ .../java/com/sysgears/user/UserQueryTest.java | 2 +- .../app/src/main/resources/application.yml | 2 + 16 files changed, 414 insertions(+), 31 deletions(-) create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/config/JWTPreAuthenticationToken.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java new file mode 100644 index 0000000000..fe0ff2ca9c --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java @@ -0,0 +1,22 @@ +package com.sysgears.authentication.resolvers; + +import com.sysgears.authentication.model.jwt.Tokens; +import com.sysgears.authentication.service.jwt.JwtGenerator; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JwtMutationResolver implements GraphQLMutationResolver { + + private final JwtGenerator jwtGenerator; + + public Tokens refreshTokens(String refreshToken) { + //todo implement + return null; + } + +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java new file mode 100644 index 0000000000..5113876a19 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java @@ -0,0 +1,5 @@ +package com.sysgears.authentication.service.jwt; + +public interface JwtParser { + Integer getIdFromToken(String token); +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java new file mode 100644 index 0000000000..dcf324c423 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java @@ -0,0 +1,67 @@ +package com.sysgears.authentication.service.jwt; + +import com.sysgears.authentication.config.JwtConfig; +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.authentication.model.jwt.Tokens; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JwtService implements JwtGenerator, JwtParser { + private final Key secretKey; + private final JwtConfig jwtConfig; + + public Tokens generateTokens(JwtUserIdentity identity) { + return new Tokens(generateAccessToken(identity), generateRefreshToken(identity)); + } + + public Integer getIdFromToken(String token) { + return (Integer) Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .get("identity", Map.class) + .get("id"); + } + + private String generateAccessToken(JwtUserIdentity identity) { + Map claims = new HashMap<>(); + claims.put("identity", identity); + log.debug("Generating new access JWT for user {}", identity.getId()); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(jwtConfig.getAccessTokenExpirationInSec(), ChronoUnit.SECONDS))) + .setHeaderParam("typ", "JWT") + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + private String generateRefreshToken(JwtUserIdentity identity) { + Map claims = new HashMap<>(); + claims.put("id", identity.getId()); + log.debug("Generating new refresh JWT for user {}", identity.getId()); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(jwtConfig.getRefreshTokenExpirationInSec(), ChronoUnit.SECONDS))) + .setHeaderParam("typ", "JWT") + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/JWTPreAuthenticationToken.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/JWTPreAuthenticationToken.java new file mode 100644 index 0000000000..0d8536e711 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/JWTPreAuthenticationToken.java @@ -0,0 +1,15 @@ +package com.sysgears.user.config; + +import com.sysgears.user.model.User; +import lombok.Getter; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +@Getter +public class JWTPreAuthenticationToken extends PreAuthenticatedAuthenticationToken { + + public JWTPreAuthenticationToken(User principal, WebAuthenticationDetails details) { + super(principal, null, principal.getAuthorities()); + super.setDetails(details); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java new file mode 100644 index 0000000000..c1c4bd419e --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java @@ -0,0 +1,44 @@ +package com.sysgears.user.config; + +import com.sysgears.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final Pattern BEARER_PATTERN = Pattern.compile("^Bearer (.+?)$"); + private final UserService userService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + getToken(request) + .map(userService::loadUserByToken) + .map(user -> new JWTPreAuthenticationToken(user, new WebAuthenticationDetailsSource().buildDetails(request))) + .ifPresent(authentication -> SecurityContextHolder.getContext().setAuthentication(authentication)); + filterChain.doFilter(request, response); + } + + private Optional getToken(HttpServletRequest request) { + return Optional + .ofNullable(request.getHeader(AUTHORIZATION_HEADER)) + .filter(Predicate.not(String::isEmpty)) + .map(BEARER_PATTERN::matcher) + .filter(Matcher::find) + .map(matcher -> matcher.group(1)); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java index d91ded5d14..ee6984e747 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java @@ -1,23 +1,42 @@ package com.sysgears.user.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +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.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.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; @Configuration +@RequiredArgsConstructor +// if async mode for graphQL will be disabled, spring security @PreAuthorize can be used +//@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { + private final UserDetailsService userService; + private final JwtFilter jwtFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Override + protected void configure(final AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); + } + @Override protected void configure(HttpSecurity http) throws Exception { - // todo: Security disabled - http.csrf().disable().authorizeRequests().anyRequest().permitAll(); + http.csrf().disable() + .authorizeRequests().anyRequest().permitAll() + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterBefore(jwtFilter, RequestHeaderAuthenticationFilter.class); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java new file mode 100644 index 0000000000..4badcde27d --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java @@ -0,0 +1,12 @@ +package com.sysgears.user.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class PasswordInvalidException extends RuntimeException { + + public PasswordInvalidException() { + super("Specified password is invalid"); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java index a87ed1b5ae..740c7ceb19 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java @@ -5,14 +5,18 @@ import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.GenericGenerator; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; +import java.util.Collection; +import java.util.List; @Entity @Data @NoArgsConstructor @Table(name = "USERS") -public class User { +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") @GenericGenerator(name = "native", strategy = "native") @@ -52,4 +56,29 @@ public User(String username, this.isActive = isActive; this.email = email; } + + @Override + public Collection getAuthorities() { + return List.of(() -> role); + } + + @Override + public boolean isAccountNonExpired() { + return isActive; + } + + @Override + public boolean isAccountNonLocked() { + return isActive; + } + + @Override + public boolean isCredentialsNonExpired() { + return isActive; + } + + @Override + public boolean isEnabled() { + return isActive; + } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java index d92104210f..5daa7928fe 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/DefaultUserRepository.java @@ -43,7 +43,7 @@ public void delete(User user) { } @Override - public CompletableFuture> findByCriteria(Optional orderBy, Optional filter) { + public List findByCriteria(Optional orderBy, Optional filter) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(User.class); @@ -51,17 +51,22 @@ public CompletableFuture> findByCriteria(Optional o List predicates = new ArrayList<>(); filter.ifPresent(filterUserInput -> { - filterUserInput.getRole().ifPresent(role -> predicates.add(builder.like(user.get("role"), role))); - filterUserInput.getIsActive().ifPresent(isActive -> predicates.add(builder.equal(user.get("isActive"), isActive))); - filterUserInput.getSearchText().ifPresent(searchText -> { - predicates.add(builder.or((builder.like(user.get("username"), searchText)), - builder.like(user.get("email"), searchText))); - }); + filterUserInput.getRole().filter(s -> !s.isBlank()) + .ifPresent(role -> predicates.add(builder.like(user.get("role"), role))); + filterUserInput.getIsActive() + .ifPresent(isActive -> predicates.add(builder.equal(user.get("isActive"), isActive))); + filterUserInput.getSearchText().filter(s -> !s.isBlank()) + .ifPresent(searchText -> predicates.add(builder.or( + builder.like(user.get("username"), searchText), + builder.like(user.get("email"), searchText) + ))); }); orderBy.ifPresent(orderByUserInput -> { - String orderColumn = orderByUserInput.getColumn().orElse("id"); - if (orderByUserInput.getOrder().isPresent() && orderByUserInput.getOrder().get().toLowerCase().equals("desc")) { + String orderColumn = orderByUserInput.getColumn() + .filter(s -> !s.isBlank()) + .orElse("id"); + if (orderByUserInput.getOrder().filter(s -> !s.isBlank()).isPresent() && orderByUserInput.getOrder().get().toLowerCase().equals("desc")) { query.orderBy(builder.desc(user.get(orderColumn))); } else { query.orderBy(builder.asc(user.get(orderColumn))); @@ -69,6 +74,21 @@ public CompletableFuture> findByCriteria(Optional o }); query.where(predicates.toArray(new Predicate[0])); - return CompletableFuture.supplyAsync(() -> entityManager.createQuery(query).getResultList()); + return entityManager.createQuery(query).getResultList(); + } + + @Override + public Optional findByUsernameOrAndEmail(String usernameOrEmail) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(User.class); + + Root user = query.from(User.class); + + query.where(builder.or( + builder.equal(user.get("username"), usernameOrEmail), + builder.equal(user.get("email"), usernameOrEmail) + )); + + return Optional.ofNullable(entityManager.createQuery(query).getSingleResult()); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java index 3e767a4bcf..59cefb33a7 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java @@ -17,5 +17,7 @@ public interface UserRepository { void delete(User user); - CompletableFuture> findByCriteria(Optional orderBy, Optional filter); + List findByCriteria(Optional orderBy, Optional filter); + + Optional findByUsernameOrAndEmail(String usernameOrEmail); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index 026c5b5b25..d1f0c83d75 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -111,16 +111,15 @@ public CompletableFuture editUser(EditUserInput input) { @Transactional public CompletableFuture deleteUser(int id) { - return repository.findUserById(id) - .thenApplyAsync(user -> { - if (user == null) throw new UserNotFoundException(id); + return repository.findUserById(id).thenApplyAsync(user -> { + if (user == null) throw new UserNotFoundException(id); - repository.delete(user); + repository.delete(user); - publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.DELETE_USER, user)); + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.DELETE_USER, user)); - return new UserPayload(user); - }); + return new UserPayload(user); + }); } private UserAuth from(AuthInput authInput) { diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java index b3400f3618..f68cf2b5ca 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java @@ -1,39 +1,37 @@ package com.sysgears.user.resolvers; -import com.sysgears.user.constant.UserConstants; import com.sysgears.user.dto.UserPayload; import com.sysgears.user.dto.input.FilterUserInput; import com.sysgears.user.dto.input.OrderByUserInput; -import com.sysgears.user.exception.UserNotFoundException; import com.sysgears.user.model.User; -import com.sysgears.user.repository.UserRepository; +import com.sysgears.user.service.UserService; import graphql.kickstart.tools.GraphQLQueryResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @Slf4j -@Service +@Component @RequiredArgsConstructor public class UserQueryResolver implements GraphQLQueryResolver { - private final UserRepository userRepository; + private final UserService userService; public CompletableFuture currentUser() { - return userRepository.findUserById(UserConstants.ID); + return CompletableFuture.supplyAsync(() -> userService.getCurrentAuditor().orElse(null)); } public CompletableFuture user(int id) { return CompletableFuture.supplyAsync(() -> { - User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); + User user = userService.getById(id); return new UserPayload(user); }); } public CompletableFuture> users(Optional orderBy, Optional filter) { - return userRepository.findByCriteria(orderBy, filter); + return CompletableFuture.supplyAsync(() -> userService.findByCriteria(orderBy, filter)); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java new file mode 100644 index 0000000000..15f8ba913c --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java @@ -0,0 +1,82 @@ +package com.sysgears.user.resolvers.password; + +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.authentication.model.jwt.Tokens; +import com.sysgears.authentication.service.jwt.JwtGenerator; +import com.sysgears.user.config.JWTPreAuthenticationToken; +import com.sysgears.user.dto.AuthPayload; +import com.sysgears.user.dto.UserPayload; +import com.sysgears.user.dto.input.ForgotPasswordInput; +import com.sysgears.user.dto.input.LoginUserInput; +import com.sysgears.user.dto.input.RegisterUserInput; +import com.sysgears.user.dto.input.ResetPasswordInput; +import com.sysgears.user.exception.PasswordInvalidException; +import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.model.User; +import com.sysgears.user.repository.UserRepository; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JwtResolver implements GraphQLMutationResolver { + private final UserRepository userRepository; + private final BCryptPasswordEncoder passwordEncoder; + private final JwtGenerator jwtGenerator; + + @Transactional(readOnly = true) + public CompletableFuture login(LoginUserInput loginUserInput) { + return CompletableFuture.supplyAsync(() -> { + Optional userOpt = userRepository.findByUsernameOrAndEmail(loginUserInput.getUsernameOrEmail()); + if (userOpt.isEmpty()) { + throw new UserNotFoundException(); + } + + User user = userOpt.get(); + boolean matches = passwordEncoder.matches(loginUserInput.getPassword(), user.getPassword()); + if (!matches) { + log.debug("Password is invalid"); + throw new PasswordInvalidException(); + } + + JwtUserIdentity jwtUserIdentity = new JwtUserIdentity( + user.getId(), + user.getUsername(), + user.getPassword(), + user.getRole(), + user.getIsActive(), + user.getEmail(), + user.getProfile() == null ? null : user.getProfile().getFirstName(), + user.getProfile() == null ? null : user.getProfile().getLastName() + ); + Tokens tokens = jwtGenerator.generateTokens(jwtUserIdentity); + SecurityContextHolder.getContext().setAuthentication(new JWTPreAuthenticationToken(user, null)); + + return new AuthPayload(user, tokens); + }); + } + + public CompletableFuture forgotPassword(ForgotPasswordInput input) { + //todo implement + return null; + } + + public CompletableFuture resetPassword(ResetPasswordInput input) { + //todo implement + return null; + } + + public CompletableFuture register(RegisterUserInput input) { + //todo implement + return null; + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java new file mode 100644 index 0000000000..d4aac51cc0 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java @@ -0,0 +1,67 @@ +package com.sysgears.user.service; + +import com.sysgears.authentication.service.jwt.JwtParser; +import com.sysgears.user.dto.input.FilterUserInput; +import com.sysgears.user.dto.input.OrderByUserInput; +import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.model.User; +import com.sysgears.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserService implements UserDetailsService, AuditorAware { + + private final UserRepository userRepository; + private final JwtParser jwtParser; + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userRepository.findByUsernameOrAndEmail(username).orElseThrow(UserNotFoundException::new); + } + + @Override + public Optional getCurrentAuditor() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + // no user + return Optional.empty(); + } + + if (authentication.getPrincipal() instanceof User) { + return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + } else { + // anonymous user + return Optional.empty(); + } + } + + @Transactional(readOnly = true) + public User getById(Integer id) { + return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); + } + + @Transactional(readOnly = true) + public User loadUserByToken(String token) { + Integer userId = jwtParser.getIdFromToken(token); + return userRepository.findById(userId).orElseThrow(UserNotFoundException::new); + } + + @Transactional(readOnly = true) + public List findByCriteria(Optional orderBy, Optional filter) { + return userRepository.findByCriteria(orderBy, filter); + } +} diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java index aaf946b2a9..8a8bcbc41a 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java @@ -59,7 +59,7 @@ void init() { @Test void users() throws IOException { when(repository.findByCriteria(Optional.empty(), Optional.empty())) - .thenReturn(CompletableFuture.completedFuture(Collections.singletonList(user))); + .thenReturn(Collections.singletonList(user)); GraphQLResponse response = template.postForResource("query/users.graphql"); diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 987e5e892a..9a891852dd 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -33,6 +33,8 @@ graphiql: endpoint: subscriptions: /graphql mapping: /graphiql + headers: + Authorization: Bearer generated-token jwt: secret: "676918AB4D29BFE59CCB943F3C09F5CC8FB3A8511E23E502B67DE95AB9A9D00C" From f998d4dfa6e82711c5b8344f9daa3e2ade01254b Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 5 Jan 2021 18:50:12 +0200 Subject: [PATCH 34/85] added JwtGenerator interface --- .../service/jwt/JWTGenerator.java | 58 +------------------ 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java index 3f05dd3e54..41609bd01e 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JWTGenerator.java @@ -1,60 +1,8 @@ package com.sysgears.authentication.service.jwt; -import com.sysgears.authentication.config.JwtConfig; import com.sysgears.authentication.model.jwt.JwtUserIdentity; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import com.sysgears.authentication.model.jwt.Tokens; -import java.security.Key; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -public class JWTGenerator { - private final Key secretKey; - private final JwtConfig jwtConfig; - - //todo: need to separate generation and parsing JWT. - public String generateToken(JwtUserIdentity identity) { - Map claims = new HashMap<>(); - claims.put("identity", identity); - log.debug("Generating new access JWT for user {}", identity.getId()); - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(Date.from(Instant.now())) - .setExpiration(Date.from(Instant.now().plus(jwtConfig.getAccessTokenExpirationInSec(), ChronoUnit.SECONDS))) - .setHeaderParam("typ", "JWT") - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); - } - - public String generateRefreshToken(JwtUserIdentity identity) { - Map claims = new HashMap<>(); - claims.put("id", identity.getId()); - log.debug("Generating new refresh JWT for user {}", identity.getId()); - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(Date.from(Instant.now())) - .setExpiration(Date.from(Instant.now().plus(jwtConfig.getRefreshTokenExpirationInSec(), ChronoUnit.SECONDS))) - .setHeaderParam("typ", "JWT") - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); - } - - - public String getAllClaimsFromToken(String token) { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .toString(); - } +public interface JwtGenerator { + Tokens generateTokens(JwtUserIdentity identity); } From af533ec77e860966c3b01f2f594a5e7c4a078c0d Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 6 Jan 2021 18:27:53 +0200 Subject: [PATCH 35/85] updated build configuration and Dockerfile --- Dockerfile-java | 2 +- modules/authentication/server-java/build.gradle | 5 +---- packages/server-java/build.gradle | 7 ++++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Dockerfile-java b/Dockerfile-java index 693d632cd2..38fc70760f 100644 --- a/Dockerfile-java +++ b/Dockerfile-java @@ -3,7 +3,7 @@ COPY ./packages/server-java ./packages/server-java COPY ./modules ./modules WORKDIR ./packages/server-java -RUN ./gradlew clean build +RUN ./gradlew clean bootJar FROM openjdk:11-jre-slim WORKDIR /server-java/ diff --git a/modules/authentication/server-java/build.gradle b/modules/authentication/server-java/build.gradle index f440c19f5b..943905c9d8 100644 --- a/modules/authentication/server-java/build.gradle +++ b/modules/authentication/server-java/build.gradle @@ -1,8 +1,5 @@ -plugins { - id 'java-library' -} - dependencies { + implementation project(':core') implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index 34080ea6e4..53cbc981a6 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '2.4.1' + springBootVersion = '2.3.7.RELEASE' } repositories { maven { url "https://plugins.gradle.org/m2/" } @@ -13,6 +13,7 @@ buildscript { subprojects { apply plugin: 'idea' apply plugin: 'java' + apply plugin: 'java-library' apply plugin: 'io.spring.dependency-management' apply plugin: 'org.springframework.boot' @@ -26,8 +27,6 @@ subprojects { } bootJar { enabled = false - launchScript() - mainClassName = 'com.sysgears.Application' } repositories { mavenCentral() @@ -82,5 +81,7 @@ project(':app') { } bootJar { enabled = true + launchScript() + mainClassName = 'com.sysgears.Application' } } \ No newline at end of file From 53ef7fe9677474df3eb73ff3b1ae4d43b770122b Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 6 Jan 2021 18:31:34 +0200 Subject: [PATCH 36/85] implemented refrehTokens API --- .../model/jwt/JwtUserIdentity.java | 2 ++ .../resolvers/JwtMutationResolver.java | 21 ----------- .../authentication/service/jwt/JwtParser.java | 4 ++- .../service/jwt/JwtService.java | 12 ++++++- .../user/resolvers/password/JwtResolver.java | 35 ++++++++++++++++--- 5 files changed, 47 insertions(+), 27 deletions(-) delete mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java index 2b33f306ed..8407817912 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/JwtUserIdentity.java @@ -1,8 +1,10 @@ package com.sysgears.authentication.model.jwt; +import lombok.Builder; import lombok.Data; @Data +@Builder public class JwtUserIdentity { private final int id; private final String username; diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java deleted file mode 100644 index 35c853d85f..0000000000 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/JwtMutationResolver.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sysgears.authentication.resolvers; - -import com.sysgears.authentication.model.jwt.Tokens; -import com.sysgears.authentication.service.jwt.JwtGenerator; -import graphql.kickstart.tools.GraphQLMutationResolver; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class JwtMutationResolver implements GraphQLMutationResolver { - - private final JwtGenerator jwtGenerator; - - public Tokens refreshTokens(String refreshToken) { - //todo implement - return null; - } -} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java index 5113876a19..22b675f4a9 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java @@ -1,5 +1,7 @@ package com.sysgears.authentication.service.jwt; public interface JwtParser { - Integer getIdFromToken(String token); + Integer getIdFromAccessToken(String token); + + Integer getIdFromRefreshToken(String token); } diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java index dcf324c423..6bad3badb2 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java @@ -27,7 +27,7 @@ public Tokens generateTokens(JwtUserIdentity identity) { return new Tokens(generateAccessToken(identity), generateRefreshToken(identity)); } - public Integer getIdFromToken(String token) { + public Integer getIdFromAccessToken(String token) { return (Integer) Jwts.parserBuilder() .setSigningKey(secretKey) .build() @@ -37,6 +37,16 @@ public Integer getIdFromToken(String token) { .get("id"); } + @Override + public Integer getIdFromRefreshToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .get("id", Integer.class); + } + private String generateAccessToken(JwtUserIdentity identity) { Map claims = new HashMap<>(); claims.put("identity", identity); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java index 15f8ba913c..8a060e1630 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java @@ -3,6 +3,8 @@ import com.sysgears.authentication.model.jwt.JwtUserIdentity; import com.sysgears.authentication.model.jwt.Tokens; import com.sysgears.authentication.service.jwt.JwtGenerator; +import com.sysgears.authentication.service.jwt.JwtParser; +import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.AuthPayload; import com.sysgears.user.dto.UserPayload; @@ -17,12 +19,10 @@ import graphql.kickstart.tools.GraphQLMutationResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; import java.util.concurrent.CompletableFuture; @Slf4j @@ -30,8 +30,9 @@ @RequiredArgsConstructor public class JwtResolver implements GraphQLMutationResolver { private final UserRepository userRepository; - private final BCryptPasswordEncoder passwordEncoder; + private final PasswordEncoder passwordEncoder; private final JwtGenerator jwtGenerator; + private final JwtParser jwtParser; @Transactional(readOnly = true) public CompletableFuture login(LoginUserInput loginUserInput) { @@ -79,4 +80,30 @@ public CompletableFuture register(RegisterUserInput input) { //todo implement return null; } + + public CompletableFuture refreshTokens(String refreshToken) { + return CompletableFuture.supplyAsync(() -> { + Integer userId = jwtParser.getIdFromRefreshToken(refreshToken); + User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); + return jwtGenerator.generateTokens(convert(user)); + }); + } + + private JwtUserIdentity convert(User user) { + JwtUserIdentity.JwtUserIdentityBuilder builder = JwtUserIdentity.builder(); + builder + .id(user.getId()) + .username(user.getUsername()) + .passwordHash(user.getPassword()) + .role(user.getRole()) + .isActive(user.getIsActive()) + .email(user.getEmail()); + + if (user.getProfile() != null) { + builder + .firstName(user.getProfile().getFirstName()) + .lastName(user.getProfile().getLastName()); + } + return builder.build(); + } } From 4257a02f945e27433d3b16f128714af95b074b38 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 6 Jan 2021 18:32:07 +0200 Subject: [PATCH 37/85] moved refreshTokens schema --- .../server-java/src/main/resources/jwt/schema.graphqls | 4 ---- .../server-java/src/main/resources/password/schema.graphqls | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 modules/authentication/server-java/src/main/resources/jwt/schema.graphqls diff --git a/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls b/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls deleted file mode 100644 index c751942f1b..0000000000 --- a/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls +++ /dev/null @@ -1,4 +0,0 @@ -extend type Mutation { - # Refresh user tokens - refreshTokens(refreshToken: String!): Tokens! -} diff --git a/modules/user/server-java/src/main/resources/password/schema.graphqls b/modules/user/server-java/src/main/resources/password/schema.graphqls index ee5e64726c..c67225d8a2 100644 --- a/modules/user/server-java/src/main/resources/password/schema.graphqls +++ b/modules/user/server-java/src/main/resources/password/schema.graphqls @@ -38,4 +38,6 @@ extend type Mutation { resetPassword(input: ResetPasswordInput!): String # Register user register(input: RegisterUserInput!): UserPayload! + # Refresh user tokens + refreshTokens(refreshToken: String!): Tokens! } From a0908d33b22d6df2ffd495bdc53b0088562d4823 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 6 Jan 2021 18:32:30 +0200 Subject: [PATCH 38/85] removed CORS config --- .../sysgears/user/config/SecurityConfig.java | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java index 4f24e43b8a..ee6984e747 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; 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.WebSecurityConfigurerAdapter; @@ -12,10 +11,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -@EnableAsync + @Configuration @RequiredArgsConstructor // if async mode for graphQL will be disabled, spring security @PreAuthorize can be used @@ -41,20 +37,6 @@ protected void configure(HttpSecurity http) throws Exception { .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() - .addFilterBefore(jwtFilter, RequestHeaderAuthenticationFilter.class) - .cors().configurationSource(securityCorsConfiguration()); - } - @Bean - public CorsConfigurationSource securityCorsConfiguration() { - final CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); - config.addAllowedOriginPattern("*"); - config.addAllowedHeader("*"); - config.addAllowedMethod("*"); - - final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - - return source; + .addFilterBefore(jwtFilter, RequestHeaderAuthenticationFilter.class); } } From a23e697a8e736db55224582d8ac75721fcf68d40 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 6 Jan 2021 18:34:18 +0200 Subject: [PATCH 39/85] fixed and improve login/logout --- .../resolvers/SessionMutationResolver.java | 18 ------------------ .../session/SessionMutationResolver.java | 19 +++++++++++++++++++ .../authentication/utils/SessionUtils.java | 11 +++++++++++ .../com/sysgears/user/config/JwtFilter.java | 4 ++-- .../user/resolvers/password/JwtResolver.java | 14 ++------------ 5 files changed, 34 insertions(+), 32 deletions(-) delete mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/session/SessionMutationResolver.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java deleted file mode 100644 index 56af688eb5..0000000000 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/SessionMutationResolver.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sysgears.authentication.resolvers; - -import graphql.kickstart.tools.GraphQLMutationResolver; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class SessionMutationResolver implements GraphQLMutationResolver { - - public String logout() { - SecurityContextHolder.getContext().setAuthentication(null); - return null; - } -} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/session/SessionMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/session/SessionMutationResolver.java new file mode 100644 index 0000000000..2fb46b2758 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/session/SessionMutationResolver.java @@ -0,0 +1,19 @@ +package com.sysgears.authentication.resolvers.session; + +import com.sysgears.authentication.utils.SessionUtils; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class SessionMutationResolver implements GraphQLMutationResolver { + + public String logout() { + if (SessionUtils.SECURITY_CONTEXT.getAuthentication() != null) { + log.info("Logout user '{}'", SessionUtils.SECURITY_CONTEXT.getAuthentication().getName()); + SessionUtils.SECURITY_CONTEXT.setAuthentication(null); + } + return null; + } +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java new file mode 100644 index 0000000000..d738325753 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java @@ -0,0 +1,11 @@ +package com.sysgears.authentication.utils; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class SessionUtils { + public static final SecurityContext SECURITY_CONTEXT = SecurityContextHolder.createEmptyContext(); + + private SessionUtils() { + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java index c1c4bd419e..fe34161a7b 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java @@ -1,8 +1,8 @@ package com.sysgears.user.config; +import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.user.service.UserService; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -29,7 +29,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse getToken(request) .map(userService::loadUserByToken) .map(user -> new JWTPreAuthenticationToken(user, new WebAuthenticationDetailsSource().buildDetails(request))) - .ifPresent(authentication -> SecurityContextHolder.getContext().setAuthentication(authentication)); + .ifPresent(authentication -> SessionUtils.SECURITY_CONTEXT.setAuthentication(authentication)); filterChain.doFilter(request, response); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java index 8a060e1630..2452610e85 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java @@ -49,18 +49,8 @@ public CompletableFuture login(LoginUserInput loginUserInput) { throw new PasswordInvalidException(); } - JwtUserIdentity jwtUserIdentity = new JwtUserIdentity( - user.getId(), - user.getUsername(), - user.getPassword(), - user.getRole(), - user.getIsActive(), - user.getEmail(), - user.getProfile() == null ? null : user.getProfile().getFirstName(), - user.getProfile() == null ? null : user.getProfile().getLastName() - ); - Tokens tokens = jwtGenerator.generateTokens(jwtUserIdentity); - SecurityContextHolder.getContext().setAuthentication(new JWTPreAuthenticationToken(user, null)); + Tokens tokens = jwtGenerator.generateTokens(convert(user)); + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); return new AuthPayload(user, tokens); }); From dd2bf58bc7083811be5bf9e975690219fed4c88d Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 6 Jan 2021 18:35:12 +0200 Subject: [PATCH 40/85] refactoring graphQL resolvers + enable async + fixed tests --- .../counter/repository/CounterRepository.java | 2 ++ .../resolvers/CounterMutationResolver.java | 4 +-- .../resolvers/CounterQueryResolver.java | 8 ++--- .../CounterSubscriptionResolver.java | 4 +-- .../sysgears/counter/CounterQueryTest.java | 2 ++ .../sysgears/user/constant/UserConstants.java | 8 ----- .../user/repository/CustomUserRepository.java | 4 ++- .../user/repository/UserRepositoryImpl.java | 6 ++-- .../user/resolvers/UserMutationResolver.java | 23 ++++++--------- .../user/resolvers/UserQueryResolver.java | 5 +--- .../user/resolvers/password/JwtResolver.java | 8 +---- .../sysgears/user/service/UserService.java | 29 ++++++++++++------- .../com/sysgears/user/UserMutationTest.java | 4 +-- .../java/com/sysgears/user/UserQueryTest.java | 12 ++++---- .../main/java/com/sysgears/Application.java | 2 ++ 15 files changed, 59 insertions(+), 62 deletions(-) delete mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java index f21c386692..c1b61f2398 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/repository/CounterRepository.java @@ -2,9 +2,11 @@ import com.sysgears.counter.model.Counter; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.scheduling.annotation.Async; import java.util.concurrent.CompletableFuture; public interface CounterRepository extends JpaRepository { + @Async CompletableFuture findById(int id); } \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java index 7c1c159756..c75a1f5a8a 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java @@ -7,13 +7,13 @@ import graphql.kickstart.tools.GraphQLMutationResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.CompletableFuture; @Slf4j -@Service +@Component @RequiredArgsConstructor public class CounterMutationResolver implements GraphQLMutationResolver { private final CounterRepository repository; diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java index acf301ae71..05184f1a71 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java @@ -1,18 +1,18 @@ package com.sysgears.counter.resolvers; import com.sysgears.counter.constant.CounterConstants; +import com.sysgears.counter.model.Counter; +import com.sysgears.counter.repository.CounterRepository; import graphql.kickstart.tools.GraphQLQueryResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.sysgears.counter.model.Counter; -import org.springframework.stereotype.Service; -import com.sysgears.counter.repository.CounterRepository; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.CompletableFuture; @Slf4j -@Service +@Component @RequiredArgsConstructor public class CounterQueryResolver implements GraphQLQueryResolver { private final CounterRepository repository; diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java index 51f535650f..7ccbb0626f 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterSubscriptionResolver.java @@ -6,10 +6,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; @Slf4j -@Service +@Component @RequiredArgsConstructor public class CounterSubscriptionResolver implements GraphQLSubscriptionResolver { private final Subscriber subscriber; diff --git a/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java b/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java index 0282c2d901..6f80b4bb8b 100644 --- a/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java +++ b/modules/counter/server-java/src/test/java/com/sysgears/counter/CounterQueryTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; import java.io.IOException; @@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional public class CounterQueryTest { @Autowired private GraphQLTestTemplate graphQLTestTemplate; diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java b/modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java deleted file mode 100644 index ed8b9a3590..0000000000 --- a/modules/user/server-java/src/main/java/com/sysgears/user/constant/UserConstants.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.sysgears.user.constant; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class UserConstants { - public static final int ID = 1; -} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java index 72528cb1b8..91ec000b12 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java @@ -3,13 +3,15 @@ import com.sysgears.user.dto.input.FilterUserInput; import com.sysgears.user.dto.input.OrderByUserInput; import com.sysgears.user.model.User; +import org.springframework.scheduling.annotation.Async; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; public interface CustomUserRepository { + @Async CompletableFuture> findByCriteria(Optional orderBy, Optional filter); - Optional findByUsernameOrAndEmail(String usernameOrEmail); + CompletableFuture findByUsernameOrAndEmail(String usernameOrEmail); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java index fc279fb34b..6ffdc169bc 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java @@ -53,11 +53,11 @@ public CompletableFuture> findByCriteria(Optional o }); query.where(predicates.toArray(new Predicate[0])); - return CompletableFuture.supplyAsync(()->entityManager.createQuery(query).getResultList()); + return CompletableFuture.supplyAsync(() -> entityManager.createQuery(query).getResultList()); } @Override - public Optional findByUsernameOrAndEmail(String usernameOrEmail) { + public CompletableFuture findByUsernameOrAndEmail(String usernameOrEmail) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(User.class); @@ -68,6 +68,6 @@ public Optional findByUsernameOrAndEmail(String usernameOrEmail) { builder.equal(user.get("email"), usernameOrEmail) )); - return Optional.ofNullable(entityManager.createQuery(query).getSingleResult()); + return CompletableFuture.supplyAsync(() -> entityManager.createQuery(query).getSingleResult()); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index 3a7210cf5a..40ffb7260a 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -11,27 +11,24 @@ import com.sysgears.user.model.UserAuth; import com.sysgears.user.model.UserProfile; import com.sysgears.user.model.auth.*; -import com.sysgears.user.repository.CustomUserRepository; -import com.sysgears.user.repository.UserRepository; +import com.sysgears.user.service.UserService; import com.sysgears.user.subscription.UserUpdatedEvent; import graphql.kickstart.tools.GraphQLMutationResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.stereotype.Component; import java.util.concurrent.CompletableFuture; @Slf4j -@Service +@Component @RequiredArgsConstructor public class UserMutationResolver implements GraphQLMutationResolver { - private final UserRepository repository; + private final UserService userService; private final Publisher publisher; private final PasswordEncoder passwordEncoder; - @Transactional public CompletableFuture addUser(AddUserInput input) { User user = new User( input.getUsername(), @@ -50,16 +47,15 @@ public CompletableFuture addUser(AddUserInput input) { input.getAuth() .map(this::from) .ifPresent(user::setAuth); - repository.save(user); + userService.save(user); publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.ADD_USER, user)); return CompletableFuture.completedFuture(new UserPayload(user)); } - @Transactional public CompletableFuture editUser(EditUserInput input) { - return repository.findUserById(input.getId()) + return userService.findUserById(input.getId()) .thenApplyAsync(user -> { if (user == null) throw new UserNotFoundException(input.getId()); @@ -102,7 +98,7 @@ public CompletableFuture editUser(EditUserInput input) { input.getAuth().map(this::from).ifPresent(user::setAuth); } - repository.save(user); + userService.save(user); publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.EDIT_USER, user)); @@ -110,12 +106,11 @@ public CompletableFuture editUser(EditUserInput input) { }); } - @Transactional public CompletableFuture deleteUser(int id) { - return repository.findUserById(id).thenApplyAsync(user -> { + return userService.findUserById(id).thenApplyAsync(user -> { if (user == null) throw new UserNotFoundException(id); - repository.delete(user); + userService.delete(user); publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.DELETE_USER, user)); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java index 39f5461623..8d9705d4ac 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserQueryResolver.java @@ -25,10 +25,7 @@ public CompletableFuture currentUser() { } public CompletableFuture user(int id) { - return CompletableFuture.supplyAsync(() -> { - User user = userService.getById(id); - return new UserPayload(user); - }); + return userService.findUserById(id).thenApply(UserPayload::new); } public CompletableFuture> users(Optional orderBy, Optional filter) { diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java index 2452610e85..eea92340fa 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java @@ -36,13 +36,7 @@ public class JwtResolver implements GraphQLMutationResolver { @Transactional(readOnly = true) public CompletableFuture login(LoginUserInput loginUserInput) { - return CompletableFuture.supplyAsync(() -> { - Optional userOpt = userRepository.findByUsernameOrAndEmail(loginUserInput.getUsernameOrEmail()); - if (userOpt.isEmpty()) { - throw new UserNotFoundException(); - } - - User user = userOpt.get(); + return userRepository.findByUsernameOrAndEmail(loginUserInput.getUsernameOrEmail()).thenApply(user -> { boolean matches = passwordEncoder.matches(loginUserInput.getPassword(), user.getPassword()); if (!matches) { log.debug("Password is invalid"); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java index 6385d19080..60bba503b7 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java @@ -1,6 +1,7 @@ package com.sysgears.user.service; import com.sysgears.authentication.service.jwt.JwtParser; +import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.user.dto.input.FilterUserInput; import com.sysgears.user.dto.input.OrderByUserInput; import com.sysgears.user.exception.UserNotFoundException; @@ -9,7 +10,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; @Service +@Transactional @RequiredArgsConstructor public class UserService implements UserDetailsService, AuditorAware { @@ -30,12 +31,12 @@ public class UserService implements UserDetailsService, AuditorAware { @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return userRepository.findByUsernameOrAndEmail(username).orElseThrow(UserNotFoundException::new); + return userRepository.findByUsernameOrAndEmail(username).join(); } @Override public Optional getCurrentAuditor() { - final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final Authentication authentication = SessionUtils.SECURITY_CONTEXT.getAuthentication(); if (authentication == null) { // no user @@ -43,21 +44,16 @@ public Optional getCurrentAuditor() { } if (authentication.getPrincipal() instanceof User) { - return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + return Optional.of((User) SessionUtils.SECURITY_CONTEXT.getAuthentication().getPrincipal()); } else { // anonymous user return Optional.empty(); } } - @Transactional(readOnly = true) - public User getById(Integer id) { - return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); - } - @Transactional(readOnly = true) public User loadUserByToken(String token) { - Integer userId = jwtParser.getIdFromToken(token); + Integer userId = jwtParser.getIdFromAccessToken(token); return userRepository.findById(userId).orElseThrow(UserNotFoundException::new); } @@ -65,4 +61,17 @@ public User loadUserByToken(String token) { public CompletableFuture> findByCriteria(Optional orderBy, Optional filter) { return userRepository.findByCriteria(orderBy, filter); } + + public User save(User user) { + return userRepository.save(user); + } + + @Transactional(readOnly = true) + public CompletableFuture findUserById(int id) { + return userRepository.findUserById(id); + } + + public void delete(User user) { + userRepository.delete(user); + } } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java index f520749e89..d15bb145b2 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; import java.io.IOException; @@ -20,7 +20,7 @@ import static org.mockito.Mockito.mock; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@Transactional public class UserMutationTest { @Autowired GraphQLTestTemplate template; diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java index aaf946b2a9..412b1b48f1 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java @@ -4,7 +4,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; -import com.sysgears.user.constant.UserConstants; +import com.sysgears.authentication.utils.SessionUtils; +import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.UserPayload; import com.sysgears.user.model.User; import com.sysgears.user.model.UserAuth; @@ -16,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.util.Collections; @@ -28,6 +30,7 @@ import static org.mockito.Mockito.when; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional public class UserQueryTest { @Autowired GraphQLTestTemplate template; @@ -87,8 +90,8 @@ void user() throws IOException { ObjectNode node = mapper.createObjectNode(); node.put("id", 1); - when(repository.findById(1)) - .thenReturn(Optional.of(user)); + when(repository.findUserById(1)) + .thenReturn(CompletableFuture.completedFuture(user)); GraphQLResponse response = template.perform("query/user.graphql", node); @@ -109,8 +112,7 @@ void user() throws IOException { @Test void currentUser() throws IOException { - when(repository.findUserById(UserConstants.ID)) - .thenReturn(CompletableFuture.completedFuture(user)); + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); GraphQLResponse response = template.postForResource("query/current-user.graphql"); diff --git a/packages/server-java/app/src/main/java/com/sysgears/Application.java b/packages/server-java/app/src/main/java/com/sysgears/Application.java index 7d7d7a77cf..566eaeb0e2 100644 --- a/packages/server-java/app/src/main/java/com/sysgears/Application.java +++ b/packages/server-java/app/src/main/java/com/sysgears/Application.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +@EnableAsync @SpringBootApplication(scanBasePackages = {"com.sysgears"}) public class Application { public static void main(String[] args) { From 433ff0f3e9adba71b62c2d64898a1366a105cd77 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 14:08:48 +0200 Subject: [PATCH 41/85] added cors --- .../sysgears/user/config/SecurityConfig.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java index ee6984e747..e7edd3b6e7 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java @@ -11,6 +11,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @RequiredArgsConstructor @@ -37,6 +40,21 @@ protected void configure(HttpSecurity http) throws Exception { .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() - .addFilterBefore(jwtFilter, RequestHeaderAuthenticationFilter.class); + .addFilterBefore(jwtFilter, RequestHeaderAuthenticationFilter.class) + .cors().configurationSource(securityCorsConfiguration()); + } + + @Bean + public CorsConfigurationSource securityCorsConfiguration() { + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return source; } } From 93c4e56abe8818f37f2871449e8f9a2d331c1dfd Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 14:10:23 +0200 Subject: [PATCH 42/85] moved refreshTokens API to authentication module + refactoring/improving user service --- .../RefreshTokenInvalidException.java | 12 +++ .../resolvers/jwt/JwtMutationResolver.java | 28 ++++++ .../resolvers/jwt/JwtUserIdentityService.java | 9 ++ .../src/main/resources/jwt/schema.graphqls | 4 + .../user/exception/UserNotFoundException.java | 1 + .../user/repository/CustomUserRepository.java | 2 +- .../user/repository/UserRepositoryImpl.java | 2 +- .../user/resolvers/password/JwtResolver.java | 93 ------------------- .../password/LoginMutationResolver.java | 84 +++++++++++++++++ .../sysgears/user/service/UserService.java | 30 +++++- .../sysgears/user/util/UserIdentityUtils.java | 27 ++++++ .../main/resources/password/schema.graphqls | 2 - 12 files changed, 194 insertions(+), 100 deletions(-) create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java create mode 100644 modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtUserIdentityService.java create mode 100644 modules/authentication/server-java/src/main/resources/jwt/schema.graphqls delete mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java new file mode 100644 index 0000000000..a405d0f505 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java @@ -0,0 +1,12 @@ +package com.sysgears.authentication.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class RefreshTokenInvalidException extends RuntimeException { + + public RefreshTokenInvalidException() { + super("Refresh token is invalid."); + } +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java new file mode 100644 index 0000000000..5c91456398 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java @@ -0,0 +1,28 @@ +package com.sysgears.authentication.resolvers.jwt; + +import com.sysgears.authentication.exception.RefreshTokenInvalidException; +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.authentication.model.jwt.Tokens; +import com.sysgears.authentication.service.jwt.JwtGenerator; +import com.sysgears.authentication.service.jwt.JwtParser; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class JwtMutationResolver implements GraphQLMutationResolver { + private final JwtParser jwtParser; + private final JwtGenerator jwtGenerator; + private final JwtUserIdentityService userIdentityService; + + public CompletableFuture refreshTokens(String refreshToken) { + return CompletableFuture.supplyAsync(() -> { + Integer userId = jwtParser.getIdFromRefreshToken(refreshToken); + JwtUserIdentity userIdentity = userIdentityService.findById(userId).orElseThrow(RefreshTokenInvalidException::new); + return jwtGenerator.generateTokens(userIdentity); + }); + } +} diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtUserIdentityService.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtUserIdentityService.java new file mode 100644 index 0000000000..1f47c256e7 --- /dev/null +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtUserIdentityService.java @@ -0,0 +1,9 @@ +package com.sysgears.authentication.resolvers.jwt; + +import com.sysgears.authentication.model.jwt.JwtUserIdentity; + +import java.util.Optional; + +public interface JwtUserIdentityService { + Optional findById(Integer id); +} diff --git a/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls b/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls new file mode 100644 index 0000000000..c751942f1b --- /dev/null +++ b/modules/authentication/server-java/src/main/resources/jwt/schema.graphqls @@ -0,0 +1,4 @@ +extend type Mutation { + # Refresh user tokens + refreshTokens(refreshToken: String!): Tokens! +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java index bf71dd8aab..21e28a68f8 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java @@ -12,6 +12,7 @@ public UserNotFoundException() { public UserNotFoundException(String message) { super(message); } + public UserNotFoundException(int id) { super(String.format("User with id %d not found.", id)); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java index 91ec000b12..da3d9f41bf 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java @@ -13,5 +13,5 @@ public interface CustomUserRepository { @Async CompletableFuture> findByCriteria(Optional orderBy, Optional filter); - CompletableFuture findByUsernameOrAndEmail(String usernameOrEmail); + CompletableFuture findByUsernameOrEmail(String usernameOrEmail); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java index 6ffdc169bc..17f5cf2d68 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java @@ -57,7 +57,7 @@ public CompletableFuture> findByCriteria(Optional o } @Override - public CompletableFuture findByUsernameOrAndEmail(String usernameOrEmail) { + public CompletableFuture findByUsernameOrEmail(String usernameOrEmail) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(User.class); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java deleted file mode 100644 index eea92340fa..0000000000 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/JwtResolver.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.sysgears.user.resolvers.password; - -import com.sysgears.authentication.model.jwt.JwtUserIdentity; -import com.sysgears.authentication.model.jwt.Tokens; -import com.sysgears.authentication.service.jwt.JwtGenerator; -import com.sysgears.authentication.service.jwt.JwtParser; -import com.sysgears.authentication.utils.SessionUtils; -import com.sysgears.user.config.JWTPreAuthenticationToken; -import com.sysgears.user.dto.AuthPayload; -import com.sysgears.user.dto.UserPayload; -import com.sysgears.user.dto.input.ForgotPasswordInput; -import com.sysgears.user.dto.input.LoginUserInput; -import com.sysgears.user.dto.input.RegisterUserInput; -import com.sysgears.user.dto.input.ResetPasswordInput; -import com.sysgears.user.exception.PasswordInvalidException; -import com.sysgears.user.exception.UserNotFoundException; -import com.sysgears.user.model.User; -import com.sysgears.user.repository.UserRepository; -import graphql.kickstart.tools.GraphQLMutationResolver; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.concurrent.CompletableFuture; - -@Slf4j -@Service -@RequiredArgsConstructor -public class JwtResolver implements GraphQLMutationResolver { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtGenerator jwtGenerator; - private final JwtParser jwtParser; - - @Transactional(readOnly = true) - public CompletableFuture login(LoginUserInput loginUserInput) { - return userRepository.findByUsernameOrAndEmail(loginUserInput.getUsernameOrEmail()).thenApply(user -> { - boolean matches = passwordEncoder.matches(loginUserInput.getPassword(), user.getPassword()); - if (!matches) { - log.debug("Password is invalid"); - throw new PasswordInvalidException(); - } - - Tokens tokens = jwtGenerator.generateTokens(convert(user)); - SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); - - return new AuthPayload(user, tokens); - }); - } - - public CompletableFuture forgotPassword(ForgotPasswordInput input) { - //todo implement - return null; - } - - public CompletableFuture resetPassword(ResetPasswordInput input) { - //todo implement - return null; - } - - public CompletableFuture register(RegisterUserInput input) { - //todo implement - return null; - } - - public CompletableFuture refreshTokens(String refreshToken) { - return CompletableFuture.supplyAsync(() -> { - Integer userId = jwtParser.getIdFromRefreshToken(refreshToken); - User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - return jwtGenerator.generateTokens(convert(user)); - }); - } - - private JwtUserIdentity convert(User user) { - JwtUserIdentity.JwtUserIdentityBuilder builder = JwtUserIdentity.builder(); - builder - .id(user.getId()) - .username(user.getUsername()) - .passwordHash(user.getPassword()) - .role(user.getRole()) - .isActive(user.getIsActive()) - .email(user.getEmail()); - - if (user.getProfile() != null) { - builder - .firstName(user.getProfile().getFirstName()) - .lastName(user.getProfile().getLastName()); - } - return builder.build(); - } -} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java new file mode 100644 index 0000000000..cc92311771 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java @@ -0,0 +1,84 @@ +package com.sysgears.user.resolvers.password; + +import com.sysgears.authentication.model.jwt.Tokens; +import com.sysgears.authentication.service.jwt.JwtGenerator; +import com.sysgears.authentication.utils.SessionUtils; +import com.sysgears.user.config.JWTPreAuthenticationToken; +import com.sysgears.user.dto.AuthPayload; +import com.sysgears.user.dto.UserPayload; +import com.sysgears.user.dto.input.ForgotPasswordInput; +import com.sysgears.user.dto.input.LoginUserInput; +import com.sysgears.user.dto.input.RegisterUserInput; +import com.sysgears.user.dto.input.ResetPasswordInput; +import com.sysgears.user.exception.PasswordInvalidException; +import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.model.User; +import com.sysgears.user.service.UserService; +import com.sysgears.user.util.UserIdentityUtils; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.NoResultException; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LoginMutationResolver implements GraphQLMutationResolver { + private final UserService userService; + private final PasswordEncoder passwordEncoder; + private final JwtGenerator jwtGenerator; + + @Transactional(readOnly = true) + public CompletableFuture login(LoginUserInput loginUserInput) { + return userService.findUserByUsernameOrEmail(loginUserInput.getUsernameOrEmail()) + .thenApply(user -> { + boolean matches = passwordEncoder.matches(loginUserInput.getPassword(), user.getPassword()); + if (!matches) { + log.debug("Password is invalid"); + throw new PasswordInvalidException(); + } + + Tokens tokens = jwtGenerator.generateTokens(UserIdentityUtils.convert(user)); + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); + + return new AuthPayload(user, tokens); + }) + .exceptionally(throwable -> { + if (throwable.getCause() instanceof NoResultException) { + throw new UserNotFoundException("No user with specified email or username."); + } else { + log.error("Unexpected error happens when find user with " + loginUserInput.getUsernameOrEmail(), throwable); + throw new RuntimeException(throwable.getCause()); + } + }); + } + + public CompletableFuture forgotPassword(ForgotPasswordInput input) { + //todo implement + return null; + } + + public CompletableFuture resetPassword(ResetPasswordInput input) { + //todo implement + return null; + } + + public CompletableFuture register(RegisterUserInput input) { + return CompletableFuture.supplyAsync(() -> { + User user = new User( + input.getUsername(), + passwordEncoder.encode(input.getPassword()), + "user", + false, + input.getEmail()); + User registered = userService.save(user); + // todo: send email to activate user. + return new UserPayload(registered); + }); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java index 60bba503b7..a16261c7e7 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java @@ -1,5 +1,7 @@ package com.sysgears.user.service; +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.authentication.resolvers.jwt.JwtUserIdentityService; import com.sysgears.authentication.service.jwt.JwtParser; import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.user.dto.input.FilterUserInput; @@ -7,7 +9,9 @@ import com.sysgears.user.exception.UserNotFoundException; import com.sysgears.user.model.User; import com.sysgears.user.repository.UserRepository; +import com.sysgears.user.util.UserIdentityUtils; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; @@ -16,14 +20,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.persistence.NoResultException; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +@Slf4j @Service @Transactional @RequiredArgsConstructor -public class UserService implements UserDetailsService, AuditorAware { +public class UserService implements UserDetailsService, AuditorAware, JwtUserIdentityService { private final UserRepository userRepository; private final JwtParser jwtParser; @@ -31,7 +37,20 @@ public class UserService implements UserDetailsService, AuditorAware { @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return userRepository.findByUsernameOrAndEmail(username).join(); + return findUserByUsernameOrEmail(username).handle((user, throwable) -> { + if (throwable != null) { + if (throwable.getCause() instanceof NoResultException) { + throw new UsernameNotFoundException(String.format("User with username '%s' not found.", username)); + } + log.error(String.format("Unexpected error happened when load user by username '%s'", username), throwable); + } + return user; + }).join(); + } + + @Transactional(readOnly = true) + public CompletableFuture findUserByUsernameOrEmail(String usernameOrEmail) { + return userRepository.findByUsernameOrEmail(usernameOrEmail); } @Override @@ -67,11 +86,16 @@ public User save(User user) { } @Transactional(readOnly = true) - public CompletableFuture findUserById(int id) { + public CompletableFuture findUserById(Integer id) { return userRepository.findUserById(id); } public void delete(User user) { userRepository.delete(user); } + + @Override + public Optional findById(Integer userId) { + return userRepository.findById(userId).map(UserIdentityUtils::convert); + } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java b/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java new file mode 100644 index 0000000000..9a3e62cd03 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java @@ -0,0 +1,27 @@ +package com.sysgears.user.util; + +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.user.model.User; +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class UserIdentityUtils { + + public static JwtUserIdentity convert(User user) { + JwtUserIdentity.JwtUserIdentityBuilder builder = JwtUserIdentity.builder(); + builder + .id(user.getId()) + .username(user.getUsername()) + .passwordHash(user.getPassword()) + .role(user.getRole()) + .isActive(user.getIsActive()) + .email(user.getEmail()); + + if (user.getProfile() != null) { + builder + .firstName(user.getProfile().getFirstName()) + .lastName(user.getProfile().getLastName()); + } + return builder.build(); + } +} diff --git a/modules/user/server-java/src/main/resources/password/schema.graphqls b/modules/user/server-java/src/main/resources/password/schema.graphqls index c67225d8a2..ee5e64726c 100644 --- a/modules/user/server-java/src/main/resources/password/schema.graphqls +++ b/modules/user/server-java/src/main/resources/password/schema.graphqls @@ -38,6 +38,4 @@ extend type Mutation { resetPassword(input: ResetPasswordInput!): String # Register user register(input: RegisterUserInput!): UserPayload! - # Refresh user tokens - refreshTokens(refreshToken: String!): Tokens! } From eac9b562f0fd78fed7758313701752bbcaa6b6dc Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 18:38:20 +0200 Subject: [PATCH 43/85] added check on a blank string for a refresh token --- .../authentication/resolvers/jwt/JwtMutationResolver.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java index 5c91456398..1eff767aa1 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolver.java @@ -20,6 +20,9 @@ public class JwtMutationResolver implements GraphQLMutationResolver { public CompletableFuture refreshTokens(String refreshToken) { return CompletableFuture.supplyAsync(() -> { + if (refreshToken.isBlank()) { + throw new IllegalArgumentException(); + } Integer userId = jwtParser.getIdFromRefreshToken(refreshToken); JwtUserIdentity userIdentity = userIdentityService.findById(userId).orElseThrow(RefreshTokenInvalidException::new); return jwtGenerator.generateTokens(userIdentity); From f04de2657b6cc3960305d9c1dc82a4bb711fa078 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 18:38:44 +0200 Subject: [PATCH 44/85] implemented graphql error handling --- .../exception/CustomGraphQLException.java | 52 +++++++++++++++++++ .../core/exception/FieldErrorException.java | 36 +++++++++++++ .../exception/GraphQLExceptionHandler.java | 21 ++++++++ 3 files changed, 109 insertions(+) create mode 100644 modules/core/server-java/src/main/java/com/sysgears/core/exception/CustomGraphQLException.java create mode 100644 modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java create mode 100644 modules/core/server-java/src/main/java/com/sysgears/core/exception/GraphQLExceptionHandler.java diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/exception/CustomGraphQLException.java b/modules/core/server-java/src/main/java/com/sysgears/core/exception/CustomGraphQLException.java new file mode 100644 index 0000000000..ae99475104 --- /dev/null +++ b/modules/core/server-java/src/main/java/com/sysgears/core/exception/CustomGraphQLException.java @@ -0,0 +1,52 @@ +package com.sysgears.core.exception; + +import graphql.GraphQLError; +import graphql.kickstart.execution.error.GenericGraphQLError; + +import java.util.Map; +import java.util.Objects; + +public class CustomGraphQLException extends GenericGraphQLError { + + private final Throwable throwable; + + public CustomGraphQLException(Throwable throwable) { + this(throwable, throwable.getMessage()); + } + + public CustomGraphQLException(Throwable throwable, String message) { + super(message); + + this.throwable = throwable; + } + + public String getType() { + return throwable.getClass().getSimpleName(); + } + + @Override + public Map getExtensions() { + if (throwable instanceof GraphQLError) { + return ((GraphQLError) throwable).getExtensions(); + } else { + return null; + } + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomGraphQLException)) { + return false; + } + CustomGraphQLException that = (CustomGraphQLException) o; + return Objects.equals(throwable, that.throwable) && Objects.equals(getMessage(), that.getMessage()); + } + + @Override + public final int hashCode() { + return Objects.hash(throwable, getMessage()); + } +} diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java b/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java new file mode 100644 index 0000000000..90e1e68660 --- /dev/null +++ b/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java @@ -0,0 +1,36 @@ +package com.sysgears.core.exception; + +import graphql.ErrorClassification; +import graphql.GraphQLError; +import graphql.language.SourceLocation; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public abstract class FieldErrorException extends RuntimeException implements GraphQLError { + private final Map errors = new HashMap<>(); + + public FieldErrorException(String message, Map errors) { + super(message); + this.errors.putAll(errors); + } + + @Override + public List getLocations() { + return null; + } + + @Override + public ErrorClassification getErrorType() { + return null; + } + + @Override + public Map getExtensions() { + return Map.of("exception", Map.of("errors", errors)); + } +} diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/exception/GraphQLExceptionHandler.java b/modules/core/server-java/src/main/java/com/sysgears/core/exception/GraphQLExceptionHandler.java new file mode 100644 index 0000000000..615b48ee23 --- /dev/null +++ b/modules/core/server-java/src/main/java/com/sysgears/core/exception/GraphQLExceptionHandler.java @@ -0,0 +1,21 @@ +package com.sysgears.core.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.concurrent.CompletionException; + +@Slf4j +@Component +public class GraphQLExceptionHandler { + + @ExceptionHandler(CompletionException.class) + public CustomGraphQLException handle(CompletionException exception) { + if (exception.getCause() != null) { + return new CustomGraphQLException(exception.getCause()); + } else { + return new CustomGraphQLException(exception); + } + } +} From bfbf45ff98089c6b09c4ab8bbb7039109e7770ce Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 18:46:19 +0200 Subject: [PATCH 45/85] added checks on login and register user --- .../user/exception/LoginFailedException.java | 12 +++++++++ .../exception/PasswordInvalidException.java | 12 --------- .../exception/UserAlreadyExistsException.java | 12 +++++++++ .../user/repository/UserRepository.java | 4 +++ .../password/LoginMutationResolver.java | 25 +++++++++++++++---- .../sysgears/user/service/UserService.java | 12 +++++++++ 6 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java delete mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java new file mode 100644 index 0000000000..f7d6695b4e --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java @@ -0,0 +1,12 @@ +package com.sysgears.user.exception; + +import com.sysgears.core.exception.FieldErrorException; + +import java.util.Map; + +public class LoginFailedException extends FieldErrorException { + + public LoginFailedException(Map errors) { + super("Login failed.", errors); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java deleted file mode 100644 index 4badcde27d..0000000000 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/PasswordInvalidException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.sysgears.user.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.BAD_REQUEST) -public class PasswordInvalidException extends RuntimeException { - - public PasswordInvalidException() { - super("Specified password is invalid"); - } -} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java new file mode 100644 index 0000000000..94aac46de8 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java @@ -0,0 +1,12 @@ +package com.sysgears.user.exception; + +import com.sysgears.core.exception.FieldErrorException; + +import java.util.Map; + +public class UserAlreadyExistsException extends FieldErrorException { + + public UserAlreadyExistsException(Map errors) { + super("User already exists.", errors); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java index 05a8fbaf35..a121e93809 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepository.java @@ -9,4 +9,8 @@ public interface UserRepository extends JpaRepository, CustomUserRepository { @Async CompletableFuture findUserById(int id); + + Boolean existsByEmail(String email); + + Boolean existsByUsername(String username); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java index cc92311771..33e29f1b01 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java @@ -10,8 +10,8 @@ import com.sysgears.user.dto.input.LoginUserInput; import com.sysgears.user.dto.input.RegisterUserInput; import com.sysgears.user.dto.input.ResetPasswordInput; -import com.sysgears.user.exception.PasswordInvalidException; -import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.exception.LoginFailedException; +import com.sysgears.user.exception.UserAlreadyExistsException; import com.sysgears.user.model.User; import com.sysgears.user.service.UserService; import com.sysgears.user.util.UserIdentityUtils; @@ -23,7 +23,10 @@ import org.springframework.transaction.annotation.Transactional; import javax.persistence.NoResultException; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; @Slf4j @Component @@ -40,7 +43,7 @@ public CompletableFuture login(LoginUserInput loginUserInput) { boolean matches = passwordEncoder.matches(loginUserInput.getPassword(), user.getPassword()); if (!matches) { log.debug("Password is invalid"); - throw new PasswordInvalidException(); + throw new LoginFailedException(Map.of("password", "Please enter a valid password.")); } Tokens tokens = jwtGenerator.generateTokens(UserIdentityUtils.convert(user)); @@ -50,10 +53,11 @@ public CompletableFuture login(LoginUserInput loginUserInput) { }) .exceptionally(throwable -> { if (throwable.getCause() instanceof NoResultException) { - throw new UserNotFoundException("No user with specified email or username."); + log.warn("Specified email or username '{}' is invalid.", loginUserInput.getUsernameOrEmail()); + throw new LoginFailedException(Map.of("usernameOrEmail", "Please enter a valid username or e-mail.")); } else { log.error("Unexpected error happens when find user with " + loginUserInput.getUsernameOrEmail(), throwable); - throw new RuntimeException(throwable.getCause()); + throw (CompletionException) throwable; } }); } @@ -70,6 +74,17 @@ public CompletableFuture resetPassword(ResetPasswordInput input) { public CompletableFuture register(RegisterUserInput input) { return CompletableFuture.supplyAsync(() -> { + Map errors = new HashMap<>(); + if (userService.existsByEmail(input.getEmail())) { + errors.put("email", "E-mail already exists."); + } + if (userService.existsByUsername(input.getUsername())) { + errors.put("username", "Username already exists."); + } + if (!errors.isEmpty()) { + throw new UserAlreadyExistsException(errors); + } + User user = new User( input.getUsername(), passwordEncoder.encode(input.getPassword()), diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java index a16261c7e7..3fff0dff3f 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java @@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.AuditorAware; +import org.springframework.lang.NonNull; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -53,6 +54,7 @@ public CompletableFuture findUserByUsernameOrEmail(String usernameOrEmail) return userRepository.findByUsernameOrEmail(usernameOrEmail); } + @NonNull @Override public Optional getCurrentAuditor() { final Authentication authentication = SessionUtils.SECURITY_CONTEXT.getAuthentication(); @@ -98,4 +100,14 @@ public void delete(User user) { public Optional findById(Integer userId) { return userRepository.findById(userId).map(UserIdentityUtils::convert); } + + @Transactional(readOnly = true) + public Boolean existsByEmail(String email) { + return userRepository.existsByEmail(email); + } + + @Transactional(readOnly = true) + public Boolean existsByUsername(String username) { + return userRepository.existsByUsername(username); + } } From 6d201df55721dd4408fe666096e82b7f19ce27dc Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 19:34:58 +0200 Subject: [PATCH 46/85] made skeleton for mailer module --- modules/mailer/server-java/.gitignore | 1 + modules/mailer/server-java/build.gradle | 4 ++++ .../sysgears/mailer/service/DefaultEmailService.java | 7 +++++++ .../com/sysgears/mailer/service/EmailService.java | 4 ++++ .../app/src/main/resources/application.yml | 12 ++++++++++++ packages/server-java/build.gradle | 1 + packages/server-java/settings.gradle | 2 +- 7 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 modules/mailer/server-java/.gitignore create mode 100644 modules/mailer/server-java/build.gradle create mode 100644 modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java create mode 100644 modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java diff --git a/modules/mailer/server-java/.gitignore b/modules/mailer/server-java/.gitignore new file mode 100644 index 0000000000..6b19dc7f40 --- /dev/null +++ b/modules/mailer/server-java/.gitignore @@ -0,0 +1 @@ +/.gradle \ No newline at end of file diff --git a/modules/mailer/server-java/build.gradle b/modules/mailer/server-java/build.gradle new file mode 100644 index 0000000000..d96503a842 --- /dev/null +++ b/modules/mailer/server-java/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':core') + implementation 'org.springframework.boot:spring-boot-starter-mail' +} diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java new file mode 100644 index 0000000000..9607915125 --- /dev/null +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java @@ -0,0 +1,7 @@ +package com.sysgears.mailer.service; + +import org.springframework.stereotype.Service; + +@Service +public class DefaultEmailService implements EmailService { +} diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java new file mode 100644 index 0000000000..7a8f1ccb91 --- /dev/null +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java @@ -0,0 +1,4 @@ +package com.sysgears.mailer.service; + +public interface EmailService { +} diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 9a891852dd..26fe96e640 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -16,6 +16,18 @@ spring: console: enabled: true path: /h2-console +# mail: +# host: EMAIL_HOST +# port: 587 +# username: EMAIL_USER +# password: EMAIL_PASSWORD +# sender: EMAIL_SENDER # todo: no such property in Spring, need to handle manually +# properties: +# mail: +# smtp: +# auth: true +# starttls: +# enable: true graphql: servlet: async-mode-enabled: true diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index 53cbc981a6..e768b88d30 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -78,6 +78,7 @@ project(':app') { implementation project(':counter') implementation project(':user') implementation project(':authentication') + implementation project(':mailer') } bootJar { enabled = true diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index 17ad33e330..b373704b47 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user', ':authentication' +include ':app', ':core', ':counter', ':user', ':authentication', 'mailer' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') From 5f040b7872d1d03daa66d07c8d2b9de78eb6595b Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 19:35:21 +0200 Subject: [PATCH 47/85] removed redundant graphql property --- .../main/java/com/sysgears/user/config/SecurityConfig.java | 1 + packages/server-java/app/src/main/resources/application.yml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java index e7edd3b6e7..0e54112912 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/SecurityConfig.java @@ -18,6 +18,7 @@ @Configuration @RequiredArgsConstructor // if async mode for graphQL will be disabled, spring security @PreAuthorize can be used +// graphql-spring-boot-starter from version 8.0.0 use graphql.execution.AsyncExecutionStrategy by default. //@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userService; diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 26fe96e640..08fe7d7153 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -30,7 +30,6 @@ spring: # enable: true graphql: servlet: - async-mode-enabled: true exception-handlers-enabled: true cors: allowed-origins: "*" @@ -47,6 +46,9 @@ graphiql: mapping: /graphiql headers: Authorization: Bearer generated-token + props: + variables: + headerEditorEnabled: true jwt: secret: "676918AB4D29BFE59CCB943F3C09F5CC8FB3A8511E23E502B67DE95AB9A9D00C" From 4ee62364cf1618363d034c2ea1ba1ed354258018 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 8 Jan 2021 19:38:17 +0200 Subject: [PATCH 48/85] identify mailer module in settings.gradle --- packages/server-java/settings.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index b373704b47..fdf4fc5578 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -4,4 +4,5 @@ include ':app', ':core', ':counter', ':user', ':authentication', 'mailer' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') project(':user').projectDir = new File('../../modules/user/server-java') -project(':authentication').projectDir = new File('../../modules/authentication/server-java') \ No newline at end of file +project(':authentication').projectDir = new File('../../modules/authentication/server-java') +project(':mailer').projectDir = new File('../../modules/mailer/server-java') From ff8432ef5acb8c2da4df32d5d234d0baaa2eca7c Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 13 Jan 2021 18:08:58 +0200 Subject: [PATCH 49/85] added registration confirm functionality --- modules/mailer/server-java/build.gradle | 1 + .../mailer/config/ThymeleafConfiguration.java | 34 +++++++++ .../mailer/service/DefaultEmailService.java | 74 +++++++++++++++++++ .../sysgears/mailer/service/EmailService.java | 6 ++ modules/user/server-java/build.gradle | 3 +- .../password/LoginMutationResolver.java | 12 ++- .../templates/confirm-registration.html | 15 ++++ 7 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/ThymeleafConfiguration.java create mode 100644 packages/server-java/app/src/main/resources/templates/confirm-registration.html diff --git a/modules/mailer/server-java/build.gradle b/modules/mailer/server-java/build.gradle index d96503a842..358ccd4d4a 100644 --- a/modules/mailer/server-java/build.gradle +++ b/modules/mailer/server-java/build.gradle @@ -1,4 +1,5 @@ dependencies { implementation project(':core') implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' } diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/ThymeleafConfiguration.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/ThymeleafConfiguration.java new file mode 100644 index 0000000000..23774b626e --- /dev/null +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/ThymeleafConfiguration.java @@ -0,0 +1,34 @@ +package com.sysgears.mailer.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; +import org.thymeleaf.spring5.view.ThymeleafViewResolver; +import org.thymeleaf.templatemode.TemplateMode; + +@Configuration +public class ThymeleafConfiguration { + @Bean + public SpringTemplateEngine templateEngine() { + SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.setTemplateResolver(thymeleafTemplateResolver()); + return templateEngine; + } + + @Bean + public SpringResourceTemplateResolver thymeleafTemplateResolver() { + SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); + templateResolver.setPrefix("classpath:/templates/"); + templateResolver.setSuffix(".html"); + templateResolver.setTemplateMode(TemplateMode.HTML); + return templateResolver; + } + + @Bean + public ThymeleafViewResolver thymeleafViewResolver() { + ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); + viewResolver.setTemplateEngine(templateEngine()); + return viewResolver; + } +} diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java index 9607915125..cfdc370972 100644 --- a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java @@ -1,7 +1,81 @@ package com.sysgears.mailer.service; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring5.SpringTemplateEngine; + +import javax.mail.internet.MimeMessage; +import java.util.Map; @Service public class DefaultEmailService implements EmailService { + private final JavaMailSender mailSender; + private final SpringTemplateEngine templateEngine; + private final String baseUrl; + private final String profileRedirectedUrl; + + public DefaultEmailService(JavaMailSender mailSender, + SpringTemplateEngine templateEngine, + @Value("${app.server.baseUrl}") String baseUrl, + @Value("${app.redirect.profile}") String profileRedirectedUrl) { + this.mailSender = mailSender; + this.templateEngine = templateEngine; + this.baseUrl = baseUrl; + this.profileRedirectedUrl = profileRedirectedUrl; + } + + public void sendRegistrationConfirmEmail(String name, String email, String confirmationPath) { + Context context = new Context(); + String confirmationLink = UriComponentsBuilder.newInstance() + .scheme(baseUrl.contains("localhost") ? "http" : "https") + .host(baseUrl) + .path(confirmationPath) + .build() + .toString(); + context.setVariables(Map.of("name", name, "followLink", confirmationLink)); + + String emailBody = templateEngine.process("confirm-registration", context); + + mailSender.send(createMessage(email, "Confirm Email", emailBody)); + } + + public void sendResetPasswordEmail(String email, String resetPasswordPath) { + Context context = new Context(); + String confirmationLink = UriComponentsBuilder.newInstance() + .scheme(baseUrl.contains("localhost") ? "http" : "https") + .host(baseUrl) + .path(resetPasswordPath) + .build() + .toString(); + context.setVariables(Map.of("followLink", confirmationLink)); + + String emailBody = templateEngine.process("reset-password", context); + + mailSender.send(createMessage(email, "Reset Password", emailBody)); + } + + public void sendPasswordUpdatedEmail(String email) { + Context context = new Context(); + + context.setVariables(Map.of("followLink", profileRedirectedUrl)); + + String emailBody = templateEngine.process("password-updated", context); + + mailSender.send(createMessage(email, "Your Password Has Been Updated", emailBody)); + } + + @SneakyThrows + public MimeMessage createMessage(String to, String subject, String text) { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper messageHelper = new MimeMessageHelper(message, false, "UTF-8"); + messageHelper.setTo(to); + messageHelper.setSubject(subject); + messageHelper.setText(text, true); + return message; + } } diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java index 7a8f1ccb91..bef4d700b1 100644 --- a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java @@ -1,4 +1,10 @@ package com.sysgears.mailer.service; public interface EmailService { + + void sendRegistrationConfirmEmail(String name, String email, String confirmationLink); + + void sendResetPasswordEmail(String email, String resetPasswordPath); + + void sendPasswordUpdatedEmail(String email); } diff --git a/modules/user/server-java/build.gradle b/modules/user/server-java/build.gradle index c88b3aacb4..7afd989c22 100644 --- a/modules/user/server-java/build.gradle +++ b/modules/user/server-java/build.gradle @@ -1,4 +1,5 @@ dependencies { implementation project(':core') implementation project(':authentication') -} \ No newline at end of file + implementation project(':mailer') +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java index 33e29f1b01..12dd882948 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java @@ -2,7 +2,9 @@ import com.sysgears.authentication.model.jwt.Tokens; import com.sysgears.authentication.service.jwt.JwtGenerator; +import com.sysgears.authentication.service.jwt.JwtParser; import com.sysgears.authentication.utils.SessionUtils; +import com.sysgears.mailer.service.EmailService; import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.AuthPayload; import com.sysgears.user.dto.UserPayload; @@ -35,6 +37,8 @@ public class LoginMutationResolver implements GraphQLMutationResolver { private final UserService userService; private final PasswordEncoder passwordEncoder; private final JwtGenerator jwtGenerator; + private final JwtParser jwtParser; + private final EmailService emailService; @Transactional(readOnly = true) public CompletableFuture login(LoginUserInput loginUserInput) { @@ -92,7 +96,13 @@ public CompletableFuture register(RegisterUserInput input) { false, input.getEmail()); User registered = userService.save(user); - // todo: send email to activate user. + + emailService.sendRegistrationConfirmEmail( + user.getUsername(), + user.getEmail(), + "/user/confirm?key=" + jwtGenerator.generateVerificationToken(UserIdentityUtils.convert(registered)) + ); + return new UserPayload(registered); }); } diff --git a/packages/server-java/app/src/main/resources/templates/confirm-registration.html b/packages/server-java/app/src/main/resources/templates/confirm-registration.html new file mode 100644 index 0000000000..4a92888268 --- /dev/null +++ b/packages/server-java/app/src/main/resources/templates/confirm-registration.html @@ -0,0 +1,15 @@ + + + + Confirm Email + + + +

+ +

Welcome to Apollo Universal Starter Kit.

+

Please click the following link to confirm your email: + +

+ + From c4e9152ac388d2a03fd19ae497ba6ff49478c2ca Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 13 Jan 2021 18:09:15 +0200 Subject: [PATCH 50/85] added registration confirm functionality --- .../service/jwt/JwtGenerator.java | 2 + .../authentication/service/jwt/JwtParser.java | 2 + .../service/jwt/JwtService.java | 21 ++++++- .../user/dto/input/ForgotPasswordInput.java | 6 +- .../exception/ResetPasswordException.java | 12 ++++ .../user/exception/UserNotFoundException.java | 16 +++-- .../password/LoginMutationResolver.java | 38 ++++++++++-- .../sysgears/user/rest/UserController.java | 58 +++++++++++++++++++ .../resources/templates/password-updated.html | 12 ++++ .../resources/templates/reset-password.html | 13 +++++ 10 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java create mode 100644 packages/server-java/app/src/main/resources/templates/password-updated.html create mode 100644 packages/server-java/app/src/main/resources/templates/reset-password.html diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtGenerator.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtGenerator.java index 41609bd01e..aa5db4fb1e 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtGenerator.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtGenerator.java @@ -5,4 +5,6 @@ public interface JwtGenerator { Tokens generateTokens(JwtUserIdentity identity); + + String generateVerificationToken(JwtUserIdentity identity); } diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java index 22b675f4a9..07c9564934 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtParser.java @@ -4,4 +4,6 @@ public interface JwtParser { Integer getIdFromAccessToken(String token); Integer getIdFromRefreshToken(String token); + + Integer getIdFromVerificationToken(String token); } diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java index 6bad3badb2..cb99a7ee8d 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/service/jwt/JwtService.java @@ -3,6 +3,7 @@ import com.sysgears.authentication.config.JwtConfig; import com.sysgears.authentication.model.jwt.JwtUserIdentity; import com.sysgears.authentication.model.jwt.Tokens; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; @@ -27,6 +28,16 @@ public Tokens generateTokens(JwtUserIdentity identity) { return new Tokens(generateAccessToken(identity), generateRefreshToken(identity)); } + public String generateVerificationToken(JwtUserIdentity identity) { + return Jwts.builder() + .setSubject(String.valueOf(identity.getId())) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(jwtConfig.getAccessTokenExpirationInSec(), ChronoUnit.SECONDS))) + .setHeaderParam("typ", "JWT") + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + public Integer getIdFromAccessToken(String token) { return (Integer) Jwts.parserBuilder() .setSigningKey(secretKey) @@ -37,7 +48,6 @@ public Integer getIdFromAccessToken(String token) { .get("id"); } - @Override public Integer getIdFromRefreshToken(String token) { return Jwts.parserBuilder() .setSigningKey(secretKey) @@ -47,6 +57,15 @@ public Integer getIdFromRefreshToken(String token) { .get("id", Integer.class); } + public Integer getIdFromVerificationToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + return Integer.parseInt(claims.getSubject()); + } + private String generateAccessToken(JwtUserIdentity identity) { Map claims = new HashMap<>(); claims.put("identity", identity); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java index 7653e7a9d0..24fd299249 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ForgotPasswordInput.java @@ -1,10 +1,14 @@ package com.sysgears.user.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class ForgotPasswordInput { @NonNull - private final String email; + private String email; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java new file mode 100644 index 0000000000..8623910a96 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java @@ -0,0 +1,12 @@ +package com.sysgears.user.exception; + +import com.sysgears.core.exception.FieldErrorException; + +import java.util.Map; + +public class ResetPasswordException extends FieldErrorException { + + public ResetPasswordException(Map errors) { + super("Failed reset password", errors); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java index 21e28a68f8..94a91555d8 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java @@ -1,19 +1,27 @@ package com.sysgears.user.exception; +import com.sysgears.core.exception.FieldErrorException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; +import java.util.Collections; +import java.util.Map; + @ResponseStatus(HttpStatus.NOT_FOUND) -public class UserNotFoundException extends RuntimeException { +public class UserNotFoundException extends FieldErrorException { public UserNotFoundException() { - super(); + super("No user found", Collections.emptyMap()); } public UserNotFoundException(String message) { - super(message); + super(message, Collections.emptyMap()); } public UserNotFoundException(int id) { - super(String.format("User with id %d not found.", id)); + super(String.format("User with id %d not found.", id), Collections.emptyMap()); + } + + public UserNotFoundException(Map errors) { + super("No user found", errors); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java index 12dd882948..cfad89bd1d 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java @@ -13,7 +13,9 @@ import com.sysgears.user.dto.input.RegisterUserInput; import com.sysgears.user.dto.input.ResetPasswordInput; import com.sysgears.user.exception.LoginFailedException; +import com.sysgears.user.exception.ResetPasswordException; import com.sysgears.user.exception.UserAlreadyExistsException; +import com.sysgears.user.exception.UserNotFoundException; import com.sysgears.user.model.User; import com.sysgears.user.service.UserService; import com.sysgears.user.util.UserIdentityUtils; @@ -25,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.persistence.NoResultException; +import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -67,13 +70,40 @@ public CompletableFuture login(LoginUserInput loginUserInput) { } public CompletableFuture forgotPassword(ForgotPasswordInput input) { - //todo implement - return null; + if (!userService.existsByEmail(input.getEmail())) { + throw new UserNotFoundException(Map.of("email", "No user with specified email.")); + } + + return userService.findUserByUsernameOrEmail(input.getEmail()).thenApply(user -> { + emailService.sendResetPasswordEmail( + input.getEmail(), + "/user/reset-password?key=" + jwtGenerator.generateVerificationToken(UserIdentityUtils.convert(user)) + ); + return null; + }); } public CompletableFuture resetPassword(ResetPasswordInput input) { - //todo implement - return null; + return CompletableFuture.supplyAsync(() -> { + String token = new String(Base64.getDecoder().decode(input.getToken())); + return jwtParser.getIdFromVerificationToken(token); + }) + .thenCompose(userService::findUserById) + .thenCompose(user -> { + if (user == null) { + throw new UserNotFoundException(); + } + if (!input.getPassword().equals(input.getPasswordConfirmation())) { + throw new ResetPasswordException(Map.of("passwordConfirmation", "Must match the field 'password'")); + } + + user.setPassword(passwordEncoder.encode(input.getPassword())); + userService.save(user); + + emailService.sendPasswordUpdatedEmail(user.getEmail()); + + return CompletableFuture.completedFuture(null); + }); } public CompletableFuture register(RegisterUserInput input) { diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java new file mode 100644 index 0000000000..f9199f2f78 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java @@ -0,0 +1,58 @@ +package com.sysgears.user.rest; + +import com.sysgears.authentication.service.jwt.JwtParser; +import com.sysgears.user.exception.UserNotFoundException; +import com.sysgears.user.service.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.view.RedirectView; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@RestController +@RequestMapping("/user") +public class UserController { + private final UserService userService; + private final JwtParser jwtParser; + private final String confirmRegistrationRedirectUrl; + private final String resetPasswordRedirectUrl; + + public UserController(UserService userService, + JwtParser jwtParser, + @Value("${app.redirect.confirm-registration}") String confirmRegistrationRedirectUrl, + @Value("${app.redirect.reset-password}") String resetPasswordRedirectUrl) { + this.userService = userService; + this.jwtParser = jwtParser; + this.confirmRegistrationRedirectUrl = confirmRegistrationRedirectUrl; + this.resetPasswordRedirectUrl = resetPasswordRedirectUrl; + } + + @GetMapping("/confirm") + public RedirectView confirmRegistration(@RequestParam String key) { + Integer userId = jwtParser.getIdFromVerificationToken(key); + userService.findUserById(userId).thenApply(user -> { + if (user == null) throw new UserNotFoundException(); + + user.setIsActive(true); + return userService.save(user); + } + ); + return new RedirectView(confirmRegistrationRedirectUrl); + } + + @GetMapping("/reset-password") + public RedirectView initiateResetPassword(@RequestParam String key) { + Integer userId = jwtParser.getIdFromVerificationToken(key); + userService.findUserById(userId).thenApply(user -> { + if (user == null) throw new UserNotFoundException(); + return user; + } + ); + String base64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8)); + return new RedirectView(resetPasswordRedirectUrl + base64Key); + } +} diff --git a/packages/server-java/app/src/main/resources/templates/password-updated.html b/packages/server-java/app/src/main/resources/templates/password-updated.html new file mode 100644 index 0000000000..d1a3ba9da9 --- /dev/null +++ b/packages/server-java/app/src/main/resources/templates/password-updated.html @@ -0,0 +1,12 @@ + + + + Password Has Been Updated + + + +

As you requested, your account password has been updated.

+

To view or edit your account settings, please visit the “Profile” page at

+

+ + diff --git a/packages/server-java/app/src/main/resources/templates/reset-password.html b/packages/server-java/app/src/main/resources/templates/reset-password.html new file mode 100644 index 0000000000..2be70a272b --- /dev/null +++ b/packages/server-java/app/src/main/resources/templates/reset-password.html @@ -0,0 +1,13 @@ + + + + Reset Password + + + +

Please click this link to reset your password:

+

+ +

+ + From b84c361aa3989be99fde764fba29f381be9407c7 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 14 Jan 2021 13:29:01 +0200 Subject: [PATCH 51/85] updated mail configuration --- .../mailer/config/MailConfiguration.java | 17 +++++++++++++++++ .../app/src/main/resources/application.yml | 14 ++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java new file mode 100644 index 0000000000..dd7e3bc2b5 --- /dev/null +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java @@ -0,0 +1,17 @@ +package com.sysgears.mailer.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfiguration { + + @ConditionalOnMissingBean // required to run the application without mail properties + @Bean + public JavaMailSender javaMailSender() { + return new JavaMailSenderImpl(); + } +} diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 08fe7d7153..f11500d436 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -16,12 +16,12 @@ spring: console: enabled: true path: /h2-console +# Uncomment properties below to use mailer module # mail: # host: EMAIL_HOST # port: 587 # username: EMAIL_USER # password: EMAIL_PASSWORD -# sender: EMAIL_SENDER # todo: no such property in Spring, need to handle manually # properties: # mail: # smtp: @@ -53,4 +53,14 @@ graphiql: jwt: secret: "676918AB4D29BFE59CCB943F3C09F5CC8FB3A8511E23E502B67DE95AB9A9D00C" access-token-expiration-in-sec: 3600 - refresh-token-expiration-in-sec: 604800 \ No newline at end of file + refresh-token-expiration-in-sec: 604800 + +app: + server: + baseUrl: "localhost:8080" + client: + baseUrl: "http://localhost:3000" + redirect: + confirm-registration: "${app.client.baseUrl}/login/?email-verified" + reset-password: "${app.client.baseUrl}/reset-password/" + profile: "${app.client.baseUrl}/profile" From 4b003652dc20ee044a1c77ef0526c30e6fd60b3e Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 14 Jan 2021 14:53:58 +0200 Subject: [PATCH 52/85] fixed mail configuration --- .../mailer/config/MailConfiguration.java | 4 ++-- .../app/src/main/resources/application.yml | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java index dd7e3bc2b5..3786125b28 100644 --- a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/config/MailConfiguration.java @@ -1,6 +1,6 @@ package com.sysgears.mailer.config; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mail.javamail.JavaMailSender; @@ -9,8 +9,8 @@ @Configuration public class MailConfiguration { - @ConditionalOnMissingBean // required to run the application without mail properties @Bean + @ConditionalOnProperty(prefix = "spring.mail", name = "username", havingValue = "EMAIL_USER") public JavaMailSender javaMailSender() { return new JavaMailSenderImpl(); } diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index f11500d436..11b7bc2437 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -16,18 +16,18 @@ spring: console: enabled: true path: /h2-console -# Uncomment properties below to use mailer module -# mail: -# host: EMAIL_HOST -# port: 587 -# username: EMAIL_USER -# password: EMAIL_PASSWORD -# properties: -# mail: -# smtp: -# auth: true -# starttls: -# enable: true +# Fill real host, username and password for working with mailer module + mail: + host: EMAIL_HOST + port: 587 + username: EMAIL_USER + password: EMAIL_PASSWORD + properties: + mail: + smtp: + auth: true + starttls: + enable: true graphql: servlet: exception-handlers-enabled: true From 7656cd1d925f6f859e98398cc7a9fed4a269b47d Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 14 Jan 2021 17:40:16 +0200 Subject: [PATCH 53/85] implemented contact module --- modules/contact/server-java/.gitignore | 1 + modules/contact/server-java/build.gradle | 4 +++ .../sysgears/contact/dto/ContactInput.java | 18 ++++++++++ .../resolvers/ContactMutationResolver.java | 22 ++++++++++++ .../src/main/resources/schema.graphqls | 13 +++++++ modules/mailer/server-java/build.gradle | 2 ++ .../mailer/service/DefaultEmailService.java | 35 +++++++++++++++---- .../sysgears/mailer/service/EmailService.java | 2 ++ .../main/resources/templates/contact-us.html | 11 ++++++ packages/server-java/build.gradle | 3 +- packages/server-java/settings.gradle | 3 +- 11 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 modules/contact/server-java/.gitignore create mode 100644 modules/contact/server-java/build.gradle create mode 100644 modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java create mode 100644 modules/contact/server-java/src/main/java/com/sysgears/contact/resolvers/ContactMutationResolver.java create mode 100644 modules/contact/server-java/src/main/resources/schema.graphqls create mode 100644 packages/server-java/app/src/main/resources/templates/contact-us.html diff --git a/modules/contact/server-java/.gitignore b/modules/contact/server-java/.gitignore new file mode 100644 index 0000000000..6b19dc7f40 --- /dev/null +++ b/modules/contact/server-java/.gitignore @@ -0,0 +1 @@ +/.gradle \ No newline at end of file diff --git a/modules/contact/server-java/build.gradle b/modules/contact/server-java/build.gradle new file mode 100644 index 0000000000..47d74d8978 --- /dev/null +++ b/modules/contact/server-java/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':core') + implementation project(':mailer') +} diff --git a/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java b/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java new file mode 100644 index 0000000000..bcd98a0ba1 --- /dev/null +++ b/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java @@ -0,0 +1,18 @@ +package com.sysgears.contact.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.NonNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ContactInput { + @NonNull + private String name; + @NonNull + private String email; + @NonNull + private String content; +} diff --git a/modules/contact/server-java/src/main/java/com/sysgears/contact/resolvers/ContactMutationResolver.java b/modules/contact/server-java/src/main/java/com/sysgears/contact/resolvers/ContactMutationResolver.java new file mode 100644 index 0000000000..6d57417ee8 --- /dev/null +++ b/modules/contact/server-java/src/main/java/com/sysgears/contact/resolvers/ContactMutationResolver.java @@ -0,0 +1,22 @@ +package com.sysgears.contact.resolvers; + +import com.sysgears.contact.dto.ContactInput; +import com.sysgears.mailer.service.EmailService; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class ContactMutationResolver implements GraphQLMutationResolver { + private final EmailService emailService; + + public CompletableFuture contact(ContactInput input) { + return CompletableFuture.supplyAsync(() -> { + emailService.sendContactUsEmail(input.getName(), input.getEmail(), input.getContent()); + return null; + }); + } +} diff --git a/modules/contact/server-java/src/main/resources/schema.graphqls b/modules/contact/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..5fa8ad9679 --- /dev/null +++ b/modules/contact/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,13 @@ +# Payload for contact Mutation + +extend type Mutation { + # Send contact us info + contact(input: ContactInput!): String +} + +# Input for addPost Mutation +input ContactInput { + name: String! + email: String! + content: String! +} diff --git a/modules/mailer/server-java/build.gradle b/modules/mailer/server-java/build.gradle index 358ccd4d4a..031d9900e1 100644 --- a/modules/mailer/server-java/build.gradle +++ b/modules/mailer/server-java/build.gradle @@ -2,4 +2,6 @@ dependencies { implementation project(':core') implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile group: 'com.sun.mail', name: 'jakarta.mail', version: '1.6.5' + } diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java index cfdc370972..cfe9c8ad0b 100644 --- a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/DefaultEmailService.java @@ -11,6 +11,7 @@ import javax.mail.internet.MimeMessage; import java.util.Map; +import java.util.Optional; @Service public class DefaultEmailService implements EmailService { @@ -18,15 +19,19 @@ public class DefaultEmailService implements EmailService { private final SpringTemplateEngine templateEngine; private final String baseUrl; private final String profileRedirectedUrl; + private final String emailSender; + public DefaultEmailService(JavaMailSender mailSender, SpringTemplateEngine templateEngine, @Value("${app.server.baseUrl}") String baseUrl, - @Value("${app.redirect.profile}") String profileRedirectedUrl) { + @Value("${app.redirect.profile}") String profileRedirectedUrl, + @Value("${spring.mail.username}") String emailSender) { this.mailSender = mailSender; this.templateEngine = templateEngine; this.baseUrl = baseUrl; this.profileRedirectedUrl = profileRedirectedUrl; + this.emailSender = emailSender; } public void sendRegistrationConfirmEmail(String name, String email, String confirmationPath) { @@ -41,7 +46,7 @@ public void sendRegistrationConfirmEmail(String name, String email, String confi String emailBody = templateEngine.process("confirm-registration", context); - mailSender.send(createMessage(email, "Confirm Email", emailBody)); + mailSender.send(createMessage(Optional.empty(), email, "Confirm Email", emailBody)); } public void sendResetPasswordEmail(String email, String resetPasswordPath) { @@ -56,23 +61,41 @@ public void sendResetPasswordEmail(String email, String resetPasswordPath) { String emailBody = templateEngine.process("reset-password", context); - mailSender.send(createMessage(email, "Reset Password", emailBody)); + mailSender.send(createMessage(Optional.empty(), email, "Reset Password", emailBody)); } public void sendPasswordUpdatedEmail(String email) { Context context = new Context(); - context.setVariables(Map.of("followLink", profileRedirectedUrl)); String emailBody = templateEngine.process("password-updated", context); - mailSender.send(createMessage(email, "Your Password Has Been Updated", emailBody)); + mailSender.send(createMessage(Optional.empty(), email, "Your Password Has Been Updated", emailBody)); + } + + @SneakyThrows + public void sendContactUsEmail(String name, String email, String content) { + Context context = new Context(); + context.setVariables(Map.of("name", name, "content", content)); + + String emailBody = templateEngine.process("contact-us", context); + + mailSender.send(createMessage(Optional.of(email), emailSender, "New email through contact us page", emailBody)); } @SneakyThrows - public MimeMessage createMessage(String to, String subject, String text) { + private MimeMessage createMessage(Optional from, String to, String subject, String text) { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(message, false, "UTF-8"); + + // it will be ignored for gmail + // https://stackoverflow.com/a/27420030 + if (from.isPresent()) { + messageHelper.setFrom(from.get()); + } else { + messageHelper.setFrom(emailSender); + } + messageHelper.setTo(to); messageHelper.setSubject(subject); messageHelper.setText(text, true); diff --git a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java index bef4d700b1..fc3bfe21c7 100644 --- a/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java +++ b/modules/mailer/server-java/src/main/java/com/sysgears/mailer/service/EmailService.java @@ -7,4 +7,6 @@ public interface EmailService { void sendResetPasswordEmail(String email, String resetPasswordPath); void sendPasswordUpdatedEmail(String email); + + void sendContactUsEmail(String name, String email, String content); } diff --git a/packages/server-java/app/src/main/resources/templates/contact-us.html b/packages/server-java/app/src/main/resources/templates/contact-us.html new file mode 100644 index 0000000000..0b5a493dd7 --- /dev/null +++ b/packages/server-java/app/src/main/resources/templates/contact-us.html @@ -0,0 +1,11 @@ + + + + Reset Password + + + +

+

+ + diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index e768b88d30..710bbf827c 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -79,10 +79,11 @@ project(':app') { implementation project(':user') implementation project(':authentication') implementation project(':mailer') + implementation project(':contact') } bootJar { enabled = true launchScript() mainClassName = 'com.sysgears.Application' } -} \ No newline at end of file +} diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index fdf4fc5578..c982828b58 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,8 +1,9 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user', ':authentication', 'mailer' +include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') project(':user').projectDir = new File('../../modules/user/server-java') project(':authentication').projectDir = new File('../../modules/authentication/server-java') project(':mailer').projectDir = new File('../../modules/mailer/server-java') +project(':contact').projectDir = new File('../../modules/contact/server-java') From ca313938885d65e3104d73a27a95c1b591227dd9 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 14 Jan 2021 18:33:21 +0200 Subject: [PATCH 54/85] removed redundant dependency --- modules/mailer/server-java/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/mailer/server-java/build.gradle b/modules/mailer/server-java/build.gradle index 031d9900e1..358ccd4d4a 100644 --- a/modules/mailer/server-java/build.gradle +++ b/modules/mailer/server-java/build.gradle @@ -2,6 +2,4 @@ dependencies { implementation project(':core') implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - compile group: 'com.sun.mail', name: 'jakarta.mail', version: '1.6.5' - } From 47ba270ed5c391c9a363b7adfeba04a447d6aee3 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 15 Jan 2021 18:14:00 +0200 Subject: [PATCH 55/85] implemented upload module --- modules/upload/server-java/.gitignore | 1 + modules/upload/server-java/build.gradle | 3 ++ .../config/UploadScalarConfiguration.java | 45 +++++++++++++++++++ .../java/com/sysgears/upload/dto/File.java | 18 ++++++++ .../exception/FileNotFoundException.java | 8 ++++ .../com/sysgears/upload/file/FileStorage.java | 10 +++++ .../upload/file/LocalFileStorage.java | 38 ++++++++++++++++ .../sysgears/upload/model/FileMetadata.java | 37 +++++++++++++++ .../repository/FileMetadataRepository.java | 7 +++ .../resolvers/UploadMutationResolver.java | 39 ++++++++++++++++ .../upload/resolvers/UploadQueryResolver.java | 28 ++++++++++++ .../src/main/resources/schema.graphqls | 19 ++++++++ .../app/src/main/resources/application.yml | 4 ++ packages/server-java/build.gradle | 1 + packages/server-java/settings.gradle | 4 +- 15 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 modules/upload/server-java/.gitignore create mode 100644 modules/upload/server-java/build.gradle create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/config/UploadScalarConfiguration.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/exception/FileNotFoundException.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/file/FileStorage.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/file/LocalFileStorage.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/model/FileMetadata.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/repository/FileMetadataRepository.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java create mode 100644 modules/upload/server-java/src/main/resources/schema.graphqls diff --git a/modules/upload/server-java/.gitignore b/modules/upload/server-java/.gitignore new file mode 100644 index 0000000000..6b19dc7f40 --- /dev/null +++ b/modules/upload/server-java/.gitignore @@ -0,0 +1 @@ +/.gradle \ No newline at end of file diff --git a/modules/upload/server-java/build.gradle b/modules/upload/server-java/build.gradle new file mode 100644 index 0000000000..e4dbb7fe36 --- /dev/null +++ b/modules/upload/server-java/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation project(':core') +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/config/UploadScalarConfiguration.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/config/UploadScalarConfiguration.java new file mode 100644 index 0000000000..fc6cf72163 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/config/UploadScalarConfiguration.java @@ -0,0 +1,45 @@ +package com.sysgears.upload.config; + +import graphql.schema.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.http.Part; + +@Configuration +public class UploadScalarConfiguration { + + @Bean + public GraphQLScalarType uploadScalar() { + return GraphQLScalarType.newScalar() + .name("FileUpload") + .description("A file part in a multipart request") + .coercing(new Coercing() { + @Override + public Void serialize(Object dataFetcherResult) { + throw new CoercingSerializeException("Upload is an input-only type"); + } + + @Override + public Part parseValue(Object input) { + if (input instanceof Part) { + return (Part) input; + } else if (null == input) { + return null; + } else { + throw new CoercingParseValueException("Expected type " + + Part.class.getName() + + " but was " + + input.getClass().getName()); + } + } + + @Override + public Part parseLiteral(Object input) { + throw new CoercingParseLiteralException( + "Must use variables to specify Upload values"); + } + }) + .build(); + } +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java new file mode 100644 index 0000000000..8be6fffb12 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java @@ -0,0 +1,18 @@ +package com.sysgears.upload.dto; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class File { + @NonNull + private final Integer id; + @NonNull + private final String name; + @NonNull + private final String type; + @NonNull + private final Long size; + @NonNull + private final String path; +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/exception/FileNotFoundException.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/exception/FileNotFoundException.java new file mode 100644 index 0000000000..f80e20d167 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/exception/FileNotFoundException.java @@ -0,0 +1,8 @@ +package com.sysgears.upload.exception; + +public class FileNotFoundException extends RuntimeException { + + public FileNotFoundException(Integer id) { + super(String.format("File with id %d not found", id)); + } +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/file/FileStorage.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/file/FileStorage.java new file mode 100644 index 0000000000..ebded48911 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/file/FileStorage.java @@ -0,0 +1,10 @@ +package com.sysgears.upload.file; + +import javax.servlet.http.Part; + +public interface FileStorage { + + String writeFile(String fileName, Part part); + + void deleteFile(String filePath); +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/file/LocalFileStorage.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/file/LocalFileStorage.java new file mode 100644 index 0000000000..34c25ee9b6 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/file/LocalFileStorage.java @@ -0,0 +1,38 @@ +package com.sysgears.upload.file; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.servlet.http.Part; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Slf4j +@Component +public class LocalFileStorage implements FileStorage { + private static final String FILES_DIRECTORY = "files"; + + @Override + @SneakyThrows + public String writeFile(String fileName, Part part) { + final Path filePath = Paths.get(FILES_DIRECTORY).resolve(fileName); + filePath.getParent().toFile().mkdirs(); //create directories if not exist + + File file = filePath.toFile(); + + part.write(file.getAbsolutePath()); + log.debug("File write to '{}'", file.getPath()); + + return file.getPath(); + } + + @Override + @SneakyThrows + public void deleteFile(String filePath) { + log.debug("Deleting file '{}'", filePath); + Files.deleteIfExists(Paths.get(filePath)); + } +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/model/FileMetadata.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/model/FileMetadata.java new file mode 100644 index 0000000000..dad23c3f8c --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/model/FileMetadata.java @@ -0,0 +1,37 @@ +package com.sysgears.upload.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "FILES") +public class FileMetadata { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "NAME", nullable = false) + private String name; + + @Column(name = "CONTENT_TYPE", nullable = false) + private String contentType; + + @Column(name = "SIZE", nullable = false) + private Long size; + + @Column(name = "PATH", nullable = false) + private String path; + + public FileMetadata(String name, String contentType, Long size, String path) { + this.name = name; + this.contentType = contentType; + this.size = size; + this.path = path; + } +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/repository/FileMetadataRepository.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/repository/FileMetadataRepository.java new file mode 100644 index 0000000000..cb526b1806 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/repository/FileMetadataRepository.java @@ -0,0 +1,7 @@ +package com.sysgears.upload.repository; + +import com.sysgears.upload.model.FileMetadata; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FileMetadataRepository extends JpaRepository { +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java new file mode 100644 index 0000000000..e73629e17a --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java @@ -0,0 +1,39 @@ +package com.sysgears.upload.resolvers; + +import com.sysgears.upload.exception.FileNotFoundException; +import com.sysgears.upload.file.FileStorage; +import com.sysgears.upload.model.FileMetadata; +import com.sysgears.upload.repository.FileMetadataRepository; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.servlet.http.Part; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class UploadMutationResolver implements GraphQLMutationResolver { + private final FileMetadataRepository fileMetadataRepository; + private final FileStorage fileStorage; + + public boolean uploadFiles(List files) { + for (Part part : files) { + String fileName = part.getSubmittedFileName().replace(" ", "_"); + String path = fileStorage.writeFile(fileName, part); + + fileMetadataRepository.save(new FileMetadata(fileName, part.getContentType(), part.getSize(), path)); + } + + return true; + } + + public boolean removeFile(Integer id) { + FileMetadata fileMetadata = fileMetadataRepository.findById(id).orElseThrow(()-> new FileNotFoundException(id)); + + fileStorage.deleteFile(fileMetadata.getPath()); + fileMetadataRepository.deleteById(id); + + return true; + } +} diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java new file mode 100644 index 0000000000..757b5cb6cb --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java @@ -0,0 +1,28 @@ +package com.sysgears.upload.resolvers; + +import com.sysgears.upload.dto.File; +import com.sysgears.upload.repository.FileMetadataRepository; +import graphql.kickstart.tools.GraphQLQueryResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class UploadQueryResolver implements GraphQLQueryResolver { + private final FileMetadataRepository fileMetadataRepository; + + public List files() { + return fileMetadataRepository.findAll().stream() + .map(file -> new File( + file.getId(), + file.getName(), + file.getContentType(), + file.getSize(), + file.getPath() + )) + .collect(Collectors.toList()); + } +} diff --git a/modules/upload/server-java/src/main/resources/schema.graphqls b/modules/upload/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..9dcb82743a --- /dev/null +++ b/modules/upload/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,19 @@ +scalar FileUpload + +type File { + id: Int! + name: String! + type: String! + size: Int! + path: String! +} + +extend type Query { + files: [File] +} + +extend type Mutation { + uploadFiles(files: [FileUpload]!): Boolean! + + removeFile(id: Int!): Boolean! +} diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 11b7bc2437..0be8417fe9 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -28,6 +28,10 @@ spring: auth: true starttls: enable: true + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB graphql: servlet: exception-handlers-enabled: true diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index 710bbf827c..fff616fdaa 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -80,6 +80,7 @@ project(':app') { implementation project(':authentication') implementation project(':mailer') implementation project(':contact') + implementation project(':upload') } bootJar { enabled = true diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index c982828b58..391cb47e4e 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact' +include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') @@ -7,3 +7,5 @@ project(':user').projectDir = new File('../../modules/user/server-java') project(':authentication').projectDir = new File('../../modules/authentication/server-java') project(':mailer').projectDir = new File('../../modules/mailer/server-java') project(':contact').projectDir = new File('../../modules/contact/server-java') +project(':upload').projectDir = new File('../../modules/upload/server-java') +project(':post').projectDir = new File('../../modules/post/server-java') From c8d0380b781409b728057730d0ebeb2372a63c26 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 15 Jan 2021 18:14:11 +0200 Subject: [PATCH 56/85] added post module skeleton; added dtos --- modules/post/server-java/.gitignore | 1 + modules/post/server-java/build.gradle | 3 + .../java/com/sysgears/post/dto/Comment.java | 12 ++ .../main/java/com/sysgears/post/dto/Post.java | 17 +++ .../java/com/sysgears/post/dto/PostEdges.java | 9 ++ .../com/sysgears/post/dto/PostPageInfo.java | 10 ++ .../java/com/sysgears/post/dto/Posts.java | 11 ++ .../post/dto/input/AddCommentInput.java | 12 ++ .../sysgears/post/dto/input/AddPostInput.java | 12 ++ .../post/dto/input/DeleteCommentInput.java | 12 ++ .../post/dto/input/EditCommentInput.java | 14 +++ .../post/dto/input/EditPostInput.java | 14 +++ .../subscription/UpdateCommentPayload.java | 16 +++ .../dto/subscription/UpdatePostPayload.java | 15 +++ .../java/com/sysgears/post/model/.gitkeep | 0 .../com/sysgears/post/repository/.gitkeep | 0 .../java/com/sysgears/post/resolvers/.gitkeep | 0 .../java/com/sysgears/post/service/.gitkeep | 0 .../com/sysgears/post/subscription/.gitkeep | 0 .../src/main/resources/schema.graphqls | 113 ++++++++++++++++++ 20 files changed, 271 insertions(+) create mode 100644 modules/post/server-java/.gitignore create mode 100644 modules/post/server-java/build.gradle create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/model/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/repository/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/service/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/.gitkeep create mode 100644 modules/post/server-java/src/main/resources/schema.graphqls diff --git a/modules/post/server-java/.gitignore b/modules/post/server-java/.gitignore new file mode 100644 index 0000000000..6b19dc7f40 --- /dev/null +++ b/modules/post/server-java/.gitignore @@ -0,0 +1 @@ +/.gradle \ No newline at end of file diff --git a/modules/post/server-java/build.gradle b/modules/post/server-java/build.gradle new file mode 100644 index 0000000000..e4dbb7fe36 --- /dev/null +++ b/modules/post/server-java/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation project(':core') +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java new file mode 100644 index 0000000000..a6639a3e94 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java @@ -0,0 +1,12 @@ +package com.sysgears.post.dto; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class Comment { + @NonNull + private final Integer id; + @NonNull + private final String content; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java new file mode 100644 index 0000000000..a7f12d3268 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java @@ -0,0 +1,17 @@ +package com.sysgears.post.dto; + +import lombok.Data; +import org.springframework.lang.NonNull; + +import java.util.List; + +@Data +public class Post { + @NonNull + private final Integer id; + @NonNull + private final String title; + @NonNull + private final String content; + private final List comments; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java new file mode 100644 index 0000000000..d13d678952 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java @@ -0,0 +1,9 @@ +package com.sysgears.post.dto; + +import lombok.Data; + +@Data +public class PostEdges { + private Post node; + private Integer cursor; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java new file mode 100644 index 0000000000..dc674f54ee --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java @@ -0,0 +1,10 @@ +package com.sysgears.post.dto; + +import lombok.Data; + +@Data +public class PostPageInfo { + private Integer endCursor; + private Boolean hasNextPage; + private PostPageInfo pageInfo; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java new file mode 100644 index 0000000000..ef33e4597c --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java @@ -0,0 +1,11 @@ +package com.sysgears.post.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class Posts { + private Integer totalCount; + private List edges; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java new file mode 100644 index 0000000000..90ebc8f6b3 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java @@ -0,0 +1,12 @@ +package com.sysgears.post.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class AddCommentInput { + @NonNull + private final Integer postId; + @NonNull + private final String content; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java new file mode 100644 index 0000000000..5b612a8952 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java @@ -0,0 +1,12 @@ +package com.sysgears.post.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class AddPostInput { + @NonNull + private final String title; + @NonNull + private final String content; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java new file mode 100644 index 0000000000..00eb6530bd --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java @@ -0,0 +1,12 @@ +package com.sysgears.post.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class DeleteCommentInput { + @NonNull + private final Integer id; + @NonNull + private final Integer postId; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java new file mode 100644 index 0000000000..2149d955d2 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java @@ -0,0 +1,14 @@ +package com.sysgears.post.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class EditCommentInput { + @NonNull + private final Integer id; + @NonNull + private final Integer postId; + @NonNull + private final String content; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java new file mode 100644 index 0000000000..05befbb9cc --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java @@ -0,0 +1,14 @@ +package com.sysgears.post.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class EditPostInput { + @NonNull + private final Integer id; + @NonNull + private final String title; + @NonNull + private final String content; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java new file mode 100644 index 0000000000..c341e378a5 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java @@ -0,0 +1,16 @@ +package com.sysgears.post.dto.subscription; + +import com.sysgears.post.dto.Comment; +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class UpdateCommentPayload { + private final Integer id; + @NonNull + private final Integer postId; + @NonNull + private final String mutation; + private final Comment node; + +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java new file mode 100644 index 0000000000..a369f999fb --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java @@ -0,0 +1,15 @@ +package com.sysgears.post.dto.subscription; + +import com.sysgears.post.dto.Post; +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class UpdatePostPayload { + @NonNull + private final Integer id; + @NonNull + private final String mutation; + private final Post node; + +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/model/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/model/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/repository/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/repository/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/service/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/service/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/post/server-java/src/main/resources/schema.graphqls b/modules/post/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..0c1d7b31d9 --- /dev/null +++ b/modules/post/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,113 @@ +# Post +type Post { + id: Int! + title: String! + content: String! + comments: [Comment] +} + +# Comment +type Comment { + id: Int! + content: String! +} + +# Edges for Posts +type PostEdges { + node: Post + cursor: Int +} + +# PageInfo for Posts +type PostPageInfo { + endCursor: Int + hasNextPage: Boolean +} + +# Posts relay-style pagination query +type Posts { + totalCount: Int + edges: [PostEdges] + pageInfo: PostPageInfo +} + +extend type Query { + # Posts pagination query + posts(limit: Int, after: Int): Posts + # Post + post(id: Int!): Post +} + +extend type Mutation { + # Create new post + addPost(input: AddPostInput!): Post + # Delete a post + deletePost(id: Int!): Post + # Edit a post + editPost(input: EditPostInput!): Post + # Add comment to post + addComment(input: AddCommentInput!): Comment + # Delete a comment + deleteComment(input: DeleteCommentInput!): Comment + # Edit a comment + editComment(input: EditCommentInput!): Comment +} + +# Input for addPost Mutation +input AddPostInput { + title: String! + content: String! +} + +# Input for editPost Mutation +input EditPostInput { + id: Int! + title: String! + content: String! +} + +# Input for addComment Mutation +input AddCommentInput { + content: String! + # Needed for commentUpdated Subscription filter + postId: Int! +} + +# Input for editComment Mutation +input DeleteCommentInput { + id: Int! + # Needed for commentUpdated Subscription filter + postId: Int! +} + +# Input for deleteComment Mutation +input EditCommentInput { + id: Int! + content: String! + # Needed for commentUpdated Subscription filter + postId: Int! +} + +extend type Subscription { + # Subscription for when editing a post + postUpdated(id: Int!): UpdatePostPayload + # Subscription for post list + postsUpdated(endCursor: Int!): UpdatePostPayload + # Subscription for comments + commentUpdated(postId: Int!): UpdateCommentPayload +} + +# Payload for postsUpdated Subscription +type UpdatePostPayload { + mutation: String! + id: Int! + node: Post +} + +# Payload for commentUpdated Subscription +type UpdateCommentPayload { + mutation: String! + id: Int + postId: Int! + node: Comment +} From dff2f7d24fa2894214931de38bf8cd3576dd8b84 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 19 Jan 2021 20:10:25 +0200 Subject: [PATCH 57/85] implemented post module --- .../com/sysgears/post/PostDBInitializer.java | 39 ++++++ .../dto/{Comment.java => CommentPayload.java} | 2 +- .../main/java/com/sysgears/post/dto/Post.java | 17 --- .../java/com/sysgears/post/dto/PostEdges.java | 4 +- .../com/sysgears/post/dto/PostPageInfo.java | 3 +- .../com/sysgears/post/dto/PostPayload.java | 24 ++++ .../java/com/sysgears/post/dto/Posts.java | 5 +- .../subscription/UpdateCommentPayload.java | 4 +- .../dto/subscription/UpdatePostPayload.java | 4 +- .../exception/CommentNotFoundException.java | 8 ++ .../post/exception/PostNotFoundException.java | 8 ++ .../java/com/sysgears/post/model/.gitkeep | 0 .../java/com/sysgears/post/model/Comment.java | 29 ++++ .../java/com/sysgears/post/model/Post.java | 49 +++++++ .../com/sysgears/post/repository/.gitkeep | 0 .../post/repository/CommentRepository.java | 7 + .../post/repository/OffsetPageRequest.java | 73 ++++++++++ .../post/repository/PostRepository.java | 13 ++ .../java/com/sysgears/post/resolvers/.gitkeep | 0 .../resolvers/CommentMutationResolver.java | 56 ++++++++ .../CommentSubscriptionResolver.java | 30 ++++ .../post/resolvers/PostMutationResolver.java | 60 ++++++++ .../post/resolvers/PostQueryResolver.java | 32 +++++ .../resolvers/PostSubscriptionResolver.java | 31 +++++ .../java/com/sysgears/post/service/.gitkeep | 0 .../sysgears/post/service/PostService.java | 128 ++++++++++++++++++ .../com/sysgears/post/subscription/.gitkeep | 0 .../subscription/CommentPubSubService.java | 8 ++ .../subscription/CommentUpdatedEvent.java | 11 ++ .../sysgears/post/subscription/Mutation.java | 5 + .../post/subscription/PostPubSubService.java | 8 ++ .../post/subscription/PostUpdatedEvent.java | 10 ++ packages/server-java/build.gradle | 1 + 33 files changed, 644 insertions(+), 25 deletions(-) create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/PostDBInitializer.java rename modules/post/server-java/src/main/java/com/sysgears/post/dto/{Comment.java => CommentPayload.java} (86%) delete mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/exception/CommentNotFoundException.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/exception/PostNotFoundException.java delete mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/model/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/model/Comment.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/model/Post.java delete mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/repository/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/repository/CommentRepository.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/repository/PostRepository.java delete mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentMutationResolver.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentSubscriptionResolver.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostMutationResolver.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostSubscriptionResolver.java delete mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/service/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/service/PostService.java delete mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/.gitkeep create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentPubSubService.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentUpdatedEvent.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/Mutation.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostPubSubService.java create mode 100644 modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostUpdatedEvent.java diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/PostDBInitializer.java b/modules/post/server-java/src/main/java/com/sysgears/post/PostDBInitializer.java new file mode 100644 index 0000000000..a755b89a9a --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/PostDBInitializer.java @@ -0,0 +1,39 @@ +package com.sysgears.post; + +import com.sysgears.post.model.Comment; +import com.sysgears.post.model.Post; +import com.sysgears.post.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PostDBInitializer { + private final PostRepository repository; + + @EventListener + public void onApplicationStartedEvent(ApplicationStartedEvent event) { + long count = repository.count(); + if (count == 0) { + List posts = IntStream.rangeClosed(1, 20).mapToObj(i -> { + Post post = new Post(); + post.setTitle("Post title " + i); + post.setContent("Some content " + i); + Comment comment = new Comment(); + comment.setContent("123"); + post.setComments(Set.of(comment)); + return post; + }).collect(Collectors.toList()); + repository.saveAll(posts); + } + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java similarity index 86% rename from modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java rename to modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java index a6639a3e94..77f6616066 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Comment.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java @@ -4,7 +4,7 @@ import org.springframework.lang.NonNull; @Data -public class Comment { +public class CommentPayload { @NonNull private final Integer id; @NonNull diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java deleted file mode 100644 index a7f12d3268..0000000000 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Post.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sysgears.post.dto; - -import lombok.Data; -import org.springframework.lang.NonNull; - -import java.util.List; - -@Data -public class Post { - @NonNull - private final Integer id; - @NonNull - private final String title; - @NonNull - private final String content; - private final List comments; -} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java index d13d678952..67b4280d4b 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java @@ -1,9 +1,11 @@ package com.sysgears.post.dto; +import lombok.Builder; import lombok.Data; @Data +@Builder public class PostEdges { - private Post node; + private PostPayload node; private Integer cursor; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java index dc674f54ee..b41bd74d43 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java @@ -1,10 +1,11 @@ package com.sysgears.post.dto; +import lombok.Builder; import lombok.Data; @Data +@Builder public class PostPageInfo { private Integer endCursor; private Boolean hasNextPage; - private PostPageInfo pageInfo; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java new file mode 100644 index 0000000000..0c69d14c8d --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java @@ -0,0 +1,24 @@ +package com.sysgears.post.dto; + +import lombok.Data; +import org.springframework.lang.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Data +public class PostPayload { + @NonNull + private final Integer id; + @NonNull + private final String title; + @NonNull + private final String content; + + private final List comments = new ArrayList<>(); + + public void addComments(Collection comments) { + this.comments.addAll(comments); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java index ef33e4597c..ad490abeeb 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java @@ -1,11 +1,14 @@ package com.sysgears.post.dto; +import lombok.Builder; import lombok.Data; import java.util.List; @Data +@Builder public class Posts { - private Integer totalCount; + private Long totalCount; private List edges; + private PostPageInfo pageInfo; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java index c341e378a5..9f24cfd610 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdateCommentPayload.java @@ -1,6 +1,6 @@ package com.sysgears.post.dto.subscription; -import com.sysgears.post.dto.Comment; +import com.sysgears.post.dto.CommentPayload; import lombok.Data; import org.springframework.lang.NonNull; @@ -11,6 +11,6 @@ public class UpdateCommentPayload { private final Integer postId; @NonNull private final String mutation; - private final Comment node; + private final CommentPayload node; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java index a369f999fb..bb42876f93 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/subscription/UpdatePostPayload.java @@ -1,6 +1,6 @@ package com.sysgears.post.dto.subscription; -import com.sysgears.post.dto.Post; +import com.sysgears.post.dto.PostPayload; import lombok.Data; import org.springframework.lang.NonNull; @@ -10,6 +10,6 @@ public class UpdatePostPayload { private final Integer id; @NonNull private final String mutation; - private final Post node; + private final PostPayload node; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/exception/CommentNotFoundException.java b/modules/post/server-java/src/main/java/com/sysgears/post/exception/CommentNotFoundException.java new file mode 100644 index 0000000000..9e57f5db6c --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/exception/CommentNotFoundException.java @@ -0,0 +1,8 @@ +package com.sysgears.post.exception; + +public class CommentNotFoundException extends RuntimeException { + + public CommentNotFoundException(Integer id) { + super(String.format("Comment with id %d not found", id)); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/exception/PostNotFoundException.java b/modules/post/server-java/src/main/java/com/sysgears/post/exception/PostNotFoundException.java new file mode 100644 index 0000000000..77303fb210 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/exception/PostNotFoundException.java @@ -0,0 +1,8 @@ +package com.sysgears.post.exception; + +public class PostNotFoundException extends RuntimeException { + + public PostNotFoundException(Integer id) { + super(String.format("Post with id %d not found", id)); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/model/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/model/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/model/Comment.java b/modules/post/server-java/src/main/java/com/sysgears/post/model/Comment.java new file mode 100644 index 0000000000..2ee62adc03 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/model/Comment.java @@ -0,0 +1,29 @@ +package com.sysgears.post.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "СOMMENT") +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "CONTENT", nullable = false) + private String content; + + @ManyToOne + private Post post; + + public Comment(String content) { + this.content = content; + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/model/Post.java b/modules/post/server-java/src/main/java/com/sysgears/post/model/Post.java new file mode 100644 index 0000000000..15ad9b2b38 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/model/Post.java @@ -0,0 +1,49 @@ +package com.sysgears.post.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "POST") +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "TITLE", nullable = false) + private String title; + + @Column(name = "CONTENT", nullable = false) + private String content; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @Fetch(FetchMode.JOIN) + @JoinTable(name = "POST_COMMENT", + joinColumns = { @JoinColumn(name = "POST_ID", referencedColumnName = "id") }, + inverseJoinColumns = { @JoinColumn(name = "COMMENT_ID", referencedColumnName = "id") }) + private Set comments = new HashSet<>(); + + public Post(String title, String content) { + this.title = title; + this.content = content; + } + + public void addComment(Comment comment) { + comments.add(comment); + } + + public void removeComment(Comment comment) { + comments.remove(comment); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/repository/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/repository/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/repository/CommentRepository.java b/modules/post/server-java/src/main/java/com/sysgears/post/repository/CommentRepository.java new file mode 100644 index 0000000000..d525552efc --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.sysgears.post.repository; + +import com.sysgears.post.model.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java b/modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java new file mode 100644 index 0000000000..d321c19b0a --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java @@ -0,0 +1,73 @@ +package com.sysgears.post.repository; + +import lombok.Data; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +@Data +public class OffsetPageRequest implements Pageable { + private final int offset; + private final int limit; + private final Sort sort; + + public OffsetPageRequest(int offset, int limit) { + this(offset, limit, Sort.unsorted()); + } + + public OffsetPageRequest(int offset, int limit, Sort sort) { + if (offset < 0) { + throw new IllegalArgumentException("Offset index must not be less than zero!"); + } + + if (limit < 1) { + throw new IllegalArgumentException("Limit must not be less than one!"); + } + this.offset = offset; + this.limit = limit; + this.sort = sort; + } + + @Override + public int getPageNumber() { + return offset / limit; + } + + @Override + public int getPageSize() { + return limit; + } + + @Override + public long getOffset() { + return offset; + } + + @Override + public Sort getSort() { + return sort; + } + + @Override + public Pageable next() { + return new OffsetPageRequest((int) getOffset() + getPageSize(), getPageSize(), getSort()); + } + + public OffsetPageRequest previous() { + return hasPrevious() ? new OffsetPageRequest((int) getOffset() - getPageSize(), getPageSize(), getSort()) : this; + } + + @Override + public Pageable previousOrFirst() { + return hasPrevious() ? previous() : first(); + } + + @Override + public Pageable first() { + return new OffsetPageRequest(0, getPageSize(), getSort()); + } + + @Override + public boolean hasPrevious() { + return offset > limit; + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/repository/PostRepository.java b/modules/post/server-java/src/main/java/com/sysgears/post/repository/PostRepository.java new file mode 100644 index 0000000000..be3aea9005 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/repository/PostRepository.java @@ -0,0 +1,13 @@ +package com.sysgears.post.repository; + +import com.sysgears.post.model.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.scheduling.annotation.Async; + +import java.util.concurrent.CompletableFuture; + +public interface PostRepository extends JpaRepository { + + @Async + CompletableFuture getById(Integer id); +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentMutationResolver.java b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentMutationResolver.java new file mode 100644 index 0000000000..6ba6e29fbe --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentMutationResolver.java @@ -0,0 +1,56 @@ +package com.sysgears.post.resolvers; + +import com.sysgears.core.subscription.Publisher; +import com.sysgears.post.dto.CommentPayload; +import com.sysgears.post.dto.input.AddCommentInput; +import com.sysgears.post.dto.input.DeleteCommentInput; +import com.sysgears.post.dto.input.EditCommentInput; +import com.sysgears.post.model.Comment; +import com.sysgears.post.service.PostService; +import com.sysgears.post.subscription.CommentUpdatedEvent; +import com.sysgears.post.subscription.Mutation; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class CommentMutationResolver implements GraphQLMutationResolver { + private final PostService postService; + private final Publisher commentPublisher; + + public CompletableFuture addComment(AddCommentInput input) { + return CompletableFuture.supplyAsync(() -> { + Comment comment = postService.addComment(input.getPostId(), input.getContent()); + + CommentPayload commentPayload = new CommentPayload(comment.getId(), comment.getContent()); + commentPublisher.publish(new CommentUpdatedEvent(Mutation.CREATED, input.getPostId(), commentPayload)); + + return commentPayload; + }); + } + + public CompletableFuture deleteComment(DeleteCommentInput input) { + return CompletableFuture.supplyAsync(() -> { + Comment deletedComment = postService.deleteComment(input.getPostId(), input.getId()); + + CommentPayload commentPayload = new CommentPayload(deletedComment.getId(), deletedComment.getContent()); + commentPublisher.publish(new CommentUpdatedEvent(Mutation.DELETED, input.getPostId(), commentPayload)); + + return commentPayload; + }); + } + + public CompletableFuture editComment(EditCommentInput input) { + return CompletableFuture.supplyAsync(() -> { + Comment editedComment = postService.editComment(input.getPostId(), input.getId(), input.getContent()); + + CommentPayload commentPayload = new CommentPayload(editedComment.getId(), editedComment.getContent()); + commentPublisher.publish(new CommentUpdatedEvent(Mutation.UPDATED, input.getPostId(), commentPayload)); + + return commentPayload; + }); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentSubscriptionResolver.java b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentSubscriptionResolver.java new file mode 100644 index 0000000000..48f8ce19a7 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/CommentSubscriptionResolver.java @@ -0,0 +1,30 @@ +package com.sysgears.post.resolvers; + +import com.sysgears.core.subscription.Subscriber; +import com.sysgears.post.dto.subscription.UpdateCommentPayload; +import com.sysgears.post.subscription.CommentUpdatedEvent; +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class CommentSubscriptionResolver implements GraphQLSubscriptionResolver { + private final Subscriber commentSubscriber; + + public CompletableFuture> commentUpdated(Integer postId) { + return CompletableFuture.supplyAsync(() -> commentSubscriber.subscribe(e -> e.getPostId().equals(postId)) + .map(event -> + new UpdateCommentPayload( + event.getComment().getId(), + event.getPostId(), + event.getMutation().name(), + event.getComment() + ) + ) + ); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostMutationResolver.java b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostMutationResolver.java new file mode 100644 index 0000000000..e7aca8c48a --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostMutationResolver.java @@ -0,0 +1,60 @@ +package com.sysgears.post.resolvers; + +import com.sysgears.core.subscription.Publisher; +import com.sysgears.post.dto.PostPayload; +import com.sysgears.post.dto.input.AddPostInput; +import com.sysgears.post.dto.input.EditPostInput; +import com.sysgears.post.model.Post; +import com.sysgears.post.service.PostService; +import com.sysgears.post.subscription.Mutation; +import com.sysgears.post.subscription.PostUpdatedEvent; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class PostMutationResolver implements GraphQLMutationResolver { + private final PostService postService; + private final Publisher postPublisher; + + public CompletableFuture addPost(AddPostInput input) { + return CompletableFuture.supplyAsync(() -> { + Post post = new Post(input.getTitle(), input.getContent()); + Post created = postService.create(post); + + PostPayload postPayload = new PostPayload(created.getId(), created.getTitle(), created.getContent()); + postPublisher.publish(new PostUpdatedEvent(Mutation.CREATED, postPayload)); + + return postPayload; + }); + } + + public CompletableFuture deletePost(Integer id) { + return CompletableFuture.supplyAsync(() -> { + Post deletedPost = postService.deleteById(id); + + PostPayload postPayload = new PostPayload(deletedPost.getId(), deletedPost.getTitle(), deletedPost.getContent()); + postPublisher.publish(new PostUpdatedEvent(Mutation.DELETED, postPayload)); + + return postPayload; + }); + } + + public CompletableFuture editPost(EditPostInput input) { + return CompletableFuture.supplyAsync(() -> { + Post post = postService.findById(input.getId()); + post.setTitle(input.getTitle()); + post.setContent(input.getContent()); + + Post updated = postService.update(post); + + PostPayload postPayload = new PostPayload(updated.getId(), updated.getTitle(), updated.getContent()); + postPublisher.publish(new PostUpdatedEvent(Mutation.UPDATED, postPayload)); + + return postPayload; + }); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java new file mode 100644 index 0000000000..0d1d0fa7a6 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java @@ -0,0 +1,32 @@ +package com.sysgears.post.resolvers; + +import com.sysgears.post.dto.PostPayload; +import com.sysgears.post.dto.Posts; +import com.sysgears.post.repository.OffsetPageRequest; +import com.sysgears.post.service.PostService; +import graphql.kickstart.tools.GraphQLQueryResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class PostQueryResolver implements GraphQLQueryResolver { + private final PostService postService; + + public CompletableFuture posts(Optional limit, Optional after) { + return CompletableFuture.supplyAsync(() -> { + int offset = after.orElse(0); + int size = limit.orElse(10); + Pageable pageRequest = new OffsetPageRequest(offset, size); + return postService.findAll(pageRequest); + }); + } + + public CompletableFuture post(Integer id) { + return postService.getById(id); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostSubscriptionResolver.java b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostSubscriptionResolver.java new file mode 100644 index 0000000000..e3e4e5592c --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostSubscriptionResolver.java @@ -0,0 +1,31 @@ +package com.sysgears.post.resolvers; + +import com.sysgears.core.subscription.Subscriber; +import com.sysgears.post.dto.subscription.UpdatePostPayload; +import com.sysgears.post.subscription.Mutation; +import com.sysgears.post.subscription.PostUpdatedEvent; +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class PostSubscriptionResolver implements GraphQLSubscriptionResolver { + private final Subscriber postSubscriber; + + public CompletableFuture> postUpdated(Integer id) { + return CompletableFuture.supplyAsync(() -> postSubscriber.subscribe(e -> + e.getPost().getId().equals(id) && (e.getMutation() == Mutation.UPDATED || e.getMutation() == Mutation.DELETED)) + .map(event -> new UpdatePostPayload(event.getPost().getId(), event.getMutation().name(), event.getPost())) + ); + } + + public CompletableFuture> postsUpdated(Integer endCursor) { + return CompletableFuture.supplyAsync(() -> postSubscriber.subscribe(e -> e.getPost().getId() <= endCursor) + .map(event -> new UpdatePostPayload(event.getPost().getId(), event.getMutation().name(), event.getPost())) + ); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/service/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/service/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/service/PostService.java b/modules/post/server-java/src/main/java/com/sysgears/post/service/PostService.java new file mode 100644 index 0000000000..0e9b152e37 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/service/PostService.java @@ -0,0 +1,128 @@ +package com.sysgears.post.service; + +import com.sysgears.post.dto.*; +import com.sysgears.post.exception.CommentNotFoundException; +import com.sysgears.post.exception.PostNotFoundException; +import com.sysgears.post.model.Comment; +import com.sysgears.post.model.Post; +import com.sysgears.post.repository.CommentRepository; +import com.sysgears.post.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PostService { + private final PostRepository repository; + private final CommentRepository commentRepository; + + @Transactional(readOnly = true) + public Posts findAll(Pageable pageable) { + Page posts = repository.findAll(pageable); + + List postEdgesList = new ArrayList<>(); + + for (int i = 0; i < posts.getContent().size(); i++) { + Post post = posts.getContent().get(i); + postEdgesList.add( + PostEdges.builder() + .cursor((int) (pageable.getOffset() + i)) + .node(new PostPayload(post.getId(), post.getTitle(), post.getContent())) + .build() + ); + } + + return Posts.builder() + .edges(postEdgesList) + .pageInfo( + PostPageInfo.builder() + .endCursor((int) (pageable.getOffset() + posts.getSize() - 1)) + .hasNextPage(!posts.isLast()) + .build() + ) + .totalCount(posts.getTotalElements()) + .build(); + } + + @Transactional(readOnly = true) + public CompletableFuture getById(Integer id) { + return repository.getById(id).thenApply(post -> { + if (post == null) throw new PostNotFoundException(id); + + PostPayload postPayload = new PostPayload(id, post.getTitle(), post.getContent()); + postPayload.addComments(convertComments(post.getComments())); + return postPayload; + }); + } + + @Transactional(readOnly = true) + public Post findById(Integer id) { + return repository.findById(id).orElseThrow(() -> new PostNotFoundException(id)); + } + + @Transactional + public Post create(Post post) { + return repository.save(post); + } + + @Transactional + public Post deleteById(Integer id) { + Post post = findById(id); + + repository.delete(post); + return post; + } + + @Transactional + public Post update(Post post) { + return repository.save(post); + } + + @Transactional + public Comment addComment(Integer postId, String commentContent) { + Post post = findById(postId); + + Comment comment = commentRepository.save(new Comment(commentContent)); + post.addComment(comment); + + update(post); + + return comment; + } + + @Transactional + public Comment deleteComment(Integer postId, Integer commentId) { + Post post = findById(postId); + + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new CommentNotFoundException(commentId)); + post.removeComment(comment); + + update(post); + + return comment; + } + + @Transactional + public Comment editComment(Integer postId, Integer commentId, String content) { + Post post = findById(postId); + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new CommentNotFoundException(commentId)); + comment.setContent(content); + + update(post); + + return comment; + } + + private Set convertComments(Collection comments) { + return comments.stream() + .map(comment -> new CommentPayload(comment.getId(), comment.getContent())) + .collect(Collectors.toSet()); + } +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/.gitkeep b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentPubSubService.java b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentPubSubService.java new file mode 100644 index 0000000000..0f88f96467 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentPubSubService.java @@ -0,0 +1,8 @@ +package com.sysgears.post.subscription; + +import com.sysgears.core.subscription.AbstractPubSubService; +import org.springframework.stereotype.Component; + +@Component +public class CommentPubSubService extends AbstractPubSubService { +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentUpdatedEvent.java b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentUpdatedEvent.java new file mode 100644 index 0000000000..828db7f063 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/CommentUpdatedEvent.java @@ -0,0 +1,11 @@ +package com.sysgears.post.subscription; + +import com.sysgears.post.dto.CommentPayload; +import lombok.Data; + +@Data +public class CommentUpdatedEvent { + private final Mutation mutation; + private final Integer postId; + private final CommentPayload comment; +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/Mutation.java b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/Mutation.java new file mode 100644 index 0000000000..82c186c4db --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/Mutation.java @@ -0,0 +1,5 @@ +package com.sysgears.post.subscription; + +public enum Mutation { + DELETED, UPDATED, CREATED +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostPubSubService.java b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostPubSubService.java new file mode 100644 index 0000000000..e4c0cb3655 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostPubSubService.java @@ -0,0 +1,8 @@ +package com.sysgears.post.subscription; + +import com.sysgears.core.subscription.AbstractPubSubService; +import org.springframework.stereotype.Component; + +@Component +public class PostPubSubService extends AbstractPubSubService { +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostUpdatedEvent.java b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostUpdatedEvent.java new file mode 100644 index 0000000000..65619a3705 --- /dev/null +++ b/modules/post/server-java/src/main/java/com/sysgears/post/subscription/PostUpdatedEvent.java @@ -0,0 +1,10 @@ +package com.sysgears.post.subscription; + +import com.sysgears.post.dto.PostPayload; +import lombok.Data; + +@Data +public class PostUpdatedEvent { + private final Mutation mutation; + private final PostPayload post; +} diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index fff616fdaa..6698ce0c84 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -81,6 +81,7 @@ project(':app') { implementation project(':mailer') implementation project(':contact') implementation project(':upload') + implementation project(':post') } bootJar { enabled = true From 72f4bdfbbadcc907a8023732b44dfd0abf46b536 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 19 Jan 2021 20:10:38 +0200 Subject: [PATCH 58/85] refactoring; cleaning up code --- .../RefreshTokenInvalidException.java | 1 - .../authentication/utils/SessionUtils.java | 5 +++-- modules/contact/server-java/.gitignore | 1 - .../sysgears/contact/dto/ContactInput.java | 8 +++----- modules/core/server-java/.gitignore | 4 ---- .../core/exception/FieldErrorException.java | 3 --- modules/counter/server-java/.gitignore | 1 - .../counter/constant/CounterConstants.java | 9 +++++---- .../resolvers/CounterMutationResolver.java | 3 +-- .../resolvers/CounterQueryResolver.java | 1 - .../CounterPubSubService.java | 2 +- modules/mailer/server-java/.gitignore | 1 - modules/post/server-java/.gitignore | 1 - modules/upload/server-java/.gitignore | 1 - modules/user/server-java/.gitignore | 1 - .../com/sysgears/user/config/JwtFilter.java | 2 +- .../user/exception/UserNotFoundException.java | 5 ----- .../java/com/sysgears/user/model/User.java | 4 ++-- .../com/sysgears/user/model/UserAuth.java | 10 +++++----- .../user/repository/CustomUserRepository.java | 3 +-- .../user/repository/UserRepositoryImpl.java | 19 +++++++++++-------- .../resolvers/UserSubscriptionResolver.java | 3 ++- .../sysgears/user/util/UserIdentityUtils.java | 5 +++-- 23 files changed, 38 insertions(+), 55 deletions(-) delete mode 100644 modules/contact/server-java/.gitignore delete mode 100644 modules/core/server-java/.gitignore delete mode 100644 modules/counter/server-java/.gitignore rename modules/counter/server-java/src/main/java/com/sysgears/counter/{resolvers => subscription}/CounterPubSubService.java (84%) delete mode 100644 modules/mailer/server-java/.gitignore delete mode 100644 modules/post/server-java/.gitignore delete mode 100644 modules/upload/server-java/.gitignore delete mode 100644 modules/user/server-java/.gitignore diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java index a405d0f505..fdf8cf6dfb 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/exception/RefreshTokenInvalidException.java @@ -3,7 +3,6 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.BAD_REQUEST) public class RefreshTokenInvalidException extends RuntimeException { public RefreshTokenInvalidException() { diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java index d738325753..f4bf04af0c 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/utils/SessionUtils.java @@ -1,11 +1,12 @@ package com.sysgears.authentication.utils; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public final class SessionUtils { public static final SecurityContext SECURITY_CONTEXT = SecurityContextHolder.createEmptyContext(); - private SessionUtils() { - } } diff --git a/modules/contact/server-java/.gitignore b/modules/contact/server-java/.gitignore deleted file mode 100644 index 6b19dc7f40..0000000000 --- a/modules/contact/server-java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.gradle \ No newline at end of file diff --git a/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java b/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java index bcd98a0ba1..fb2e60f08e 100644 --- a/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java +++ b/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java @@ -6,13 +6,11 @@ import org.springframework.lang.NonNull; @Data -@NoArgsConstructor -@AllArgsConstructor public class ContactInput { @NonNull - private String name; + private final String name; @NonNull - private String email; + private final String email; @NonNull - private String content; + private final String content; } diff --git a/modules/core/server-java/.gitignore b/modules/core/server-java/.gitignore deleted file mode 100644 index 4774d29aad..0000000000 --- a/modules/core/server-java/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target/ -/.idea -/build -/.gradle \ No newline at end of file diff --git a/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java b/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java index 90e1e68660..182180f00a 100644 --- a/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java +++ b/modules/core/server-java/src/main/java/com/sysgears/core/exception/FieldErrorException.java @@ -3,14 +3,11 @@ import graphql.ErrorClassification; import graphql.GraphQLError; import graphql.language.SourceLocation; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; import java.util.HashMap; import java.util.List; import java.util.Map; -@ResponseStatus(HttpStatus.BAD_REQUEST) public abstract class FieldErrorException extends RuntimeException implements GraphQLError { private final Map errors = new HashMap<>(); diff --git a/modules/counter/server-java/.gitignore b/modules/counter/server-java/.gitignore deleted file mode 100644 index ac96206f31..0000000000 --- a/modules/counter/server-java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.gradle/ \ No newline at end of file diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java index 4f0d395c8a..4893f46ae2 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/constant/CounterConstants.java @@ -1,8 +1,9 @@ package com.sysgears.counter.constant; -import lombok.experimental.UtilityClass; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; -@UtilityClass -public class CounterConstants { +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class CounterConstants { public static final int ID = 1; -} \ No newline at end of file +} diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java index c75a1f5a8a..305a0d919b 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterMutationResolver.java @@ -19,7 +19,6 @@ public class CounterMutationResolver implements GraphQLMutationResolver { private final CounterRepository repository; private final Publisher publisher; - @Transactional public CompletableFuture addServerCounter(int amount) { log.debug("Updating counter"); return repository.findById(CounterConstants.ID) @@ -31,4 +30,4 @@ public CompletableFuture addServerCounter(int amount) { } ); } -} \ No newline at end of file +} diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java index 05184f1a71..e888c0f34b 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterQueryResolver.java @@ -17,7 +17,6 @@ public class CounterQueryResolver implements GraphQLQueryResolver { private final CounterRepository repository; - @Transactional(readOnly = true) public CompletableFuture serverCounter() { log.debug("Get counter"); return repository.findById(CounterConstants.ID); diff --git a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java b/modules/counter/server-java/src/main/java/com/sysgears/counter/subscription/CounterPubSubService.java similarity index 84% rename from modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java rename to modules/counter/server-java/src/main/java/com/sysgears/counter/subscription/CounterPubSubService.java index 23d237e584..c4a84b08c3 100644 --- a/modules/counter/server-java/src/main/java/com/sysgears/counter/resolvers/CounterPubSubService.java +++ b/modules/counter/server-java/src/main/java/com/sysgears/counter/subscription/CounterPubSubService.java @@ -1,4 +1,4 @@ -package com.sysgears.counter.resolvers; +package com.sysgears.counter.subscription; import com.sysgears.core.subscription.AbstractPubSubService; import com.sysgears.counter.model.Counter; diff --git a/modules/mailer/server-java/.gitignore b/modules/mailer/server-java/.gitignore deleted file mode 100644 index 6b19dc7f40..0000000000 --- a/modules/mailer/server-java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.gradle \ No newline at end of file diff --git a/modules/post/server-java/.gitignore b/modules/post/server-java/.gitignore deleted file mode 100644 index 6b19dc7f40..0000000000 --- a/modules/post/server-java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.gradle \ No newline at end of file diff --git a/modules/upload/server-java/.gitignore b/modules/upload/server-java/.gitignore deleted file mode 100644 index 6b19dc7f40..0000000000 --- a/modules/upload/server-java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.gradle \ No newline at end of file diff --git a/modules/user/server-java/.gitignore b/modules/user/server-java/.gitignore deleted file mode 100644 index 6b19dc7f40..0000000000 --- a/modules/user/server-java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.gradle \ No newline at end of file diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java b/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java index fe34161a7b..2f904ee22d 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/config/JwtFilter.java @@ -29,7 +29,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse getToken(request) .map(userService::loadUserByToken) .map(user -> new JWTPreAuthenticationToken(user, new WebAuthenticationDetailsSource().buildDetails(request))) - .ifPresent(authentication -> SessionUtils.SECURITY_CONTEXT.setAuthentication(authentication)); + .ifPresent(SessionUtils.SECURITY_CONTEXT::setAuthentication); filterChain.doFilter(request, response); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java index 94a91555d8..effbb002aa 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java @@ -7,16 +7,11 @@ import java.util.Collections; import java.util.Map; -@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends FieldErrorException { public UserNotFoundException() { super("No user found", Collections.emptyMap()); } - public UserNotFoundException(String message) { - super(message, Collections.emptyMap()); - } - public UserNotFoundException(int id) { super(String.format("User with id %d not found.", id), Collections.emptyMap()); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java index 740c7ceb19..c7c1ef02ec 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/User.java @@ -37,11 +37,11 @@ public class User implements UserDetails { @Column(name = "EMAIL", nullable = false) private String email; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private UserProfile profile; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private UserAuth auth; diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java b/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java index 321b684843..a6b05fdb4f 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/model/UserAuth.java @@ -21,23 +21,23 @@ public class UserAuth { @Column(name = "ID") private int id; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private CertificateAuth certificate; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private FacebookAuth facebook; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private GoogleAuth google; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private GithubAuth github; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.JOIN) private LinkedInAuth linkedin; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java index da3d9f41bf..467bee2d80 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/CustomUserRepository.java @@ -3,14 +3,13 @@ import com.sysgears.user.dto.input.FilterUserInput; import com.sysgears.user.dto.input.OrderByUserInput; import com.sysgears.user.model.User; -import org.springframework.scheduling.annotation.Async; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; public interface CustomUserRepository { - @Async + CompletableFuture> findByCriteria(Optional orderBy, Optional filter); CompletableFuture findByUsernameOrEmail(String usernameOrEmail); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java index 17f5cf2d68..0c0260bc8c 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/repository/UserRepositoryImpl.java @@ -58,16 +58,19 @@ public CompletableFuture> findByCriteria(Optional o @Override public CompletableFuture findByUsernameOrEmail(String usernameOrEmail) { - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - CriteriaQuery query = builder.createQuery(User.class); + return CompletableFuture.supplyAsync(() -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - Root user = query.from(User.class); + CriteriaQuery query = builder.createQuery(User.class); - query.where(builder.or( - builder.equal(user.get("username"), usernameOrEmail), - builder.equal(user.get("email"), usernameOrEmail) - )); + Root user = query.from(User.class); - return CompletableFuture.supplyAsync(() -> entityManager.createQuery(query).getSingleResult()); + query.where(builder.or( + builder.equal(user.get("username"), usernameOrEmail), + builder.equal(user.get("email"), usernameOrEmail) + )); + + return entityManager.createQuery(query).getSingleResult(); + }); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java index 349e8fd92b..4f0877c46a 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java @@ -8,13 +8,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.Optional; import java.util.concurrent.CompletableFuture; @Slf4j -@Service +@Component @RequiredArgsConstructor public class UserSubscriptionResolver implements GraphQLSubscriptionResolver { private final Subscriber subscriber; diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java b/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java index 9a3e62cd03..22943fd629 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/util/UserIdentityUtils.java @@ -2,9 +2,10 @@ import com.sysgears.authentication.model.jwt.JwtUserIdentity; import com.sysgears.user.model.User; -import lombok.experimental.UtilityClass; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; -@UtilityClass +@NoArgsConstructor(access = AccessLevel.PRIVATE) public final class UserIdentityUtils { public static JwtUserIdentity convert(User user) { From 422ebfdf00a8e7c24eddd878446a68d428574400 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 20 Jan 2021 19:02:21 +0200 Subject: [PATCH 59/85] moved custom pageable implementation to core module --- .../java/com/sysgears/core/pagination}/OffsetPageRequest.java | 2 +- .../java/com/sysgears/post/resolvers/PostQueryResolver.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename modules/{post/server-java/src/main/java/com/sysgears/post/repository => core/server-java/src/main/java/com/sysgears/core/pagination}/OffsetPageRequest.java (97%) diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java b/modules/core/server-java/src/main/java/com/sysgears/core/pagination/OffsetPageRequest.java similarity index 97% rename from modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java rename to modules/core/server-java/src/main/java/com/sysgears/core/pagination/OffsetPageRequest.java index d321c19b0a..07cb0dc697 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/repository/OffsetPageRequest.java +++ b/modules/core/server-java/src/main/java/com/sysgears/core/pagination/OffsetPageRequest.java @@ -1,4 +1,4 @@ -package com.sysgears.post.repository; +package com.sysgears.core.pagination; import lombok.Data; import org.springframework.data.domain.Pageable; diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java index 0d1d0fa7a6..86c1d7ab46 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/resolvers/PostQueryResolver.java @@ -2,7 +2,7 @@ import com.sysgears.post.dto.PostPayload; import com.sysgears.post.dto.Posts; -import com.sysgears.post.repository.OffsetPageRequest; +import com.sysgears.core.pagination.OffsetPageRequest; import com.sysgears.post.service.PostService; import graphql.kickstart.tools.GraphQLQueryResolver; import lombok.RequiredArgsConstructor; From d15d40f8330783a55205666ea41275c8e144d4d3 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 20 Jan 2021 19:02:37 +0200 Subject: [PATCH 60/85] initialized skeleton for chat module --- modules/chat/server-java/build.gradle | 4 + .../com/sysgears/chat/dto/MessageEdges.java | 11 +++ .../sysgears/chat/dto/MessagePageInfo.java | 11 +++ .../com/sysgears/chat/dto/MessagePayload.java | 20 +++++ .../java/com/sysgears/chat/dto/Messages.java | 14 +++ .../com/sysgears/chat/dto/QuotedMessage.java | 12 +++ .../chat/dto/input/AddMessageInput.java | 14 +++ .../chat/dto/input/EditMessageInput.java | 12 +++ .../subscription/UpdateMessagesPayload.java | 14 +++ .../java/com/sysgears/chat/model/Message.java | 50 +++++++++++ .../chat/repository/MessageRepository.java | 7 ++ .../resolvers/MessageMutationResolver.java | 30 +++++++ .../chat/resolvers/MessageQueryResolver.java | 34 ++++++++ .../MessageSubscriptionResolver.java | 19 ++++ .../sysgears/chat/service/MessageService.java | 71 +++++++++++++++ .../com/sysgears/chat/subscription/.gitkeep | 0 .../src/main/resources/schema.graphqls | 87 +++++++++++++++++++ packages/server-java/build.gradle | 1 + packages/server-java/settings.gradle | 3 +- 19 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 modules/chat/server-java/build.gradle create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/dto/subscription/UpdateMessagesPayload.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/.gitkeep create mode 100644 modules/chat/server-java/src/main/resources/schema.graphqls diff --git a/modules/chat/server-java/build.gradle b/modules/chat/server-java/build.gradle new file mode 100644 index 0000000000..a6e7624109 --- /dev/null +++ b/modules/chat/server-java/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':core') + implementation project(':upload') +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java new file mode 100644 index 0000000000..859d1fab9e --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java @@ -0,0 +1,11 @@ +package com.sysgears.chat.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class MessageEdges { + private final MessagePayload node; + private final Integer cursor; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java new file mode 100644 index 0000000000..4d12aa18c7 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java @@ -0,0 +1,11 @@ +package com.sysgears.chat.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class MessagePageInfo { + private final Integer endCursor; + private final Boolean hasNextPage; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java new file mode 100644 index 0000000000..e959593e5f --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java @@ -0,0 +1,20 @@ +package com.sysgears.chat.dto; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class MessagePayload { + @NonNull + private final Integer id; + private final String text; + private final Integer userId; + private final String createdAt; + private final String username; + private final String uuid; + private final Integer quotedId; + private final String filename; + private final String path; + private final QuotedMessage quotedMessage; + +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java new file mode 100644 index 0000000000..d6f0c25268 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java @@ -0,0 +1,14 @@ +package com.sysgears.chat.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class Messages { + private final Long totalCount; + private final List edges; + private final MessagePageInfo pageInfo; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java new file mode 100644 index 0000000000..9efffab047 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java @@ -0,0 +1,12 @@ +package com.sysgears.chat.dto; + +import lombok.Data; + +@Data +public class QuotedMessage { + private final Integer id; + private final String text; + private final String username; + private final String filename; + private final String path; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java new file mode 100644 index 0000000000..8155b743b8 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java @@ -0,0 +1,14 @@ +package com.sysgears.chat.dto.input; + +import lombok.Data; + +import javax.servlet.http.Part; + +@Data +public class AddMessageInput { + private final String text; + private final Integer userId; + private final String uuid; + private final Integer quotedId; + private final Part attachment; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java new file mode 100644 index 0000000000..8a34a7f53a --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java @@ -0,0 +1,12 @@ +package com.sysgears.chat.dto.input; + +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class EditMessageInput { + @NonNull + private final Integer id; + private final String text; + private final Integer userId; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/subscription/UpdateMessagesPayload.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/subscription/UpdateMessagesPayload.java new file mode 100644 index 0000000000..ad60f8ef69 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/subscription/UpdateMessagesPayload.java @@ -0,0 +1,14 @@ +package com.sysgears.chat.dto.subscription; + +import com.sysgears.chat.dto.MessagePayload; +import lombok.Data; +import org.springframework.lang.NonNull; + +@Data +public class UpdateMessagesPayload { + @NonNull + private final String mutation; + private final Integer id; + @NonNull + private final MessagePayload node; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java new file mode 100644 index 0000000000..ffeea85f55 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java @@ -0,0 +1,50 @@ +package com.sysgears.chat.model; + +import com.sysgears.upload.model.FileMetadata; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.GenericGenerator; +import org.springframework.data.annotation.CreatedDate; + +import javax.persistence.*; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "MESSAGE") +public class Message { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "TEXT") + private String text; + + @Column(name = "USER_ID") + private Integer userId; + + @CreatedDate + @Column(name = "CREATED_AT", updatable = false) + private Instant createdAt; + + @Column(name = "USERNAME") + private String username; + + @Column(name = "UUID") + private UUID uuid; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "QUOTED_ID") + private Message quoted; + + @OneToOne + @Fetch(FetchMode.JOIN) + @JoinColumn(name = "attachment_id") + private FileMetadata attachment; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java new file mode 100644 index 0000000000..ae62022223 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java @@ -0,0 +1,7 @@ +package com.sysgears.chat.repository; + +import com.sysgears.chat.model.Message; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MessageRepository extends JpaRepository { +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java new file mode 100644 index 0000000000..14f8a05615 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java @@ -0,0 +1,30 @@ +package com.sysgears.chat.resolvers; + +import com.sysgears.chat.dto.MessagePayload; +import com.sysgears.chat.dto.input.AddMessageInput; +import com.sysgears.chat.dto.input.EditMessageInput; +import graphql.kickstart.tools.GraphQLMutationResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class MessageMutationResolver implements GraphQLMutationResolver { + + public CompletableFuture addMessage(AddMessageInput input) { + //todo implement + return null; + } + + public CompletableFuture deleteMessage(Integer id) { + //todo implement + return null; + } + + public CompletableFuture editMessage(EditMessageInput input) { + //todo implement + return null; + } +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java new file mode 100644 index 0000000000..6fed0b7308 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java @@ -0,0 +1,34 @@ +package com.sysgears.chat.resolvers; + +import com.sysgears.chat.dto.MessagePayload; +import com.sysgears.chat.dto.Messages; +import com.sysgears.chat.service.MessageService; +import com.sysgears.core.pagination.OffsetPageRequest; +import graphql.kickstart.tools.GraphQLQueryResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class MessageQueryResolver implements GraphQLQueryResolver { + private final MessageService messageService; + + public CompletableFuture messages(Optional limit, Optional after) { + return CompletableFuture.supplyAsync(() -> { + int offset = after.orElse(0); + int size = limit.orElse(10); + Pageable pageRequest = new OffsetPageRequest(offset, size); + + return messageService.findAll(pageRequest); + }); + } + + public CompletableFuture message(Integer id) { + //todo implement + return null; + } +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java new file mode 100644 index 0000000000..77e30cd5b8 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java @@ -0,0 +1,19 @@ +package com.sysgears.chat.resolvers; + +import com.sysgears.chat.dto.subscription.UpdateMessagesPayload; +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class MessageSubscriptionResolver implements GraphQLSubscriptionResolver { + + public CompletableFuture> messagesUpdated(Integer endCursor) { + //todo implement + return null; + } +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java new file mode 100644 index 0000000000..dc3a29f57e --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java @@ -0,0 +1,71 @@ +package com.sysgears.chat.service; + +import com.sysgears.chat.dto.*; +import com.sysgears.chat.model.Message; +import com.sysgears.chat.repository.MessageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class MessageService { + private final MessageRepository repository; + + @Transactional(readOnly = true) + public Messages findAll(Pageable pageable) { + Page messages = repository.findAll(pageable); + + List messageEdgesList = new ArrayList<>(); + + + for (int i = 0; i < messages.getContent().size(); i++) { + Message message = messages.getContent().get(i); + messageEdgesList.add( + MessageEdges.builder() + .cursor((int) (pageable.getOffset() + i)) + .node(convert(message)) + .build() + ); + } + + return Messages.builder() + .edges(messageEdgesList) + .pageInfo( + MessagePageInfo.builder() + .endCursor((int) (pageable.getOffset() + messages.getSize() - 1)) + .hasNextPage(!messages.isLast()) + .build() + ) + .totalCount(messages.getTotalElements()) + .build(); + } + + private MessagePayload convert(Message message) { + QuotedMessage quotedMessage = new QuotedMessage( + message.getQuoted().getId(), + message.getQuoted().getText(), + message.getQuoted().getUsername(), + message.getQuoted().getAttachment().getName(), + message.getQuoted().getAttachment().getPath() + ); + return new MessagePayload( + message.getId(), + message.getText(), + message.getUserId(), + message.getCreatedAt().toString(), + message.getUsername(), + message.getUuid().toString(), + quotedMessage.getId(), + message.getAttachment().getName(), + message.getAttachment().getPath(), + quotedMessage + ); + } +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/.gitkeep b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/chat/server-java/src/main/resources/schema.graphqls b/modules/chat/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..873c58a2b6 --- /dev/null +++ b/modules/chat/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,87 @@ +scalar FileUpload + +type QuotedMessage { + id: Int + text: String + username: String + filename: String + path: String +} + +# Message +type Message { + id: Int! + text: String + userId: Int + createdAt: String + username: String + uuid: String + quotedId: Int + filename: String + path: String + quotedMessage: QuotedMessage +} + +# Edges for Messages +type MessageEdges { + node: Message + cursor: Int +} + +# PageInfo for Messages +type MessagePageInfo { + endCursor: Int + hasNextPage: Boolean +} + +# Messages relay-style pagination query +type Messages { + totalCount: Int + edges: [MessageEdges] + pageInfo: MessagePageInfo +} + +extend type Query { + # Messages + messages(limit: Int, after: Int): Messages + # Message + message(id: Int!): Message +} + +extend type Mutation { + # Create new message + addMessage(input: AddMessageInput!): Message + # Delete a message + deleteMessage(id: Int!): Message + # Edit a message + editMessage(input: EditMessageInput!): Message +} + +# Input for addMessage Mutation +input AddMessageInput { + text: String + userId: Int + uuid: String + quotedId: Int + attachment: FileUpload +} + +# Input for editMessage Mutation +input EditMessageInput { + id: Int! + text: String + userId: Int +} + +extend type Subscription { + # Subscription fired when anyone changes messages list + messagesUpdated(endCursor: Int!): UpdateMessagesPayload + # messagesUpdated: UpdateMessagesPayload +} + +# Payload for messagesUpdated Subscription +type UpdateMessagesPayload { + mutation: String! + id: Int + node: Message! +} diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index 6698ce0c84..2a81f4e1c7 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -82,6 +82,7 @@ project(':app') { implementation project(':contact') implementation project(':upload') implementation project(':post') + implementation project(':chat') } bootJar { enabled = true diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index 391cb47e4e..1ee9325c73 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post' +include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post', 'chat' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') @@ -9,3 +9,4 @@ project(':mailer').projectDir = new File('../../modules/mailer/server-java') project(':contact').projectDir = new File('../../modules/contact/server-java') project(':upload').projectDir = new File('../../modules/upload/server-java') project(':post').projectDir = new File('../../modules/post/server-java') +project(':chat').projectDir = new File('../../modules/chat/server-java') From 31ee83c018e4376cea80bb09adf9aa1713083365 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 25 Jan 2021 19:06:14 +0200 Subject: [PATCH 61/85] updated java version to 14 --- packages/server-java/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index 2a81f4e1c7..dd2151565d 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -20,7 +20,7 @@ subprojects { group = 'com.sysgears' version = 'release' - sourceCompatibility = 11 + sourceCompatibility = 14 jar { enabled = true From d862f97a571c4cb29bf78a7fc291bfccbca1034b Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 25 Jan 2021 19:33:01 +0200 Subject: [PATCH 62/85] added pub-sub for chat module --- .../resolvers/MessageSubscriptionResolver.java | 11 +++++++---- .../chat/subscription/MessagePubSubService.java | 8 ++++++++ .../chat/subscription/MessageUpdatedEvent.java | 14 ++++++++++++++ .../com/sysgears/chat/subscription/Mutation.java | 5 +++++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessagePubSubService.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessageUpdatedEvent.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/Mutation.java diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java index 77e30cd5b8..bf10430602 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageSubscriptionResolver.java @@ -1,6 +1,8 @@ package com.sysgears.chat.resolvers; import com.sysgears.chat.dto.subscription.UpdateMessagesPayload; +import com.sysgears.chat.subscription.MessageUpdatedEvent; +import com.sysgears.core.subscription.Subscriber; import graphql.kickstart.tools.GraphQLSubscriptionResolver; import lombok.RequiredArgsConstructor; import org.reactivestreams.Publisher; @@ -11,9 +13,10 @@ @Component @RequiredArgsConstructor public class MessageSubscriptionResolver implements GraphQLSubscriptionResolver { + private final Subscriber messageSubscriber; - public CompletableFuture> messagesUpdated(Integer endCursor) { - //todo implement - return null; - } + public CompletableFuture> messagesUpdated(Integer endCursor) { + return CompletableFuture.supplyAsync(() -> messageSubscriber.subscribe(e -> e.getMessage().getId() <= endCursor) + .map(event -> new UpdateMessagesPayload(event.getMutation().name(), event.getMessage().getId(), event.getMessage()))); + } } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessagePubSubService.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessagePubSubService.java new file mode 100644 index 0000000000..9c7e474bf4 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessagePubSubService.java @@ -0,0 +1,8 @@ +package com.sysgears.chat.subscription; + +import com.sysgears.core.subscription.AbstractPubSubService; +import org.springframework.stereotype.Component; + +@Component +public class MessagePubSubService extends AbstractPubSubService { +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessageUpdatedEvent.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessageUpdatedEvent.java new file mode 100644 index 0000000000..66366761e8 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/MessageUpdatedEvent.java @@ -0,0 +1,14 @@ +package com.sysgears.chat.subscription; + +import com.sysgears.chat.dto.MessagePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MessageUpdatedEvent { + private Mutation mutation; + private MessagePayload message; +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/Mutation.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/Mutation.java new file mode 100644 index 0000000000..25cb0255b9 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/Mutation.java @@ -0,0 +1,5 @@ +package com.sysgears.chat.subscription; + +public enum Mutation { + DELETED, UPDATED, CREATED +} From 80fa7d081c7d98eed5fd38c54e8a7a5be2711fb7 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 25 Jan 2021 19:35:36 +0200 Subject: [PATCH 63/85] refactored upload module --- .../resolvers/UploadMutationResolver.java | 15 +++------ .../sysgears/upload/service/FileService.java | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 modules/upload/server-java/src/main/java/com/sysgears/upload/service/FileService.java diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java index e73629e17a..b3f18da752 100644 --- a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java @@ -1,9 +1,6 @@ package com.sysgears.upload.resolvers; -import com.sysgears.upload.exception.FileNotFoundException; -import com.sysgears.upload.file.FileStorage; -import com.sysgears.upload.model.FileMetadata; -import com.sysgears.upload.repository.FileMetadataRepository; +import com.sysgears.upload.service.FileService; import graphql.kickstart.tools.GraphQLMutationResolver; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -14,15 +11,11 @@ @Component @RequiredArgsConstructor public class UploadMutationResolver implements GraphQLMutationResolver { - private final FileMetadataRepository fileMetadataRepository; - private final FileStorage fileStorage; + private final FileService fileService; public boolean uploadFiles(List files) { - for (Part part : files) { - String fileName = part.getSubmittedFileName().replace(" ", "_"); - String path = fileStorage.writeFile(fileName, part); - - fileMetadataRepository.save(new FileMetadata(fileName, part.getContentType(), part.getSize(), path)); + for (Part file : files) { + fileService.create(file); } return true; diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/service/FileService.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/service/FileService.java new file mode 100644 index 0000000000..64a4327522 --- /dev/null +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/service/FileService.java @@ -0,0 +1,33 @@ +package com.sysgears.upload.service; + +import com.sysgears.upload.exception.FileNotFoundException; +import com.sysgears.upload.file.FileStorage; +import com.sysgears.upload.model.FileMetadata; +import com.sysgears.upload.repository.FileMetadataRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.Part; + +@Service +@Transactional +@RequiredArgsConstructor +public class FileService { + private final FileMetadataRepository fileMetadataRepository; + private final FileStorage fileStorage; + + public FileMetadata create(Part file) { + String fileName = file.getSubmittedFileName().replace(" ", "_"); + String path = fileStorage.writeFile(fileName, file); + + return fileMetadataRepository.save(new FileMetadata(fileName, file.getContentType(), file.getSize(), path)); + } + + public void deleteById(Integer id) { + FileMetadata fileMetadata = fileMetadataRepository.findById(id).orElseThrow(()-> new FileNotFoundException(id)); + + fileStorage.deleteFile(fileMetadata.getPath()); + fileMetadataRepository.deleteById(id); + } +} From 01967fb43ba74cc4c102f6ddb544783ae4f35916 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 25 Jan 2021 19:36:23 +0200 Subject: [PATCH 64/85] use delete method from service for upload module --- .../sysgears/upload/resolvers/UploadMutationResolver.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java index b3f18da752..124344f82a 100644 --- a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadMutationResolver.java @@ -22,10 +22,7 @@ public boolean uploadFiles(List files) { } public boolean removeFile(Integer id) { - FileMetadata fileMetadata = fileMetadataRepository.findById(id).orElseThrow(()-> new FileNotFoundException(id)); - - fileStorage.deleteFile(fileMetadata.getPath()); - fileMetadataRepository.deleteById(id); + fileService.deleteById(id); return true; } From 654b554e4473daa0696d633b173c8ba8a633285c Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 25 Jan 2021 19:36:35 +0200 Subject: [PATCH 65/85] implemented mutation and query resolvers --- .../sysgears/chat/config/JacksonConfig.java | 32 +++++++ .../com/sysgears/chat/dto/MessagePayload.java | 25 ++++++ .../com/sysgears/chat/dto/QuotedMessage.java | 12 +-- .../chat/dto/input/AddMessageInput.java | 14 ++-- .../exception/MessageNotFoundException.java | 8 ++ .../java/com/sysgears/chat/model/Message.java | 2 +- .../chat/repository/MessageRepository.java | 5 ++ .../resolvers/MessageMutationResolver.java | 84 ++++++++++++++++--- .../chat/resolvers/MessageQueryResolver.java | 5 +- .../sysgears/chat/service/MessageService.java | 53 +++++++----- 10 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/config/JacksonConfig.java create mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/exception/MessageNotFoundException.java diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/config/JacksonConfig.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/config/JacksonConfig.java new file mode 100644 index 0000000000..e205b7134f --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/config/JacksonConfig.java @@ -0,0 +1,32 @@ +package com.sysgears.chat.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.http.Part; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + SimpleModule module = new SimpleModule(); + module.addDeserializer(Part.class, new PartDeserializer()); + objectMapper.registerModule(module); + return objectMapper; + } + + // Mock deserializer for Part to use Part in DTO as in graphQL input + public static class PartDeserializer extends JsonDeserializer { + @Override + public Part deserialize(JsonParser p, DeserializationContext ctxt) { + return null; + } + } +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java index e959593e5f..69e49dbf5b 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java @@ -1,5 +1,6 @@ package com.sysgears.chat.dto; +import com.sysgears.chat.model.Message; import lombok.Data; import org.springframework.lang.NonNull; @@ -17,4 +18,28 @@ public class MessagePayload { private final String path; private final QuotedMessage quotedMessage; + public static MessagePayload from(Message message) { + QuotedMessage quotedMessage = new QuotedMessage(); + if (message.getQuoted() != null) { + quotedMessage.setId(message.getQuoted().getId()); + quotedMessage.setText(message.getQuoted().getText()); + quotedMessage.setUsername(message.getQuoted().getUsername()); + if (message.getQuoted().getAttachment() != null) { + quotedMessage.setFilename(message.getQuoted().getAttachment().getName()); + quotedMessage.setPath(message.getQuoted().getAttachment().getPath()); + } + } + return new MessagePayload( + message.getId(), + message.getText(), + message.getUserId(), + message.getCreatedAt().toString(), + message.getUsername(), + message.getUuid().toString(), + quotedMessage.getId(), + message.getAttachment() != null ? message.getAttachment().getName() : null, + message.getAttachment() != null ? message.getAttachment().getPath() : null, + quotedMessage + ); + } } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java index 9efffab047..0380032605 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/QuotedMessage.java @@ -1,12 +1,14 @@ package com.sysgears.chat.dto; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor public class QuotedMessage { - private final Integer id; - private final String text; - private final String username; - private final String filename; - private final String path; + private Integer id; + private String text; + private String username; + private String filename; + private String path; } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java index 8155b743b8..352e72fdaa 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/AddMessageInput.java @@ -1,14 +1,16 @@ package com.sysgears.chat.dto.input; -import lombok.Data; +import lombok.*; import javax.servlet.http.Part; @Data +@NoArgsConstructor +@AllArgsConstructor public class AddMessageInput { - private final String text; - private final Integer userId; - private final String uuid; - private final Integer quotedId; - private final Part attachment; + private String text; + private Integer userId; + private String uuid; + private Integer quotedId; + private Part attachment; } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/exception/MessageNotFoundException.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/exception/MessageNotFoundException.java new file mode 100644 index 0000000000..ca3fdbdd04 --- /dev/null +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/exception/MessageNotFoundException.java @@ -0,0 +1,8 @@ +package com.sysgears.chat.exception; + +public class MessageNotFoundException extends RuntimeException { + + public MessageNotFoundException(Integer id) { + super(String.format("Message with id %d not found", id)); + } +} diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java index ffeea85f55..ae7b28ffdd 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java @@ -31,7 +31,7 @@ public class Message { @CreatedDate @Column(name = "CREATED_AT", updatable = false) - private Instant createdAt; + private final Instant createdAt = Instant.now(); @Column(name = "USERNAME") private String username; diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java index ae62022223..4f897e86a1 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/repository/MessageRepository.java @@ -2,6 +2,11 @@ import com.sysgears.chat.model.Message; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.scheduling.annotation.Async; + +import java.util.concurrent.CompletableFuture; public interface MessageRepository extends JpaRepository { + @Async + CompletableFuture findMessageById(Integer id); } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java index 14f8a05615..ced9ccc37b 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java @@ -3,28 +3,88 @@ import com.sysgears.chat.dto.MessagePayload; import com.sysgears.chat.dto.input.AddMessageInput; import com.sysgears.chat.dto.input.EditMessageInput; +import com.sysgears.chat.model.Message; +import com.sysgears.chat.service.MessageService; +import com.sysgears.chat.subscription.MessageUpdatedEvent; +import com.sysgears.chat.subscription.Mutation; +import com.sysgears.core.subscription.Publisher; +import com.sysgears.upload.model.FileMetadata; +import com.sysgears.upload.service.FileService; import graphql.kickstart.tools.GraphQLMutationResolver; +import graphql.schema.DataFetchingEnvironment; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import javax.servlet.http.Part; +import java.util.LinkedHashMap; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; @Component @RequiredArgsConstructor public class MessageMutationResolver implements GraphQLMutationResolver { + private final MessageService messageService; + private final FileService fileService; + private final Publisher messagePublisher; - public CompletableFuture addMessage(AddMessageInput input) { - //todo implement - return null; - } - public CompletableFuture deleteMessage(Integer id) { - //todo implement - return null; - } + public CompletableFuture addMessage(AddMessageInput input, DataFetchingEnvironment environment) { + return CompletableFuture.supplyAsync(() -> { + Message message = new Message(); - public CompletableFuture editMessage(EditMessageInput input) { - //todo implement - return null; - } + if (input.getQuotedId() != null) { + Message quotedMessage = messageService.findById(input.getQuotedId()); + message.setQuoted(quotedMessage); + } + + message.setText(input.getText()); + message.setUserId(input.getUserId()); + message.setUuid(UUID.fromString(input.getUuid())); + + resolveAttachment(environment).ifPresent(attachment -> { + FileMetadata fileMetadata = fileService.create(attachment); + message.setAttachment(fileMetadata); + }); + + MessagePayload created = messageService.create(message); + + messagePublisher.publish(new MessageUpdatedEvent(Mutation.CREATED, created)); + + return created; + }); + } + + public CompletableFuture deleteMessage(Integer id) { + return CompletableFuture.supplyAsync(() -> { + final Message deleted = messageService.deleteById(id); + if (deleted.getAttachment() != null) { + fileService.deleteById(deleted.getAttachment().getId()); + } + + final MessagePayload messagePayload = MessagePayload.from(deleted); + messagePublisher.publish(new MessageUpdatedEvent(Mutation.DELETED, messagePayload)); + + return messagePayload; + }); + } + + public CompletableFuture editMessage(EditMessageInput input) { + return CompletableFuture.supplyAsync(() -> { + final Message message = messageService.findById(input.getId()); + message.setText(input.getText()); + message.setUserId(input.getUserId()); + + final MessagePayload updated = messageService.update(message); + + messagePublisher.publish(new MessageUpdatedEvent(Mutation.UPDATED, updated)); + + return updated; + }); + } + + private Optional resolveAttachment(DataFetchingEnvironment environment) { + final LinkedHashMap inputMap = environment.getArgument("input"); + return Optional.ofNullable((Part) inputMap.get("attachment")); + } } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java index 6fed0b7308..c2765f78a3 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageQueryResolver.java @@ -20,7 +20,7 @@ public class MessageQueryResolver implements GraphQLQueryResolver { public CompletableFuture messages(Optional limit, Optional after) { return CompletableFuture.supplyAsync(() -> { int offset = after.orElse(0); - int size = limit.orElse(10); + int size = limit.orElse(50); Pageable pageRequest = new OffsetPageRequest(offset, size); return messageService.findAll(pageRequest); @@ -28,7 +28,6 @@ public CompletableFuture messages(Optional limit, Optional message(Integer id) { - //todo implement - return null; + return messageService.getById(id); } } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java index dc3a29f57e..2fbb1223f9 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/service/MessageService.java @@ -1,6 +1,7 @@ package com.sysgears.chat.service; import com.sysgears.chat.dto.*; +import com.sysgears.chat.exception.MessageNotFoundException; import com.sysgears.chat.model.Message; import com.sysgears.chat.repository.MessageRepository; import lombok.RequiredArgsConstructor; @@ -11,6 +12,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; @Service @RequiredArgsConstructor @@ -30,7 +32,7 @@ public Messages findAll(Pageable pageable) { messageEdgesList.add( MessageEdges.builder() .cursor((int) (pageable.getOffset() + i)) - .node(convert(message)) + .node(MessagePayload.from(message)) .build() ); } @@ -47,25 +49,34 @@ public Messages findAll(Pageable pageable) { .build(); } - private MessagePayload convert(Message message) { - QuotedMessage quotedMessage = new QuotedMessage( - message.getQuoted().getId(), - message.getQuoted().getText(), - message.getQuoted().getUsername(), - message.getQuoted().getAttachment().getName(), - message.getQuoted().getAttachment().getPath() - ); - return new MessagePayload( - message.getId(), - message.getText(), - message.getUserId(), - message.getCreatedAt().toString(), - message.getUsername(), - message.getUuid().toString(), - quotedMessage.getId(), - message.getAttachment().getName(), - message.getAttachment().getPath(), - quotedMessage - ); + @Transactional(readOnly = true) + public CompletableFuture getById(Integer id) { + return repository.findMessageById(id).thenApply(message -> { + if (message == null) throw new MessageNotFoundException(id); + + return MessagePayload.from(message); + }); + } + + @Transactional(readOnly = true) + public Message findById(Integer id) { + return repository.findById(id).orElseThrow(() -> new MessageNotFoundException(id)); + } + + public MessagePayload create(Message message) { + final Message created = repository.save(message); + return MessagePayload.from(created); + } + + public MessagePayload update(Message message) { + final Message created = repository.save(message); + return MessagePayload.from(created); + } + + public Message deleteById(Integer id) { + final Message message = findById(id); + repository.delete(message); + + return message; } } From 355ddbdf84b8bcce94828cc8e2c2b2c11ed8c874 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 25 Jan 2021 19:37:15 +0200 Subject: [PATCH 66/85] delete gitkeep --- .../src/main/java/com/sysgears/chat/subscription/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/.gitkeep diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/.gitkeep b/modules/chat/server-java/src/main/java/com/sysgears/chat/subscription/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 From f1c397b9f79b4f9480f1e4b60e684cab269b186c Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 26 Jan 2021 13:55:18 +0200 Subject: [PATCH 67/85] upgraded switch expression --- .../resolvers/UserSubscriptionResolver.java | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java index 4f0877c46a..c310fff02c 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserSubscriptionResolver.java @@ -9,7 +9,6 @@ import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -24,24 +23,18 @@ public CompletableFuture> usersUpdated(Optional subscriber.subscribe(event -> { if (filter.isEmpty()) return true; - switch (event.getMutation()) { - case DELETE_USER: - return true; - - case ADD_USER: - case EDIT_USER: - return filter.get().getIsActive().map(isActive -> - event.getUser().getIsActive().equals(isActive)).orElse(true) && - filter.get().getRole().map(role -> - event.getUser().getRole().toLowerCase().equals(role.toLowerCase())).orElse(true) && - (filter.get().getSearchText().map(searchText -> - event.getUser().getEmail().toLowerCase().equals(searchText.toLowerCase())).orElse(true) || - filter.get().getSearchText().map(searchText -> - event.getUser().getUsername().toLowerCase().equals(searchText.toLowerCase())).orElse(true)); - - default: - return false; - } + return switch (event.getMutation()) { + case DELETE_USER -> true; + case ADD_USER, EDIT_USER -> filter.get().getIsActive().map(isActive -> + event.getUser().getIsActive().equals(isActive)).orElse(true) && + filter.get().getRole().map(role -> + event.getUser().getRole().equalsIgnoreCase(role)).orElse(true) && + (filter.get().getSearchText().map(searchText -> + event.getUser().getEmail().equalsIgnoreCase(searchText)).orElse(true) || + filter.get().getSearchText().map(searchText -> + event.getUser().getUsername().equalsIgnoreCase(searchText)).orElse(true)); + default -> false; + }; }).map(event -> new UpdateUserPayload(event.getMutation().getOperation(), event.getUser()))); } } From f5cb54423863da884f9c8480b9e5fb2f3b17cd60 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 26 Jan 2021 13:56:11 +0200 Subject: [PATCH 68/85] updated lombok configuration for input DTOs --- .../chat/dto/input/EditMessageInput.java | 10 +++++++--- .../sysgears/contact/dto/ContactInput.java | 8 +++++--- .../post/dto/input/AddCommentInput.java | 8 ++++++-- .../sysgears/post/dto/input/AddPostInput.java | 8 ++++++-- .../post/dto/input/DeleteCommentInput.java | 8 ++++++-- .../post/dto/input/EditCommentInput.java | 10 +++++++--- .../post/dto/input/EditPostInput.java | 10 +++++++--- .../sysgears/user/dto/input/AddUserInput.java | 18 ++++++++++------- .../user/dto/input/EditUserInput.java | 20 +++++++++++-------- .../user/dto/input/FilterUserInput.java | 9 ++++++--- .../user/dto/input/LoginUserInput.java | 8 +++++--- .../user/dto/input/OrderByUserInput.java | 6 ++++-- .../sysgears/user/dto/input/ProfileInput.java | 6 ++++-- .../user/dto/input/RegisterUserInput.java | 10 +++++++--- .../user/dto/input/ResetPasswordInput.java | 10 +++++++--- .../user/dto/input/auth/AuthInput.java | 12 ++++++----- 16 files changed, 107 insertions(+), 54 deletions(-) diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java index 8a34a7f53a..e32c94221c 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/input/EditMessageInput.java @@ -1,12 +1,16 @@ package com.sysgears.chat.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class EditMessageInput { @NonNull - private final Integer id; - private final String text; - private final Integer userId; + private Integer id; + private String text; + private Integer userId; } diff --git a/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java b/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java index fb2e60f08e..bcd98a0ba1 100644 --- a/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java +++ b/modules/contact/server-java/src/main/java/com/sysgears/contact/dto/ContactInput.java @@ -6,11 +6,13 @@ import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class ContactInput { @NonNull - private final String name; + private String name; @NonNull - private final String email; + private String email; @NonNull - private final String content; + private String content; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java index 90ebc8f6b3..35b5a50fe6 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddCommentInput.java @@ -1,12 +1,16 @@ package com.sysgears.post.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class AddCommentInput { @NonNull - private final Integer postId; + private Integer postId; @NonNull - private final String content; + private String content; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java index 5b612a8952..528288e83d 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/AddPostInput.java @@ -1,12 +1,16 @@ package com.sysgears.post.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class AddPostInput { @NonNull - private final String title; + private String title; @NonNull - private final String content; + private String content; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java index 00eb6530bd..4e0c18ace3 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/DeleteCommentInput.java @@ -1,12 +1,16 @@ package com.sysgears.post.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class DeleteCommentInput { @NonNull - private final Integer id; + private Integer id; @NonNull - private final Integer postId; + private Integer postId; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java index 2149d955d2..da685504b4 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditCommentInput.java @@ -1,14 +1,18 @@ package com.sysgears.post.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class EditCommentInput { @NonNull - private final Integer id; + private Integer id; @NonNull - private final Integer postId; + private Integer postId; @NonNull - private final String content; + private String content; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java index 05befbb9cc..d8ec1e4c6d 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/input/EditPostInput.java @@ -1,14 +1,18 @@ package com.sysgears.post.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class EditPostInput { @NonNull - private final Integer id; + private Integer id; @NonNull - private final String title; + private String title; @NonNull - private final String content; + private String content; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java index d1e079c3c6..591bfe7568 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java @@ -1,22 +1,26 @@ package com.sysgears.user.dto.input; import com.sysgears.user.dto.input.auth.AuthInput; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; import java.util.Optional; @Data +@NoArgsConstructor +@AllArgsConstructor public class AddUserInput { @NonNull - private final String username; + private String username; @NonNull - private final String password; + private String password; @NonNull - private final String role; - private final boolean isActive; + private String role; + private boolean isActive; @NonNull - private final String email; - private final Optional profile; - private final Optional auth; + private String email; + private final Optional profile = Optional.empty(); + private final Optional auth = Optional.empty(); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java index c376ebe651..0ba83354ae 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/EditUserInput.java @@ -1,23 +1,27 @@ package com.sysgears.user.dto.input; import com.sysgears.user.dto.input.auth.AuthInput; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; import java.util.Optional; @Data +@NoArgsConstructor +@AllArgsConstructor public class EditUserInput { @NonNull - private final int id; + private int id; @NonNull - private final String username; + private String username; @NonNull - private final String role; - private final Optional isActive; + private String role; @NonNull - private final String email; - private final Optional password; - private final Optional profile; - private final Optional auth; + private String email; + private final Optional isActive = Optional.empty(); + private final Optional password = Optional.empty(); + private final Optional profile = Optional.empty(); + private final Optional auth = Optional.empty(); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java index 90a8261cb5..c7446b27f4 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/FilterUserInput.java @@ -1,12 +1,15 @@ package com.sysgears.user.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Optional; @Data +@NoArgsConstructor public class FilterUserInput { - private final Optional searchText; - private final Optional role; - private final Optional isActive; + private final Optional searchText = Optional.empty(); + private final Optional role = Optional.empty(); + private final Optional isActive = Optional.empty(); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java index 83336753bb..dad5eaf822 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/LoginUserInput.java @@ -1,12 +1,14 @@ package com.sysgears.user.dto.input; -import lombok.Data; +import lombok.*; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class LoginUserInput { @NonNull - private final String usernameOrEmail; + private String usernameOrEmail; @NonNull - private final String password; + private String password; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java index f264be8c3e..9038202c0f 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/OrderByUserInput.java @@ -1,11 +1,13 @@ package com.sysgears.user.dto.input; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Optional; @Data +@NoArgsConstructor public class OrderByUserInput { - private final Optional column; - private final Optional order; + private final Optional column = Optional.empty(); + private final Optional order = Optional.empty(); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java index 77905b3259..3928088102 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ProfileInput.java @@ -1,11 +1,13 @@ package com.sysgears.user.dto.input; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Optional; @Data +@NoArgsConstructor public class ProfileInput { - private final Optional firstName; - private final Optional lastName; + private final Optional firstName = Optional.empty(); + private final Optional lastName = Optional.empty(); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java index 0d224fee60..23822f69ab 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/RegisterUserInput.java @@ -1,14 +1,18 @@ package com.sysgears.user.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class RegisterUserInput { @NonNull - private final String username; + private String username; @NonNull - private final String email; + private String email; @NonNull - private final String password; + private String password; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java index c08e361b14..d17b498b5b 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/ResetPasswordInput.java @@ -1,14 +1,18 @@ package com.sysgears.user.dto.input; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class ResetPasswordInput { @NonNull - private final String token; + private String token; @NonNull - private final String password; + private String password; @NonNull - private final String passwordConfirmation; + private String passwordConfirmation; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java index 52bb73611f..d0b5335009 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/auth/AuthInput.java @@ -1,14 +1,16 @@ package com.sysgears.user.dto.input.auth; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Optional; @Data +@NoArgsConstructor public class AuthInput { - private final Optional certificate; - private final Optional facebook; - private final Optional google; - private final Optional github; - private final Optional linkedin; + private final Optional certificate = Optional.empty(); + private final Optional facebook = Optional.empty(); + private final Optional google = Optional.empty(); + private final Optional github = Optional.empty(); + private final Optional linkedin = Optional.empty(); } From 412d066779ab541e8b9e95a1a7041c1cd44942af Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 26 Jan 2021 14:52:20 +0200 Subject: [PATCH 69/85] fixed issue connected with adding new active user --- .../src/main/java/com/sysgears/user/dto/input/AddUserInput.java | 2 +- .../java/com/sysgears/user/resolvers/UserMutationResolver.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java index 591bfe7568..99e5dc3ce3 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/input/AddUserInput.java @@ -18,9 +18,9 @@ public class AddUserInput { private String password; @NonNull private String role; - private boolean isActive; @NonNull private String email; + private final Boolean isActive = false; private final Optional profile = Optional.empty(); private final Optional auth = Optional.empty(); } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index 40ffb7260a..ae30d4b538 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -34,7 +34,7 @@ public CompletableFuture addUser(AddUserInput input) { input.getUsername(), passwordEncoder.encode(input.getPassword()), input.getRole(), - input.isActive(), + input.getIsActive(), input.getEmail() ); input.getProfile().map(profileInput -> From 680625b9d6dba4b12a6a1ca50a0a832255742de9 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 26 Jan 2021 15:40:41 +0200 Subject: [PATCH 70/85] refactoring resolvers method for user and upload modules --- .../upload/resolvers/UploadQueryResolver.java | 27 ++++++----- .../user/resolvers/UserMutationResolver.java | 45 ++++++++++--------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java index 757b5cb6cb..7cfbf5c30f 100644 --- a/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/resolvers/UploadQueryResolver.java @@ -7,22 +7,25 @@ import org.springframework.stereotype.Component; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @Component @RequiredArgsConstructor public class UploadQueryResolver implements GraphQLQueryResolver { - private final FileMetadataRepository fileMetadataRepository; + private final FileMetadataRepository fileMetadataRepository; - public List files() { - return fileMetadataRepository.findAll().stream() - .map(file -> new File( - file.getId(), - file.getName(), - file.getContentType(), - file.getSize(), - file.getPath() - )) - .collect(Collectors.toList()); - } + public CompletableFuture> files() { + return CompletableFuture.supplyAsync(() -> + fileMetadataRepository.findAll().stream() + .map(file -> new File( + file.getId(), + file.getName(), + file.getContentType(), + file.getSize(), + file.getPath() + )) + .collect(Collectors.toList()) + ); + } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index ae30d4b538..fb278d8647 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -30,28 +30,29 @@ public class UserMutationResolver implements GraphQLMutationResolver { private final PasswordEncoder passwordEncoder; public CompletableFuture addUser(AddUserInput input) { - User user = new User( - input.getUsername(), - passwordEncoder.encode(input.getPassword()), - input.getRole(), - input.getIsActive(), - input.getEmail() - ); - input.getProfile().map(profileInput -> - new UserProfile( - profileInput.getFirstName().orElse(""), - profileInput.getLastName().orElse("") - ) - ).ifPresent(user::setProfile); - - input.getAuth() - .map(this::from) - .ifPresent(user::setAuth); - userService.save(user); - - publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.ADD_USER, user)); - - return CompletableFuture.completedFuture(new UserPayload(user)); + return CompletableFuture.supplyAsync(()-> { + User user = new User( + input.getUsername(), + passwordEncoder.encode(input.getPassword()), + input.getRole(), + input.getIsActive(), + input.getEmail() + ); + input.getProfile().map(profileInput -> + new UserProfile( + profileInput.getFirstName().orElse(""), + profileInput.getLastName().orElse("") + ) + ).ifPresent(user::setProfile); + + input.getAuth() + .map(this::from) + .ifPresent(user::setAuth); + userService.save(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.ADD_USER, user)); + return new UserPayload(user); + }); } public CompletableFuture editUser(EditUserInput input) { From 321feb67107de93b3ad9b9f5a0a04d192a768ae1 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 26 Jan 2021 19:30:00 +0200 Subject: [PATCH 71/85] implemented reports module --- modules/reports/server-java/build.gradle | 0 .../sysgears/reports/ReportDBInitializer.java | 39 +++++++++++++++++++ .../sysgears/reports/dto/ReportPayload.java | 20 ++++++++++ .../com/sysgears/reports/model/Report.java | 33 ++++++++++++++++ .../reports/repository/ReportRepository.java | 7 ++++ .../resolvers/ReportQueryResolver.java | 20 ++++++++++ .../src/main/resources/schema.graphqls | 12 ++++++ packages/server-java/build.gradle | 1 + packages/server-java/settings.gradle | 3 +- 9 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 modules/reports/server-java/build.gradle create mode 100644 modules/reports/server-java/src/main/java/com/sysgears/reports/ReportDBInitializer.java create mode 100644 modules/reports/server-java/src/main/java/com/sysgears/reports/dto/ReportPayload.java create mode 100644 modules/reports/server-java/src/main/java/com/sysgears/reports/model/Report.java create mode 100644 modules/reports/server-java/src/main/java/com/sysgears/reports/repository/ReportRepository.java create mode 100644 modules/reports/server-java/src/main/java/com/sysgears/reports/resolvers/ReportQueryResolver.java create mode 100644 modules/reports/server-java/src/main/resources/schema.graphqls diff --git a/modules/reports/server-java/build.gradle b/modules/reports/server-java/build.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/reports/server-java/src/main/java/com/sysgears/reports/ReportDBInitializer.java b/modules/reports/server-java/src/main/java/com/sysgears/reports/ReportDBInitializer.java new file mode 100644 index 0000000000..fb9356676d --- /dev/null +++ b/modules/reports/server-java/src/main/java/com/sysgears/reports/ReportDBInitializer.java @@ -0,0 +1,39 @@ +package com.sysgears.reports; + +import com.sysgears.reports.model.Report; +import com.sysgears.reports.repository.ReportRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Component +@RequiredArgsConstructor +public class ReportDBInitializer { + private final ReportRepository repository; + + @EventListener + public void onApplicationStartedEvent(ApplicationStartedEvent event) { + long count = repository.count(); + if (count == 0) { + Report tomJacksonReport = new Report("Tom Jackson", "555-444-333", "tom@gmail.com"); + Report mikeJamesReport = new Report("Mike James", "555-777-888", "mikejames@gmail.com"); + Report janetLarsonReport = new Report("Janet Larson", "555-222-111", "janetlarson@gmail.com"); + Report clarkThompsonReport = new Report("Clark Thompson", "555-444-333", "clark123@gmail.com"); + Report emmaPageReport = new Report("Emma Page", "555-444-333", "emma1page@gmail.com"); + + repository.saveAll( + List.of( + tomJacksonReport, + mikeJamesReport, + janetLarsonReport, + clarkThompsonReport, + emmaPageReport + ) + ); + } + } +} diff --git a/modules/reports/server-java/src/main/java/com/sysgears/reports/dto/ReportPayload.java b/modules/reports/server-java/src/main/java/com/sysgears/reports/dto/ReportPayload.java new file mode 100644 index 0000000000..c6ebca47b4 --- /dev/null +++ b/modules/reports/server-java/src/main/java/com/sysgears/reports/dto/ReportPayload.java @@ -0,0 +1,20 @@ +package com.sysgears.reports.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.lang.NonNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ReportPayload { + @NonNull + private Integer id; + @NonNull + private String name; + @NonNull + private String phone; + @NonNull + private String email; +} diff --git a/modules/reports/server-java/src/main/java/com/sysgears/reports/model/Report.java b/modules/reports/server-java/src/main/java/com/sysgears/reports/model/Report.java new file mode 100644 index 0000000000..5c48651ae9 --- /dev/null +++ b/modules/reports/server-java/src/main/java/com/sysgears/reports/model/Report.java @@ -0,0 +1,33 @@ +package com.sysgears.reports.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "REPORT") +public class Report { + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private int id; + + @Column(name = "NAME", nullable = false) + private String name; + + @Column(name = "PHONE", nullable = false) + private String phone; + + @Column(name = "EMAIL", nullable = false) + private String email; + + public Report(String name, String phone, String email) { + this.name = name; + this.phone = phone; + this.email = email; + } +} diff --git a/modules/reports/server-java/src/main/java/com/sysgears/reports/repository/ReportRepository.java b/modules/reports/server-java/src/main/java/com/sysgears/reports/repository/ReportRepository.java new file mode 100644 index 0000000000..5aa80d8ac8 --- /dev/null +++ b/modules/reports/server-java/src/main/java/com/sysgears/reports/repository/ReportRepository.java @@ -0,0 +1,7 @@ +package com.sysgears.reports.repository; + +import com.sysgears.reports.model.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { +} diff --git a/modules/reports/server-java/src/main/java/com/sysgears/reports/resolvers/ReportQueryResolver.java b/modules/reports/server-java/src/main/java/com/sysgears/reports/resolvers/ReportQueryResolver.java new file mode 100644 index 0000000000..636aebaa22 --- /dev/null +++ b/modules/reports/server-java/src/main/java/com/sysgears/reports/resolvers/ReportQueryResolver.java @@ -0,0 +1,20 @@ +package com.sysgears.reports.resolvers; + +import com.sysgears.reports.model.Report; +import com.sysgears.reports.repository.ReportRepository; +import graphql.kickstart.tools.GraphQLQueryResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Component +@RequiredArgsConstructor +public class ReportQueryResolver implements GraphQLQueryResolver { + private final ReportRepository reportRepository; + + public CompletableFuture> report() { + return CompletableFuture.supplyAsync(reportRepository::findAll); + } +} diff --git a/modules/reports/server-java/src/main/resources/schema.graphqls b/modules/reports/server-java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..0362747df6 --- /dev/null +++ b/modules/reports/server-java/src/main/resources/schema.graphqls @@ -0,0 +1,12 @@ +# Report +type Report { + id: Int! + name: String! + phone: String! + email: String! +} + +extend type Query { + #Report + report: [Report] +} diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index dd2151565d..b44bd3ef5a 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -83,6 +83,7 @@ project(':app') { implementation project(':upload') implementation project(':post') implementation project(':chat') + implementation project(':reports') } bootJar { enabled = true diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index 1ee9325c73..1a8cf51fca 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post', 'chat' +include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post', 'chat', 'reports' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') @@ -10,3 +10,4 @@ project(':contact').projectDir = new File('../../modules/contact/server-java') project(':upload').projectDir = new File('../../modules/upload/server-java') project(':post').projectDir = new File('../../modules/post/server-java') project(':chat').projectDir = new File('../../modules/chat/server-java') +project(':reports').projectDir = new File('../../modules/reports/server-java') From 03b5a90fd509988546f3c6618f63649fabe71d7a Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 26 Jan 2021 19:30:16 +0200 Subject: [PATCH 72/85] added tests for user module --- .../authentication/model/jwt/Tokens.java | 8 +- .../com/sysgears/user/dto/AuthPayload.java | 8 +- .../com/sysgears/user/LoginMutationTest.java | 120 ++++++++++ .../com/sysgears/user/UserMutationTest.java | 51 ++++- .../java/com/sysgears/user/UserQueryTest.java | 215 ++++++++++-------- .../src/test/resources/mutation/login.graphql | 18 ++ 6 files changed, 312 insertions(+), 108 deletions(-) create mode 100644 modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java create mode 100644 modules/user/server-java/src/test/resources/mutation/login.graphql diff --git a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java index df9013e721..33e12a6b97 100644 --- a/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java +++ b/modules/authentication/server-java/src/main/java/com/sysgears/authentication/model/jwt/Tokens.java @@ -1,9 +1,13 @@ package com.sysgears.authentication.model.jwt; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class Tokens { - private final String accessToken; - private final String refreshToken; + private String accessToken; + private String refreshToken; } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java b/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java index 0ef1bd7cc2..2faa423f0e 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/dto/AuthPayload.java @@ -2,10 +2,14 @@ import com.sysgears.authentication.model.jwt.Tokens; import com.sysgears.user.model.User; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class AuthPayload { - private final User user; - private final Tokens tokens; + private User user; + private Tokens tokens; } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java new file mode 100644 index 0000000000..9d1dbf83e9 --- /dev/null +++ b/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java @@ -0,0 +1,120 @@ +package com.sysgears.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.mailer.service.EmailService; +import com.sysgears.user.dto.AuthPayload; +import com.sysgears.user.model.User; +import com.sysgears.user.repository.UserRepository; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional +@Disabled //todo figure out tests fails when run with coverage and when build project with gradle +public class LoginMutationTest { + @Autowired + private GraphQLTestTemplate template; + @MockBean + private EmailService emailService; + @Autowired + UserRepository userRepository; + + @Test + void login_with_username() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("usernameOrEmail", "user"); + node.put("password", "user1234"); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/login.graphql", input); + + assertTrue(response.isOk()); + System.err.println(response.getRawResponse().getBody()); + AuthPayload payload = response.get("$.data.login", AuthPayload.class); + + User user = payload.getUser(); + assertEquals("user", user.getUsername()); + assertEquals("user", user.getRole()); + assertTrue(user.getIsActive()); + assertEquals("user@example.com", user.getEmail()); + assertNull(user.getProfile()); + + assertNotNull(payload.getTokens().getAccessToken()); + assertNotNull(payload.getTokens().getRefreshToken()); + } + + @Test + void login_with_email() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("usernameOrEmail", "admin@example.com"); + node.put("password", "admin123"); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/login.graphql", input); + + assertTrue(response.isOk()); + AuthPayload payload = response.get("$.data.login", AuthPayload.class); + + User user = payload.getUser(); + assertEquals("admin", user.getUsername()); + assertEquals("admin", user.getRole()); + assertTrue(user.getIsActive()); + assertEquals("admin@example.com", user.getEmail()); + assertNull(user.getProfile()); + + assertNotNull(payload.getTokens().getAccessToken()); + assertNotNull(payload.getTokens().getRefreshToken()); + } + + @Test + void login_invalid_email() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("usernameOrEmail", "invalid_user"); + node.put("password", "supersecret"); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/login.graphql", input); + + assertTrue(response.isOk()); + assertEquals("Login failed.", response.get("$.errors[0].message")); + assertEquals("Please enter a valid username or e-mail.", response.get("$.errors[0].extensions.exception.errors.usernameOrEmail")); + } + + @Test + void login_invalid_password() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("usernameOrEmail", "admin"); + node.put("password", "supersecret"); + + input.set("input", node); + + GraphQLResponse response = template.perform("mutation/login.graphql", input); + + assertTrue(response.isOk()); + assertEquals("Login failed.", response.get("$.errors[0].message")); + assertEquals("Please enter a valid password.", response.get("$.errors[0].extensions.exception.errors.password")); + } +} diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java index d15bb145b2..48d0d5c243 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java @@ -4,33 +4,28 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.Application; import com.sysgears.user.dto.UserPayload; import com.sysgears.user.model.User; import com.sysgears.user.model.UserAuth; -import org.junit.jupiter.api.AfterEach; +import com.sysgears.user.model.UserProfile; +import com.sysgears.user.repository.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional public class UserMutationTest { @Autowired - GraphQLTestTemplate template; + private GraphQLTestTemplate template; @Autowired - UserDBInitializer userDBInitializer; - - @AfterEach - void initUser() { - userDBInitializer.onApplicationStartedEvent(mock(ApplicationStartedEvent.class)); - } + private UserRepository userRepository; @Test void addUser() throws IOException { @@ -131,6 +126,42 @@ void editUser() throws IOException { assertEquals("LinkedIn", userAuth.getLinkedin().getDisplayName()); } + @Test + void editUser_with_already_saved_profile_data() throws IOException { + final User user = userRepository.findById(1).get(); + user.setProfile(new UserProfile("James", "Abrams")); + userRepository.save(user); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + ObjectNode profile = mapper.createObjectNode(); + + node.put("id", 1); + node.put("username", "admin"); + node.put("role", "ADMIN"); + node.put("isActive", true); + node.put("email", "admin@sysgears.com"); + + profile.put("firstName", "John"); + profile.put("lastName", "Sinna"); + node.set("profile", profile); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/edit-user.graphql", input); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.editUser", UserPayload.class); + User createdUser = payload.getUser(); + + assertEquals(1, createdUser.getId()); + assertEquals("admin", createdUser.getUsername()); + assertEquals("ADMIN", createdUser.getRole()); + assertTrue(createdUser.getIsActive()); + assertEquals("admin@sysgears.com", createdUser.getEmail()); + assertEquals("John Sinna", createdUser.getProfile().getFullName()); + } + @Test void deleteUser() throws IOException { ObjectMapper mapper = new ObjectMapper(); diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java index 412b1b48f1..7194d09387 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.Application; import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.UserPayload; @@ -32,98 +33,124 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional public class UserQueryTest { - @Autowired - GraphQLTestTemplate template; - @MockBean - private UserRepository repository; - - private User user; - private final CertificateAuth certificateAuth = new CertificateAuth(UUID.randomUUID().toString()); - private final FacebookAuth facebookAuth = new FacebookAuth(UUID.randomUUID().toString(), "facebookAuthName"); - private final GithubAuth githubAuth = new GithubAuth(UUID.randomUUID().toString(), "githubAuthName"); - private final GoogleAuth googleAuth = new GoogleAuth(UUID.randomUUID().toString(), "googleAuthName"); - private final LinkedInAuth linkedInAuth = new LinkedInAuth(UUID.randomUUID().toString(), "linkedInAuthName"); - - @BeforeEach - void init() { - user = new User("admin", "pass", "ADMIN", true, "example@sysgears.com"); - user.setProfile(new UserProfile("John", "Smith")); - user.setAuth( - UserAuth.builder() - .certificate(certificateAuth) - .facebook(facebookAuth) - .github(githubAuth) - .google(googleAuth) - .linkedin(linkedInAuth) - .build() - ); - } - - @Test - void users() throws IOException { - when(repository.findByCriteria(Optional.empty(), Optional.empty())) - .thenReturn(CompletableFuture.completedFuture(Collections.singletonList(user))); - - GraphQLResponse response = template.postForResource("query/users.graphql"); - - assertTrue(response.isOk()); - User actualUser = response.get("$.data.users[0]", User.class); - assertThat(actualUser) - .hasFieldOrPropertyWithValue("username", "admin") - .hasFieldOrPropertyWithValue("role", "ADMIN") - .hasFieldOrPropertyWithValue("email", "example@sysgears.com") - .hasFieldOrPropertyWithValue("isActive", true) - .hasFieldOrPropertyWithValue("profile.firstName", "John") - .hasFieldOrPropertyWithValue("profile.lastName", "Smith") - .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); - assertNotNull(actualUser.getAuth()); - assertEquals(certificateAuth, actualUser.getAuth().getCertificate()); - assertEquals(facebookAuth, actualUser.getAuth().getFacebook()); - assertEquals(githubAuth, actualUser.getAuth().getGithub()); - assertEquals(googleAuth, actualUser.getAuth().getGoogle()); - assertEquals(linkedInAuth, actualUser.getAuth().getLinkedin()); - } - - @Test - void user() throws IOException { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode node = mapper.createObjectNode(); - node.put("id", 1); - - when(repository.findUserById(1)) - .thenReturn(CompletableFuture.completedFuture(user)); - - GraphQLResponse response = template.perform("query/user.graphql", node); - - assertTrue(response.isOk()); - UserPayload payload = response.get("$.data.user", UserPayload.class); - User actualUser = payload.getUser(); - assertThat(actualUser) - .hasFieldOrPropertyWithValue("username", "admin") - .hasFieldOrPropertyWithValue("role", "ADMIN") - .hasFieldOrPropertyWithValue("email", "example@sysgears.com") - .hasFieldOrPropertyWithValue("isActive", true) - .hasFieldOrPropertyWithValue("profile.firstName", "John") - .hasFieldOrPropertyWithValue("profile.lastName", "Smith") - .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); - assertNotNull(actualUser.getAuth()); - assertEquals(certificateAuth, actualUser.getAuth().getCertificate()); - } - - @Test - void currentUser() throws IOException { - SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); - - GraphQLResponse response = template.postForResource("query/current-user.graphql"); - - assertTrue(response.isOk()); - User actualUser = response.get("$.data.currentUser", User.class); - assertThat(actualUser) - .hasFieldOrPropertyWithValue("username", "admin") - .hasFieldOrPropertyWithValue("profile.firstName", "John") - .hasFieldOrPropertyWithValue("profile.lastName", "Smith") - .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); - assertNotNull(actualUser.getAuth()); - assertEquals(facebookAuth, actualUser.getAuth().getFacebook()); - } + @Autowired + private GraphQLTestTemplate template; + @MockBean + private UserRepository repository; + + private User user; + private final CertificateAuth certificateAuth = new CertificateAuth(UUID.randomUUID().toString()); + private final FacebookAuth facebookAuth = new FacebookAuth(UUID.randomUUID().toString(), "facebookAuthName"); + private final GithubAuth githubAuth = new GithubAuth(UUID.randomUUID().toString(), "githubAuthName"); + private final GoogleAuth googleAuth = new GoogleAuth(UUID.randomUUID().toString(), "googleAuthName"); + private final LinkedInAuth linkedInAuth = new LinkedInAuth(UUID.randomUUID().toString(), "linkedInAuthName"); + + @BeforeEach + void init() { + SessionUtils.SECURITY_CONTEXT.setAuthentication(null); + user = new User("admin", "pass", "ADMIN", true, "example@sysgears.com"); + user.setProfile(new UserProfile("John", "Smith")); + user.setAuth( + UserAuth.builder() + .certificate(certificateAuth) + .facebook(facebookAuth) + .github(githubAuth) + .google(googleAuth) + .linkedin(linkedInAuth) + .build() + ); + } + + @Test + void users() throws IOException { + when(repository.findByCriteria(Optional.empty(), Optional.empty())) + .thenReturn(CompletableFuture.completedFuture(Collections.singletonList(user))); + + GraphQLResponse response = template.postForResource("query/users.graphql"); + + assertTrue(response.isOk()); + User actualUser = response.get("$.data.users[0]", User.class); + assertThat(actualUser) + .hasFieldOrPropertyWithValue("username", "admin") + .hasFieldOrPropertyWithValue("role", "ADMIN") + .hasFieldOrPropertyWithValue("email", "example@sysgears.com") + .hasFieldOrPropertyWithValue("isActive", true) + .hasFieldOrPropertyWithValue("profile.firstName", "John") + .hasFieldOrPropertyWithValue("profile.lastName", "Smith") + .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); + assertNotNull(actualUser.getAuth()); + assertEquals(certificateAuth, actualUser.getAuth().getCertificate()); + assertEquals(facebookAuth, actualUser.getAuth().getFacebook()); + assertEquals(githubAuth, actualUser.getAuth().getGithub()); + assertEquals(googleAuth, actualUser.getAuth().getGoogle()); + assertEquals(linkedInAuth, actualUser.getAuth().getLinkedin()); + } + + @Test + void user() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 1); + + when(repository.findUserById(1)) + .thenReturn(CompletableFuture.completedFuture(user)); + + GraphQLResponse response = template.perform("query/user.graphql", node); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.user", UserPayload.class); + User actualUser = payload.getUser(); + assertThat(actualUser) + .hasFieldOrPropertyWithValue("username", "admin") + .hasFieldOrPropertyWithValue("role", "ADMIN") + .hasFieldOrPropertyWithValue("email", "example@sysgears.com") + .hasFieldOrPropertyWithValue("isActive", true) + .hasFieldOrPropertyWithValue("profile.firstName", "John") + .hasFieldOrPropertyWithValue("profile.lastName", "Smith") + .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); + assertNotNull(actualUser.getAuth()); + assertEquals(certificateAuth, actualUser.getAuth().getCertificate()); + } + + @Test + void user_not_found() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 115); + + when(repository.findUserById(115)) + .thenReturn(CompletableFuture.completedFuture(null)); + + GraphQLResponse response = template.perform("query/user.graphql", node); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.user", UserPayload.class); + assertNull(payload.getUser()); + } + + @Test + void currentUser() throws IOException { + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); + + GraphQLResponse response = template.postForResource("query/current-user.graphql"); + + assertTrue(response.isOk()); + User actualUser = response.get("$.data.currentUser", User.class); + assertThat(actualUser) + .hasFieldOrPropertyWithValue("username", "admin") + .hasFieldOrPropertyWithValue("profile.firstName", "John") + .hasFieldOrPropertyWithValue("profile.lastName", "Smith") + .hasFieldOrPropertyWithValue("profile.fullName", "John Smith"); + assertNotNull(actualUser.getAuth()); + assertEquals(facebookAuth, actualUser.getAuth().getFacebook()); + } + + @Test + void currentUser_not_login_yet() throws IOException { + GraphQLResponse response = template.postForResource("query/current-user.graphql"); + + assertTrue(response.isOk()); + User actualUser = response.get("$.data.currentUser", User.class); + assertNull(actualUser); + } } diff --git a/modules/user/server-java/src/test/resources/mutation/login.graphql b/modules/user/server-java/src/test/resources/mutation/login.graphql new file mode 100644 index 0000000000..f9f8d53bff --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/login.graphql @@ -0,0 +1,18 @@ +mutation login($input: LoginUserInput!) { + login(input: $input) { + user { + id + username + role + isActive + email + profile { + fullName + } + } + tokens { + accessToken + refreshToken + } + } +} From 6f463d31707a8330853aee5113da9971a845f93e Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 27 Jan 2021 18:51:27 +0200 Subject: [PATCH 73/85] resolved issue with failed tests; added new tests for forgot and reset password --- .../com/sysgears/user/LoginMutationTest.java | 169 +++++++++++++++--- .../com/sysgears/user/UserMutationTest.java | 5 +- .../mutation/forgot-password.graphql | 3 + .../resources/mutation/reset-password.graphql | 3 + 4 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 modules/user/server-java/src/test/resources/mutation/forgot-password.graphql create mode 100644 modules/user/server-java/src/test/resources/mutation/reset-password.graphql diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java index 9d1dbf83e9..025ba13648 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java @@ -4,31 +4,52 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.authentication.service.jwt.JwtParser; import com.sysgears.mailer.service.EmailService; import com.sysgears.user.dto.AuthPayload; import com.sysgears.user.model.User; import com.sysgears.user.repository.UserRepository; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Transactional -@Disabled //todo figure out tests fails when run with coverage and when build project with gradle +@AutoConfigureTestDatabase public class LoginMutationTest { @Autowired private GraphQLTestTemplate template; + @Autowired + private UserRepository userRepository; + @Autowired + private PasswordEncoder passwordEncoder; @MockBean private EmailService emailService; - @Autowired - UserRepository userRepository; + @SpyBean + private JwtParser jwtParser; + + private User user; + private User admin; + + @BeforeEach + void init() { + user = userRepository.findByUsernameOrEmail("user").join(); + admin = userRepository.findByUsernameOrEmail("admin").join(); + } @Test void login_with_username() throws IOException { @@ -36,22 +57,21 @@ void login_with_username() throws IOException { ObjectNode input = mapper.createObjectNode(); ObjectNode node = mapper.createObjectNode(); - node.put("usernameOrEmail", "user"); + node.put("usernameOrEmail", user.getUsername()); node.put("password", "user1234"); input.set("input", node); GraphQLResponse response = template.perform("mutation/login.graphql", input); assertTrue(response.isOk()); - System.err.println(response.getRawResponse().getBody()); AuthPayload payload = response.get("$.data.login", AuthPayload.class); - User user = payload.getUser(); - assertEquals("user", user.getUsername()); - assertEquals("user", user.getRole()); - assertTrue(user.getIsActive()); - assertEquals("user@example.com", user.getEmail()); - assertNull(user.getProfile()); + User userActual = payload.getUser(); + assertEquals(user.getUsername(), userActual.getUsername()); + assertEquals(user.getRole(), userActual.getRole()); + assertTrue(userActual.getIsActive()); + assertEquals(user.getEmail(), userActual.getEmail()); + assertNull(userActual.getProfile()); assertNotNull(payload.getTokens().getAccessToken()); assertNotNull(payload.getTokens().getRefreshToken()); @@ -63,7 +83,7 @@ void login_with_email() throws IOException { ObjectNode input = mapper.createObjectNode(); ObjectNode node = mapper.createObjectNode(); - node.put("usernameOrEmail", "admin@example.com"); + node.put("usernameOrEmail", admin.getEmail()); node.put("password", "admin123"); input.set("input", node); @@ -72,12 +92,12 @@ void login_with_email() throws IOException { assertTrue(response.isOk()); AuthPayload payload = response.get("$.data.login", AuthPayload.class); - User user = payload.getUser(); - assertEquals("admin", user.getUsername()); - assertEquals("admin", user.getRole()); - assertTrue(user.getIsActive()); - assertEquals("admin@example.com", user.getEmail()); - assertNull(user.getProfile()); + User userActual = payload.getUser(); + assertEquals(admin.getUsername(), userActual.getUsername()); + assertEquals(admin.getRole(), userActual.getRole()); + assertTrue(userActual.getIsActive()); + assertEquals(admin.getEmail(), userActual.getEmail()); + assertNull(userActual.getProfile()); assertNotNull(payload.getTokens().getAccessToken()); assertNotNull(payload.getTokens().getRefreshToken()); @@ -106,7 +126,7 @@ void login_invalid_password() throws IOException { ObjectNode input = mapper.createObjectNode(); ObjectNode node = mapper.createObjectNode(); - node.put("usernameOrEmail", "admin"); + node.put("usernameOrEmail", admin.getUsername()); node.put("password", "supersecret"); input.set("input", node); @@ -117,4 +137,111 @@ void login_invalid_password() throws IOException { assertEquals("Login failed.", response.get("$.errors[0].message")); assertEquals("Please enter a valid password.", response.get("$.errors[0].extensions.exception.errors.password")); } + + @Test + void forgotPassword() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("email", user.getEmail()); + input.set("input", node); + + final GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); + + assertTrue(response.isOk()); + + verify(emailService).sendResetPasswordEmail(eq(user.getEmail()), startsWith("/user/reset-password?key=")); + } + + @Test + void forgotPassword_invalid_email() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("email", "user+wrong@example.com"); + input.set("input", node); + + final GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); + + assertTrue(response.isOk()); + assertEquals("No user found", response.get("$.errors[0].message")); + assertEquals("No user with specified email.", response.get("$.errors[0].extensions.exception.errors.email")); + + verify(emailService, never()).sendResetPasswordEmail(anyString(), anyString()); + } + + @Test + @Transactional + @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void resetPassword() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String token = "some.verification.token"; + String password = "newpassword"; + + node.put("token", Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))); + node.put("password", password); + node.put("passwordConfirmation", password); + input.set("input", node); + + doReturn(user.getId()).when(jwtParser).getIdFromVerificationToken(token); + + final GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); + assertTrue(response.isOk()); + + final User updatedUser = userRepository.getOne(user.getId()); + assertTrue(passwordEncoder.matches(password, updatedUser.getPassword())); + + verify(emailService).sendPasswordUpdatedEmail(this.user.getEmail()); + } + + @Test + void resetPassword_user_not_found() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String token = "some.verification.token"; + String password = "newpassword"; + + node.put("token", Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))); + node.put("password", password); + node.put("passwordConfirmation", password); + input.set("input", node); + + doReturn(12313).when(jwtParser).getIdFromVerificationToken(token); + + final GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); + assertTrue(response.isOk()); + assertEquals("No user found", response.get("$.errors[0].message")); + + verify(emailService, never()).sendPasswordUpdatedEmail(anyString()); + } + + @Test + void resetPassword_specified_password_not_matches() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String token = "some.verification.token"; + String password = "newpassword"; + + node.put("token", Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))); + node.put("password", password); + node.put("passwordConfirmation", password + "1"); + input.set("input", node); + + doReturn(user.getId()).when(jwtParser).getIdFromVerificationToken(token); + + final GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); + assertTrue(response.isOk()); + assertEquals("Failed reset password", response.get("$.errors[0].message")); + assertEquals("Must match the field 'password'", response.get("$.errors[0].extensions.exception.errors.passwordConfirmation")); + + verify(emailService, never()).sendPasswordUpdatedEmail(anyString()); } } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java index 48d0d5c243..7fe1b21bd5 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; -import com.sysgears.Application; import com.sysgears.user.dto.UserPayload; import com.sysgears.user.model.User; import com.sysgears.user.model.UserAuth; @@ -12,15 +11,15 @@ import com.sysgears.user.repository.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Transactional +@AutoConfigureTestDatabase public class UserMutationTest { @Autowired private GraphQLTestTemplate template; diff --git a/modules/user/server-java/src/test/resources/mutation/forgot-password.graphql b/modules/user/server-java/src/test/resources/mutation/forgot-password.graphql new file mode 100644 index 0000000000..e31316773f --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/forgot-password.graphql @@ -0,0 +1,3 @@ +mutation forgotPassword($input: ForgotPasswordInput!) { + forgotPassword(input: $input) +} diff --git a/modules/user/server-java/src/test/resources/mutation/reset-password.graphql b/modules/user/server-java/src/test/resources/mutation/reset-password.graphql new file mode 100644 index 0000000000..75d40798eb --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/reset-password.graphql @@ -0,0 +1,3 @@ +mutation resetPassword($input: ResetPasswordInput!) { + resetPassword(input: $input) +} From 81a0baf6ec65c9f874a13aa618705ada5e0e461c Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 28 Jan 2021 17:43:23 +0200 Subject: [PATCH 74/85] fixed confirm registration endpoint --- .../sysgears/user/exception/UserNotFoundException.java | 1 + .../main/java/com/sysgears/user/rest/UserController.java | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java index effbb002aa..d67595d91a 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.Map; +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends FieldErrorException { public UserNotFoundException() { super("No user found", Collections.emptyMap()); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java index f9199f2f78..fed7d8e023 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java @@ -34,14 +34,15 @@ public UserController(UserService userService, @GetMapping("/confirm") public RedirectView confirmRegistration(@RequestParam String key) { Integer userId = jwtParser.getIdFromVerificationToken(key); - userService.findUserById(userId).thenApply(user -> { + return userService.findUserById(userId).thenApply(user -> { if (user == null) throw new UserNotFoundException(); user.setIsActive(true); - return userService.save(user); + userService.save(user); + + return new RedirectView(confirmRegistrationRedirectUrl); } - ); - return new RedirectView(confirmRegistrationRedirectUrl); + ).join(); } @GetMapping("/reset-password") From 4123730015a31389eccb9a351198b39bb5c22f07 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 28 Jan 2021 17:43:55 +0200 Subject: [PATCH 75/85] added tests for registration and confirm registration --- .../{ => resolvers}/LoginMutationTest.java | 63 ++++++++++++++-- .../{ => resolvers}/UserMutationTest.java | 2 +- .../user/{ => resolvers}/UserQueryTest.java | 3 +- .../user/rest/UserControllerTest.java | 73 +++++++++++++++++++ .../test/resources/mutation/register.graphql | 11 +++ 5 files changed, 141 insertions(+), 11 deletions(-) rename modules/user/server-java/src/test/java/com/sysgears/user/{ => resolvers}/LoginMutationTest.java (78%) rename modules/user/server-java/src/test/java/com/sysgears/user/{ => resolvers}/UserMutationTest.java (99%) rename modules/user/server-java/src/test/java/com/sysgears/user/{ => resolvers}/UserQueryTest.java (99%) create mode 100644 modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java create mode 100644 modules/user/server-java/src/test/resources/mutation/register.graphql diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java similarity index 78% rename from modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java rename to modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java index 025ba13648..5224b284a4 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/LoginMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java @@ -1,4 +1,4 @@ -package com.sysgears.user; +package com.sysgears.user.resolvers; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -147,7 +147,7 @@ void forgotPassword() throws IOException { node.put("email", user.getEmail()); input.set("input", node); - final GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); + GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); assertTrue(response.isOk()); @@ -163,7 +163,7 @@ void forgotPassword_invalid_email() throws IOException { node.put("email", "user+wrong@example.com"); input.set("input", node); - final GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); + GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); assertTrue(response.isOk()); assertEquals("No user found", response.get("$.errors[0].message")); @@ -190,10 +190,10 @@ void resetPassword() throws IOException { doReturn(user.getId()).when(jwtParser).getIdFromVerificationToken(token); - final GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); + GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); assertTrue(response.isOk()); - final User updatedUser = userRepository.getOne(user.getId()); + User updatedUser = userRepository.getOne(user.getId()); assertTrue(passwordEncoder.matches(password, updatedUser.getPassword())); verify(emailService).sendPasswordUpdatedEmail(this.user.getEmail()); @@ -215,7 +215,7 @@ void resetPassword_user_not_found() throws IOException { doReturn(12313).when(jwtParser).getIdFromVerificationToken(token); - final GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); + GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); assertTrue(response.isOk()); assertEquals("No user found", response.get("$.errors[0].message")); @@ -238,10 +238,57 @@ void resetPassword_specified_password_not_matches() throws IOException { doReturn(user.getId()).when(jwtParser).getIdFromVerificationToken(token); - final GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); + GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); assertTrue(response.isOk()); assertEquals("Failed reset password", response.get("$.errors[0].message")); assertEquals("Must match the field 'password'", response.get("$.errors[0].extensions.exception.errors.passwordConfirmation")); - verify(emailService, never()).sendPasswordUpdatedEmail(anyString()); } + verify(emailService, never()).sendPasswordUpdatedEmail(anyString()); + } + + @Test + void register() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String username = "someone"; + String email = "someone@example.com"; + node.put("username", username); + node.put("email", email); + node.put("password", "supersecret"); + + input.set("input", node); + GraphQLResponse response = template.perform("/mutation/register.graphql", input); + assertTrue(response.isOk()); + + User registeredUser = response.get("$.data.register.user", User.class); + assertTrue(registeredUser.getId() != 0); + assertEquals(username, registeredUser.getUsername()); + assertEquals(email, registeredUser.getEmail()); + assertEquals("user", registeredUser.getRole()); + assertFalse(registeredUser.getIsActive()); + + verify(emailService).sendRegistrationConfirmEmail(eq(username), eq(email), startsWith("/user/confirm?key=")); + } + + @Test + void register_user_exists() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("username", user.getUsername()); + node.put("email", user.getEmail()); + node.put("password", "supersecret"); + + input.set("input", node); + GraphQLResponse response = template.perform("/mutation/register.graphql", input); + assertTrue(response.isOk()); + assertEquals("User already exists.", response.get("$.errors[0].message")); + assertEquals("E-mail already exists.", response.get("$.errors[0].extensions.exception.errors.email")); + assertEquals("Username already exists.", response.get("$.errors[0].extensions.exception.errors.username")); + + verify(emailService, never()).sendRegistrationConfirmEmail(anyString(), anyString(), anyString()); + } } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java similarity index 99% rename from modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java rename to modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java index 7fe1b21bd5..50171c4e68 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java @@ -1,4 +1,4 @@ -package com.sysgears.user; +package com.sysgears.user.resolvers; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserQueryTest.java similarity index 99% rename from modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java rename to modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserQueryTest.java index 7194d09387..2f3e2e967f 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/UserQueryTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserQueryTest.java @@ -1,10 +1,9 @@ -package com.sysgears.user; +package com.sysgears.user.resolvers; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; -import com.sysgears.Application; import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.UserPayload; diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java new file mode 100644 index 0000000000..d0c4ca0ebf --- /dev/null +++ b/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java @@ -0,0 +1,73 @@ +package com.sysgears.user.rest; + +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.authentication.service.jwt.JwtGenerator; +import com.sysgears.user.model.User; +import com.sysgears.user.service.UserService; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +class UserControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private JwtGenerator jwtGenerator; + @MockBean + private UserService userService; + @Value("${app.redirect.confirm-registration}") + private String confirmRegistrationRedirectUrl; + + @Test + void confirmRegistration() throws Exception { + User registeredUser = new User("username", "password", "user", false, "someone@example.com"); + registeredUser.setId(33); + String token = jwtGenerator.generateVerificationToken(JwtUserIdentity.builder().id(registeredUser.getId()).build()); + + when(userService.findUserById(registeredUser.getId())).thenReturn(CompletableFuture.completedFuture(registeredUser)); + + mockMvc.perform(get("/user/confirm").param("key", token)) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl(confirmRegistrationRedirectUrl)); + + ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); + verify(userService).save(userArgumentCaptor.capture()); + assertTrue(userArgumentCaptor.getValue().getIsActive()); + } + + @Test + void confirmRegistration_no_user_found() throws Exception { + int invalidUserId = 543; + String token = jwtGenerator.generateVerificationToken(JwtUserIdentity.builder().id(invalidUserId).build()); + + when(userService.findUserById(invalidUserId)).thenReturn(CompletableFuture.completedFuture(null)); + + mockMvc.perform(get("/user/confirm").param("key", token)) + .andDo(print()) + .andExpect(status().isNotFound()); + + verify(userService, never()).save(any(User.class)); + } + + @Test + void initiateResetPassword() { + //todo implement + } +} diff --git a/modules/user/server-java/src/test/resources/mutation/register.graphql b/modules/user/server-java/src/test/resources/mutation/register.graphql new file mode 100644 index 0000000000..af72f5ebaf --- /dev/null +++ b/modules/user/server-java/src/test/resources/mutation/register.graphql @@ -0,0 +1,11 @@ +mutation register($input: RegisterUserInput!) { + register(input: $input) { + user { + id + username + role + isActive + email + } + } +} From a098c5db55de059a3ff18108bdd8f50a36d0b53e Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 4 Feb 2021 18:51:27 +0200 Subject: [PATCH 76/85] added tests for refresh token --- .../jwt/JwtMutationResolverTest.java | 62 +++++++++++++++++++ .../src/test/resources/refresh-tokens.graphql | 6 ++ 2 files changed, 68 insertions(+) create mode 100644 modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolverTest.java create mode 100644 modules/authentication/server-java/src/test/resources/refresh-tokens.graphql diff --git a/modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolverTest.java b/modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolverTest.java new file mode 100644 index 0000000000..9e24ac6201 --- /dev/null +++ b/modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/jwt/JwtMutationResolverTest.java @@ -0,0 +1,62 @@ +package com.sysgears.authentication.resolvers.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.authentication.model.jwt.JwtUserIdentity; +import com.sysgears.authentication.model.jwt.Tokens; +import com.sysgears.authentication.service.jwt.JwtGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class JwtMutationResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private JwtGenerator jwtGenerator; + @Autowired + private JwtUserIdentityService userService; + + @Test + void refreshTokens() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + JwtUserIdentity jwtUserIdentity = userService.findById(1).get(); + + Tokens tokens = jwtGenerator.generateTokens(jwtUserIdentity); + node.put("refreshToken", tokens.getRefreshToken()); + + + GraphQLResponse response = template.perform("refresh-tokens.graphql", node); + + assertTrue(response.isOk()); + assertNotNull(response.get("$.data.refreshTokens", Tokens.class)); + } + + @Test + void refreshTokens_user_not_found() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + JwtUserIdentity jwtUserIdentity = JwtUserIdentity.builder() + .id(55555) + .build(); + + Tokens tokens = jwtGenerator.generateTokens(jwtUserIdentity); + node.put("refreshToken", tokens.getRefreshToken()); + + + GraphQLResponse response = template.perform("refresh-tokens.graphql", node); + + assertTrue(response.isOk()); + assertEquals("Refresh token is invalid.", response.get("$.errors[0].message")); + } +} diff --git a/modules/authentication/server-java/src/test/resources/refresh-tokens.graphql b/modules/authentication/server-java/src/test/resources/refresh-tokens.graphql new file mode 100644 index 0000000000..0f5135fe26 --- /dev/null +++ b/modules/authentication/server-java/src/test/resources/refresh-tokens.graphql @@ -0,0 +1,6 @@ +mutation refreshTokens($refreshToken: String!) { + refreshTokens(refreshToken: $refreshToken) { + accessToken + refreshToken + } +} From 9d3ba09b93fa21e9eb494810526e24351e61b319 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Thu, 4 Feb 2021 18:51:42 +0200 Subject: [PATCH 77/85] added tests for reset password + refactoring --- .../sysgears/user/rest/UserController.java | 10 +++---- .../user/rest/UserControllerTest.java | 30 +++++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java index fed7d8e023..65467d4e13 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java @@ -48,12 +48,12 @@ public RedirectView confirmRegistration(@RequestParam String key) { @GetMapping("/reset-password") public RedirectView initiateResetPassword(@RequestParam String key) { Integer userId = jwtParser.getIdFromVerificationToken(key); - userService.findUserById(userId).thenApply(user -> { + return userService.findUserById(userId).thenApply(user -> { if (user == null) throw new UserNotFoundException(); - return user; + + String base64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8)); + return new RedirectView(resetPasswordRedirectUrl + base64Key); } - ); - String base64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8)); - return new RedirectView(resetPasswordRedirectUrl + base64Key); + ).join(); } } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java index d0c4ca0ebf..e6633d3f4b 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/rest/UserControllerTest.java @@ -13,6 +13,8 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,6 +35,8 @@ class UserControllerTest { private UserService userService; @Value("${app.redirect.confirm-registration}") private String confirmRegistrationRedirectUrl; + @Value("${app.redirect.reset-password}") + private String resetPasswordRedirectUrl; @Test void confirmRegistration() throws Exception { @@ -67,7 +71,29 @@ void confirmRegistration_no_user_found() throws Exception { } @Test - void initiateResetPassword() { - //todo implement + void initiateResetPassword() throws Exception { + User user = new User("username", "password", "user", false, "someone@example.com"); + user.setId(33); + String token = jwtGenerator.generateVerificationToken(JwtUserIdentity.builder().id(user.getId()).build()); + String expectedRedirectedUrl = resetPasswordRedirectUrl + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)); + + when(userService.findUserById(user.getId())).thenReturn(CompletableFuture.completedFuture(user)); + + mockMvc.perform(get("/user/reset-password").param("key", token)) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl(expectedRedirectedUrl)); + } + + @Test + void initiateResetPassword_no_user_found() throws Exception { + int invalidUserId = 1234; + String token = jwtGenerator.generateVerificationToken(JwtUserIdentity.builder().id(invalidUserId).build()); + + when(userService.findUserById(invalidUserId)).thenReturn(CompletableFuture.completedFuture(null)); + + mockMvc.perform(get("/user/reset-password").param("key", token)) + .andDo(print()) + .andExpect(status().isNotFound()); } } From c39caf91a0fb623b2228827d1ed97fd4e2a54dd8 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 8 Feb 2021 18:06:27 +0200 Subject: [PATCH 78/85] added test for logout user --- .../session/SessionMutationResolverTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/session/SessionMutationResolverTest.java diff --git a/modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/session/SessionMutationResolverTest.java b/modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/session/SessionMutationResolverTest.java new file mode 100644 index 0000000000..e7ba43f147 --- /dev/null +++ b/modules/authentication/server-java/src/test/java/com/sysgears/authentication/resolvers/session/SessionMutationResolverTest.java @@ -0,0 +1,32 @@ +package com.sysgears.authentication.resolvers.session; + +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.authentication.utils.SessionUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class SessionMutationResolverTest { + @Autowired + private GraphQLTestTemplate template; + + @Test + void logout() throws IOException { + SessionUtils.SECURITY_CONTEXT.setAuthentication(new UsernamePasswordAuthenticationToken("username", "password")); + GraphQLResponse response = template.postForResource("logout.graphql"); + + assertTrue(response.isOk()); + assertNull(SessionUtils.SECURITY_CONTEXT.getAuthentication()); + assertNull(response.get("$.data.logout")); + } +} From e0f815c75ed6ab267ed26c4f740cdec7c7e07df4 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Mon, 8 Feb 2021 18:07:24 +0200 Subject: [PATCH 79/85] added tests for post module + refactoring DTOs --- .../src/test/resources/logout.graphql | 3 + .../com/sysgears/post/dto/CommentPayload.java | 8 +- .../java/com/sysgears/post/dto/PostEdges.java | 4 + .../com/sysgears/post/dto/PostPageInfo.java | 4 + .../com/sysgears/post/dto/PostPayload.java | 10 +- .../java/com/sysgears/post/dto/Posts.java | 4 + .../CommentMutationResolverTest.java | 153 ++++++++++++++++++ .../resolvers/PostMutationResolverTest.java | 121 ++++++++++++++ .../post/resolvers/PostQueryResolverTest.java | 124 ++++++++++++++ .../resources/comment/add-comment.graphql | 6 + .../resources/comment/delete-comment.graphql | 6 + .../resources/comment/edit-comment.graphql | 6 + .../src/test/resources/post/add-post.graphql | 11 ++ .../test/resources/post/delete-post.graphql | 11 ++ .../src/test/resources/post/edit-post.graphql | 11 ++ .../src/test/resources/post/get-post.graphql | 11 ++ .../src/test/resources/post/get-posts.graphql | 21 +++ 17 files changed, 509 insertions(+), 5 deletions(-) create mode 100644 modules/authentication/server-java/src/test/resources/logout.graphql create mode 100644 modules/post/server-java/src/test/java/com/sysgears/post/resolvers/CommentMutationResolverTest.java create mode 100644 modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostMutationResolverTest.java create mode 100644 modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostQueryResolverTest.java create mode 100644 modules/post/server-java/src/test/resources/comment/add-comment.graphql create mode 100644 modules/post/server-java/src/test/resources/comment/delete-comment.graphql create mode 100644 modules/post/server-java/src/test/resources/comment/edit-comment.graphql create mode 100644 modules/post/server-java/src/test/resources/post/add-post.graphql create mode 100644 modules/post/server-java/src/test/resources/post/delete-post.graphql create mode 100644 modules/post/server-java/src/test/resources/post/edit-post.graphql create mode 100644 modules/post/server-java/src/test/resources/post/get-post.graphql create mode 100644 modules/post/server-java/src/test/resources/post/get-posts.graphql diff --git a/modules/authentication/server-java/src/test/resources/logout.graphql b/modules/authentication/server-java/src/test/resources/logout.graphql new file mode 100644 index 0000000000..1b6779f833 --- /dev/null +++ b/modules/authentication/server-java/src/test/resources/logout.graphql @@ -0,0 +1,3 @@ +mutation logout { + logout +} diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java index 77f6616066..c6857315ed 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/CommentPayload.java @@ -1,12 +1,16 @@ package com.sysgears.post.dto; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class CommentPayload { @NonNull - private final Integer id; + private Integer id; @NonNull - private final String content; + private String content; } diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java index 67b4280d4b..d350b6c4b5 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostEdges.java @@ -1,10 +1,14 @@ package com.sysgears.post.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class PostEdges { private PostPayload node; private Integer cursor; diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java index b41bd74d43..693c9fd659 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPageInfo.java @@ -1,10 +1,14 @@ package com.sysgears.post.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class PostPageInfo { private Integer endCursor; private Boolean hasNextPage; diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java index 0c69d14c8d..508a5cece2 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/PostPayload.java @@ -1,6 +1,8 @@ package com.sysgears.post.dto; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; import java.util.ArrayList; @@ -8,13 +10,15 @@ import java.util.List; @Data +@NoArgsConstructor +@AllArgsConstructor public class PostPayload { @NonNull - private final Integer id; + private Integer id; @NonNull - private final String title; + private String title; @NonNull - private final String content; + private String content; private final List comments = new ArrayList<>(); diff --git a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java index ad490abeeb..0356309511 100644 --- a/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java +++ b/modules/post/server-java/src/main/java/com/sysgears/post/dto/Posts.java @@ -1,12 +1,16 @@ package com.sysgears.post.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Posts { private Long totalCount; private List edges; diff --git a/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/CommentMutationResolverTest.java b/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/CommentMutationResolverTest.java new file mode 100644 index 0000000000..5e32791c73 --- /dev/null +++ b/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/CommentMutationResolverTest.java @@ -0,0 +1,153 @@ +package com.sysgears.post.resolvers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.post.dto.CommentPayload; +import com.sysgears.post.model.Comment; +import com.sysgears.post.model.Post; +import com.sysgears.post.repository.PostRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class CommentMutationResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private PostRepository postRepository; + + @Test + void addComment() throws IOException { + Post post = postRepository.saveAndFlush(new Post("Title", "Content")); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String content = "Comment content"; + node.put("postId", post.getId()); + node.put("content", content); + + input.set("input", node); + GraphQLResponse response = template.perform("comment/add-comment.graphql", input); + + assertTrue(response.isOk()); + CommentPayload commentPayload = response.get("$.data.addComment", CommentPayload.class); + assertNotNull(commentPayload.getId()); + assertEquals(content, commentPayload.getContent()); + } + + @Test + void addComment_for_non_existent_post() throws IOException { + int invalidPostId = 111111; + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String content = "Comment content"; + node.put("postId", invalidPostId); + node.put("content", content); + + input.set("input", node); + GraphQLResponse response = template.perform("comment/add-comment.graphql", input); + + assertTrue(response.isOk()); + assertEquals(String.format("Post with id %d not found", invalidPostId), response.get("$.errors[0].message")); + } + + @Test + void deleteComment() throws IOException { + Post post = new Post("Title", "Content"); + Comment comment = new Comment("comment..."); + post.addComment(comment); + Post savedPost = postRepository.saveAndFlush(post); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", comment.getId()); + node.put("postId", savedPost.getId()); + + input.set("input", node); + GraphQLResponse response = template.perform("comment/delete-comment.graphql", input); + + assertTrue(response.isOk()); + CommentPayload commentPayload = response.get("$.data.deleteComment", CommentPayload.class); + assertEquals(comment.getId(), commentPayload.getId()); + assertEquals(comment.getContent(), commentPayload.getContent()); + } + + @Test + void deleteComment_with_invalid_id() throws IOException { + Post post = postRepository.saveAndFlush(new Post("Title", "Content")); + int invalidCommentId = 4123111; + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", invalidCommentId); + node.put("postId", post.getId()); + + input.set("input", node); + GraphQLResponse response = template.perform("comment/delete-comment.graphql", input); + + assertTrue(response.isOk()); + assertEquals(String.format("Comment with id %d not found", invalidCommentId), response.get("$.errors[0].message")); + } + + @Test + void editComment() throws IOException { + Post post = new Post("Post title", "some post content..."); + Comment comment = new Comment("comment..."); + post.addComment(comment); + Post savedPost = postRepository.saveAndFlush(post); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String content = "new comment content"; + node.put("id", comment.getId()); + node.put("postId", savedPost.getId()); + node.put("content", content); + + input.set("input", node); + GraphQLResponse response = template.perform("comment/edit-comment.graphql", input); + + assertTrue(response.isOk()); + CommentPayload commentPayload = response.get("$.data.editComment", CommentPayload.class); + assertEquals(comment.getId(), commentPayload.getId()); + assertEquals(content, commentPayload.getContent()); + } + + @Test + void editComment_with_invalid_id() throws IOException { + Post post = postRepository.saveAndFlush(new Post("Post Title", "Post Content")); + int invalidCommentId = 662525; + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String content = "Comment content"; + node.put("id", invalidCommentId); + node.put("postId", post.getId()); + node.put("content", content); + + input.set("input", node); + GraphQLResponse response = template.perform("comment/edit-comment.graphql", input); + + assertTrue(response.isOk()); + assertEquals(String.format("Comment with id %d not found", invalidCommentId), response.get("$.errors[0].message")); + } +} diff --git a/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostMutationResolverTest.java b/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostMutationResolverTest.java new file mode 100644 index 0000000000..53553f565c --- /dev/null +++ b/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostMutationResolverTest.java @@ -0,0 +1,121 @@ +package com.sysgears.post.resolvers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.post.dto.PostPayload; +import com.sysgears.post.model.Post; +import com.sysgears.post.repository.PostRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class PostMutationResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private PostRepository postRepository; + + @Test + void addPost() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String title = "Post title"; + String content = "Post content"; + node.put("title", title); + node.put("content", content); + + input.set("input", node); + GraphQLResponse response = template.perform("post/add-post.graphql", input); + + assertTrue(response.isOk()); + PostPayload postPayload = response.get("$.data.addPost", PostPayload.class); + assertNotNull(postPayload.getId()); + assertEquals(title, postPayload.getTitle()); + assertEquals(content, postPayload.getContent()); + assertTrue(postPayload.getComments().isEmpty()); + } + + @Test + void deletePost() throws IOException { + Post post = postRepository.saveAndFlush(new Post("Title", "Content")); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", post.getId()); + + GraphQLResponse response = template.perform("post/delete-post.graphql", node); + + assertTrue(response.isOk()); + PostPayload postPayload = response.get("$.data.deletePost", PostPayload.class); + assertEquals(post.getId(), postPayload.getId()); + assertEquals(post.getTitle(), postPayload.getTitle()); + assertEquals(post.getContent(), postPayload.getContent()); + } + + @Test + void deletePost_with_invalid_id() throws IOException { + int invalidPostId = 23456; + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", invalidPostId); + + GraphQLResponse response = template.perform("post/delete-post.graphql", node); + + assertTrue(response.isOk()); + assertEquals(String.format("Post with id %d not found", invalidPostId), response.get("$.errors[0].message")); + } + + @Test + void editPost() throws IOException { + Post post = postRepository.saveAndFlush(new Post("Title", "Content")); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String title = "Post title"; + String content = "Post content"; + node.put("id", post.getId()); + node.put("title", title); + node.put("content", content); + + input.set("input", node); + GraphQLResponse response = template.perform("post/edit-post.graphql", input); + + assertTrue(response.isOk()); + PostPayload postPayload = response.get("$.data.editPost", PostPayload.class); + assertEquals(post.getId(), postPayload.getId()); + assertEquals(title, postPayload.getTitle()); + assertEquals(content, postPayload.getContent()); + assertTrue(postPayload.getComments().isEmpty()); + } + + @Test + void editPost_with_invalid_id() throws IOException { + int invalidPostId = 99999; + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", invalidPostId); + node.put("title", "New title"); + node.put("content", "New content"); + + input.set("input", node); + GraphQLResponse response = template.perform("post/edit-post.graphql", input); + + assertTrue(response.isOk()); + assertEquals(String.format("Post with id %d not found", invalidPostId), response.get("$.errors[0].message")); + } +} diff --git a/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostQueryResolverTest.java b/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostQueryResolverTest.java new file mode 100644 index 0000000000..ed00535c03 --- /dev/null +++ b/modules/post/server-java/src/test/java/com/sysgears/post/resolvers/PostQueryResolverTest.java @@ -0,0 +1,124 @@ +package com.sysgears.post.resolvers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.post.dto.CommentPayload; +import com.sysgears.post.dto.PostEdges; +import com.sysgears.post.dto.PostPayload; +import com.sysgears.post.dto.Posts; +import com.sysgears.post.model.Comment; +import com.sysgears.post.model.Post; +import com.sysgears.post.repository.PostRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class PostQueryResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private PostRepository postRepository; + private List postList; + @BeforeEach + void init() { + postList = preparePosts(); + } + + @Test + void posts_with_default_limit() throws IOException { + GraphQLResponse response = template.postForResource("post/get-posts.graphql"); + + assertTrue(response.isOk()); + Posts posts = response.get("$.data.posts", Posts.class); + assertEquals(postList.size(), posts.getTotalCount()); + assertEquals(postList.size() - 1, posts.getPageInfo().getEndCursor()); + assertFalse(posts.getPageInfo().getHasNextPage()); + + List expected = postList.stream().map(post -> new PostPayload(post.getId(), post.getTitle(), post.getContent())).collect(Collectors.toList()); + List actual = posts.getEdges().stream().map(PostEdges::getNode).collect(Collectors.toList()); + Assertions.assertThat(actual).containsExactlyInAnyOrder(expected.toArray(PostPayload[]::new)); + } + + @Test + void posts_with_custom_limit() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("limit", 5); + node.put("after", 0); + + GraphQLResponse response = template.perform("post/get-posts.graphql", node); + + assertTrue(response.isOk()); + Posts posts = response.get("$.data.posts", Posts.class); + assertEquals(postList.size(), posts.getTotalCount()); + assertEquals(4, posts.getPageInfo().getEndCursor()); + assertTrue(posts.getPageInfo().getHasNextPage()); + + List expected = postList.subList(0, 5).stream().map(post -> new PostPayload(post.getId(), post.getTitle(), post.getContent())).collect(Collectors.toList()); + List actual = posts.getEdges().stream().map(PostEdges::getNode).collect(Collectors.toList()); + + Assertions.assertThat(actual).containsExactlyInAnyOrder(expected.toArray(PostPayload[]::new)); + } + + @Test + void post() throws IOException { + Post expectedPost = postList.get(0); + List expectedComments = expectedPost.getComments().stream().map(comment -> new CommentPayload(comment.getId(), comment.getContent())).collect(Collectors.toList()); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", expectedPost.getId()); + + GraphQLResponse response = template.perform("post/get-post.graphql", node); + + assertTrue(response.isOk()); + PostPayload postPayload = response.get("$.data.post", PostPayload.class); + assertEquals(expectedPost.getId(), postPayload.getId()); + assertEquals(expectedPost.getTitle(), postPayload.getTitle()); + assertEquals(expectedPost.getContent(), postPayload.getContent()); + Assertions.assertThat(postPayload.getComments()).containsExactlyInAnyOrder(expectedComments.toArray(CommentPayload[]::new)); + } + + @Test + void post_not_fount() throws IOException { + int notExistentPostId = 56562777; + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", notExistentPostId); + + GraphQLResponse response = template.perform("post/get-post.graphql", node); + + assertTrue(response.isOk()); + assertEquals(String.format("Post with id %d not found", notExistentPostId), response.get("$.errors[0].message")); + } + + private List preparePosts() { + postRepository.deleteAll(); + + List posts = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Post post = new Post("title " + i, "content " + i); + post.addComment(new Comment("comment 1 for post " + i)); + post.addComment(new Comment("comment 2 for post " + i)); + posts.add(post); + } + return postRepository.saveAll(posts); + } +} diff --git a/modules/post/server-java/src/test/resources/comment/add-comment.graphql b/modules/post/server-java/src/test/resources/comment/add-comment.graphql new file mode 100644 index 0000000000..00549ef567 --- /dev/null +++ b/modules/post/server-java/src/test/resources/comment/add-comment.graphql @@ -0,0 +1,6 @@ +mutation addComment($input: AddCommentInput!) { + addComment(input: $input) { + id + content + } +} diff --git a/modules/post/server-java/src/test/resources/comment/delete-comment.graphql b/modules/post/server-java/src/test/resources/comment/delete-comment.graphql new file mode 100644 index 0000000000..57432b80f3 --- /dev/null +++ b/modules/post/server-java/src/test/resources/comment/delete-comment.graphql @@ -0,0 +1,6 @@ +mutation deleteComment($input: DeleteCommentInput!) { + deleteComment(input: $input) { + id + content + } +} diff --git a/modules/post/server-java/src/test/resources/comment/edit-comment.graphql b/modules/post/server-java/src/test/resources/comment/edit-comment.graphql new file mode 100644 index 0000000000..f3a2363965 --- /dev/null +++ b/modules/post/server-java/src/test/resources/comment/edit-comment.graphql @@ -0,0 +1,6 @@ +mutation editComment($input: EditCommentInput!) { + editComment(input: $input) { + id + content + } +} diff --git a/modules/post/server-java/src/test/resources/post/add-post.graphql b/modules/post/server-java/src/test/resources/post/add-post.graphql new file mode 100644 index 0000000000..bb7b898265 --- /dev/null +++ b/modules/post/server-java/src/test/resources/post/add-post.graphql @@ -0,0 +1,11 @@ +mutation addPost($input: AddPostInput!) { + addPost(input: $input) { + id + title + content + comments { + id + content + } + } +} diff --git a/modules/post/server-java/src/test/resources/post/delete-post.graphql b/modules/post/server-java/src/test/resources/post/delete-post.graphql new file mode 100644 index 0000000000..8e6d510614 --- /dev/null +++ b/modules/post/server-java/src/test/resources/post/delete-post.graphql @@ -0,0 +1,11 @@ +mutation deletePost($id: Int!) { + deletePost(id: $id) { + id + title + content + comments { + id + content + } + } +} diff --git a/modules/post/server-java/src/test/resources/post/edit-post.graphql b/modules/post/server-java/src/test/resources/post/edit-post.graphql new file mode 100644 index 0000000000..36becbaf81 --- /dev/null +++ b/modules/post/server-java/src/test/resources/post/edit-post.graphql @@ -0,0 +1,11 @@ +mutation editPost($input: EditPostInput!) { + editPost(input: $input) { + id + title + content + comments { + id + content + } + } +} diff --git a/modules/post/server-java/src/test/resources/post/get-post.graphql b/modules/post/server-java/src/test/resources/post/get-post.graphql new file mode 100644 index 0000000000..d2bbfd9e75 --- /dev/null +++ b/modules/post/server-java/src/test/resources/post/get-post.graphql @@ -0,0 +1,11 @@ +query post($id: Int!) { + post(id: $id) { + id + title + content + comments { + id + content + } + } +} diff --git a/modules/post/server-java/src/test/resources/post/get-posts.graphql b/modules/post/server-java/src/test/resources/post/get-posts.graphql new file mode 100644 index 0000000000..daef511474 --- /dev/null +++ b/modules/post/server-java/src/test/resources/post/get-posts.graphql @@ -0,0 +1,21 @@ +query posts($limit: Int, $after: Int) { + posts(limit: $limit, after: $after) { + totalCount + edges { + cursor + node { + id + title + content + comments { + id + content + } + } + } + pageInfo { + endCursor + hasNextPage + } + } +} From 69b76acd42ec66dfd729842e08fb0c31872ca852 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 12 Feb 2021 17:34:41 +0200 Subject: [PATCH 80/85] added tests for chat module --- .../MessageMutationResolverTest.java | 278 ++++++++++++++++++ .../resolvers/MessageQueryResolverTest.java | 121 ++++++++ .../src/test/resources/add-message.graphql | 20 ++ .../src/test/resources/delete-message.graphql | 20 ++ .../src/test/resources/edit-message.graphql | 20 ++ .../src/test/resources/get-message.graphql | 20 ++ .../src/test/resources/get-messages.graphql | 30 ++ 7 files changed, 509 insertions(+) create mode 100644 modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java create mode 100644 modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java create mode 100644 modules/chat/server-java/src/test/resources/add-message.graphql create mode 100644 modules/chat/server-java/src/test/resources/delete-message.graphql create mode 100644 modules/chat/server-java/src/test/resources/edit-message.graphql create mode 100644 modules/chat/server-java/src/test/resources/get-message.graphql create mode 100644 modules/chat/server-java/src/test/resources/get-messages.graphql diff --git a/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java new file mode 100644 index 0000000000..1cc4432841 --- /dev/null +++ b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java @@ -0,0 +1,278 @@ +package com.sysgears.chat.resolvers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.chat.dto.MessagePayload; +import com.sysgears.chat.model.Message; +import com.sysgears.chat.service.MessageService; +import com.sysgears.user.model.User; +import com.sysgears.user.service.UserService; +import org.apache.tomcat.util.http.fileupload.FileUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockPart; + +import javax.servlet.http.Part; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class MessageMutationResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private MessageService messageService; + @MockBean + private UserService userService; + + @Test + void addMessage() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + User user = new User(); + user.setId(1); + user.setUsername("user"); + String text = "Hello"; + String uuid = UUID.randomUUID().toString(); + node.put("text", text); + node.put("userId", user.getId()); + node.put("uuid", uuid); + + input.set("input", node); + + when(userService.findUserById(user.getId())).thenReturn(CompletableFuture.completedFuture(user)); + + GraphQLResponse response = template.perform("add-message.graphql", input); + assertTrue(response.isOk()); + + MessagePayload messagePayload = response.get("$.data.addMessage", MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertEquals(text, messagePayload.getText()); + assertEquals(user.getId(), messagePayload.getUserId()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertEquals(user.getUsername(), messagePayload.getUsername()); + assertEquals(uuid, messagePayload.getUuid()); + assertNull(messagePayload.getQuotedId()); + assertNull(messagePayload.getQuotedMessage().getId()); + assertNull(messagePayload.getFilename()); + assertNull(messagePayload.getPath()); + } + + @Test + void addMessage_userId_not_specified() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + User user = new User(); + user.setId(2); + user.setUsername("test"); + String text = "Foo"; + String uuid = UUID.randomUUID().toString(); + node.put("text", text); + node.put("uuid", uuid); + + input.set("input", node); + + when(userService.getCurrentAuditor()).thenReturn(Optional.of(user)); + + GraphQLResponse response = template.perform("add-message.graphql", input); + assertTrue(response.isOk()); + + MessagePayload messagePayload = response.get("$.data.addMessage", MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertEquals(text, messagePayload.getText()); + assertEquals(user.getId(), messagePayload.getUserId()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertEquals(user.getUsername(), messagePayload.getUsername()); + assertEquals(uuid, messagePayload.getUuid()); + assertNull(messagePayload.getQuotedId()); + assertNull(messagePayload.getQuotedMessage().getId()); + assertNull(messagePayload.getFilename()); + assertNull(messagePayload.getPath()); + } + + @Test + void addMessage_from_anonymous() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String text = "bar"; + String uuid = UUID.randomUUID().toString(); + node.put("text", text); + node.put("uuid", uuid); + + input.set("input", node); + + GraphQLResponse response = template.perform("add-message.graphql", input); + assertTrue(response.isOk()); + + MessagePayload messagePayload = response.get("$.data.addMessage", MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertEquals(text, messagePayload.getText()); + assertNull(messagePayload.getUserId()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertNull(messagePayload.getUsername()); + assertEquals(uuid, messagePayload.getUuid()); + assertNull(messagePayload.getQuotedId()); + assertNull(messagePayload.getQuotedMessage().getId()); + assertNull(messagePayload.getFilename()); + assertNull(messagePayload.getPath()); + } + + @Test + void addMessage_quote() throws IOException { + Message message = new Message(); + message.setUuid(UUID.randomUUID()); + message.setText("foo bar"); + messageService.create(message); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String text = "baz"; + String uuid = UUID.randomUUID().toString(); + node.put("text", text); + node.put("quotedId", message.getId()); + node.put("uuid", uuid); + + input.set("input", node); + + GraphQLResponse response = template.perform("add-message.graphql", input); + assertTrue(response.isOk()); + + MessagePayload messagePayload = response.get("$.data.addMessage", MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertEquals(text, messagePayload.getText()); + assertNull(messagePayload.getUserId()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertNull(messagePayload.getUsername()); + assertEquals(uuid, messagePayload.getUuid()); + assertEquals(message.getId(), messagePayload.getQuotedId()); + assertEquals(message.getId(), messagePayload.getQuotedMessage().getId()); + assertEquals(message.getText(), messagePayload.getQuotedMessage().getText()); + assertNull(messagePayload.getFilename()); + assertNull(messagePayload.getPath()); + } + + @Test + void deleteMessage() throws IOException { + Message message = new Message(); + message.setUuid(UUID.randomUUID()); + message.setText("foo bar"); + messageService.create(message); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", message.getId()); + + GraphQLResponse response = template.perform("delete-message.graphql", node); + assertTrue(response.isOk()); + + MessagePayload messagePayload = response.get("$.data.deleteMessage", MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertEquals(message.getText(), messagePayload.getText()); + assertNull(messagePayload.getUserId()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertNull(messagePayload.getUsername()); + assertEquals(message.getUuid().toString(), messagePayload.getUuid()); + assertNull(messagePayload.getQuotedId()); + assertNull(messagePayload.getQuotedMessage().getId()); + assertNull(messagePayload.getFilename()); + assertNull(messagePayload.getPath()); + } + + @Test + void deleteMessage_not_exists() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + int invalidMessageId = 5555; + node.put("id", invalidMessageId); + + GraphQLResponse response = template.perform("delete-message.graphql", node); + assertTrue(response.isOk()); + assertEquals(String.format("Message with id %d not found", invalidMessageId), response.get("$.errors[0].message")); + } + + @Test + void editMessage() throws IOException { + Message message = new Message(); + message.setUuid(UUID.randomUUID()); + message.setText("foo bar"); + messageService.create(message); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + String text = "baz"; + node.put("id", message.getId()); + node.put("text", text); + + input.set("input", node); + + GraphQLResponse response = template.perform("edit-message.graphql", input); + assertTrue(response.isOk()); + + MessagePayload messagePayload = response.get("$.data.editMessage", MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertEquals(text, messagePayload.getText()); + assertNull(messagePayload.getUserId()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertNull(messagePayload.getUsername()); + assertEquals(message.getUuid().toString(), messagePayload.getUuid()); + assertNull(messagePayload.getQuotedId()); + assertNull(messagePayload.getQuotedMessage().getId()); + assertNull(messagePayload.getFilename()); + assertNull(messagePayload.getPath()); + } + + @Test + void editMessage_not_exists() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + + int invalidMessageId = 2414; + String text = "new message"; + node.put("id", invalidMessageId); + node.put("text", text); + + input.set("input", node); + + GraphQLResponse response = template.perform("edit-message.graphql", input); + assertTrue(response.isOk()); + assertEquals(String.format("Message with id %d not found", invalidMessageId), response.get("$.errors[0].message")); + } +} diff --git a/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java new file mode 100644 index 0000000000..39b69d68f5 --- /dev/null +++ b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java @@ -0,0 +1,121 @@ +package com.sysgears.chat.resolvers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.chat.dto.MessageEdges; +import com.sysgears.chat.dto.MessagePayload; +import com.sysgears.chat.dto.Messages; +import com.sysgears.chat.model.Message; +import com.sysgears.chat.repository.MessageRepository; +import com.sysgears.chat.service.MessageService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class MessageQueryResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private MessageRepository messageRepository; + + private List messageList; + @BeforeEach + void init() { + messageList = prepare(); + } + + @Test + void messages_with_default_limit() throws IOException { + GraphQLResponse response = template.postForResource("get-messages.graphql"); + + assertTrue(response.isOk()); + Messages messages = response.get("$.data.messages", Messages.class); + assertEquals(messageList.size(), messages.getTotalCount()); + assertEquals(49, messages.getPageInfo().getEndCursor()); + assertFalse(messages.getPageInfo().getHasNextPage()); + + List expected = messageList.stream().map(MessagePayload::from).collect(Collectors.toList()); + List actual = messages.getEdges().stream().map(MessageEdges::getNode).collect(Collectors.toList()); + Assertions.assertThat(actual).containsExactlyInAnyOrder(expected.toArray(MessagePayload[]::new)); + } + + @Test + void messages_with_custom_limit() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("limit", 5); + node.put("after", 0); + + GraphQLResponse response = template.perform("get-messages.graphql", node); + + assertTrue(response.isOk()); + Messages messages = response.get("$.data.messages", Messages.class); + assertEquals(messageList.size(), messages.getTotalCount()); + assertEquals(4, messages.getPageInfo().getEndCursor()); + assertTrue(messages.getPageInfo().getHasNextPage()); + + List expected = messageList.subList(0, 5).stream().map(MessagePayload::from).collect(Collectors.toList()); + List actual = messages.getEdges().stream().map(MessageEdges::getNode).collect(Collectors.toList()); + Assertions.assertThat(actual).containsExactlyInAnyOrder(expected.toArray(MessagePayload[]::new)); + } + + @Test + void message() throws IOException { + Message expectedMessage = messageList.get(0); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", expectedMessage.getId()); + + GraphQLResponse response = template.perform("get-message.graphql", node); + + assertTrue(response.isOk()); + MessagePayload messagePayload = response.get("$.data.message", MessagePayload.class); + assertEquals(expectedMessage.getId(), messagePayload.getId()); + assertEquals(expectedMessage.getText(), messagePayload.getText()); + assertEquals(expectedMessage.getUuid().toString(), messagePayload.getUuid()); + } + + @Test + void message_not_exists() throws IOException { + Message expectedMessage = messageList.get(0); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + + node.put("id", expectedMessage.getId()); + + GraphQLResponse response = template.perform("get-message.graphql", node); + + assertTrue(response.isOk()); + MessagePayload messagePayload = response.get("$.data.message", MessagePayload.class); + assertEquals(expectedMessage.getId(), messagePayload.getId()); + assertEquals(expectedMessage.getText(), messagePayload.getText()); + assertEquals(expectedMessage.getUuid().toString(), messagePayload.getUuid()); + } + + private List prepare() { + List messages = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Message message = new Message(); + message.setText("message " + i); + message.setUuid(UUID.randomUUID()); + messages.add(message); + } + return messageRepository.saveAll(messages); + } +} diff --git a/modules/chat/server-java/src/test/resources/add-message.graphql b/modules/chat/server-java/src/test/resources/add-message.graphql new file mode 100644 index 0000000000..ba26a5393a --- /dev/null +++ b/modules/chat/server-java/src/test/resources/add-message.graphql @@ -0,0 +1,20 @@ +mutation addMessage($input: AddMessageInput!) { + addMessage(input: $input) { + id + text + userId + createdAt + username + uuid + quotedId + filename + path + quotedMessage { + id + text + username + filename + path + } + } +} diff --git a/modules/chat/server-java/src/test/resources/delete-message.graphql b/modules/chat/server-java/src/test/resources/delete-message.graphql new file mode 100644 index 0000000000..1659cf1ab9 --- /dev/null +++ b/modules/chat/server-java/src/test/resources/delete-message.graphql @@ -0,0 +1,20 @@ +mutation deleteMessage($id: Int!) { + deleteMessage(id: $id) { + id + text + userId + createdAt + username + uuid + quotedId + filename + path + quotedMessage { + id + text + username + filename + path + } + } +} diff --git a/modules/chat/server-java/src/test/resources/edit-message.graphql b/modules/chat/server-java/src/test/resources/edit-message.graphql new file mode 100644 index 0000000000..e191aaca3a --- /dev/null +++ b/modules/chat/server-java/src/test/resources/edit-message.graphql @@ -0,0 +1,20 @@ +mutation editMessage($input: EditMessageInput!) { + editMessage(input: $input) { + id + text + userId + createdAt + username + uuid + quotedId + filename + path + quotedMessage { + id + text + username + filename + path + } + } +} diff --git a/modules/chat/server-java/src/test/resources/get-message.graphql b/modules/chat/server-java/src/test/resources/get-message.graphql new file mode 100644 index 0000000000..6531280983 --- /dev/null +++ b/modules/chat/server-java/src/test/resources/get-message.graphql @@ -0,0 +1,20 @@ +query message($id: Int!) { + message(id: $id) { + id + text + userId + createdAt + username + uuid + quotedId + filename + path + quotedMessage { + id + text + username + filename + path + } + } +} diff --git a/modules/chat/server-java/src/test/resources/get-messages.graphql b/modules/chat/server-java/src/test/resources/get-messages.graphql new file mode 100644 index 0000000000..99c8e4fff4 --- /dev/null +++ b/modules/chat/server-java/src/test/resources/get-messages.graphql @@ -0,0 +1,30 @@ +query messages($limit: Int, $after: Int) { + messages(limit: $limit, after: $after) { + totalCount + edges { + cursor + node { + id + text + userId + createdAt + username + uuid + quotedId + filename + path + quotedMessage { + id + text + username + filename + path + } + } + } + pageInfo { + endCursor + hasNextPage + } + } +} From 41b828b1a87f874c43ddaac571c46025a7855bdc Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 12 Feb 2021 17:34:58 +0200 Subject: [PATCH 81/85] fixed issue with empty user for chat --- modules/chat/server-java/build.gradle | 1 + .../com/sysgears/chat/dto/MessageEdges.java | 8 +++-- .../sysgears/chat/dto/MessagePageInfo.java | 8 +++-- .../com/sysgears/chat/dto/MessagePayload.java | 32 +++++++++++-------- .../java/com/sysgears/chat/dto/Messages.java | 10 ++++-- .../java/com/sysgears/chat/model/Message.java | 12 +++---- .../resolvers/MessageMutationResolver.java | 19 ++++++++--- modules/user/server-java/build.gradle | 2 +- 8 files changed, 61 insertions(+), 31 deletions(-) diff --git a/modules/chat/server-java/build.gradle b/modules/chat/server-java/build.gradle index a6e7624109..60cceb3f67 100644 --- a/modules/chat/server-java/build.gradle +++ b/modules/chat/server-java/build.gradle @@ -1,4 +1,5 @@ dependencies { implementation project(':core') implementation project(':upload') + implementation project(':user') } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java index 859d1fab9e..68d081f260 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessageEdges.java @@ -1,11 +1,15 @@ package com.sysgears.chat.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class MessageEdges { - private final MessagePayload node; - private final Integer cursor; + private MessagePayload node; + private Integer cursor; } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java index 4d12aa18c7..e933c42dc7 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePageInfo.java @@ -1,11 +1,15 @@ package com.sysgears.chat.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class MessagePageInfo { - private final Integer endCursor; - private final Boolean hasNextPage; + private Integer endCursor; + private Boolean hasNextPage; } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java index 69e49dbf5b..e492cae5eb 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/MessagePayload.java @@ -1,29 +1,35 @@ package com.sysgears.chat.dto; import com.sysgears.chat.model.Message; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class MessagePayload { @NonNull - private final Integer id; - private final String text; - private final Integer userId; - private final String createdAt; - private final String username; - private final String uuid; - private final Integer quotedId; - private final String filename; - private final String path; - private final QuotedMessage quotedMessage; + private Integer id; + private String text; + private Integer userId; + private String createdAt; + private String username; + private String uuid; + private Integer quotedId; + private String filename; + private String path; + private QuotedMessage quotedMessage; public static MessagePayload from(Message message) { QuotedMessage quotedMessage = new QuotedMessage(); if (message.getQuoted() != null) { quotedMessage.setId(message.getQuoted().getId()); quotedMessage.setText(message.getQuoted().getText()); - quotedMessage.setUsername(message.getQuoted().getUsername()); + if (message.getQuoted().getUser() != null) { + quotedMessage.setUsername(message.getQuoted().getUser().getUsername()); + } if (message.getQuoted().getAttachment() != null) { quotedMessage.setFilename(message.getQuoted().getAttachment().getName()); quotedMessage.setPath(message.getQuoted().getAttachment().getPath()); @@ -32,9 +38,9 @@ public static MessagePayload from(Message message) { return new MessagePayload( message.getId(), message.getText(), - message.getUserId(), + message.getUser() != null ? message.getUser().getId() : null, message.getCreatedAt().toString(), - message.getUsername(), + message.getUser() != null ? message.getUser().getUsername() : null, message.getUuid().toString(), quotedMessage.getId(), message.getAttachment() != null ? message.getAttachment().getName() : null, diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java index d6f0c25268..e9ca593952 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/dto/Messages.java @@ -1,14 +1,18 @@ package com.sysgears.chat.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Messages { - private final Long totalCount; - private final List edges; - private final MessagePageInfo pageInfo; + private Long totalCount; + private List edges; + private MessagePageInfo pageInfo; } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java index ae7b28ffdd..9e9f6745d2 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/model/Message.java @@ -1,6 +1,7 @@ package com.sysgears.chat.model; import com.sysgears.upload.model.FileMetadata; +import com.sysgears.user.model.User; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.Fetch; @@ -26,15 +27,14 @@ public class Message { @Column(name = "TEXT") private String text; - @Column(name = "USER_ID") - private Integer userId; - @CreatedDate @Column(name = "CREATED_AT", updatable = false) private final Instant createdAt = Instant.now(); - @Column(name = "USERNAME") - private String username; + @ManyToOne + @Fetch(FetchMode.JOIN) + @JoinColumn(name = "USER_ID") + private User user; @Column(name = "UUID") private UUID uuid; @@ -45,6 +45,6 @@ public class Message { @OneToOne @Fetch(FetchMode.JOIN) - @JoinColumn(name = "attachment_id") + @JoinColumn(name = "ATTACHMENT_ID") private FileMetadata attachment; } diff --git a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java index ced9ccc37b..421d8f4edf 100644 --- a/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java +++ b/modules/chat/server-java/src/main/java/com/sysgears/chat/resolvers/MessageMutationResolver.java @@ -10,6 +10,8 @@ import com.sysgears.core.subscription.Publisher; import com.sysgears.upload.model.FileMetadata; import com.sysgears.upload.service.FileService; +import com.sysgears.user.model.User; +import com.sysgears.user.service.UserService; import graphql.kickstart.tools.GraphQLMutationResolver; import graphql.schema.DataFetchingEnvironment; import lombok.RequiredArgsConstructor; @@ -27,19 +29,24 @@ public class MessageMutationResolver implements GraphQLMutationResolver { private final MessageService messageService; private final FileService fileService; private final Publisher messagePublisher; + private final UserService userService; public CompletableFuture addMessage(AddMessageInput input, DataFetchingEnvironment environment) { return CompletableFuture.supplyAsync(() -> { Message message = new Message(); - + if (input.getUserId() != null) { + User user = userService.findUserById(input.getUserId()).join(); + message.setUser(user); + } else { + userService.getCurrentAuditor().ifPresent(message::setUser); + } if (input.getQuotedId() != null) { Message quotedMessage = messageService.findById(input.getQuotedId()); message.setQuoted(quotedMessage); } message.setText(input.getText()); - message.setUserId(input.getUserId()); message.setUuid(UUID.fromString(input.getUuid())); resolveAttachment(environment).ifPresent(attachment -> { @@ -73,8 +80,12 @@ public CompletableFuture editMessage(EditMessageInput input) { return CompletableFuture.supplyAsync(() -> { final Message message = messageService.findById(input.getId()); message.setText(input.getText()); - message.setUserId(input.getUserId()); - + if (input.getUserId() != null) { + User user = userService.findUserById(input.getUserId()).join(); + message.setUser(user); + } else { + userService.getCurrentAuditor().ifPresent(message::setUser); + } final MessagePayload updated = messageService.update(message); messagePublisher.publish(new MessageUpdatedEvent(Mutation.UPDATED, updated)); diff --git a/modules/user/server-java/build.gradle b/modules/user/server-java/build.gradle index 7afd989c22..1a0cfce89f 100644 --- a/modules/user/server-java/build.gradle +++ b/modules/user/server-java/build.gradle @@ -1,5 +1,5 @@ dependencies { implementation project(':core') - implementation project(':authentication') + api project(':authentication') implementation project(':mailer') } From 08aa578abdf2a341fed1eb1efa3cf7722f6e83e4 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 16 Feb 2021 18:42:19 +0200 Subject: [PATCH 82/85] added tests for chat and upload modules --- .../MessageMutationResolverTest.java | 51 ++++++-- .../resolvers/MessageQueryResolverTest.java | 2 + .../java/com/sysgears/upload/dto/File.java | 14 ++- .../resolvers/UploadMutationResolverTest.java | 110 ++++++++++++++++++ .../resolvers/UploadQueryResolverTest.java | 71 +++++++++++ .../src/test/resources/get-all-files.graphql | 9 ++ .../src/test/resources/remove-file.graphql | 3 + .../src/test/resources/upload-files.graphql | 4 + 8 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadMutationResolverTest.java create mode 100644 modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadQueryResolverTest.java create mode 100644 modules/upload/server-java/src/test/resources/get-all-files.graphql create mode 100644 modules/upload/server-java/src/test/resources/remove-file.graphql create mode 100644 modules/upload/server-java/src/test/resources/upload-files.graphql diff --git a/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java index 1cc4432841..a756c19bf0 100644 --- a/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java +++ b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageMutationResolverTest.java @@ -1,5 +1,6 @@ package com.sysgears.chat.resolvers; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; @@ -9,25 +10,21 @@ import com.sysgears.chat.service.MessageService; import com.sysgears.user.model.User; import com.sysgears.user.service.UserService; -import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.mock.web.MockPart; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.*; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; -import javax.servlet.http.Part; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; -import java.util.Base64; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -42,6 +39,8 @@ class MessageMutationResolverTest { private GraphQLTestTemplate template; @Autowired private MessageService messageService; + @Autowired + private TestRestTemplate restTemplate; @MockBean private UserService userService; @@ -275,4 +274,40 @@ void editMessage_not_exists() throws IOException { assertTrue(response.isOk()); assertEquals(String.format("Message with id %d not found", invalidMessageId), response.get("$.errors[0].message")); } + + @Test + void addMessage_with_attachment() throws Exception { + FileWriter writer = new FileWriter("filename.txt"); + writer.write("some file content"); + writer.close(); + File file = new File("filename.txt"); + + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("operations", "{\"query\":\"mutation addMessage($input: AddMessageInput!) { addMessage(input: $input) { id text userId createdAt username uuid quotedId filename path quotedMessage { id text username filename path }}}\",\"variables\":{ \"input\": {\"text\": \"rr\", \"uuid\": \"83f92165-f0c6-40c1-9764-6325e9548799\", \"attachment\": null}}}"); + params.add("map", "{\"0\": [\"variables.input.attachment\"]}"); + params.add("0", new FileSystemResource(file)); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + ResponseEntity response = restTemplate.exchange( + "/graphql", + HttpMethod.POST, + new HttpEntity<>(params, headers), + String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(response.getBody()); + MessagePayload messagePayload = objectMapper.readValue(jsonNode.findPath("addMessage").traverse(), MessagePayload.class); + + assertNotNull(messagePayload.getId()); + assertNotNull(messagePayload.getText()); + assertFalse(messagePayload.getCreatedAt().isBlank()); + assertEquals(file.getName(), messagePayload.getFilename()); + assertEquals("files/" + file.getName(), messagePayload.getPath()); + + Files.deleteIfExists(file.toPath()); + Files.deleteIfExists(new File(messagePayload.getPath()).toPath()); + } } diff --git a/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java index 39b69d68f5..f0529a2446 100644 --- a/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java +++ b/modules/chat/server-java/src/test/java/com/sysgears/chat/resolvers/MessageQueryResolverTest.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.util.ArrayList; @@ -35,6 +36,7 @@ class MessageQueryResolverTest { private List messageList; @BeforeEach void init() { + messageRepository.deleteAll(); messageList = prepare(); } diff --git a/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java b/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java index 8be6fffb12..ec2eb918f0 100644 --- a/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java +++ b/modules/upload/server-java/src/main/java/com/sysgears/upload/dto/File.java @@ -1,18 +1,22 @@ package com.sysgears.upload.dto; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; @Data +@NoArgsConstructor +@AllArgsConstructor public class File { @NonNull - private final Integer id; + private Integer id; @NonNull - private final String name; + private String name; @NonNull - private final String type; + private String type; @NonNull - private final Long size; + private Long size; @NonNull - private final String path; + private String path; } diff --git a/modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadMutationResolverTest.java b/modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadMutationResolverTest.java new file mode 100644 index 0000000000..8ce7594377 --- /dev/null +++ b/modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadMutationResolverTest.java @@ -0,0 +1,110 @@ +package com.sysgears.upload.resolvers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.upload.model.FileMetadata; +import com.sysgears.upload.repository.FileMetadataRepository; +import org.apache.tomcat.util.http.fileupload.FileUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class UploadMutationResolverTest { + @Autowired + private GraphQLTestTemplate template; + @Autowired + private TestRestTemplate restTemplate; + @Autowired + private FileMetadataRepository fileMetadataRepository; + + @Test + void uploadFiles() throws Exception { + List files = prepare("file1", "file2"); + + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("operations", "{ \"query\": \"mutation uploadFiles($files: [FileUpload!]!) { uploadFiles(files: $files) }\", \"variables\": { \"files\": [null, null] } }"); + params.add("map", "{\"0\": [\"variables.files.0\"], \"1\": [\"variables.files.1\"]}"); + params.add("0", new FileSystemResource(files.get(0))); + params.add("1", new FileSystemResource(files.get(1))); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + ResponseEntity response = restTemplate.exchange( + "/graphql", + HttpMethod.POST, + new HttpEntity<>(params, headers), + String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + + List all = fileMetadataRepository.findAll(); + assertEquals(2, all.size()); + assertEquals(files.get(0).getName(), all.get(0).getName()); + assertEquals(files.get(1).getName(), all.get(1).getName()); + + // remove directories and files after test + FileUtils.deleteDirectory(files.get(0).getParentFile()); + FileUtils.deleteDirectory(new File(all.get(0).getPath()).getParentFile()); + } + + @Test + void removeFile() throws Exception { + File file = prepare("file1").get(0); + + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("operations", "{ \"query\": \"mutation uploadFiles($files: [FileUpload!]!) { uploadFiles(files: $files) }\", \"variables\": { \"files\": [null] } }"); + params.add("map", "{\"0\": [\"variables.files.0\"]}"); + params.add("0", new FileSystemResource(file)); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + restTemplate.exchange( + "/graphql", + HttpMethod.POST, + new HttpEntity<>(params, headers), + String.class); + + List all = fileMetadataRepository.findAll(); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", all.get(0).getId()); + + template.perform("remove-file.graphql", node); + + assertFalse(fileMetadataRepository.existsById(all.get(0).getId())); + + FileUtils.deleteDirectory(file.getParentFile()); + } + + private List prepare(String... filenames) throws IOException { + List files = new ArrayList<>(); + Paths.get("src/test/resources/upload-files/").toFile().mkdirs(); //create directories if not exist + for (String filename : filenames) { + FileWriter writer = new FileWriter("src/test/resources/upload-files/" + filename + ".txt"); + writer.write("some file content"); + writer.close(); + files.add(new File("src/test/resources/upload-files/" + filename + ".txt")); + } + return files; + } +} diff --git a/modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadQueryResolverTest.java b/modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadQueryResolverTest.java new file mode 100644 index 0000000000..9fed3f3116 --- /dev/null +++ b/modules/upload/server-java/src/test/java/com/sysgears/upload/resolvers/UploadQueryResolverTest.java @@ -0,0 +1,71 @@ +package com.sysgears.upload.resolvers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.upload.dto.File; +import com.sysgears.upload.model.FileMetadata; +import com.sysgears.upload.repository.FileMetadataRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase +class UploadQueryResolverTest { + @Autowired + private GraphQLTestTemplate template; + @MockBean + private FileMetadataRepository fileMetadataRepository; + + @Test + void files() throws IOException { + FileMetadata jpegMetadata = new FileMetadata( + "filename", + "image/jpeg", + 123L, + "/fake_path/image.jpeg" + ); + jpegMetadata.setId(1); + FileMetadata pngMetadata = new FileMetadata( + "filename2", + "image/png", + 456L, + "/fake_path/image.png" + ); + pngMetadata.setId(2); + when(fileMetadataRepository.findAll()).thenReturn(List.of(jpegMetadata, pngMetadata)); + + GraphQLResponse response = template.postForResource("get-all-files.graphql"); + assertTrue(response.isOk()); + + String responseBody = response.getRawResponse().getBody(); + ObjectMapper mapper = new ObjectMapper(); + JsonParser filesJson = mapper.readTree(responseBody).findPath("files").traverse(); + List files = mapper.readValue(filesJson, new TypeReference<>() { + }); + + assertEquals(2, files.size()); + assertEquals(jpegMetadata.getId(), files.get(0).getId()); + assertEquals(jpegMetadata.getName(), files.get(0).getName()); + assertEquals(jpegMetadata.getContentType(), files.get(0).getType()); + assertEquals(jpegMetadata.getSize(), files.get(0).getSize()); + assertEquals(jpegMetadata.getPath(), files.get(0).getPath()); + assertEquals(pngMetadata.getId(), files.get(1).getId()); + assertEquals(pngMetadata.getName(), files.get(1).getName()); + assertEquals(pngMetadata.getContentType(), files.get(1).getType()); + assertEquals(pngMetadata.getSize(), files.get(1).getSize()); + assertEquals(pngMetadata.getPath(), files.get(1).getPath()); + } +} diff --git a/modules/upload/server-java/src/test/resources/get-all-files.graphql b/modules/upload/server-java/src/test/resources/get-all-files.graphql new file mode 100644 index 0000000000..90605374fa --- /dev/null +++ b/modules/upload/server-java/src/test/resources/get-all-files.graphql @@ -0,0 +1,9 @@ +query files { + files { + id + name + type + size + path + } +} diff --git a/modules/upload/server-java/src/test/resources/remove-file.graphql b/modules/upload/server-java/src/test/resources/remove-file.graphql new file mode 100644 index 0000000000..e06be69dbd --- /dev/null +++ b/modules/upload/server-java/src/test/resources/remove-file.graphql @@ -0,0 +1,3 @@ +mutation removeFile($id: Int!) { + removeFile(id: $id) +} diff --git a/modules/upload/server-java/src/test/resources/upload-files.graphql b/modules/upload/server-java/src/test/resources/upload-files.graphql new file mode 100644 index 0000000000..06d8224db1 --- /dev/null +++ b/modules/upload/server-java/src/test/resources/upload-files.graphql @@ -0,0 +1,4 @@ +mutation uploadFiles($files: [FileUpload]!) { + uploadFiles(files: $files) { + } +} From 301debf6bb703b9e072f227c876a7f731ba70561 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 19 Feb 2021 13:51:31 +0200 Subject: [PATCH 83/85] implemented i18n module --- modules/i18n/server-java/build.gradle | 0 .../com/sysgears/config/LocaleConfig.java | 34 +++++++++++++++++++ .../service/DefaultMessageResolver.java | 17 ++++++++++ .../com/sysgears/service/MessageResolver.java | 6 ++++ .../app/src/main/resources/application.yml | 2 ++ .../main/resources/i18n/messages.properties | 0 .../resources/i18n/messages_ru.properties | 0 packages/server-java/build.gradle | 1 + packages/server-java/settings.gradle | 3 +- 9 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 modules/i18n/server-java/build.gradle create mode 100644 modules/i18n/server-java/src/main/java/com/sysgears/config/LocaleConfig.java create mode 100644 modules/i18n/server-java/src/main/java/com/sysgears/service/DefaultMessageResolver.java create mode 100644 modules/i18n/server-java/src/main/java/com/sysgears/service/MessageResolver.java create mode 100644 packages/server-java/app/src/main/resources/i18n/messages.properties create mode 100644 packages/server-java/app/src/main/resources/i18n/messages_ru.properties diff --git a/modules/i18n/server-java/build.gradle b/modules/i18n/server-java/build.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/i18n/server-java/src/main/java/com/sysgears/config/LocaleConfig.java b/modules/i18n/server-java/src/main/java/com/sysgears/config/LocaleConfig.java new file mode 100644 index 0000000000..c1a0300820 --- /dev/null +++ b/modules/i18n/server-java/src/main/java/com/sysgears/config/LocaleConfig.java @@ -0,0 +1,34 @@ +package com.sysgears.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +import java.util.Locale; + +@Configuration +public class LocaleConfig implements WebMvcConfigurer { + + @Bean + public LocaleResolver localeResolver() { + CookieLocaleResolver localeResolver = new CookieLocaleResolver(); + localeResolver.setDefaultLocale(Locale.US); + return localeResolver; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); + interceptor.setParamName("lang"); + return interceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()).addPathPatterns("/**"); + } +} diff --git a/modules/i18n/server-java/src/main/java/com/sysgears/service/DefaultMessageResolver.java b/modules/i18n/server-java/src/main/java/com/sysgears/service/DefaultMessageResolver.java new file mode 100644 index 0000000000..1813d523c8 --- /dev/null +++ b/modules/i18n/server-java/src/main/java/com/sysgears/service/DefaultMessageResolver.java @@ -0,0 +1,17 @@ +package com.sysgears.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DefaultMessageResolver implements MessageResolver { + private final MessageSource messageSource; + + @Override + public String getLocalisedMessage(String code, Object... interpolationArguments) { + return messageSource.getMessage(code, interpolationArguments, LocaleContextHolder.getLocale()); + } +} diff --git a/modules/i18n/server-java/src/main/java/com/sysgears/service/MessageResolver.java b/modules/i18n/server-java/src/main/java/com/sysgears/service/MessageResolver.java new file mode 100644 index 0000000000..4f00ba6fd4 --- /dev/null +++ b/modules/i18n/server-java/src/main/java/com/sysgears/service/MessageResolver.java @@ -0,0 +1,6 @@ +package com.sysgears.service; + +public interface MessageResolver { + + String getLocalisedMessage(String code, Object... interpolationArguments); +} diff --git a/packages/server-java/app/src/main/resources/application.yml b/packages/server-java/app/src/main/resources/application.yml index 0be8417fe9..d37b8b0697 100644 --- a/packages/server-java/app/src/main/resources/application.yml +++ b/packages/server-java/app/src/main/resources/application.yml @@ -32,6 +32,8 @@ spring: multipart: max-file-size: 50MB max-request-size: 50MB + messages: + basename: i18n/messages graphql: servlet: exception-handlers-enabled: true diff --git a/packages/server-java/app/src/main/resources/i18n/messages.properties b/packages/server-java/app/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server-java/app/src/main/resources/i18n/messages_ru.properties b/packages/server-java/app/src/main/resources/i18n/messages_ru.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server-java/build.gradle b/packages/server-java/build.gradle index b44bd3ef5a..4d87df4fb1 100644 --- a/packages/server-java/build.gradle +++ b/packages/server-java/build.gradle @@ -84,6 +84,7 @@ project(':app') { implementation project(':post') implementation project(':chat') implementation project(':reports') + implementation project(':i18n') } bootJar { enabled = true diff --git a/packages/server-java/settings.gradle b/packages/server-java/settings.gradle index 1a8cf51fca..efb6a031ad 100644 --- a/packages/server-java/settings.gradle +++ b/packages/server-java/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'server-java' -include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post', 'chat', 'reports' +include ':app', ':core', ':counter', ':user', ':authentication', 'mailer', 'contact', 'upload', 'post', 'chat', 'reports', 'i18n' project(':core').projectDir = new File('../../modules/core/server-java') project(':counter').projectDir = new File('../../modules/counter/server-java') @@ -11,3 +11,4 @@ project(':upload').projectDir = new File('../../modules/upload/server-java') project(':post').projectDir = new File('../../modules/post/server-java') project(':chat').projectDir = new File('../../modules/chat/server-java') project(':reports').projectDir = new File('../../modules/reports/server-java') +project(':i18n').projectDir = new File('../../modules/i18n/server-java') From 576f8cfab31816387ffb0f6d8f609b255d904567 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 19 Feb 2021 13:52:04 +0200 Subject: [PATCH 84/85] added i18n support for user module --- modules/core/server-java/build.gradle | 3 + .../user/exception/LoginFailedException.java | 4 +- .../exception/ResetPasswordException.java | 4 +- .../exception/UserAlreadyExistsException.java | 4 +- .../user/exception/UserDeletionException.java | 8 + .../user/exception/UserNotFoundException.java | 11 +- .../user/resolvers/UserMutationResolver.java | 15 +- .../password/LoginMutationResolver.java | 33 +++- .../sysgears/user/rest/UserController.java | 8 +- .../sysgears/user/service/UserService.java | 158 +++++++++--------- .../main/resources/i18n/messages.properties | 15 ++ .../resources/i18n/messages_ru.properties | 15 ++ 12 files changed, 175 insertions(+), 103 deletions(-) create mode 100644 modules/user/server-java/src/main/java/com/sysgears/user/exception/UserDeletionException.java diff --git a/modules/core/server-java/build.gradle b/modules/core/server-java/build.gradle index e69de29bb2..9e196bc443 100644 --- a/modules/core/server-java/build.gradle +++ b/modules/core/server-java/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':i18n') +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java index f7d6695b4e..4a6555d924 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/LoginFailedException.java @@ -6,7 +6,7 @@ public class LoginFailedException extends FieldErrorException { - public LoginFailedException(Map errors) { - super("Login failed.", errors); + public LoginFailedException(String message, Map errors) { + super(message, errors); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java index 8623910a96..8f308b6509 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/ResetPasswordException.java @@ -6,7 +6,7 @@ public class ResetPasswordException extends FieldErrorException { - public ResetPasswordException(Map errors) { - super("Failed reset password", errors); + public ResetPasswordException(String message, Map errors) { + super(message, errors); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java index 94aac46de8..0081403071 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserAlreadyExistsException.java @@ -6,7 +6,7 @@ public class UserAlreadyExistsException extends FieldErrorException { - public UserAlreadyExistsException(Map errors) { - super("User already exists.", errors); + public UserAlreadyExistsException(String message, Map errors) { + super(message, errors); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserDeletionException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserDeletionException.java new file mode 100644 index 0000000000..bde035ef40 --- /dev/null +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserDeletionException.java @@ -0,0 +1,8 @@ +package com.sysgears.user.exception; + +public class UserDeletionException extends RuntimeException { + + public UserDeletionException(String message) { + super(message); + } +} diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java index d67595d91a..e59e9aebb0 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/exception/UserNotFoundException.java @@ -9,15 +9,12 @@ @ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends FieldErrorException { - public UserNotFoundException() { - super("No user found", Collections.emptyMap()); - } - public UserNotFoundException(int id) { - super(String.format("User with id %d not found.", id), Collections.emptyMap()); + public UserNotFoundException(String message) { + super(message, Collections.emptyMap()); } - public UserNotFoundException(Map errors) { - super("No user found", errors); + public UserNotFoundException(String message, Map errors) { + super(message, errors); } } diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index fb278d8647..b137479432 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -1,11 +1,14 @@ package com.sysgears.user.resolvers; +import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.core.subscription.Publisher; +import com.sysgears.service.MessageResolver; import com.sysgears.user.dto.UserPayload; import com.sysgears.user.dto.input.AddUserInput; import com.sysgears.user.dto.input.EditUserInput; import com.sysgears.user.dto.input.ProfileInput; import com.sysgears.user.dto.input.auth.AuthInput; +import com.sysgears.user.exception.UserDeletionException; import com.sysgears.user.exception.UserNotFoundException; import com.sysgears.user.model.User; import com.sysgears.user.model.UserAuth; @@ -28,6 +31,7 @@ public class UserMutationResolver implements GraphQLMutationResolver { private final UserService userService; private final Publisher publisher; private final PasswordEncoder passwordEncoder; + private final MessageResolver messageResolver; public CompletableFuture addUser(AddUserInput input) { return CompletableFuture.supplyAsync(()-> { @@ -58,7 +62,7 @@ public CompletableFuture addUser(AddUserInput input) { public CompletableFuture editUser(EditUserInput input) { return userService.findUserById(input.getId()) .thenApplyAsync(user -> { - if (user == null) throw new UserNotFoundException(input.getId()); + if (user == null) throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userWithIdNotExists", input.getId())); user.setUsername(input.getUsername()); user.setRole(input.getRole()); @@ -109,7 +113,14 @@ public CompletableFuture editUser(EditUserInput input) { public CompletableFuture deleteUser(int id) { return userService.findUserById(id).thenApplyAsync(user -> { - if (user == null) throw new UserNotFoundException(id); + if (user == null) throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userWithIdNotExists", id)); + + User currentUser = (User) SessionUtils.SECURITY_CONTEXT.getAuthentication().getPrincipal(); + if (currentUser == null || !currentUser.getRole().equals("admin")) { + throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.notEnoughPermission")); + } else if (currentUser.getId() == user.getId()){ + throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.cannotDeleteYourself")); + } userService.delete(user); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java index cfad89bd1d..315a5758fa 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/password/LoginMutationResolver.java @@ -5,6 +5,7 @@ import com.sysgears.authentication.service.jwt.JwtParser; import com.sysgears.authentication.utils.SessionUtils; import com.sysgears.mailer.service.EmailService; +import com.sysgears.service.MessageResolver; import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.AuthPayload; import com.sysgears.user.dto.UserPayload; @@ -42,6 +43,7 @@ public class LoginMutationResolver implements GraphQLMutationResolver { private final JwtGenerator jwtGenerator; private final JwtParser jwtParser; private final EmailService emailService; + private final MessageResolver messageResolver; @Transactional(readOnly = true) public CompletableFuture login(LoginUserInput loginUserInput) { @@ -50,7 +52,10 @@ public CompletableFuture login(LoginUserInput loginUserInput) { boolean matches = passwordEncoder.matches(loginUserInput.getPassword(), user.getPassword()); if (!matches) { log.debug("Password is invalid"); - throw new LoginFailedException(Map.of("password", "Please enter a valid password.")); + throw new LoginFailedException( + messageResolver.getLocalisedMessage("errors.login.failed"), + Map.of("password", messageResolver.getLocalisedMessage("errors.login.invalidPassword")) + ); } Tokens tokens = jwtGenerator.generateTokens(UserIdentityUtils.convert(user)); @@ -61,7 +66,10 @@ public CompletableFuture login(LoginUserInput loginUserInput) { .exceptionally(throwable -> { if (throwable.getCause() instanceof NoResultException) { log.warn("Specified email or username '{}' is invalid.", loginUserInput.getUsernameOrEmail()); - throw new LoginFailedException(Map.of("usernameOrEmail", "Please enter a valid username or e-mail.")); + throw new LoginFailedException( + messageResolver.getLocalisedMessage("errors.login.failed"), + Map.of("usernameOrEmail", messageResolver.getLocalisedMessage("errors.login.invalidUsernameOrEmail")) + ); } else { log.error("Unexpected error happens when find user with " + loginUserInput.getUsernameOrEmail(), throwable); throw (CompletionException) throwable; @@ -71,7 +79,10 @@ public CompletableFuture login(LoginUserInput loginUserInput) { public CompletableFuture forgotPassword(ForgotPasswordInput input) { if (!userService.existsByEmail(input.getEmail())) { - throw new UserNotFoundException(Map.of("email", "No user with specified email.")); + throw new UserNotFoundException( + messageResolver.getLocalisedMessage("errors.userNotExists"), + Map.of("email", messageResolver.getLocalisedMessage("errors.forgotPassword.noUserWithEmail")) + ); } return userService.findUserByUsernameOrEmail(input.getEmail()).thenApply(user -> { @@ -91,10 +102,13 @@ public CompletableFuture resetPassword(ResetPasswordInput input) { .thenCompose(userService::findUserById) .thenCompose(user -> { if (user == null) { - throw new UserNotFoundException(); + throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userNotExists")); } if (!input.getPassword().equals(input.getPasswordConfirmation())) { - throw new ResetPasswordException(Map.of("passwordConfirmation", "Must match the field 'password'")); + throw new ResetPasswordException( + messageResolver.getLocalisedMessage("errors.resetPassword.failed"), + Map.of("passwordConfirmation", messageResolver.getLocalisedMessage("errors.resetPassword.passwordsIsNotMatch")) + ); } user.setPassword(passwordEncoder.encode(input.getPassword())); @@ -110,13 +124,16 @@ public CompletableFuture register(RegisterUserInput input) { return CompletableFuture.supplyAsync(() -> { Map errors = new HashMap<>(); if (userService.existsByEmail(input.getEmail())) { - errors.put("email", "E-mail already exists."); + errors.put("email", messageResolver.getLocalisedMessage("errors.register.emailIsExisted")); } if (userService.existsByUsername(input.getUsername())) { - errors.put("username", "Username already exists."); + errors.put("username", messageResolver.getLocalisedMessage("errors.register.usernameIsExisted")); } if (!errors.isEmpty()) { - throw new UserAlreadyExistsException(errors); + throw new UserAlreadyExistsException( + messageResolver.getLocalisedMessage("errors.userAlreadyExists"), + errors + ); } User user = new User( diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java index 65467d4e13..4eb17552c2 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/rest/UserController.java @@ -1,6 +1,7 @@ package com.sysgears.user.rest; import com.sysgears.authentication.service.jwt.JwtParser; +import com.sysgears.service.MessageResolver; import com.sysgears.user.exception.UserNotFoundException; import com.sysgears.user.service.UserService; import org.springframework.beans.factory.annotation.Value; @@ -18,15 +19,18 @@ public class UserController { private final UserService userService; private final JwtParser jwtParser; + private final MessageResolver messageResolver; private final String confirmRegistrationRedirectUrl; private final String resetPasswordRedirectUrl; public UserController(UserService userService, JwtParser jwtParser, + MessageResolver messageResolver, @Value("${app.redirect.confirm-registration}") String confirmRegistrationRedirectUrl, @Value("${app.redirect.reset-password}") String resetPasswordRedirectUrl) { this.userService = userService; this.jwtParser = jwtParser; + this.messageResolver = messageResolver; this.confirmRegistrationRedirectUrl = confirmRegistrationRedirectUrl; this.resetPasswordRedirectUrl = resetPasswordRedirectUrl; } @@ -35,7 +39,7 @@ public UserController(UserService userService, public RedirectView confirmRegistration(@RequestParam String key) { Integer userId = jwtParser.getIdFromVerificationToken(key); return userService.findUserById(userId).thenApply(user -> { - if (user == null) throw new UserNotFoundException(); + if (user == null) throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userNotExists")); user.setIsActive(true); userService.save(user); @@ -49,7 +53,7 @@ public RedirectView confirmRegistration(@RequestParam String key) { public RedirectView initiateResetPassword(@RequestParam String key) { Integer userId = jwtParser.getIdFromVerificationToken(key); return userService.findUserById(userId).thenApply(user -> { - if (user == null) throw new UserNotFoundException(); + if (user == null) throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userNotExists")); String base64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8)); return new RedirectView(resetPasswordRedirectUrl + base64Key); diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java index 3fff0dff3f..8ab6792d0b 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/service/UserService.java @@ -4,6 +4,7 @@ import com.sysgears.authentication.resolvers.jwt.JwtUserIdentityService; import com.sysgears.authentication.service.jwt.JwtParser; import com.sysgears.authentication.utils.SessionUtils; +import com.sysgears.service.MessageResolver; import com.sysgears.user.dto.input.FilterUserInput; import com.sysgears.user.dto.input.OrderByUserInput; import com.sysgears.user.exception.UserNotFoundException; @@ -32,82 +33,83 @@ @RequiredArgsConstructor public class UserService implements UserDetailsService, AuditorAware, JwtUserIdentityService { - private final UserRepository userRepository; - private final JwtParser jwtParser; - - @Override - @Transactional(readOnly = true) - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return findUserByUsernameOrEmail(username).handle((user, throwable) -> { - if (throwable != null) { - if (throwable.getCause() instanceof NoResultException) { - throw new UsernameNotFoundException(String.format("User with username '%s' not found.", username)); - } - log.error(String.format("Unexpected error happened when load user by username '%s'", username), throwable); - } - return user; - }).join(); - } - - @Transactional(readOnly = true) - public CompletableFuture findUserByUsernameOrEmail(String usernameOrEmail) { - return userRepository.findByUsernameOrEmail(usernameOrEmail); - } - - @NonNull - @Override - public Optional getCurrentAuditor() { - final Authentication authentication = SessionUtils.SECURITY_CONTEXT.getAuthentication(); - - if (authentication == null) { - // no user - return Optional.empty(); - } - - if (authentication.getPrincipal() instanceof User) { - return Optional.of((User) SessionUtils.SECURITY_CONTEXT.getAuthentication().getPrincipal()); - } else { - // anonymous user - return Optional.empty(); - } - } - - @Transactional(readOnly = true) - public User loadUserByToken(String token) { - Integer userId = jwtParser.getIdFromAccessToken(token); - return userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - } - - @Transactional(readOnly = true) - public CompletableFuture> findByCriteria(Optional orderBy, Optional filter) { - return userRepository.findByCriteria(orderBy, filter); - } - - public User save(User user) { - return userRepository.save(user); - } - - @Transactional(readOnly = true) - public CompletableFuture findUserById(Integer id) { - return userRepository.findUserById(id); - } - - public void delete(User user) { - userRepository.delete(user); - } - - @Override - public Optional findById(Integer userId) { - return userRepository.findById(userId).map(UserIdentityUtils::convert); - } - - @Transactional(readOnly = true) - public Boolean existsByEmail(String email) { - return userRepository.existsByEmail(email); - } - - @Transactional(readOnly = true) - public Boolean existsByUsername(String username) { - return userRepository.existsByUsername(username); - } + private final UserRepository userRepository; + private final JwtParser jwtParser; + private final MessageResolver messageResolver; + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return findUserByUsernameOrEmail(username).handle((user, throwable) -> { + if (throwable != null) { + if (throwable.getCause() instanceof NoResultException) { + throw new UsernameNotFoundException(messageResolver.getLocalisedMessage("errors.userWithUsernameNotExists", username)); + } + log.error(String.format("Unexpected error happened when load user by username '%s'", username), throwable); + } + return user; + }).join(); + } + + @Transactional(readOnly = true) + public CompletableFuture findUserByUsernameOrEmail(String usernameOrEmail) { + return userRepository.findByUsernameOrEmail(usernameOrEmail); + } + + @NonNull + @Override + public Optional getCurrentAuditor() { + final Authentication authentication = SessionUtils.SECURITY_CONTEXT.getAuthentication(); + + if (authentication == null) { + // no user + return Optional.empty(); + } + + if (authentication.getPrincipal() instanceof User) { + return Optional.of((User) SessionUtils.SECURITY_CONTEXT.getAuthentication().getPrincipal()); + } else { + // anonymous user + return Optional.empty(); + } + } + + @Transactional(readOnly = true) + public User loadUserByToken(String token) { + Integer userId = jwtParser.getIdFromAccessToken(token); + return userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userNotExists"))); + } + + @Transactional(readOnly = true) + public CompletableFuture> findByCriteria(Optional orderBy, Optional filter) { + return userRepository.findByCriteria(orderBy, filter); + } + + public User save(User user) { + return userRepository.save(user); + } + + @Transactional(readOnly = true) + public CompletableFuture findUserById(Integer id) { + return userRepository.findUserById(id); + } + + public void delete(User user) { + userRepository.delete(user); + } + + @Override + public Optional findById(Integer userId) { + return userRepository.findById(userId).map(UserIdentityUtils::convert); + } + + @Transactional(readOnly = true) + public Boolean existsByEmail(String email) { + return userRepository.existsByEmail(email); + } + + @Transactional(readOnly = true) + public Boolean existsByUsername(String username) { + return userRepository.existsByUsername(username); + } } diff --git a/packages/server-java/app/src/main/resources/i18n/messages.properties b/packages/server-java/app/src/main/resources/i18n/messages.properties index e69de29bb2..b41fcac1b1 100644 --- a/packages/server-java/app/src/main/resources/i18n/messages.properties +++ b/packages/server-java/app/src/main/resources/i18n/messages.properties @@ -0,0 +1,15 @@ +# User module +errors.login.failed=Login failed. +errors.login.invalidPassword=Please enter a valid password. +errors.login.invalidUsernameOrEmail=Please enter a valid username or e-mail. +errors.forgotPassword.noUserWithEmail=No user with specified email. +errors.resetPassword.failed=Failed reset password +errors.resetPassword.passwordsIsNotMatch=Passwords do not match. +errors.register.emailIsExisted=E-mail already exists. +errors.register.usernameIsExisted=Username already exists. +errors.userNotExists=User does not exist. +errors.userWithIdNotExists=User with id {0} does not exist. +errors.userWithUsernameNotExists=User with username {0} does not exist. +errors.userAlreadyExists=User already exists. +errors.deleteUser.cannotDeleteYourself=You can not delete your self. +errors.deleteUser.notEnoughPermission=You have not enough permission to delete users. diff --git a/packages/server-java/app/src/main/resources/i18n/messages_ru.properties b/packages/server-java/app/src/main/resources/i18n/messages_ru.properties index e69de29bb2..fe624e7fd7 100644 --- a/packages/server-java/app/src/main/resources/i18n/messages_ru.properties +++ b/packages/server-java/app/src/main/resources/i18n/messages_ru.properties @@ -0,0 +1,15 @@ +# User module +errors.login.failed=Не удалось войти. +errors.login.invalidPassword=Пожалуйста, введите действительный пароль. +errors.login.invalidUsernameOrEmail=Пожалуйста, введите Ваш username или e-mail. +errors.forgotPassword.noUserWithEmail=Пользователя с таким e-mail не существует. +errors.resetPassword.failed=Не удалось сбросить пароль +errors.resetPassword.passwordsIsNotMatch=Пароли не совпадают. +errors.register.emailIsExisted=Данный e-mail уже используется. +errors.register.usernameIsExisted=Данное имя пользователя уже используется. +errors.userNotExists=Такого пользователя не существует. +errors.userWithIdNotExists=Пользователя с идентификатором {0} не существует. +errors.userWithUsernameNotExists=Пользователя с именем {0} не существует. +errors.userAlreadyExists=Такой пользователь уже существует. +errors.deleteUser.cannotDeleteYourself=Вы не можете удалить себя. +errors.deleteUser.notEnoughPermission=У вас недостаточно прав для удаления пользоваетелей. From 3e6635850aa5c8e0bc8c1976229a311aac9b95f8 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Fri, 19 Feb 2021 15:59:51 +0200 Subject: [PATCH 85/85] fixed condition for permissions when deleting user; fixed test and added new ones. --- .../user/resolvers/UserMutationResolver.java | 252 ++++++------ .../user/resolvers/LoginMutationTest.java | 6 +- .../user/resolvers/UserMutationTest.java | 368 ++++++++++-------- .../main/resources/i18n/messages.properties | 2 +- 4 files changed, 348 insertions(+), 280 deletions(-) diff --git a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java index b137479432..5c26d354fb 100644 --- a/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java +++ b/modules/user/server-java/src/main/java/com/sysgears/user/resolvers/UserMutationResolver.java @@ -19,6 +19,7 @@ import graphql.kickstart.tools.GraphQLMutationResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -28,127 +29,132 @@ @Component @RequiredArgsConstructor public class UserMutationResolver implements GraphQLMutationResolver { - private final UserService userService; - private final Publisher publisher; - private final PasswordEncoder passwordEncoder; - private final MessageResolver messageResolver; - - public CompletableFuture addUser(AddUserInput input) { - return CompletableFuture.supplyAsync(()-> { - User user = new User( - input.getUsername(), - passwordEncoder.encode(input.getPassword()), - input.getRole(), - input.getIsActive(), - input.getEmail() - ); - input.getProfile().map(profileInput -> - new UserProfile( - profileInput.getFirstName().orElse(""), - profileInput.getLastName().orElse("") - ) - ).ifPresent(user::setProfile); - - input.getAuth() - .map(this::from) - .ifPresent(user::setAuth); - userService.save(user); - - publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.ADD_USER, user)); - return new UserPayload(user); - }); - } - - public CompletableFuture editUser(EditUserInput input) { - return userService.findUserById(input.getId()) - .thenApplyAsync(user -> { - if (user == null) throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userWithIdNotExists", input.getId())); - - user.setUsername(input.getUsername()); - user.setRole(input.getRole()); - user.setEmail(input.getEmail()); - - input.getIsActive().ifPresent(user::setIsActive); - input.getPassword().ifPresent(password -> user.setPassword(passwordEncoder.encode(password))); - - if (user.getProfile() != null) { - input.getProfile().ifPresent(profile -> { - profile.getFirstName().ifPresent(fn -> user.getProfile().setFirstName(fn)); - profile.getLastName().ifPresent(ln -> user.getProfile().setLastName(ln)); - - user.getProfile().setFullName( - user.getProfile().getFirstName() - .concat(" ") - .concat(user.getProfile().getLastName()) - ); - }); - } else { - input.getProfile().map(this::from).ifPresent(user::setProfile); - } - - if (user.getAuth() != null) { - input.getAuth().ifPresent(authInput -> { - authInput.getCertificate().ifPresent(cert -> - user.getAuth().setCertificate(new CertificateAuth(cert.getSerial()))); - authInput.getFacebook().ifPresent(fb -> - user.getAuth().setFacebook(new FacebookAuth(fb.getFbId(), fb.getDisplayName()))); - authInput.getLinkedin().ifPresent(li -> - user.getAuth().setLinkedin(new LinkedInAuth(li.getLnId(), li.getDisplayName()))); - authInput.getGoogle().ifPresent(g -> - user.getAuth().setGoogle(new GoogleAuth(g.getGoogleId(), g.getDisplayName()))); - authInput.getGithub().ifPresent(git -> - user.getAuth().setGithub(new GithubAuth(git.getGhId(), git.getDisplayName()))); - }); - } else { - input.getAuth().map(this::from).ifPresent(user::setAuth); - } - - userService.save(user); - - publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.EDIT_USER, user)); - - return new UserPayload(user); - }); - } - - public CompletableFuture deleteUser(int id) { - return userService.findUserById(id).thenApplyAsync(user -> { - if (user == null) throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userWithIdNotExists", id)); - - User currentUser = (User) SessionUtils.SECURITY_CONTEXT.getAuthentication().getPrincipal(); - if (currentUser == null || !currentUser.getRole().equals("admin")) { - throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.notEnoughPermission")); - } else if (currentUser.getId() == user.getId()){ - throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.cannotDeleteYourself")); - } - - userService.delete(user); - - publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.DELETE_USER, user)); - - return new UserPayload(user); - }); - } - - private UserAuth from(AuthInput authInput) { - UserAuth.UserAuthBuilder builder = UserAuth.builder(); - authInput.getCertificate() - .ifPresent(cert -> builder.certificate(new CertificateAuth(cert.getSerial()))); - authInput.getFacebook() - .ifPresent(fb -> builder.facebook(new FacebookAuth(fb.getFbId(), fb.getDisplayName()))); - authInput.getGithub() - .ifPresent(git -> builder.github(new GithubAuth(git.getGhId(), git.getDisplayName()))); - authInput.getGoogle() - .ifPresent(g -> builder.google(new GoogleAuth(g.getGoogleId(), g.getDisplayName()))); - authInput.getLinkedin() - .ifPresent(li -> builder.linkedin(new LinkedInAuth(li.getLnId(), li.getDisplayName()))); - return builder.build(); - } - - private UserProfile from(ProfileInput input) { - return new UserProfile( - input.getFirstName().orElse(""), - input.getLastName().orElse("") - ); - } + private final UserService userService; + private final Publisher publisher; + private final PasswordEncoder passwordEncoder; + private final MessageResolver messageResolver; + + public CompletableFuture addUser(AddUserInput input) { + return CompletableFuture.supplyAsync(() -> { + User user = new User( + input.getUsername(), + passwordEncoder.encode(input.getPassword()), + input.getRole(), + input.getIsActive(), + input.getEmail() + ); + input.getProfile().map(profileInput -> + new UserProfile( + profileInput.getFirstName().orElse(""), + profileInput.getLastName().orElse("") + ) + ).ifPresent(user::setProfile); + + input.getAuth() + .map(this::from) + .ifPresent(user::setAuth); + userService.save(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.ADD_USER, user)); + return new UserPayload(user); + }); + } + + public CompletableFuture editUser(EditUserInput input) { + return userService.findUserById(input.getId()) + .thenApplyAsync(user -> { + if (user == null) + throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userWithIdNotExists", input.getId())); + + user.setUsername(input.getUsername()); + user.setRole(input.getRole()); + user.setEmail(input.getEmail()); + + input.getIsActive().ifPresent(user::setIsActive); + input.getPassword().ifPresent(password -> user.setPassword(passwordEncoder.encode(password))); + + if (user.getProfile() != null) { + input.getProfile().ifPresent(profile -> { + profile.getFirstName().ifPresent(fn -> user.getProfile().setFirstName(fn)); + profile.getLastName().ifPresent(ln -> user.getProfile().setLastName(ln)); + + user.getProfile().setFullName( + user.getProfile().getFirstName() + .concat(" ") + .concat(user.getProfile().getLastName()) + ); + }); + } else { + input.getProfile().map(this::from).ifPresent(user::setProfile); + } + + if (user.getAuth() != null) { + input.getAuth().ifPresent(authInput -> { + authInput.getCertificate().ifPresent(cert -> + user.getAuth().setCertificate(new CertificateAuth(cert.getSerial()))); + authInput.getFacebook().ifPresent(fb -> + user.getAuth().setFacebook(new FacebookAuth(fb.getFbId(), fb.getDisplayName()))); + authInput.getLinkedin().ifPresent(li -> + user.getAuth().setLinkedin(new LinkedInAuth(li.getLnId(), li.getDisplayName()))); + authInput.getGoogle().ifPresent(g -> + user.getAuth().setGoogle(new GoogleAuth(g.getGoogleId(), g.getDisplayName()))); + authInput.getGithub().ifPresent(git -> + user.getAuth().setGithub(new GithubAuth(git.getGhId(), git.getDisplayName()))); + }); + } else { + input.getAuth().map(this::from).ifPresent(user::setAuth); + } + + userService.save(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.EDIT_USER, user)); + + return new UserPayload(user); + }); + } + + public CompletableFuture deleteUser(int id) { + return userService.findUserById(id).thenApplyAsync(user -> { + if (user == null) { + throw new UserNotFoundException(messageResolver.getLocalisedMessage("errors.userWithIdNotExists", id)); + } + + Authentication authentication = SessionUtils.SECURITY_CONTEXT.getAuthentication(); + if (authentication == null) { + throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.notEnoughPermission")); + } else if (!((User) authentication.getPrincipal()).getRole().equals("admin")) { + throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.notEnoughPermission")); + } else if (((User) authentication.getPrincipal()).getId() == user.getId()) { + throw new UserDeletionException(messageResolver.getLocalisedMessage("errors.deleteUser.cannotDeleteYourself")); + } + + userService.delete(user); + + publisher.publish(new UserUpdatedEvent(UserUpdatedEvent.Mutation.DELETE_USER, user)); + + return new UserPayload(user); + }); + } + + private UserAuth from(AuthInput authInput) { + UserAuth.UserAuthBuilder builder = UserAuth.builder(); + authInput.getCertificate() + .ifPresent(cert -> builder.certificate(new CertificateAuth(cert.getSerial()))); + authInput.getFacebook() + .ifPresent(fb -> builder.facebook(new FacebookAuth(fb.getFbId(), fb.getDisplayName()))); + authInput.getGithub() + .ifPresent(git -> builder.github(new GithubAuth(git.getGhId(), git.getDisplayName()))); + authInput.getGoogle() + .ifPresent(g -> builder.google(new GoogleAuth(g.getGoogleId(), g.getDisplayName()))); + authInput.getLinkedin() + .ifPresent(li -> builder.linkedin(new LinkedInAuth(li.getLnId(), li.getDisplayName()))); + return builder.build(); + } + + private UserProfile from(ProfileInput input) { + return new UserProfile( + input.getFirstName().orElse(""), + input.getLastName().orElse("") + ); + } } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java index 5224b284a4..09e509be04 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/LoginMutationTest.java @@ -166,7 +166,7 @@ void forgotPassword_invalid_email() throws IOException { GraphQLResponse response = template.perform("/mutation/forgot-password.graphql", input); assertTrue(response.isOk()); - assertEquals("No user found", response.get("$.errors[0].message")); + assertEquals("User does not exist.", response.get("$.errors[0].message")); assertEquals("No user with specified email.", response.get("$.errors[0].extensions.exception.errors.email")); verify(emailService, never()).sendResetPasswordEmail(anyString(), anyString()); @@ -217,7 +217,7 @@ void resetPassword_user_not_found() throws IOException { GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); assertTrue(response.isOk()); - assertEquals("No user found", response.get("$.errors[0].message")); + assertEquals("User does not exist.", response.get("$.errors[0].message")); verify(emailService, never()).sendPasswordUpdatedEmail(anyString()); } @@ -241,7 +241,7 @@ void resetPassword_specified_password_not_matches() throws IOException { GraphQLResponse response = template.perform("/mutation/reset-password.graphql", input); assertTrue(response.isOk()); assertEquals("Failed reset password", response.get("$.errors[0].message")); - assertEquals("Must match the field 'password'", response.get("$.errors[0].extensions.exception.errors.passwordConfirmation")); + assertEquals("Passwords do not match.", response.get("$.errors[0].extensions.exception.errors.passwordConfirmation")); verify(emailService, never()).sendPasswordUpdatedEmail(anyString()); } diff --git a/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java index 50171c4e68..a631902e54 100644 --- a/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java +++ b/modules/user/server-java/src/test/java/com/sysgears/user/resolvers/UserMutationTest.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.graphql.spring.boot.test.GraphQLResponse; import com.graphql.spring.boot.test.GraphQLTestTemplate; +import com.sysgears.authentication.utils.SessionUtils; +import com.sysgears.user.config.JWTPreAuthenticationToken; import com.sysgears.user.dto.UserPayload; import com.sysgears.user.model.User; import com.sysgears.user.model.UserAuth; @@ -13,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; import java.io.IOException; @@ -20,158 +23,217 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class UserMutationTest { - @Autowired - private GraphQLTestTemplate template; - @Autowired - private UserRepository userRepository; - - @Test - void addUser() throws IOException { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode input = mapper.createObjectNode(); - ObjectNode node = mapper.createObjectNode(); - ObjectNode profile = mapper.createObjectNode(); - - node.put("username", "user"); - node.put("password", "supersecret"); - node.put("role", "USER"); - node.put("isActive", true); - node.put("email", "user@sysgears.com"); - - profile.put("firstName", "Edward"); - profile.put("lastName", "Fillmore"); - node.set("profile", profile); - - input.set("input", node); - GraphQLResponse response = template.perform("mutation/add-user.graphql", input); - - assertTrue(response.isOk()); - UserPayload payload = response.get("$.data.addUser", UserPayload.class); - User createdUser = payload.getUser(); - - assertEquals("user", createdUser.getUsername()); - assertEquals("USER", createdUser.getRole()); - assertTrue(createdUser.getIsActive()); - assertEquals("user@sysgears.com", createdUser.getEmail()); - assertEquals("Edward Fillmore", createdUser.getProfile().getFullName()); - assertNull(createdUser.getAuth()); - } - - @Test - void editUser() throws IOException { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode input = mapper.createObjectNode(); - ObjectNode node = mapper.createObjectNode(); - ObjectNode profile = mapper.createObjectNode(); - ObjectNode auth = mapper.createObjectNode(); - - node.put("id", 1); - node.put("username", "admin"); - node.put("password", "supersecret"); - node.put("role", "ADMIN"); - node.put("isActive", true); - node.put("email", "admin@sysgears.com"); - - profile.put("firstName", "John"); - profile.put("lastName", "Sinna"); - node.set("profile", profile); - - ObjectNode facebook = mapper.createObjectNode(); - facebook.put("fbId", "fb_id"); - facebook.put("displayName", "some"); - ObjectNode google = mapper.createObjectNode(); - google.put("googleId", "g_id"); - google.put("displayName", "google"); - ObjectNode github = mapper.createObjectNode(); - github.put("ghId", "gh_id"); - github.put("displayName", "github"); - ObjectNode certificate = mapper.createObjectNode(); - certificate.put("serial", "some_unique_id"); - ObjectNode linkedin = mapper.createObjectNode(); - linkedin.put("lnId", "ln_id"); - linkedin.put("displayName", "LinkedIn"); - auth.set("facebook", facebook); - auth.set("google", google); - auth.set("github", github); - auth.set("certificate", certificate); - auth.set("linkedin", linkedin); - node.set("auth", auth); - - input.set("input", node); - GraphQLResponse response = template.perform("mutation/edit-user.graphql", input); - - assertTrue(response.isOk()); - UserPayload payload = response.get("$.data.editUser", UserPayload.class); - User createdUser = payload.getUser(); - - assertEquals(1, createdUser.getId()); - assertEquals("admin", createdUser.getUsername()); - assertEquals("ADMIN", createdUser.getRole()); - assertTrue(createdUser.getIsActive()); - assertEquals("admin@sysgears.com", createdUser.getEmail()); - assertEquals("John Sinna", createdUser.getProfile().getFullName()); - - UserAuth userAuth = createdUser.getAuth(); - assertNotNull(userAuth); - assertEquals("some_unique_id", userAuth.getCertificate().getSerial()); - assertEquals("fb_id", userAuth.getFacebook().getFbId()); - assertEquals("some", userAuth.getFacebook().getDisplayName()); - assertEquals("g_id", userAuth.getGoogle().getGoogleId()); - assertEquals("google", userAuth.getGoogle().getDisplayName()); - assertEquals("gh_id", userAuth.getGithub().getGhId()); - assertEquals("github", userAuth.getGithub().getDisplayName()); - assertEquals("ln_id", userAuth.getLinkedin().getLnId()); - assertEquals("LinkedIn", userAuth.getLinkedin().getDisplayName()); - } - - @Test - void editUser_with_already_saved_profile_data() throws IOException { - final User user = userRepository.findById(1).get(); - user.setProfile(new UserProfile("James", "Abrams")); - userRepository.save(user); - - ObjectMapper mapper = new ObjectMapper(); - ObjectNode input = mapper.createObjectNode(); - ObjectNode node = mapper.createObjectNode(); - ObjectNode profile = mapper.createObjectNode(); - - node.put("id", 1); - node.put("username", "admin"); - node.put("role", "ADMIN"); - node.put("isActive", true); - node.put("email", "admin@sysgears.com"); - - profile.put("firstName", "John"); - profile.put("lastName", "Sinna"); - node.set("profile", profile); - - input.set("input", node); - GraphQLResponse response = template.perform("mutation/edit-user.graphql", input); - - assertTrue(response.isOk()); - UserPayload payload = response.get("$.data.editUser", UserPayload.class); - User createdUser = payload.getUser(); - - assertEquals(1, createdUser.getId()); - assertEquals("admin", createdUser.getUsername()); - assertEquals("ADMIN", createdUser.getRole()); - assertTrue(createdUser.getIsActive()); - assertEquals("admin@sysgears.com", createdUser.getEmail()); - assertEquals("John Sinna", createdUser.getProfile().getFullName()); - } - - @Test - void deleteUser() throws IOException { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode node = mapper.createObjectNode(); - node.put("id", 1); - - GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); - - UserPayload payload = response.get("$.data.deleteUser", UserPayload.class); - User deletedUser = payload.getUser(); - - assertEquals(1, deletedUser.getId()); - } + @Autowired + private GraphQLTestTemplate template; + @Autowired + private UserRepository userRepository; + + + @Test + void addUser() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + ObjectNode profile = mapper.createObjectNode(); + + node.put("username", "user"); + node.put("password", "supersecret"); + node.put("role", "USER"); + node.put("isActive", true); + node.put("email", "user@sysgears.com"); + + profile.put("firstName", "Edward"); + profile.put("lastName", "Fillmore"); + node.set("profile", profile); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/add-user.graphql", input); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.addUser", UserPayload.class); + User createdUser = payload.getUser(); + + assertEquals("user", createdUser.getUsername()); + assertEquals("USER", createdUser.getRole()); + assertTrue(createdUser.getIsActive()); + assertEquals("user@sysgears.com", createdUser.getEmail()); + assertEquals("Edward Fillmore", createdUser.getProfile().getFullName()); + assertNull(createdUser.getAuth()); + } + + @Test + void editUser() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + ObjectNode profile = mapper.createObjectNode(); + ObjectNode auth = mapper.createObjectNode(); + + node.put("id", 1); + node.put("username", "admin"); + node.put("password", "supersecret"); + node.put("role", "ADMIN"); + node.put("isActive", true); + node.put("email", "admin@sysgears.com"); + + profile.put("firstName", "John"); + profile.put("lastName", "Sinna"); + node.set("profile", profile); + + ObjectNode facebook = mapper.createObjectNode(); + facebook.put("fbId", "fb_id"); + facebook.put("displayName", "some"); + ObjectNode google = mapper.createObjectNode(); + google.put("googleId", "g_id"); + google.put("displayName", "google"); + ObjectNode github = mapper.createObjectNode(); + github.put("ghId", "gh_id"); + github.put("displayName", "github"); + ObjectNode certificate = mapper.createObjectNode(); + certificate.put("serial", "some_unique_id"); + ObjectNode linkedin = mapper.createObjectNode(); + linkedin.put("lnId", "ln_id"); + linkedin.put("displayName", "LinkedIn"); + auth.set("facebook", facebook); + auth.set("google", google); + auth.set("github", github); + auth.set("certificate", certificate); + auth.set("linkedin", linkedin); + node.set("auth", auth); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/edit-user.graphql", input); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.editUser", UserPayload.class); + User createdUser = payload.getUser(); + + assertEquals(1, createdUser.getId()); + assertEquals("admin", createdUser.getUsername()); + assertEquals("ADMIN", createdUser.getRole()); + assertTrue(createdUser.getIsActive()); + assertEquals("admin@sysgears.com", createdUser.getEmail()); + assertEquals("John Sinna", createdUser.getProfile().getFullName()); + + UserAuth userAuth = createdUser.getAuth(); + assertNotNull(userAuth); + assertEquals("some_unique_id", userAuth.getCertificate().getSerial()); + assertEquals("fb_id", userAuth.getFacebook().getFbId()); + assertEquals("some", userAuth.getFacebook().getDisplayName()); + assertEquals("g_id", userAuth.getGoogle().getGoogleId()); + assertEquals("google", userAuth.getGoogle().getDisplayName()); + assertEquals("gh_id", userAuth.getGithub().getGhId()); + assertEquals("github", userAuth.getGithub().getDisplayName()); + assertEquals("ln_id", userAuth.getLinkedin().getLnId()); + assertEquals("LinkedIn", userAuth.getLinkedin().getDisplayName()); + } + + @Test + void editUser_with_already_saved_profile_data() throws IOException { + final User user = userRepository.findById(1).get(); + user.setProfile(new UserProfile("James", "Abrams")); + userRepository.save(user); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode input = mapper.createObjectNode(); + ObjectNode node = mapper.createObjectNode(); + ObjectNode profile = mapper.createObjectNode(); + + node.put("id", 1); + node.put("username", "admin"); + node.put("role", "ADMIN"); + node.put("isActive", true); + node.put("email", "admin@sysgears.com"); + + profile.put("firstName", "John"); + profile.put("lastName", "Sinna"); + node.set("profile", profile); + + input.set("input", node); + GraphQLResponse response = template.perform("mutation/edit-user.graphql", input); + + assertTrue(response.isOk()); + UserPayload payload = response.get("$.data.editUser", UserPayload.class); + User createdUser = payload.getUser(); + + assertEquals(1, createdUser.getId()); + assertEquals("admin", createdUser.getUsername()); + assertEquals("ADMIN", createdUser.getRole()); + assertTrue(createdUser.getIsActive()); + assertEquals("admin@sysgears.com", createdUser.getEmail()); + assertEquals("John Sinna", createdUser.getProfile().getFullName()); + } + + @Test + void deleteUser() throws IOException { + User admin = userRepository.findByUsernameOrEmail("admin").join(); + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(admin, null)); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 2); + + GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); + + UserPayload payload = response.get("$.data.deleteUser", UserPayload.class); + User deletedUser = payload.getUser(); + + assertEquals(2, deletedUser.getId()); + } + + @Test + void deleteUser_not_exists() throws IOException { + userRepository.deleteById(2); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 2); + + GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); + + assertEquals("User with id 2 does not exist.", response.get("$.errors[0].message")); + } + + @Test + void deleteUser_admin_not_login() throws IOException { + SessionUtils.SECURITY_CONTEXT.setAuthentication(null); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 2); + + GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); + + assertEquals("You have not enough permissions to delete users.", response.get("$.errors[0].message")); + } + + @Test + void deleteUser_not_enough_permission() throws IOException { + User user = userRepository.findByUsernameOrEmail("user").join(); + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(user, null)); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 1); + + GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); + + assertEquals("You have not enough permissions to delete users.", response.get("$.errors[0].message")); + } + + @Test + void deleteUser_try_delete_yourself() throws IOException { + User admin = userRepository.findByUsernameOrEmail("admin").join(); + SessionUtils.SECURITY_CONTEXT.setAuthentication(new JWTPreAuthenticationToken(admin, null)); + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = mapper.createObjectNode(); + node.put("id", 1); + + GraphQLResponse response = template.perform("mutation/delete-user.graphql", node); + + assertEquals("You can not delete your self.", response.get("$.errors[0].message")); + } } diff --git a/packages/server-java/app/src/main/resources/i18n/messages.properties b/packages/server-java/app/src/main/resources/i18n/messages.properties index b41fcac1b1..bd58210624 100644 --- a/packages/server-java/app/src/main/resources/i18n/messages.properties +++ b/packages/server-java/app/src/main/resources/i18n/messages.properties @@ -12,4 +12,4 @@ errors.userWithIdNotExists=User with id {0} does not exist. errors.userWithUsernameNotExists=User with username {0} does not exist. errors.userAlreadyExists=User already exists. errors.deleteUser.cannotDeleteYourself=You can not delete your self. -errors.deleteUser.notEnoughPermission=You have not enough permission to delete users. +errors.deleteUser.notEnoughPermission=You have not enough permissions to delete users.