diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..4293868b
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,73 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ master ]
+ schedule:
+ - cron: '27 0 * * 5'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'java', 'javascript' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+ # Learn more:
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Setup Java JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: 24
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
diff --git a/.gitignore b/.gitignore
index 013ce37f..de09ea1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
target/
!.mvn/wrapper/maven-wrapper.jar
+/src/main/resources/static
### STS ###
#.apt_generated
@@ -21,4 +22,5 @@ build/
nbbuild/
dist/
nbdist/
-.nb-gradle/
\ No newline at end of file
+.nb-gradle/
+/bin/
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..e76d1f32
--- /dev/null
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
index 9cc84ea9..2cc7d4a5 100644
Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index c3150437..bdca1ae0 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1 +1,2 @@
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
\ No newline at end of file
diff --git a/.project b/.project
index cf1bd39d..24ca68a3 100644
--- a/.project
+++ b/.project
@@ -1,15 +1,12 @@
- AngularAndSpring
+ angularandspring
+ angularandspring-backend
+ angularandspring-frontend
-
- org.eclipse.jdt.core.javabuilder
-
-
- org.eclipse.m2e.core.maven2Builder
@@ -17,7 +14,6 @@
- org.eclipse.jdt.core.javanatureorg.eclipse.m2e.core.maven2Nature
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
index 839d647e..99f26c02 100644
--- a/.settings/org.eclipse.core.resources.prefs
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -1,5 +1,2 @@
eclipse.preferences.version=1
-encoding//src/main/java=UTF-8
-encoding//src/main/resources=UTF-8
-encoding//src/test/java=UTF-8
encoding/=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
deleted file mode 100644
index 714351ae..00000000
--- a/.settings/org.eclipse.jdt.core.prefs
+++ /dev/null
@@ -1,5 +0,0 @@
-eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
-org.eclipse.jdt.core.compiler.compliance=1.8
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.8
diff --git a/.settings/org.jboss.ide.eclipse.as.core.prefs b/.settings/org.jboss.ide.eclipse.as.core.prefs
deleted file mode 100644
index cf3aa3a9..00000000
--- a/.settings/org.jboss.ide.eclipse.as.core.prefs
+++ /dev/null
@@ -1,2 +0,0 @@
-eclipse.preferences.version=1
-org.jboss.ide.eclipse.as.core.singledeployable.deployableList=
diff --git a/.settings/ts.eclipse.ide.core.prefs b/.settings/ts.eclipse.ide.core.prefs
deleted file mode 100644
index 370e6988..00000000
--- a/.settings/ts.eclipse.ide.core.prefs
+++ /dev/null
@@ -1,2 +0,0 @@
-eclipse.preferences.version=1
-typeScriptBuildPath={}
diff --git a/.travis.yml b/.travis.yml
index eb2bb3b9..0f48ad4a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,10 @@
language: java
jdk:
- - oraclejdk8
+ - oraclejdk11
+
+addons:
+ chrome: beta
services:
- docker
@@ -13,9 +16,9 @@ notifications:
on_failure: always
before_install:
- - nvm install 6.9
- - nvm use 6.9
+ - nvm install 14.15
+ - nvm use 14.15
script:
- mvn clean install
+ mvn clean install -Ddocker=true
diff --git a/Dockerfile b/Dockerfile
index 9f4e2e20..00f56c09 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
-FROM openjdk:8-jdk-alpine
+FROM eclipse-temurin:24-jdk
VOLUME /tmp
-RUN sh -c 'touch /app.jar'
-ADD target/trader-0.0.1-SNAPSHOT.jar app.jar
-ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:+UseStringDeduplication"
+ARG JAR_FILE
+ADD backend/target/${JAR_FILE} /app.jar
+ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+UseStringDeduplication -XX:MaxDirectMemorySize=64m"
ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar
diff --git a/README.md b/README.md
index c8113a0a..f71f8837 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,74 @@
-# This is an example application to show howto use Spring Boot, Angular and Mongodb with the Webflux features of Spring.
+# This is an example application to show howto use Spring Boot, Angular and Mongodb with the reactive Webflux features of Spring.
-
+[](https://github.com/Angular2Guy/AngularAndSpring/actions/workflows/codeql-analysis.yml)
Author: Sven Loesekann
-Technologies: Angular, Angular-Cli, Angular-Material, Typescript, Spring Boot, Spring Webflux, MongoDB, Maven, Docker
+Technologies: Angular, Angular-Cli, Angular-Material, Typescript, Spring Boot, Spring Webflux, Spring Security, MongoDB, Maven, Docker, ArchUnit, Kafka, Kafka-Streams, Spring Actuator with Prometheus interface
+
+## Articles
+* [Using KRaft Kafka for development and Kubernetes deployment](https://angular2guy.wordpress.com/2024/08/17/using-kraft-kafka-for-development-and-kubernetes-deployment/)
+* [Errorhandling with Spring Webclient and Reactor](https://angular2guy.wordpress.com/2022/10/05/errorhandling-with-spring-webclient-and-reactor/)
+* [Spring Boot 3 update experience](https://angular2guy.wordpress.com/2022/11/15/spring-boot-3-update-experience/)
+* [Reactive Kafka with Streaming in Spring Boot Part 1](https://angular2guy.wordpress.com/2022/05/23/reactive-kafka-with-streaming-in-spring-boot-part-1/)
+* [Reactive Kafka with Streaming in Spring Boot Part 2](https://angular2guy.wordpress.com/2022/06/09/reactive-kafka-with-streaming-in-spring-boot-part-2/)
+* [Reactive Kafka with Streaming in Spring Boot Part 3](https://angular2guy.wordpress.com/2022/06/10/reactive-kafka-with-streaming-in-spring-boot-part-3/)
+* [Performance improvement for Getter/Setter access with LambdaMetafactory](https://angular2guy.wordpress.com/2022/05/12/angularandspring-uses-lambdametafactory-for-getter-setter-access/)
+* [Spring Boot/MongoDb Performance Analysis and Improvements](https://angular2guy.wordpress.com/2022/02/15/spring-boot-mongodb-performance-analysis-and-improvements/)
+* [Ngx-Simple-Charts multiline and legend support howto](https://angular2guy.wordpress.com/2021/10/02/ngx-simple-charts-multiline-and-legend-support-howto/)
+* [Deep Links With Angular Routing and i18n in Prod Mode](https://angular2guy.wordpress.com/2021/07/31/deep-links-with-angular-routing-and-i18n-in-prod-mode/)
+* [Developing and Using Angular Libraries](https://angular2guy.wordpress.com/2021/07/31/developing-and-using-angular-libraries/)
+* [How to Modularize an Angular Application](https://angular2guy.wordpress.com/2022/04/16/how-to-modularize-an-angular-application/)
+* [Deployment Setup for Spring Boot Apps With MongoDB and Kubernetes](https://dzone.com/articles/a-developmentdeployment-setup-for-an-angular-sprin)
+* [Using Angular and Reactive Spring With JWT Tokens](https://dzone.com/articles/angular-and-reactive-spring-with-jwt-tokens)
+* [Angular and Spring Webflux](https://dzone.com/articles/angular-and-spring-webflux)
## What is the goal?
-The goal is to be reactive from top to bottom. To do that the project uses Angular in the frontend and Spring Boot with Reactive Web as server. Mongodb is the database connected with the reactive MongoDB driver. That enables a reactive chain from the browser to the DB.
+The goal is to be reactive from top to bottom. To do that the project uses Angular in the frontend and Spring Boot with Reactive Web as server. Mongodb is the database connected with the reactive MongoDB driver. That enables a reactive chain from the browser to the DB. The security is done with Jwt Tokens and the logged out tokens are invalidated. The project uses an in memory MongoDB to be just cloned build and ready to run. It serves as an example for clean architecture. The architecture is checked with ArchUnit in a test. The health and performance of the application can be monitored with Spring Actuator with Prometheus interface. With the 'kafka' and 'prod' profiles the Kafka support can be used for Jwt token revokation(Minikube setups(development/system) available).
## What is it?
-The application runs a scheduled task reads the exchange rates of cryptocurrencies and stores them in the Mongodb. The UI uses the rest service to read the rates and displays them on a table. The table updates itself regularly. A detail page shows the data of the currency and a chart of the rates of the current day, 7 days, 30 days, 90 days.
-If the user logs in the user can see the relevant part of the orderbooks for an order.
+The application runs a scheduled task reads the exchange rates of cryptocurrencies and stores them in the Mongodb. The UI uses the rest service to read the rates and displays them on a table. The table updates itself regularly and shows out of date data in blue. A detail page shows the data of the currency and a chart of the rates of the current day, 7 days, 30 days, 90 days.
+If the user logs in the user can see the relevant part of the orderbooks for an order. The orderbooks route is implemented as a lazy loading feature module. The route guard checks for the Jwt token and the logout invalidates the Jwt token.
+
+## C4 Architecture Diagrams
+The project has a [System Context Diagram](structurizr/diagrams/structurizr-1-SystemContext.svg), a [Container Diagram](structurizr/diagrams/structurizr-1-Containers.svg) and a [Component Diagram](structurizr/diagrams/structurizr-1-Components.svg). The Diagrams have been created with Structurizr. The file runStructurizr.sh contains the commands to use Structurizr and the directory structurizr contains the dsl file.
## Data Import and Preparation
-The application has two scheduled jobs. The first is the ScheduledTask class. It reads the rates of the crypto currencies once a minute with different initial delays. That job provides one mongodb collection per exchange. The collections can have different documents with currency pairs like Usd to BitCoin or Eur to Ether or one document with all currency pairs, depends on what the exchanges provide. These collections provide the data for the current day chart and the current quote. To display the 7 day, 30 day, 90 day charts, hourly or daily quotes are required. Once a day the PrepareData class runs jobs to calculate the hourly and daily quotes. The jobs run between 0 and 2 o’clock. If no values are available the for the timeframe(hour, day) a value of zero is shown. For the 7 day chart the hourly data is used and for the 30 and 90 day charts the daily data is used. The SchedulingConfig class provides a config that provides the scheduler with 5 threads to enable the running of ScheduledTask class for the imports and the PrepareData class for aggregation concurrently.
+The application has two scheduled jobs. The first is the ScheduledTask class. It reads the rates of the crypto currencies once a minute with different initial delays. That job provides one mongodb collection per exchange. The collections can have different documents with currency pairs like Usd to BitCoin or Eur to Ether or one document with all currency pairs, depends on what the exchanges provide. These collections provide the data for the current day chart and the current quote. To display the 7 day, 30 day, 90 day charts, hourly or daily quotes are required. Once a day the PrepareData class runs jobs to calculate the hourly and daily quotes. The jobs run between 0 and 4 o’clock. If no values are available the for the timeframe(hour, day) a value of zero is shown. For the 7 day chart the hourly data is used and for the 30 and 90 day charts the daily data is used. The Schedulers class provides a an elastic bounded scheduler with enough threads for each client(connection issues) of the ScheduledTask class for the quote imports. The aggregation jobs are run asynchronous(as @Async method) on application startup(@EventListener(ApplicationReadyEvent.class)) and the scheduled runs (@Scheduled(cron=...)) to do the calculation outside of the reactor event loop. The aggregation jobs are started only once(@SchedulerLock) in intervals with @Scheduled to separate them and to reduce the database load.
+
+## Minikube setup
+
+The application can now be run in a Minikube cluster with a Helm chart. The setup has a persistent volume to store the files of mongodb. A setup of mongodb with the volume and a setup for the application. It can be found in the minikube directory as a Helm chart. It uses the resource limit support of Jdk 16+ to limit memory. Kubernetes limits the cpu use and uses the startupprobes and livenessprobes that Spring Actuator provides. A Helm chart for the Kafka development setup in Minikube can be found in the directory 'minikube/kafka'. A Helm chart for the deployment of Kafka/Zookeeper/AngularAndSpring/MongoDb system setup can be found in the directory 'minikube/angularandspringwithkafka'. Further documentation can be found in the [Blog](https://angular2guy.wordpress.com) articles.
+
+## Monitoring
+The Spring Actuator interface with Prometheus interface can be used as it is described in this article:
+
+[Monitoring Spring Boot with Prometheus and Grafana](https://ordina-jworks.github.io/monitoring/2020/11/16/monitoring-spring-prometheus-grafana.html)
+
+To test the setup the application has to be started and the Docker Images for Prometheus and Grafana have to be started and configured. The scripts 'runGraphana.sh' and 'runPrometheus.sh' can be used as a starting point.
+The Spring Actuator configuration shows primarily the http performance and the Gc pauses. More metrics can be enabled in the application.properties file.
+
+## Jvm Memory management
+
+The memory state and other values of the Jvm can be watched with the jstat tool that is included in the jdk. To watch the memory of a running Jvm this command can be used:
+
+jstat -gcutil -h 10 insert_process_id 1000
## Setup
-MongoDB 3.4.x or newer.
+MongoDB 4.4.x or newer.
-Eclipse Oxygen JEE or newer.
+Eclipse IDE for Enterprise Java and Web Developers newest version.
-Plugin Typescript.Java 1.4.0 or newer.
+Java 21 or newer
-Maven 3.3.3 or newer.
+Maven 3.9.5 or newer
-Nodejs 6.9.x or newer
+Nodejs 18.19.x or newer
-Npm 3.10.x or newer
+Npm 10.2.x or newer
-Angular Cli 1.4.0 or newer.
+Angular Cli 18 or newer.
diff --git a/.classpath b/backend/.classpath
similarity index 73%
rename from .classpath
rename to backend/.classpath
index 6d7587a8..0ec47b64 100644
--- a/.classpath
+++ b/backend/.classpath
@@ -9,16 +9,26 @@
+
+
-
+
+
+
+
+
+
+
+
+
diff --git a/backend/.project b/backend/.project
new file mode 100644
index 00000000..014704a5
--- /dev/null
+++ b/backend/.project
@@ -0,0 +1,23 @@
+
+
+ angularandspring-backend
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/backend/.settings/org.eclipse.core.resources.prefs b/backend/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000..29abf999
--- /dev/null
+++ b/backend/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/=UTF-8
diff --git a/backend/.settings/org.eclipse.jdt.core.prefs b/backend/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..36705116
--- /dev/null
+++ b/backend/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,16 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=24
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=21
diff --git a/backend/.settings/org.eclipse.m2e.core.prefs b/backend/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000..f897a7f1
--- /dev/null
+++ b/backend/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/backend/pom.xml b/backend/pom.xml
new file mode 100644
index 00000000..85241211
--- /dev/null
+++ b/backend/pom.xml
@@ -0,0 +1,190 @@
+
+
+ 4.0.0
+ angularandspring-backend
+ jar
+ angularandspring-backend
+ Demo project for Spring Boot
+
+ ch.xxx
+ angularandspring
+ 0.0.1-SNAPSHOT
+
+
+
+ Apache License, Version 2.0
+ repo
+ http://www.apache.org/licenses/LICENSE-2.0.html
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.3.0
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.apache.kafka
+ kafka-streams
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ net.javacrumbs.shedlock
+ shedlock-spring
+ 6.0.1
+
+
+ net.javacrumbs.shedlock
+ shedlock-provider-mongo-reactivestreams
+ 6.0.1
+
+
+ net.sf.jasperreports
+ jasperreports
+ 6.17.0
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
+ com.tngtech.archunit
+ archunit-junit5
+ 1.4.0
+ test
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-maven-plugin
+ 1.3
+
+
+ integration-test
+
+ generate
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.1.0
+
+
+ lifecycle-mapping
+ org.eclipse.m2e
+ 1.0.0
+
+
+
+
+
+ exec-maven-plugin
+ org.codehaus.mojo
+ [3.1.0,)
+
+ exec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/src/main/java/META-INF/MANIFEST.MF b/backend/src/main/java/META-INF/MANIFEST.MF
new file mode 100644
index 00000000..5e949512
--- /dev/null
+++ b/backend/src/main/java/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Class-Path:
+
diff --git a/src/main/java/ch/xxx/trader/TraderApplication.java b/backend/src/main/java/ch/xxx/trader/TraderApplication.java
similarity index 80%
rename from src/main/java/ch/xxx/trader/TraderApplication.java
rename to backend/src/main/java/ch/xxx/trader/TraderApplication.java
index c56bfee8..17420307 100644
--- a/src/main/java/ch/xxx/trader/TraderApplication.java
+++ b/backend/src/main/java/ch/xxx/trader/TraderApplication.java
@@ -17,11 +17,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.scheduling.annotation.EnableScheduling;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Info;
@SpringBootApplication
-@ComponentScan
+@OpenAPIDefinition(info = @Info(title = "Trader API", version = "1.0", description = "Crypto Currency Information"))
public class TraderApplication {
public static void main(String[] args) {
diff --git a/src/main/java/ch/xxx/trader/clients/MongoDbClient.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/MongoDbClient.java
similarity index 83%
rename from src/main/java/ch/xxx/trader/clients/MongoDbClient.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/MongoDbClient.java
index 47353a14..58dd5437 100644
--- a/src/main/java/ch/xxx/trader/clients/MongoDbClient.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/MongoDbClient.java
@@ -13,17 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
-import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.scheduling.annotation.EnableScheduling;
-import ch.xxx.trader.TraderApplication;
-
@SpringBootApplication
@EnableScheduling
@ComponentScan
diff --git a/src/main/java/ch/xxx/trader/clients/MongoDbConfiguration.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/MongoDbConfiguration.java
similarity index 95%
rename from src/main/java/ch/xxx/trader/clients/MongoDbConfiguration.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/MongoDbConfiguration.java
index 25363fea..4d01c368 100644
--- a/src/main/java/ch/xxx/trader/clients/MongoDbConfiguration.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/MongoDbConfiguration.java
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
@@ -27,7 +27,7 @@ public class MongoDbConfiguration extends AbstractReactiveMongoConfiguration {
@Override
protected String getDatabaseName() {
- return "trader-test";
+ return "traderdb";
}
@Override
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/clients/RestOrderBookClient.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/RestOrderBookClient.java
new file mode 100644
index 00000000..5a27be28
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/RestOrderBookClient.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.clients;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import ch.xxx.trader.domain.common.WebUtils;
+import ch.xxx.trader.domain.services.MyOrderBookClient;
+import reactor.core.publisher.Mono;
+
+@Service
+public class RestOrderBookClient implements MyOrderBookClient {
+ private static final String URLBF = "https://api.bitfinex.com";
+ private static final String URLBS = "https://www.bitstamp.net/api";
+ private static final String URLIB = "https://api.itbit.com";
+
+ public Mono getOrderbookBitfinex(String currpair) {
+ WebClient wc = this.buildWebClient(URLBF);
+ return wc.get().uri("/v1/book/" + currpair + "/").accept(MediaType.APPLICATION_JSON)
+ .exchangeToMono(res -> res.bodyToMono(String.class));
+ }
+
+ public Mono getOrderbookBitstamp(String currpair) {
+ WebClient wc = this.buildWebClient(URLBS);
+ return wc.get().uri("/v2/order_book/" + currpair + "/").accept(MediaType.APPLICATION_JSON)
+ .exchangeToMono(res -> res.bodyToMono(String.class));
+ }
+
+ public Mono getOrderbookItbit(String currpair) {
+ WebClient wc = WebUtils.buildWebClient(URLIB);
+ return wc.get().uri("/v1/markets/" + currpair + "/order_book").accept(MediaType.APPLICATION_JSON)
+ .exchangeToMono(res -> res.bodyToMono(String.class));
+ }
+
+ private WebClient buildWebClient(String url) {
+ ReactorClientHttpConnector connector = new ReactorClientHttpConnector();
+ return WebClient.builder().clientConnector(connector).baseUrl(url).build();
+ }
+}
diff --git a/src/main/java/ch/xxx/trader/clients/RestClientBitfinex.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientBitfinex.java
similarity index 83%
rename from src/main/java/ch/xxx/trader/clients/RestClientBitfinex.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientBitfinex.java
index 4e4b4743..2f8ad821 100644
--- a/src/main/java/ch/xxx/trader/clients/RestClientBitfinex.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientBitfinex.java
@@ -13,12 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients.test;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
-import ch.xxx.trader.dtos.QuoteBf;
+import ch.xxx.trader.domain.model.entity.QuoteBf;
public class RestClientBitfinex {
private static final String URL = "https://api.bitfinex.com";
@@ -26,7 +26,7 @@ public class RestClientBitfinex {
public static void main(String[] args) {
WebClient wc = WebClient.create(URL);
QuoteBf quote = wc.get().uri("/v1/pubticker/xrpusd")
- .accept(MediaType.APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(QuoteBf.class))
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> response.bodyToMono(QuoteBf.class))
.map(res -> {res.setPair("xprusd");return res;}).block();
System.out.println(quote.toString());
}
diff --git a/src/main/java/ch/xxx/trader/clients/RestClientBitstamp.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientBitstamp.java
similarity index 83%
rename from src/main/java/ch/xxx/trader/clients/RestClientBitstamp.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientBitstamp.java
index 0829cd2c..5c0c5e16 100644
--- a/src/main/java/ch/xxx/trader/clients/RestClientBitstamp.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientBitstamp.java
@@ -13,12 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients.test;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
-import ch.xxx.trader.dtos.QuoteBs;
+import ch.xxx.trader.domain.model.entity.QuoteBs;
public class RestClientBitstamp {
private static final String URL = "https://www.bitstamp.net/api";
@@ -26,7 +26,7 @@ public class RestClientBitstamp {
public static void main(String[] args) {
WebClient wc = WebClient.create(URL);
QuoteBs quote = wc.get().uri("/v2/ticker/xrpeur/")
- .accept(MediaType.APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(QuoteBs.class))
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> response.bodyToMono(QuoteBs.class))
.map(res -> {res.setPair("xrpeur");return res;}).block();
System.out.println(quote.toString());
}
diff --git a/src/main/java/ch/xxx/trader/clients/RestClientCoinbase.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientCoinbase.java
similarity index 91%
rename from src/main/java/ch/xxx/trader/clients/RestClientCoinbase.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientCoinbase.java
index 7f9527e9..5bcf92b9 100644
--- a/src/main/java/ch/xxx/trader/clients/RestClientCoinbase.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientCoinbase.java
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients.test;
import java.io.BufferedReader;
import java.io.IOException;
@@ -28,8 +28,8 @@
import com.fasterxml.jackson.databind.ObjectMapper;
-import ch.xxx.trader.dtos.QuoteCb;
-import ch.xxx.trader.dtos.WrapperCb;
+import ch.xxx.trader.domain.model.dto.WrapperCb;
+import ch.xxx.trader.domain.model.entity.QuoteCb;
import reactor.core.publisher.Mono;
public class RestClientCoinbase {
@@ -40,8 +40,7 @@ public static void main(String[] args) {
// client.testIt();
WebClient wc = WebClient.create(URL);
QuoteCb quoteCb = wc.get().uri("/exchange-rates?currency=BTC")
- .accept(MediaType.APPLICATION_JSON_UTF8).exchange()
- .flatMap(response -> response.bodyToMono(WrapperCb.class))
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> response.bodyToMono(WrapperCb.class))
.flatMap(resp -> Mono.just(resp.getData()))
.flatMap(resp2 -> Mono.just(resp2.getRates()))
.block();
diff --git a/src/main/java/ch/xxx/trader/clients/RestClientItbit.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientItbit.java
similarity index 82%
rename from src/main/java/ch/xxx/trader/clients/RestClientItbit.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientItbit.java
index a4749e0b..4619cf98 100644
--- a/src/main/java/ch/xxx/trader/clients/RestClientItbit.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/RestClientItbit.java
@@ -13,12 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients.test;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
-import ch.xxx.trader.dtos.QuoteIb;
+import ch.xxx.trader.domain.model.entity.QuoteIb;
public class RestClientItbit {
@@ -27,7 +27,7 @@ public class RestClientItbit {
public static void main(String[] args) {
WebClient wc = WebClient.create(URL);
QuoteIb quote = wc.get().uri("/v1/markets/XBTUSD/ticker")
- .accept(MediaType.APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(QuoteIb.class))
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> response.bodyToMono(QuoteIb.class))
.block();
System.out.println(quote.toString());
}
diff --git a/src/main/java/ch/xxx/trader/clients/WebsocketClient.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/WebsocketClient.java
similarity index 97%
rename from src/main/java/ch/xxx/trader/clients/WebsocketClient.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/test/WebsocketClient.java
index 946c05dd..bfe8dde6 100644
--- a/src/main/java/ch/xxx/trader/clients/WebsocketClient.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/WebsocketClient.java
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients.test;
import java.math.BigDecimal;
import java.net.URI;
@@ -25,7 +25,7 @@
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
-import ch.xxx.trader.dtos.QuoteBf;
+import ch.xxx.trader.domain.model.entity.QuoteBf;
import reactor.core.publisher.Mono;
public class WebsocketClient {
diff --git a/src/main/java/ch/xxx/trader/clients/WsClient.java b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/WsClient.java
similarity index 97%
rename from src/main/java/ch/xxx/trader/clients/WsClient.java
rename to backend/src/main/java/ch/xxx/trader/adapter/clients/test/WsClient.java
index ce6f64c3..cd59cc9f 100644
--- a/src/main/java/ch/xxx/trader/clients/WsClient.java
+++ b/backend/src/main/java/ch/xxx/trader/adapter/clients/test/WsClient.java
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader.clients;
+package ch.xxx.trader.adapter.clients.test;
import java.net.URI;
import java.time.Duration;
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/ApplicationConfig.java b/backend/src/main/java/ch/xxx/trader/adapter/config/ApplicationConfig.java
new file mode 100644
index 00000000..240bda9d
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/ApplicationConfig.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import java.time.Duration;
+
+import javax.net.ssl.SSLException;
+
+import org.springframework.boot.autoconfigure.web.WebProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import ch.xxx.trader.usecase.common.DtoUtils;
+import io.netty.channel.ChannelOption;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.resources.ConnectionProvider;
+import reactor.netty.tcp.SslProvider.SslContextSpec;
+
+@Configuration
+public class ApplicationConfig {
+
+ @Bean
+ public ObjectMapper createObjectMapper() {
+ return DtoUtils.produceObjectMapper();
+ }
+
+ @Bean
+ public WebProperties.Resources resources() {
+ return new WebProperties.Resources();
+ }
+
+ @Bean
+ public WebClient.Builder createWebClient() {
+ ConnectionProvider provider = ConnectionProvider.builder("Client").maxConnections(20)
+ .maxIdleTime(Duration.ofSeconds(6)).maxLifeTime(Duration.ofSeconds(7))
+ .pendingAcquireTimeout(Duration.ofSeconds(9L)).evictInBackground(Duration.ofSeconds(10)).build();
+
+ var webClientBuilder = WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient
+ .create(provider).secure(spec -> sslTimeouts(spec)).option(ChannelOption.SO_KEEPALIVE, false)
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
+ .doOnConnected(
+ c -> c.addHandlerLast(new ReadTimeoutHandler(6)).addHandlerLast(new WriteTimeoutHandler(7)))
+ .responseTimeout(Duration.ofSeconds(7L))));
+ return webClientBuilder;
+ }
+
+ private void sslTimeouts(SslContextSpec spec) {
+ try {
+ spec.sslContext(SslContextBuilder.forClient().build()).handshakeTimeout(Duration.ofSeconds(8))
+ .closeNotifyFlushTimeout(Duration.ofSeconds(6)).closeNotifyReadTimeout(Duration.ofSeconds(6));
+ } catch (SSLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/ExceptionLoggingFilter.java b/backend/src/main/java/ch/xxx/trader/adapter/config/ExceptionLoggingFilter.java
new file mode 100644
index 00000000..bd157abf
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/ExceptionLoggingFilter.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.web.firewall.RequestRejectedException;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.GenericFilterBean;
+
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class ExceptionLoggingFilter extends GenericFilterBean {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionLoggingFilter.class);
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws IOException, ServletException {
+ try {
+ chain.doFilter(req, res);
+ } catch (RequestRejectedException exception) {
+ HttpServletRequest request = (HttpServletRequest) req;
+ HttpServletResponse response = (HttpServletResponse) res;
+ record MyEntry(String key, Object value) {
+ }
+ LOGGER.info(String.format("Exception: %s", exception.getMessage()));
+ LOGGER.info("Remote Ip: {}", request.getRemoteAddr());
+ LOGGER.info("Request URL: {}", request.getRequestURL());
+ Map attributeMap = Collections.list(request.getAttributeNames()).stream()
+ .flatMap(attName -> Stream.of(new MyEntry(attName, request.getAttribute(attName))))
+ .collect(Collectors.toMap(myEntry -> myEntry.key, myEntry -> myEntry.value));
+ LOGGER.debug("Request Attributes: {}", this.createStringFromMap(attributeMap));
+ Map headerMap = Collections.list(request.getHeaderNames()).stream()
+ .flatMap(headerName -> Stream.of(new MyEntry(headerName, request.getHeader(headerName))))
+ .collect(Collectors.toMap(myEntry -> myEntry.key, myEntry -> myEntry.value));
+ LOGGER.info("Request Headers: {}", this.createStringFromMap(headerMap));
+ LOGGER.info("Request Body length: {}", request.getContentLength());
+ try {
+ LOGGER.debug("Request Body content: {}", new String(request.getInputStream().readAllBytes()));
+ } catch (IOException e) {
+ LOGGER.warn("Failed to display body.", e);
+ }
+
+ LOGGER.warn("request_rejected: remote={}, user_agent={}, request_url={}", request.getRemoteHost(),
+ request.getHeader(HttpHeaders.USER_AGENT), request.getRequestURL(), exception);
+
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ private String createStringFromMap(Map myMap) {
+ return myMap.entrySet().stream()
+ .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue() == null ? "" : entry.getValue()))
+ .collect(Collectors.joining(" | "));
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/FlapDoodleConfig.java b/backend/src/main/java/ch/xxx/trader/adapter/config/FlapDoodleConfig.java
new file mode 100644
index 00000000..ed741f90
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/FlapDoodleConfig.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+//@Configuration
+public class FlapDoodleConfig {
+ /*
+ private static final Logger LOGGER = LoggerFactory.getLogger(FlapDoodleConfig.class);
+ private static final int MONGO_DB_PORT = 27017;
+ private MongodExecutable mongodExecutable = null;
+ private MongodProcess mongod = null;
+ @Value("${server.port:}")
+ private String serverPort;
+ @Value("${spring.profiles.active:}")
+ private String activeProfiles;
+
+ @PostConstruct
+ public void initMongoDb() {
+ if (this.serverPort.isBlank() || this.serverPort.contains("8080") || this.serverPort.matches("\\d")
+ && (this.activeProfiles.isBlank() || !this.activeProfiles.toLowerCase().contains("prod"))) {
+ try {
+ MongodStarter starter = MongodStarter.getDefaultInstance();
+ MongodConfig mongodConfig = MongodConfig.builder().version(Version.Main.V4_4)
+ .net(new Net(MONGO_DB_PORT, Network.localhostIsIPv6())).build();
+ this.mongodExecutable = starter.prepare(mongodConfig);
+ this.mongod = this.mongodExecutable.start();
+ LOGGER.info("MongoDb process: {}, state: {}", this.mongod.getProcessId(),
+ this.mongod.isProcessRunning());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @PreDestroy
+ public void stopMongoDb() {
+ if (this.serverPort.isBlank() || this.serverPort.contains("8080") || this.serverPort.matches("\\d")
+ && (this.activeProfiles.isBlank() || !this.activeProfiles.toLowerCase().contains("prod"))) {
+ try {
+ this.mongodExecutable.stop();
+ LOGGER.info("MongoDb proces: {}, state: {}", this.mongod.getProcessId(),
+ this.mongod.isProcessRunning());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ */
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/ForwardServletFilter.java b/backend/src/main/java/ch/xxx/trader/adapter/config/ForwardServletFilter.java
new file mode 100644
index 00000000..1586d6e7
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/ForwardServletFilter.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.annotation.WebFilter;
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@WebFilter
+@Component
+public class ForwardServletFilter implements Filter {
+ private static final Logger LOG = LoggerFactory.getLogger(ForwardServletFilter.class);
+ public static final List SUPPORTED_LOCALES = List.of(Locale.ENGLISH, Locale.GERMAN);
+ public static final List REST_PATHS = List.of("/bitfinex", "/bitstamp", "/coinbase", "/itbit", "/myuser",
+ "/statistics", "/actuator", "/swagger-ui.html", "/swagger-ui", "/v3");
+ public static final List LANGUAGE_PATHS = SUPPORTED_LOCALES.stream()
+ .map(myLocale -> String.format("/%s/", myLocale.getLanguage())).collect(Collectors.toList());
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletRequest myRequest = (HttpServletRequest) request;
+// LOG.info(String.format("ServletPath: %s", myRequest.getServletPath()));
+ if (REST_PATHS.stream()
+// .peek(restEndPoint -> LOG.info(restEndPoint + " " + myRequest.getServletPath() + " "
+// + myRequest.getServletPath().indexOf(restEndPoint)))
+ .anyMatch(restEndPoint -> 0 == myRequest.getServletPath().indexOf(restEndPoint))
+ || (LANGUAGE_PATHS.stream()
+// .peek(langPath -> LOG.info(langPath + " " + myRequest.getServletPath() + " " + myRequest.getServletPath().indexOf(langPath)))
+ .anyMatch(langPath -> 0 == myRequest.getServletPath().indexOf(langPath))
+ && (myRequest.getServletPath().contains(".") && !myRequest.getServletPath().contains("?")))) {
+ chain.doFilter(myRequest, response);
+ } else {
+ Iterable iterable = () -> myRequest.getLocales().asIterator();
+ Locale userLocale = StreamSupport.stream(iterable.spliterator(), false)
+ .filter(SUPPORTED_LOCALES::contains).findFirst().orElse(Locale.ENGLISH);
+ String forwardPath = String.format("/%s/index.html", userLocale.getLanguage());
+// LOG.info(String.format("Forward to: %s", forwardPath));
+ RequestDispatcher dispatcher = myRequest.getServletContext().getRequestDispatcher(forwardPath);
+ dispatcher.forward(myRequest, response);
+ return;
+ }
+ }
+
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/GlobalExceptionHandler.java b/backend/src/main/java/ch/xxx/trader/adapter/config/GlobalExceptionHandler.java
new file mode 100644
index 00000000..ac208a4f
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/GlobalExceptionHandler.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import com.mongodb.MongoTimeoutException;
+
+import ch.xxx.trader.domain.exceptions.AuthenticationException;
+import io.netty.handler.timeout.TimeoutException;
+
+@ControllerAdvice
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+ private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ @ExceptionHandler({ MongoTimeoutException.class, TimeoutException.class })
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST)
+ public Object handleException(final Exception exception, final HttpServletRequest request) {
+// record MyEntry(String key, Object value) {
+// }
+ LOGGER.info(String.format("Execption: %s", exception.getMessage()), exception);
+ LOGGER.info("Remote Ip: {}", request.getRemoteAddr());
+ LOGGER.info("Request URL: {}", request.getRequestURL());
+// Map attributeMap = Collections.list(request.getAttributeNames()).stream()
+// .flatMap(attName -> Stream.of(new MyEntry(attName, request.getAttribute(attName))))
+// .collect(Collectors.toMap(myEntry -> myEntry.key, myEntry -> myEntry.value));
+// LOGGER.debug("Request Attributes: {}", this.createStringFromMap(attributeMap));
+// Map headerMap = Collections.list(request.getHeaderNames()).stream()
+// .flatMap(headerName -> Stream.of(new MyEntry(headerName, request.getHeader(headerName))))
+// .collect(Collectors.toMap(myEntry -> myEntry.key, myEntry -> myEntry.value));
+// LOGGER.info("Request Headers: {}", this.createStringFromMap(headerMap));
+// LOGGER.info("Request Body length: {}", request.getContentLength());
+// try {
+// LOGGER.debug("Request Body content: {}", new String(request.getInputStream().readAllBytes()));
+// } catch (IOException e) {
+// LOGGER.warn("Failed to display body.", e);
+// }
+ return new Object();
+ }
+
+ @ExceptionHandler({ AuthenticationException.class })
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST)
+ public Object handleAuthenticationException(final Exception exception, final HttpServletRequest request) {
+ LOGGER.trace("AuthenticationException", exception);
+ return new Object();
+ }
+
+// private String createStringFromMap(Map myMap) {
+// return myMap.entrySet().stream()
+// .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue() == null ? "" : entry.getValue()))
+// .collect(Collectors.joining(" | "));
+// }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/JwtTokenFilter.java b/backend/src/main/java/ch/xxx/trader/adapter/config/JwtTokenFilter.java
new file mode 100644
index 00000000..0250e72f
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/JwtTokenFilter.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.GenericFilterBean;
+
+import ch.xxx.trader.usecase.services.JwtTokenService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+
+public class JwtTokenFilter extends GenericFilterBean {
+ private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenFilter.class);
+
+ private JwtTokenService jwtTokenProvider;
+
+ public JwtTokenFilter(JwtTokenService jwtTokenProvider) {
+ this.jwtTokenProvider = jwtTokenProvider;
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
+ throws IOException, ServletException {
+
+ String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
+ if (token != null && jwtTokenProvider.validateToken(token)) {
+ Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ } else {
+ LOGGER.debug("Token rejected: {}", token);
+ }
+
+ filterChain.doFilter(req, res);
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/KafkaConfig.java b/backend/src/main/java/ch/xxx/trader/adapter/config/KafkaConfig.java
new file mode 100644
index 00000000..5c8d61c5
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/KafkaConfig.java
@@ -0,0 +1,139 @@
+package ch.xxx.trader.adapter.config;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.annotation.PostConstruct;
+
+import org.apache.kafka.clients.DefaultHostResolver;
+import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.config.TopicConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.annotation.EnableKafkaStreams;
+import org.springframework.kafka.config.TopicBuilder;
+import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
+
+import reactor.kafka.receiver.ReceiverOptions;
+import reactor.kafka.sender.KafkaSender;
+import reactor.kafka.sender.SenderOptions;
+
+@Configuration
+@Profile("kafka | prod")
+@EnableKafka
+@EnableKafkaStreams
+public class KafkaConfig {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConfig.class);
+ private static final String GZIP = "gzip";
+ private static final String ZSTD = "zstd";
+ private static final String SPRING_DESERIALIZER = "spring.deserializer.value.delegate.class";
+ public static final String NEW_USER_TOPIC = "new-user-topic";
+ public static final String NEW_USER_DLT_TOPIC = "new-user-topic-retry";
+ public static final String USER_LOGOUT_SOURCE_TOPIC = "user-logout-source-topic";
+ public static final String USER_LOGOUT_SINK_TOPIC = "user-logout-sink-topic";
+ public static final String USER_LOGOUT_SINK_DLT_TOPIC = "user-logout-sink-topic-retry";
+
+ private SenderOptions senderOptions;
+ private ReceiverOptions receiverOptions;
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+ @Value("${spring.kafka.producer.compression-type}")
+ private String compressionType;
+ @Value("${spring.kafka.producer.key-serializer}")
+ private String producerKeySerializer;
+ @Value("${spring.kafka.producer.value-serializer}")
+ private String producerValueSerializer;
+ @Value("${spring.kafka.producer.enable.idempotence}")
+ private boolean enableIdempotence;
+ @Value("${spring.kafka.consumer.group-id}")
+ private String consumerGroupId;
+ @Value("${spring.kafka.consumer.auto-offset-reset}")
+ private String consumerAutoOffsetReset;
+ @Value("${spring.kafka.consumer.key-deserializer}")
+ private String consumerKeySerializer;
+ @Value("${spring.kafka.consumer.value-deserializer}")
+ private String consumerValueSerializer;
+
+ @PostConstruct
+ public void init() throws ClassNotFoundException {
+ String bootstrap = this.bootstrapServers.split(":")[0].trim();
+ if (bootstrap.matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$")) {
+ DefaultHostResolver.IP_ADDRESS = bootstrap;
+ } else if (!bootstrap.isEmpty()) {
+ DefaultHostResolver.KAFKA_SERVICE_NAME = bootstrap;
+ }
+ LOGGER.info("Kafka Servername: {} Kafka Servicename: {} Ip Address: {}", DefaultHostResolver.KAFKA_SERVER_NAME,
+ DefaultHostResolver.KAFKA_SERVICE_NAME, DefaultHostResolver.IP_ADDRESS);
+ Map props = new HashMap<>();
+ props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ //props.put(ProducerConfig.CLIENT_ID_CONFIG, "sample-producer");
+ props.put(ProducerConfig.ACKS_CONFIG, "all");
+ props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
+ props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, this.compressionType);
+ props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, Class.forName(this.producerKeySerializer));
+ props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, Class.forName(this.producerValueSerializer));
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, Class.forName(this.producerKeySerializer));
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, Class.forName(this.producerKeySerializer));
+ this.senderOptions = SenderOptions.create(props);
+// this.senderOptions.maxInFlight(10);
+ props = new HashMap<>();
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ //props.put(ProducerConfig.CLIENT_ID_CONFIG, "sample-producer");
+ props.put(ConsumerConfig.GROUP_ID_CONFIG, "all");
+ props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, this.consumerAutoOffsetReset);
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, Class.forName(this.consumerKeySerializer));
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, Class.forName(this.consumerValueSerializer));
+ props.put(SPRING_DESERIALIZER, this.consumerKeySerializer);
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
+ this.receiverOptions = ReceiverOptions.create(props);
+ this.receiverOptions.pollTimeout(Duration.ofSeconds(1L));
+ }
+
+ @Bean
+ public ReceiverOptions, ?> kafkaReceiverOptions() {
+ return this.receiverOptions;
+ }
+
+ @Bean
+ public KafkaSender, ?> kafkaSender() {
+ return KafkaSender.create(this.senderOptions);
+ }
+
+ @Bean
+ public NewTopic newUserTopic() {
+ return TopicBuilder.name(KafkaConfig.NEW_USER_TOPIC)
+ .config(TopicConfig.COMPRESSION_TYPE_CONFIG, this.compressionType).compact().build();
+ }
+
+ @Bean
+ public NewTopic newUserDltTopic() {
+ return TopicBuilder.name(KafkaConfig.NEW_USER_TOPIC)
+ .config(TopicConfig.COMPRESSION_TYPE_CONFIG, this.compressionType).compact().build();
+ }
+
+ @Bean
+ public NewTopic userLogoutSourceTopic() {
+ return TopicBuilder.name(KafkaConfig.USER_LOGOUT_SOURCE_TOPIC)
+ .config(TopicConfig.COMPRESSION_TYPE_CONFIG, this.compressionType).compact().build();
+ }
+
+ @Bean
+ public NewTopic userLogoutSinkTopic() {
+ return TopicBuilder.name(KafkaConfig.USER_LOGOUT_SINK_TOPIC)
+ .config(TopicConfig.COMPRESSION_TYPE_CONFIG, this.compressionType).compact().build();
+ }
+
+ @Bean
+ public NewTopic userLogoutSinkDltTopic() {
+ return TopicBuilder.name(KafkaConfig.USER_LOGOUT_SINK_DLT_TOPIC)
+ .config(TopicConfig.COMPRESSION_TYPE_CONFIG, this.compressionType).compact().build();
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/SchedulingConfig.java b/backend/src/main/java/ch/xxx/trader/adapter/config/SchedulingConfig.java
new file mode 100644
index 00000000..ae6b1a17
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/SchedulingConfig.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import java.util.concurrent.Executor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import io.micrometer.core.aop.TimedAspect;
+import io.micrometer.core.instrument.MeterRegistry;
+import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
+
+@Configuration
+@EnableAspectJAutoProxy
+@EnableScheduling
+@EnableAsync
+@EnableSchedulerLock(defaultLockAtMostFor = "10m")
+public class SchedulingConfig {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SchedulingConfig.class);
+
+ @Bean
+ TimedAspect timedAspect(MeterRegistry registry) {
+ return new TimedAspect(registry);
+ }
+
+ @Bean(name = "clientTaskExecutor")
+ public Executor threadPoolTaskExecutor() {
+ return this.createThreadPoolTaskExecutor(20);
+ }
+
+ private Executor createThreadPoolTaskExecutor(int maxPoolSize) {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setQueueCapacity(1);
+ executor.setKeepAliveSeconds(1);
+ executor.setAllowCoreThreadTimeOut(true);
+ return executor;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/SpringMongoConfig.java b/backend/src/main/java/ch/xxx/trader/adapter/config/SpringMongoConfig.java
new file mode 100644
index 00000000..2ecfbcd9
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/SpringMongoConfig.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.mongo.MongoProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.codec.ServerCodecConfigurer;
+import org.springframework.http.codec.support.DefaultServerCodecConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+
+import jakarta.annotation.PostConstruct;
+import net.javacrumbs.shedlock.core.LockProvider;
+import net.javacrumbs.shedlock.provider.mongo.reactivestreams.ReactiveStreamsMongoLockProvider;
+
+@Configuration
+public class SpringMongoConfig {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SpringMongoConfig.class);
+ private static final String SCHED_LOCK_DB = "schedLock";
+ @Value("${spring.data.mongodb.uri:}")
+ private String mongoDbUri;
+ @Value("${MONGODB_HOST:}")
+ private String mongoDbHost;
+ private final MongoProperties mongoProperties;
+
+ public SpringMongoConfig(MongoProperties mongoProperties) {
+ this.mongoProperties = mongoProperties;
+ }
+
+ @PostConstruct
+ public void init() {
+ LOGGER.info("MongoDbUri: {}", this.mongoDbUri);
+ LOGGER.info("MongoDbHost: {}", this.mongoDbHost);
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public MongoClient mongoClient() {
+// LOGGER.info("MongoPort: {}", this.mongoProperties.getPort());
+ LOGGER.info("MongoUri: {}", this.mongoDbUri.replace("27017",
+ this.mongoProperties.getPort() == null ? "27017" : this.mongoProperties.getPort().toString()));
+ MongoClient mongoClient = MongoClients.create(this.mongoDbUri.replace("27017",
+ this.mongoProperties.getPort() == null || this.mongoProperties.getPort() < 1 ? "27017"
+ : this.mongoProperties.getPort().toString()));
+ mongoClient.getClusterDescription().getShortDescription();
+ LOGGER.info("MongoClusterShortDescription: {}", mongoClient.getClusterDescription().getShortDescription());
+ return mongoClient;
+ }
+
+ @Bean
+ public ServerCodecConfigurer serverCodecConfigurer() {
+ return new DefaultServerCodecConfigurer();
+ }
+
+ @Bean
+ public LockProvider lockProvider(MongoClient mongo) {
+ return new ReactiveStreamsMongoLockProvider(mongo.getDatabase(SCHED_LOCK_DB));
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/config/WebSecurityConfig.java b/backend/src/main/java/ch/xxx/trader/adapter/config/WebSecurityConfig.java
new file mode 100644
index 00000000..70157f3b
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/config/WebSecurityConfig.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.config;
+
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import ch.xxx.trader.domain.common.Role;
+import ch.xxx.trader.usecase.services.JwtTokenService;
+
+@EnableWebSecurity
+@Configuration
+@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
+public class WebSecurityConfig {
+
+ private final JwtTokenService jwtTokenProvider;
+
+ public WebSecurityConfig(JwtTokenService jwtTokenProvider) {
+ this.jwtTokenProvider = jwtTokenProvider;
+ }
+
+ @Bean
+ public SecurityFilterChain configure(HttpSecurity http) throws Exception {
+ JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
+ HttpSecurity httpSecurity = http
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers(AntPathRequestMatcher.antMatcher("/*/*/orderbook"),
+ AntPathRequestMatcher.antMatcher("/*/*/*/orderbook"))
+ .hasAuthority(Role.USERS.toString()).requestMatchers(AntPathRequestMatcher.antMatcher("/**"))
+ .permitAll())
+ .csrf(myCsrf -> myCsrf.disable())
+ .sessionManagement(mySm -> mySm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .headers(myHeaders -> myHeaders.contentSecurityPolicy(myCsp -> myCsp.policyDirectives(
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';")))
+ .headers(myHeaders -> myHeaders.xssProtection(myXss -> myXss.headerValue(HeaderValue.ENABLED)))
+ .headers(myHeaders -> myHeaders.frameOptions(myFo -> myFo.sameOrigin()))
+ .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
+ return httpSecurity.build();
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/controller/BitfinexController.java b/backend/src/main/java/ch/xxx/trader/adapter/controller/BitfinexController.java
new file mode 100644
index 00000000..71180ace
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/controller/BitfinexController.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.controller;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.xxx.trader.domain.model.entity.QuoteBf;
+import ch.xxx.trader.usecase.services.BitfinexService;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/bitfinex")
+public class BitfinexController {
+ private final BitfinexService bitfinexService;
+
+ public BitfinexController(BitfinexService bitfinexService) {
+ this.bitfinexService = bitfinexService;
+ }
+
+ @GetMapping("/{currpair}/orderbook")
+ public Mono getOrderbook(@PathVariable String currpair) {
+ return this.bitfinexService.getOrderbook(currpair);
+ }
+
+ @GetMapping("/{pair}/current")
+ public Mono currentQuote(@PathVariable String pair) {
+ return this.bitfinexService.currentQuote(pair);
+ }
+
+ @GetMapping("/{pair}/{timeFrame}")
+ public Flux tfQuotes(@PathVariable String timeFrame, @PathVariable String pair) {
+ return this.bitfinexService.tfQuotes(timeFrame, pair);
+ }
+
+ @GetMapping(path="/{pair}/{timeFrame}/pdf", produces=MediaType.APPLICATION_PDF_VALUE)
+ public Mono pdfReport(@PathVariable String timeFrame, @PathVariable String pair) {
+ return this.bitfinexService.pdfReport(timeFrame, pair);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/controller/BitstampController.java b/backend/src/main/java/ch/xxx/trader/adapter/controller/BitstampController.java
new file mode 100644
index 00000000..6a5d7b71
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/controller/BitstampController.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.controller;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.xxx.trader.domain.model.entity.QuoteBs;
+import ch.xxx.trader.usecase.services.BitstampService;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/bitstamp")
+public class BitstampController {
+ private final BitstampService bitstampService;
+
+ public BitstampController(BitstampService bitstampService) {
+ this.bitstampService = bitstampService;
+ }
+
+ @GetMapping("/{currpair}/orderbook")
+ public Mono getOrderbook(@PathVariable String currpair) {
+ return this.bitstampService.getOrderbook(currpair);
+ }
+
+ @GetMapping("/{pair}/current")
+ public Mono currentQuoteBtc(@PathVariable String pair) {
+ return this.bitstampService.currentQuoteBtc(pair);
+ }
+
+ @GetMapping("/{pair}/{timeFrame}")
+ public Flux tfQuotesBtc(@PathVariable String timeFrame, @PathVariable String pair) {
+ return this.bitstampService.tfQuotesBtc(timeFrame, pair);
+ }
+
+ @GetMapping(path="/{pair}/{timeFrame}/pdf", produces=MediaType.APPLICATION_PDF_VALUE)
+ public Mono pdfReport(@PathVariable String timeFrame, @PathVariable String pair) {
+ return this.bitstampService.pdfReport(timeFrame, pair);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/controller/CoinbaseController.java b/backend/src/main/java/ch/xxx/trader/adapter/controller/CoinbaseController.java
new file mode 100644
index 00000000..315ae0c3
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/controller/CoinbaseController.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.xxx.trader.domain.model.entity.QuoteCb;
+import ch.xxx.trader.domain.model.entity.QuoteCbSmall;
+import ch.xxx.trader.usecase.services.CoinbaseService;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/coinbase")
+public class CoinbaseController {
+ private final CoinbaseService coinbaseService;
+
+ public CoinbaseController(CoinbaseService coinbaseService) {
+ this.coinbaseService = coinbaseService;
+ }
+
+ @GetMapping("/today")
+ public Flux todayQuotesBc() {
+ return this.coinbaseService.todayQuotesBc();
+ }
+
+ @GetMapping("/7days")
+ public Flux sevenDaysQuotesBc() {
+ return this.coinbaseService.sevenDaysQuotesBc();
+ }
+
+ @GetMapping("/30days")
+ public Flux thirtyDaysQuotesBc() {
+ return this.coinbaseService.thirtyDaysQuotesBc();
+ }
+
+ @GetMapping("/90days")
+ public Flux nintyDaysQuotesBc() {
+ return this.coinbaseService.nintyDaysQuotesBc();
+ }
+
+ @GetMapping("/6month")
+ public Flux sixMonthsQuotesBc() {
+ return this.coinbaseService.sixMonthsQuotesBc();
+ }
+
+ @GetMapping("/1year")
+ public Flux oneYearQuotesBc() {
+ return this.coinbaseService.oneYearQuotesBc();
+ }
+
+ @GetMapping("/current")
+ public Mono currentQuoteBc() {
+ return this.coinbaseService.currentQuoteBc();
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/controller/ItbitController.java b/backend/src/main/java/ch/xxx/trader/adapter/controller/ItbitController.java
new file mode 100644
index 00000000..c75a596c
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/controller/ItbitController.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.xxx.trader.domain.model.entity.QuoteIb;
+import ch.xxx.trader.usecase.services.ItbitService;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/itbit")
+public class ItbitController {
+ private final ItbitService itbitService;
+
+ public ItbitController(ItbitService itbitService) {
+ this.itbitService = itbitService;
+ }
+
+ @GetMapping("/{currpair}/orderbook")
+ public Mono getOrderbook(@PathVariable String currpair, HttpServletRequest request) {
+ return this.itbitService.getOrderbook(currpair);
+ }
+
+ @GetMapping("/{pair}/current")
+ public Mono currentQuote(@PathVariable String pair) {
+ return this.itbitService.currentQuote(pair);
+ }
+
+ @GetMapping("/{pair}/{timeFrame}")
+ public Flux tfQuotes(@PathVariable String timeFrame, @PathVariable String pair) {
+ return this.itbitService.tfQuotes(timeFrame, pair);
+ }
+
+ @GetMapping(path="/{pair}/{timeFrame}/pdf", produces=MediaType.APPLICATION_PDF_VALUE)
+ public Mono pdfReport(@PathVariable String timeFrame, @PathVariable String pair) {
+ return this.itbitService.pdfReport(timeFrame, pair);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/controller/MyUserController.java b/backend/src/main/java/ch/xxx/trader/adapter/controller/MyUserController.java
new file mode 100644
index 00000000..48462bb2
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/controller/MyUserController.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.controller;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.xxx.trader.domain.model.dto.AuthCheck;
+import ch.xxx.trader.domain.model.dto.RefreshTokenDto;
+import ch.xxx.trader.domain.model.entity.MyUser;
+import ch.xxx.trader.domain.services.MyUserService;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/myuser")
+public class MyUserController {
+ private final MyUserService myUserService;
+
+ public MyUserController(MyUserService myUserService) {
+ this.myUserService = myUserService;
+ }
+
+ @PostMapping("/authorize")
+ public Mono postAuthorize(@RequestBody AuthCheck authcheck, @RequestHeader Map header) {
+ return this.myUserService.postAuthorize(authcheck, header);
+ }
+
+ @PostMapping("/signin")
+ public Mono postUserSignin(@RequestBody MyUser myUser)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ return this.myUserService.postUserSignin(myUser);
+ }
+
+ @PutMapping("/logout")
+ public Mono postLogout(@RequestHeader(value = HttpHeaders.AUTHORIZATION) String bearerStr) {
+ return this.myUserService.postLogout(bearerStr);
+ }
+
+ @PostMapping("/login")
+ public Mono postUserLogin(@RequestBody MyUser myUser,HttpServletRequest request)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ return this.myUserService.postUserLogin(myUser);
+ }
+
+ @GetMapping("/refreshToken")
+ public Mono getRefreshToken(@RequestHeader(value = HttpHeaders.AUTHORIZATION) String bearerStr) {
+ return this.myUserService.refreshToken(bearerStr);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/controller/StatisticsController.java b/backend/src/main/java/ch/xxx/trader/adapter/controller/StatisticsController.java
new file mode 100644
index 00000000..6bb1f84f
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/controller/StatisticsController.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.controller;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.xxx.trader.domain.model.dto.CommonStatisticsDto;
+import ch.xxx.trader.domain.model.dto.StatisticsCommon.CoinExchange;
+import ch.xxx.trader.domain.model.dto.StatisticsCommon.StatisticsCurrPair;
+import ch.xxx.trader.usecase.services.StatisticService;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/statistics")
+public class StatisticsController {
+ private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsController.class);
+ private final StatisticService statisticService;
+
+ public StatisticsController(StatisticService statisticService) {
+ this.statisticService = statisticService;
+ }
+
+ @GetMapping("/overview/{coinExchange}/{currPair}")
+ public Mono getOverview(@PathVariable StatisticsCurrPair currPair, @PathVariable CoinExchange coinExchange) {
+ return this.statisticService.getCommonStatistics(currPair, coinExchange);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/cron/PrepareDataTask.java b/backend/src/main/java/ch/xxx/trader/adapter/cron/PrepareDataTask.java
new file mode 100644
index 00000000..ecc6551d
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/cron/PrepareDataTask.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.cron;
+
+import java.util.Optional;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import ch.xxx.trader.usecase.services.BitfinexService;
+import ch.xxx.trader.usecase.services.BitstampService;
+import ch.xxx.trader.usecase.services.CoinbaseService;
+import ch.xxx.trader.usecase.services.ItbitService;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import reactor.core.Disposable;
+
+@Component
+public class PrepareDataTask {
+ private static final Logger LOG = LoggerFactory.getLogger(PrepareDataTask.class);
+ private final BitstampService bitstampService;
+ private final BitfinexService bitfinexService;
+ private final ItbitService itbitService;
+ private final CoinbaseService coinbaseService;
+ private Optional bitstampDisposableOpt = Optional.empty();
+ private Optional bitfinexDisposableOpt = Optional.empty();
+ private Optional itbitDisposableOpt = Optional.empty();
+ private Optional coinbaseDisposableOpt = Optional.empty();
+
+ public PrepareDataTask(BitstampService bitstampService, BitfinexService bitfinexService, ItbitService itbitService,
+ CoinbaseService coinbaseService) {
+ this.bitstampService = bitstampService;
+ this.bitfinexService = bitfinexService;
+ this.itbitService = itbitService;
+ this.coinbaseService = coinbaseService;
+ }
+
+ @Async
+ @Scheduled(cron = "0 5 0,12 ? * ?")
+ @SchedulerLock(name = "bitstamp_avg_scheduledTask", lockAtLeastFor = "PT10H", lockAtMostFor = "PT11H")
+ public void createBsAvg() {
+ this.bitstampDisposableOpt.ifPresent(myDisposable -> myDisposable.dispose());
+ this.bitstampDisposableOpt = Optional.of(this.bitstampService.createBsAvg()
+ .doFinally(value -> BitstampService.singleInstanceLock = false).subscribe(result -> {
+ }, error -> LOG.warn("createBsAvg() failed.", error)));
+ }
+
+ @Async
+ @Scheduled(cron = "0 45 0,12 ? * ?")
+ @SchedulerLock(name = "bitfinex_avg_scheduledTask", lockAtLeastFor = "PT10H", lockAtMostFor = "PT11H")
+ public void createBfAvg() {
+ this.bitfinexDisposableOpt.ifPresent(myDisposable -> myDisposable.dispose());
+ this.bitfinexDisposableOpt = Optional.of(this.bitfinexService.createBfAvg()
+ .doFinally(value -> BitfinexService.singleInstanceLock = false).subscribe(result -> {
+ }, error -> LOG.warn("createBfAvg() failed.", error)));
+ }
+
+ @Async
+ @Scheduled(cron = "0 25 1,13 ? * ?")
+ @SchedulerLock(name = "itbit_avg_scheduledTask", lockAtLeastFor = "PT10H", lockAtMostFor = "PT11H")
+ public void createIbAvg() {
+ this.itbitDisposableOpt.ifPresent(myDisposable -> myDisposable.dispose());
+ this.itbitDisposableOpt = Optional.of(this.itbitService.createIbAvg()
+ .doFinally(value -> ItbitService.singleInstanceLock = false).subscribe(result -> {
+ }, error -> LOG.warn("createIbAvg() failed.", error)));
+ }
+
+ @Async
+ @Scheduled(cron = "0 10 2,14 ? * ?")
+ @SchedulerLock(name = "coinbase_avg_scheduledTask", lockAtLeastFor = "PT10H", lockAtMostFor = "PT11H")
+ public void createCbAvg() {
+ this.coinbaseDisposableOpt.ifPresent(myDisposable -> myDisposable.dispose());
+ this.coinbaseDisposableOpt = Optional.of(this.coinbaseService.createCbAvg()
+ .doFinally(value -> CoinbaseService.singleInstanceLock = false).subscribe(result -> {
+ }, error -> LOG.warn("createCbAvg() failed.", error)));
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/cron/ScheduledTask.java b/backend/src/main/java/ch/xxx/trader/adapter/cron/ScheduledTask.java
new file mode 100644
index 00000000..6366c6cb
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/cron/ScheduledTask.java
@@ -0,0 +1,374 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.cron;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.MediaType;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import ch.xxx.trader.domain.common.WebUtils;
+import ch.xxx.trader.domain.model.dto.WrapperCb;
+import ch.xxx.trader.domain.model.entity.QuoteBf;
+import ch.xxx.trader.domain.model.entity.QuoteBs;
+import ch.xxx.trader.domain.model.entity.QuoteCb;
+import ch.xxx.trader.domain.model.entity.QuoteIb;
+import ch.xxx.trader.domain.model.entity.paxos.PaxosQuote;
+import ch.xxx.trader.domain.services.MyUserService;
+import ch.xxx.trader.usecase.mappers.EventMapper;
+import ch.xxx.trader.usecase.services.BitfinexService;
+import ch.xxx.trader.usecase.services.BitstampService;
+import ch.xxx.trader.usecase.services.CoinbaseService;
+import ch.xxx.trader.usecase.services.ItbitService;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import reactor.core.Disposable;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+
+@Component
+public class ScheduledTask {
+ private static final Logger LOG = LoggerFactory.getLogger(ScheduledTask.class);
+
+ private static final String URLBS = "https://www.bitstamp.net/api";
+ private static final String URLCB = "https://api.coinbase.com/v2";
+ private static final String URLPA = "https://api.paxos.com/v2";
+ private static final String URLBF = "https://api.bitfinex.com";
+
+ private final BitstampService bitstampService;
+ private final BitfinexService bitfinexService;
+ private final ItbitService itbitService;
+ private final CoinbaseService coinbaseService;
+ private final MyUserService myUserService;
+ private final WebClient.Builder webClientBuilder;
+ private final Map> disposables = new ConcurrentHashMap<>();
+ private final Scheduler mongoImportScheduler = Schedulers.newBoundedElastic(20, 40, "mongoImport", 10);
+
+ public ScheduledTask(BitstampService bitstampService, MyUserService myUserService, EventMapper messageMapper,
+ BitfinexService bitfinexService, ItbitService itbitService, CoinbaseService coinbaseService, WebClient.Builder webClientBuilder) {
+ this.bitstampService = bitstampService;
+ this.bitfinexService = bitfinexService;
+ this.itbitService = itbitService;
+ this.coinbaseService = coinbaseService;
+ this.myUserService = myUserService;
+ this.webClientBuilder = webClientBuilder;
+ }
+
+// @PostConstruct
+// public void init() {
+//
+// }
+
+ @Scheduled(fixedRate = 90000)
+ @SchedulerLock(name = "UpdateLoggedOutUsers_scheduledTask", lockAtLeastFor = "PT80S", lockAtMostFor = "PT85S")
+ @Order(1)
+ public void updateLoggedOutUsers() {
+ this.myUserService.updateLoggedOutUsers();
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 3000)
+ @SchedulerLock(name = "BitstampQuoteBTC_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteBTC() throws InterruptedException {
+ String currPair = "btceur";
+ insertBsQuote(currPair);
+ }
+
+ private void insertBsQuote(String currPair) {
+ // LOG.info(currPair);
+ this.disposeClient(currPair);
+ LocalTime start = LocalTime.now();
+ final AtomicBoolean exceptionLogged = new AtomicBoolean(false);
+ Disposable subscribe = null;
+ try {
+ Mono request = this.webClientBuilder.build().get()
+ .uri(String.format("%s/v2/ticker/%s/", ScheduledTask.URLBS, currPair))
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> response.bodyToMono(QuoteBs.class))
+ .map(res -> {
+ res.setPair(currPair);
+// log.info(res.toString());
+ return res;
+ }).timeout(Duration.ofSeconds(5L)).onErrorResume(ex -> {
+ exceptionLogged.set(this.logRequestFailed("Bitstamp", currPair, start, ex));
+ return Mono.empty();
+ }).subscribeOn(this.mongoImportScheduler);
+ subscribe = request.flatMap(myQuote -> this.bitstampService.insertQuote(Mono.just(myQuote))
+ .timeout(Duration.ofSeconds(6L)).subscribeOn(this.mongoImportScheduler).onErrorResume(ex -> {
+ if (!exceptionLogged.get()) {
+ LOG.warn(String.format("Bitstamp data store failed for: %s", currPair), ex);
+ }
+ return Mono.empty();
+ })).subscribeOn(this.mongoImportScheduler)
+ .subscribe(x -> this.logDuration("Bitstamp", currPair, start),
+ err -> LOG.warn(String.format("Bitstamp data import failed for: %s", currPair), err));
+ } finally {
+ this.disposables.put(currPair, Optional.ofNullable(subscribe));
+ }
+ }
+
+ private void logDuration(String source, String currPair, LocalTime start) {
+ long durationInMs = Duration.between(start, LocalTime.now()).toMillis();
+ if (durationInMs > 1000) {
+ LOG.info("Source: {} Duration of {}: {}ms", source, currPair, durationInMs);
+ }
+ }
+
+ private boolean logRequestFailed(String source, String currPair, LocalTime start, Throwable ex) {
+ LOG.warn(String.format("%s data request for %s failed", source, currPair), ex);
+ return true;
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 6000)
+ @SchedulerLock(name = "BitstampQuoteETH_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteETH() throws InterruptedException {
+ String currPair = "etheur";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 9000)
+ @SchedulerLock(name = "BitstampQuoteLTC_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteLTC() throws InterruptedException {
+ String currPair = "ltceur";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 12000)
+ @SchedulerLock(name = "BitstampQuoteXRP_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteXRP() throws InterruptedException {
+ String currPair = "xrpeur";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 15000)
+ @SchedulerLock(name = "CoinbaseQuote_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertCoinbaseQuote() {
+ final String currPair = "ALLUSD";
+ // LOG.info(currPair);
+ this.disposeClient(currPair);
+ LocalTime start = LocalTime.now();
+ final AtomicBoolean exceptionLogged = new AtomicBoolean(false);
+ Disposable subscribe = null;
+ try {
+ Mono request = this.webClientBuilder.build().get().uri(ScheduledTask.URLCB + "/exchange-rates?currency=BTC")
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> {
+ return response.bodyToMono(WrapperCb.class);
+// return response.bodyToMono(String.class);
+// }).flatMap(value -> {
+// // log.info(value);
+// return Mono.just(this.messageMapper.mapJsonToObject(value, WrapperCb.class));
+ }).flatMap(resp -> Mono.just(resp.getData())).flatMap(resp2 -> {
+// log.info(resp2.getRates().toString());
+ return Mono.just(resp2.getRates());
+ }).timeout(Duration.ofSeconds(5L)).onErrorResume(ex -> {
+ exceptionLogged.set(this.logRequestFailed("Coinbase", currPair, start, ex));
+ return Mono.empty();
+ }).subscribeOn(this.mongoImportScheduler);
+ subscribe = request.flatMap(myQuote -> this.coinbaseService.insertQuote(Mono.just(myQuote))
+ .timeout(Duration.ofSeconds(6L)).subscribeOn(this.mongoImportScheduler).onErrorResume(ex -> {
+ if (!exceptionLogged.get()) {
+ LOG.warn("Coinbase data store failed", ex);
+ }
+ return Mono.empty();
+ })).subscribeOn(this.mongoImportScheduler)
+ .subscribe(x -> this.logDuration("Coinbase", currPair, start),
+ err -> LOG.warn("Coinbase data import failed.", err));
+ } finally {
+ this.disposables.put(currPair, Optional.ofNullable(subscribe));
+ }
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 21000)
+ @SchedulerLock(name = "ItbitUsdQuote_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertItbitUsdQuote() {
+ final String currPair = "BTCUSD";
+ // LOG.info(currPair);
+ this.disposeClient(currPair);
+ LocalTime start = LocalTime.now();
+ final AtomicBoolean exceptionLogged = new AtomicBoolean(false);
+ Disposable subscribe = null;
+ try {
+ Mono request = this.webClientBuilder.build().get()
+ .uri(String.format("%s/markets/%s/ticker", ScheduledTask.URLPA, currPair))
+ .accept(MediaType.APPLICATION_JSON)
+ .exchangeToMono(response -> response.bodyToMono(PaxosQuote.class)).map(res -> {
+// log.info(res.toString());
+ return res;
+ }).map(paxosQuote -> this.convert(paxosQuote)).timeout(Duration.ofSeconds(5L)).onErrorResume(ex -> {
+ exceptionLogged.set(this.logRequestFailed("Ibit", currPair, start, ex));
+ return Mono.empty();
+ }).subscribeOn(this.mongoImportScheduler);
+ subscribe = request.flatMap(myQuote -> this.itbitService.insertQuote(Mono.just(myQuote))
+ .timeout(Duration.ofSeconds(6L)).subscribeOn(this.mongoImportScheduler).onErrorResume(ex -> {
+ if (!exceptionLogged.get()) {
+ LOG.warn(String.format("Itbit data store failed for: %s", currPair), ex);
+ }
+ return Mono.empty();
+ })).subscribeOn(this.mongoImportScheduler)
+ .subscribe(x -> this.logDuration("Itbit", currPair, start),
+ err -> LOG.warn(String.format("Itbit data import failed for: %s", currPair), err));
+ } finally {
+ this.disposables.put(currPair, Optional.ofNullable(subscribe));
+ }
+ }
+
+ QuoteIb convert(PaxosQuote paxosQuote) {
+ final String currPair = "XBTUSD";
+ QuoteIb quoteIb = new QuoteIb(currPair, new BigDecimal(paxosQuote.getBestBid().getPrice()),
+ new BigDecimal(paxosQuote.getBestBid().getAmount()), new BigDecimal(paxosQuote.getBestAsk().getPrice()),
+ new BigDecimal(paxosQuote.getBestAsk().getAmount()),
+ new BigDecimal(paxosQuote.getLastExecution().getPrice()),
+ new BigDecimal(paxosQuote.getLastExecution().getAmount()),
+ new BigDecimal(paxosQuote.getLastDay().getVolume()), new BigDecimal(paxosQuote.getToday().getVolume()),
+ new BigDecimal(paxosQuote.getLastDay().getHigh()), new BigDecimal(paxosQuote.getLastDay().getLow()),
+ new BigDecimal(paxosQuote.getToday().getOpen()), new BigDecimal(paxosQuote.getToday().getHigh()),
+ new BigDecimal(paxosQuote.getToday().getLow()),
+ new BigDecimal(paxosQuote.getToday().getVolumeWeightedAveragePrice()),
+ new BigDecimal(paxosQuote.getLastDay().getVolumeWeightedAveragePrice()), paxosQuote.getSnapshotAt());
+ return quoteIb;
+ }
+
+ private void disposeClient(final String currPair) {
+ Optional optional = this.disposables.getOrDefault(currPair, Optional.empty());
+ optional.ifPresent(disposable -> disposable.dispose());
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 24000)
+ @SchedulerLock(name = "BitstampQuoteBTCUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteBTCUSD() throws InterruptedException {
+ String currPair = "btcusd";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 27000)
+ @SchedulerLock(name = "BitstampQuoteETHUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteETHUSD() throws InterruptedException {
+ String currPair = "ethusd";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 30000)
+ @SchedulerLock(name = "BitstampQuoteLTCUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteLTCUSD() throws InterruptedException {
+ String currPair = "ltcusd";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 33000)
+ @SchedulerLock(name = "BitstampQuoteXRPUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitstampQuoteXRPUSD() throws InterruptedException {
+ String currPair = "xrpusd";
+ this.insertBsQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 36000)
+ @SchedulerLock(name = "BitfinexQuoteBTCUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitfinexQuoteBTCUSD() throws InterruptedException {
+ String currPair = "btcusd";
+ insertBfQuote(currPair);
+ }
+
+ private void insertBfQuote(String currPair) {
+ // LOG.info(currPair);
+ this.disposeClient(currPair);
+ LocalTime start = LocalTime.now();
+ final AtomicBoolean exceptionLogged = new AtomicBoolean(false);
+ Disposable subscribe = null;
+ try {
+ Mono request = this.webClientBuilder.build().get()
+ .uri(String.format("%s/v1/pubticker/%s", ScheduledTask.URLBF, currPair))
+ .accept(MediaType.APPLICATION_JSON).exchangeToMono(response -> response.bodyToMono(QuoteBf.class))
+ .map(res -> {
+ res.setPair(currPair);
+ QuoteBf result = checkBfTimestamp(res);
+// log.info(res.toString());
+ return result;
+ }).timeout(Duration.ofSeconds(5L)).onErrorResume(ex -> {
+ exceptionLogged.set(this.logRequestFailed("Bitfinex", currPair, start, ex));
+ return Mono.empty();
+ }).subscribeOn(this.mongoImportScheduler);
+ subscribe = request.flatMap(myQuote -> this.bitfinexService.insertQuote(Mono.just(myQuote))
+ .timeout(Duration.ofSeconds(6L)).subscribeOn(this.mongoImportScheduler).onErrorResume(ex -> {
+ if (!exceptionLogged.get()) {
+ LOG.warn(String.format("Bitfinex data store failed for: %s", currPair), ex);
+ }
+ return Mono.empty();
+ })).subscribeOn(this.mongoImportScheduler)
+ .subscribe(x -> this.logDuration("Bitfinex", currPair, start),
+ err -> LOG.warn(String.format("Bitfinex data import failed for: %s", currPair), err));
+ } finally {
+ this.disposables.put(currPair, Optional.ofNullable(subscribe));
+ }
+ }
+
+ private QuoteBf checkBfTimestamp(QuoteBf res) {
+ QuoteBf result = res;
+ try {
+ BigDecimal timestamp = new BigDecimal(res.getTimestamp());
+ LOG.debug(timestamp.toString());
+ } catch (Exception e) {
+ LOG.warn(String.format("Failed to parse the timestamp: %s", res.getTimestamp()), e);
+ result = new QuoteBf(res.getMid(), res.getBid(), res.getAsk(), res.getLast_price(), res.getLow(),
+ res.getHigh(), res.getVolume(), "0.0");
+ }
+ return result;
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 39000)
+ @SchedulerLock(name = "BitfinexQuoteETHUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitfinexQuoteETHUSD() throws InterruptedException {
+ String currPair = "ethusd";
+ this.insertBfQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 42000)
+ @SchedulerLock(name = "BitfinexQuoteLTCUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitfinexQuoteLTCUSD() throws InterruptedException {
+ String currPair = "ltcusd";
+ this.insertBfQuote(currPair);
+ }
+
+ @Async("clientTaskExecutor")
+ @Scheduled(fixedRate = 60000, initialDelay = 45000)
+ @SchedulerLock(name = "BitfinexQuoteXRPUSD_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
+ public void insertBitfinexQuoteXRPUSD() throws InterruptedException {
+ String currPair = "xrpusd";
+ this.insertBfQuote(currPair);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/cron/TaskStarter.java b/backend/src/main/java/ch/xxx/trader/adapter/cron/TaskStarter.java
new file mode 100644
index 00000000..830e1c36
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/cron/TaskStarter.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.cron;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import ch.xxx.trader.usecase.services.BitfinexService;
+import ch.xxx.trader.usecase.services.BitstampService;
+import ch.xxx.trader.usecase.services.CoinbaseService;
+import ch.xxx.trader.usecase.services.ItbitService;
+
+@Component
+public class TaskStarter {
+ private static final Logger log = LoggerFactory.getLogger(TaskStarter.class);
+ private final BitstampService bitstampService;
+ private final BitfinexService bitfinexService;
+ private final ItbitService itbitService;
+ private final CoinbaseService coinbaseService;
+ @Value("${single.instance.deployment:false}")
+ private boolean singleInstanceDeployment;
+
+ public TaskStarter(BitstampService bitstampService, BitfinexService bitfinexService, ItbitService itbitService,
+ CoinbaseService coinbaseService) {
+ this.bitstampService = bitstampService;
+ this.bitfinexService = bitfinexService;
+ this.itbitService = itbitService;
+ this.coinbaseService = coinbaseService;
+ }
+
+ @Async
+ @EventListener(ApplicationReadyEvent.class)
+ public void initAvgs() {
+ if (this.singleInstanceDeployment) {
+ log.info("ApplicationReady");
+ this.bitstampService.createBsAvg().block();
+ this.bitfinexService.createBfAvg().block();
+ this.itbitService.createIbAvg().block();
+ this.coinbaseService.createCbAvg().block();
+ BitstampService.singleInstanceLock = false;
+ BitfinexService.singleInstanceLock = false;
+ ItbitService.singleInstanceLock = false;
+ CoinbaseService.singleInstanceLock = false;
+ }
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/events/EventConsumer.java b/backend/src/main/java/ch/xxx/trader/adapter/events/EventConsumer.java
new file mode 100644
index 00000000..b1c23c2f
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/events/EventConsumer.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2019 Sven Loesekann
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.events;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.annotation.Profile;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import ch.xxx.trader.adapter.config.KafkaConfig;
+import ch.xxx.trader.domain.model.dto.RevokedTokensDto;
+import ch.xxx.trader.domain.model.entity.MyUser;
+import ch.xxx.trader.usecase.mappers.EventMapper;
+import ch.xxx.trader.usecase.services.MyUserServiceEvents;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverOptions;
+import reactor.util.retry.Retry;
+
+@Profile("kafka | prod")
+@Service
+public class EventConsumer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(EventConsumer.class);
+ private final ReceiverOptions receiverOptions;
+ private final KafkaReceiver userLogoutReceiver;
+ private final KafkaReceiver newUserReceiver;
+ private final MyUserServiceEvents myUserServiceEvents;
+ private final EventMapper eventMapper;
+ @Value("${spring.kafka.consumer.group-id}")
+ private String consumerGroupId;
+
+ public EventConsumer(MyUserServiceEvents myUserServiceEvents, ReceiverOptions receiverOptions,
+ EventMapper eventMapper) {
+ this.receiverOptions = receiverOptions;
+ this.userLogoutReceiver = KafkaReceiver
+ .create(this.receiverOptions(List.of(KafkaConfig.USER_LOGOUT_SINK_TOPIC)));
+ this.newUserReceiver = KafkaReceiver.create(this.receiverOptions(List.of(KafkaConfig.NEW_USER_TOPIC)));
+ this.myUserServiceEvents = myUserServiceEvents;
+ this.eventMapper = eventMapper;
+ }
+
+ private ReceiverOptions receiverOptions(Collection topics) {
+ return this.receiverOptions
+ .addAssignListener(p -> LOGGER.info("Group {} partitions assigned {}", this.consumerGroupId, p))
+ .addRevokeListener(p -> LOGGER.info("Group {} partitions revoked {}", this.consumerGroupId, p))
+ .subscription(topics);
+ }
+
+ @EventListener(ApplicationReadyEvent.class)
+ public void doOnStartup() {
+ this.newUserReceiver.receiveAtmostOnce()
+ .doOnError(error -> LOGGER.error("Error receiving event, will retry", error))
+ .retryWhen(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofMinutes(1)))
+ .concatMap(myRecord -> this.myUserServiceEvents
+ .userSigninEvent(this.eventMapper.mapJsonToObject(myRecord.value(), MyUser.class)))
+ .subscribe();
+ this.userLogoutReceiver.receiveAtmostOnce()
+ .doOnError(error -> LOGGER.error("Error receiving event, will retry", error))
+ .retryWhen(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofMinutes(1)))
+ .concatMap(myRecord -> this.myUserServiceEvents
+ .logoutEvent(this.eventMapper.mapJsonToObject(myRecord.value(), RevokedTokensDto.class)))
+ .subscribe();
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/events/EventProducer.java b/backend/src/main/java/ch/xxx/trader/adapter/events/EventProducer.java
new file mode 100644
index 00000000..bde3ac54
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/events/EventProducer.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2019 Sven Loesekann
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.events;
+
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+import ch.xxx.trader.adapter.config.KafkaConfig;
+import ch.xxx.trader.domain.model.entity.MyUser;
+import ch.xxx.trader.domain.model.entity.RevokedToken;
+import ch.xxx.trader.domain.services.MyEventProducer;
+import ch.xxx.trader.usecase.mappers.EventMapper;
+import reactor.core.publisher.Mono;
+import reactor.kafka.sender.KafkaSender;
+
+@Profile("kafka | prod")
+@Service
+public class EventProducer implements MyEventProducer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(EventProducer.class);
+ private final KafkaSender kafkaSender;
+ private final EventMapper eventMapper;
+
+ public EventProducer(KafkaSender kafkaSender, EventMapper eventMapper) {
+ this.kafkaSender = kafkaSender;
+ this.eventMapper = eventMapper;
+ }
+
+ public Mono sendNewUser(MyUser dto) {
+ String dtoJson = this.eventMapper.mapDtoToString(dto);
+ return this.kafkaSender.createOutbound()
+ .send(Mono.just(new ProducerRecord<>(KafkaConfig.NEW_USER_TOPIC, dto.getSalt(), dtoJson)))
+ .then()
+ .doOnError(e -> LOGGER.error(
+ String.format("Failed to send topic: %s value: %s", KafkaConfig.NEW_USER_TOPIC, dtoJson), e))
+ .thenReturn(dto);
+
+ }
+
+ public Mono sendUserLogout(RevokedToken dto) {
+ String dtoJson = this.eventMapper.mapDtoToString(dto);
+ return this.kafkaSender.createOutbound()
+ .send(Mono.just(new ProducerRecord<>(KafkaConfig.USER_LOGOUT_SOURCE_TOPIC, dto.getName(), dtoJson)))
+ .then()
+ .doOnError(e -> LOGGER.error(String.format("Failed to send topic: %s value: %s",
+ KafkaConfig.USER_LOGOUT_SOURCE_TOPIC, dtoJson), e))
+ .thenReturn(dto);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/events/KafkaStreams.java b/backend/src/main/java/ch/xxx/trader/adapter/events/KafkaStreams.java
new file mode 100644
index 00000000..e15e2f4f
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/events/KafkaStreams.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright 2019 Sven Loesekann
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.events;
+
+import java.time.Duration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.kafka.common.serialization.Serdes;
+import org.apache.kafka.streams.StreamsBuilder;
+import org.apache.kafka.streams.StreamsConfig;
+import org.apache.kafka.streams.Topology;
+import org.apache.kafka.streams.kstream.Consumed;
+import org.apache.kafka.streams.kstream.Materialized;
+import org.apache.kafka.streams.kstream.SlidingWindows;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import ch.xxx.trader.adapter.config.KafkaConfig;
+import ch.xxx.trader.domain.model.dto.RevokedTokensDto;
+import ch.xxx.trader.domain.model.entity.RevokedToken;
+import ch.xxx.trader.usecase.common.LastlogoutTimestampExtractor;
+
+@Profile("kafka | prod")
+@Component
+public class KafkaStreams {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaStreams.class);
+ private static final long LOGOUT_TIMEOUT = 120L;
+ private static final long GRACE_TIMEOUT = 5L;
+ private static final String UNPARSEABLE_JSON = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod";
+ private ObjectMapper objectMapper;
+
+ public KafkaStreams(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ }
+
+ @Bean("UserLogoutTopology")
+ public Topology userLogout(final StreamsBuilder builder) {
+ builder.stream(KafkaConfig.USER_LOGOUT_SOURCE_TOPIC, Consumed.with(Serdes.String(), Serdes.String()))
+ .groupByKey()
+ .windowedBy(SlidingWindows.ofTimeDifferenceAndGrace(Duration.ofSeconds(KafkaStreams.LOGOUT_TIMEOUT),
+ Duration.ofSeconds(KafkaStreams.GRACE_TIMEOUT)))
+ .aggregate(LinkedList::new, (key, value, myList) -> {
+// LOGGER.info("Logout Stream value: {}", value);
+// LOGGER.info("Logout Stream key: {}", key);
+ myList.add(value);
+// LOGGER.info("Logout Stream myList: {}", myList.stream().collect(Collectors.joining(",")));
+ return myList;
+ }, Materialized.with(Serdes.String(), Serdes.ListSerde(LinkedList.class, Serdes.String()))).toStream()
+ .mapValues(value -> convertToRevokedTokens((List) value))
+ .to(KafkaConfig.USER_LOGOUT_SINK_TOPIC);
+ Properties streamsConfiguration = new Properties();
+ streamsConfiguration.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG,
+ LastlogoutTimestampExtractor.class.getName());
+ streamsConfiguration.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000L);
+ return builder.build(streamsConfiguration);
+ }
+
+ private String convertToRevokedTokens(List value) {
+ try {
+ List revokedTokenList = value.stream().map(myValue -> {
+ RevokedToken result;
+ try {
+ result = this.objectMapper.readValue(myValue, RevokedToken.class);
+ } catch (Exception e) {
+ LOGGER.warn(String.format("Failed to deserialize %s", myValue), e);
+ result = new RevokedToken(null, UNPARSEABLE_JSON, UNPARSEABLE_JSON, null);
+ }
+ return result;
+ }).filter(myRevokedToken -> !UNPARSEABLE_JSON.equalsIgnoreCase(myRevokedToken.getName())
+ && !UNPARSEABLE_JSON.equalsIgnoreCase(myRevokedToken.getUuid())).toList();
+ return this.objectMapper.writeValueAsString(new RevokedTokensDto(revokedTokenList));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/adapter/repository/ClientMongoRepository.java b/backend/src/main/java/ch/xxx/trader/adapter/repository/ClientMongoRepository.java
new file mode 100644
index 00000000..4418b994
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/adapter/repository/ClientMongoRepository.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.adapter.repository;
+
+import java.util.Collection;
+
+import jakarta.validation.Valid;
+
+import org.bson.Document;
+import org.springframework.data.domain.Sort.Direction;
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.core.index.Index;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Service;
+
+import com.mongodb.client.result.DeleteResult;
+import com.mongodb.reactivestreams.client.MongoCollection;
+
+import ch.xxx.trader.domain.model.entity.MyMongoRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+public class ClientMongoRepository implements MyMongoRepository {
+ private final ReactiveMongoOperations operations;
+
+ public ClientMongoRepository(ReactiveMongoOperations operations) {
+ this.operations = operations;
+ }
+
+ @Override
+ public Mono save(@Valid T objectToSave) {
+ return this.operations.save(objectToSave);
+ }
+
+ @Override
+ public Mono findOne(Query query, Class entityClass) {
+ return this.operations.findOne(query, entityClass);
+ }
+
+ @Override
+ public Mono findOne(Query query, Class entityClass, String name) {
+ return this.operations.findOne(query, entityClass, name);
+ }
+
+ @Override
+ public Flux find(Query query, Class entityClass) {
+ return this.operations.find(query, entityClass);
+ }
+
+ @Override
+ public Flux find(Query query, Class entityClass, String collectionName) {
+ return this.operations.find(query, entityClass, collectionName);
+ }
+
+ @Override
+ public Flux insertAll(@Valid Mono extends Collection extends T>> batchToSave, String collectionName) {
+ return this.operations.insertAll(batchToSave, collectionName);
+ }
+
+ @Override
+ public Mono ensureIndex(String collectionName, String propertyName) {
+ Index myIndex = new Index(propertyName, Direction.DESC);
+ myIndex.named(collectionName + "-" + propertyName);
+ return this.operations.indexOps(collectionName).ensureIndex(myIndex);
+ }
+
+ @Override
+ public Mono collectionExists(String collectionName) {
+ return this.operations.collectionExists(collectionName);
+ }
+
+ @Override
+ public Mono> createCollection(String collectionName) {
+ return this.operations.createCollection(collectionName);
+ }
+
+ @Override
+ public Mono insert(@Valid Mono quote) {
+ return this.operations.insert(quote);
+ }
+
+ @Override
+ public Mono remove(Mono quote) {
+ return this.operations.remove(quote);
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/domain/common/JwtUtils.java b/backend/src/main/java/ch/xxx/trader/domain/common/JwtUtils.java
new file mode 100644
index 00000000..3a947ce3
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/domain/common/JwtUtils.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright 2019 Sven Loesekann
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.domain.common;
+
+import java.security.Key;
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpHeaders;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+
+public class JwtUtils {
+ public static final String AUTHORIZATION = HttpHeaders.AUTHORIZATION.toLowerCase();
+ public static final String TOKENAUTHKEY = "auth";
+ public static final String TOKENLASTMSGKEY = "lastmsg";
+ public static final String BEARER = "Bearer ";
+ public static final String AUTHORITY = "authority";
+ public static final String UUID = "uuid";
+ public static final record TokenSubjectRole(String subject, String role) {}
+
+ public static Optional extractToken(Map headers) {
+ String authStr = headers.get(AUTHORIZATION);
+ return extractToken(Optional.ofNullable(authStr));
+ }
+
+ private static Optional extractToken(Optional authStr) {
+ if (authStr.isPresent()) {
+ authStr = Optional.ofNullable(authStr.get().startsWith(BEARER) ? authStr.get().substring(7) : null);
+ }
+ return authStr;
+ }
+
+ public static Optional resolveToken(String bearerToken) {
+ if (bearerToken != null && bearerToken.startsWith(JwtUtils.BEARER)) {
+ return Optional.of(bearerToken.substring(7, bearerToken.length()));
+ }
+ return Optional.empty();
+ }
+
+ public static Optional> getClaims(Optional token, Key jwtTokenKey) {
+ if (!token.isPresent()) {
+ return Optional.empty();
+ }
+ return Optional.of(Jwts.parserBuilder().setSigningKey(jwtTokenKey).build().parseClaimsJws(token.get()));
+ }
+
+ public static String getTokenRoles(Map headers, Key jwtTokenKey) {
+ Optional tokenStr = extractToken(headers);
+ Optional> claims = JwtUtils.getClaims(tokenStr, jwtTokenKey);
+ if (claims.isPresent() && new Date().before(claims.get().getBody().getExpiration())) {
+ return claims.get().getBody().get(TOKENAUTHKEY).toString();
+ }
+ return "";
+ }
+
+ public static TokenSubjectRole getTokenUserRoles(Map headers,
+ Key jwtTokenKey) {
+ Optional tokenStr = extractToken(headers);
+ Optional> claims = JwtUtils.getClaims(tokenStr, jwtTokenKey);
+ if (claims.isPresent() && new Date().before(claims.get().getBody().getExpiration())) {
+ return new TokenSubjectRole(claims.get().getBody().getSubject(),
+ claims.get().getBody().get(TOKENAUTHKEY).toString());
+ }
+ return new TokenSubjectRole(null, null);
+ }
+
+ public static boolean checkToken(HttpServletRequest request, Key jwtTokenKey) {
+ Optional tokenStr = JwtUtils
+ .extractToken(Optional.ofNullable(request.getHeader(JwtUtils.AUTHORIZATION)));
+ Optional> claims = JwtUtils.getClaims(tokenStr, jwtTokenKey);
+ if (claims.isPresent() && new Date().before(claims.get().getBody().getExpiration())
+ && claims.get().getBody().get(TOKENAUTHKEY).toString().contains(Role.USERS.name())
+ && !claims.get().getBody().get(TOKENAUTHKEY).toString().contains(Role.GUEST.name())) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/backend/src/main/java/ch/xxx/trader/domain/common/MongoUtils.java b/backend/src/main/java/ch/xxx/trader/domain/common/MongoUtils.java
new file mode 100644
index 00000000..22dd25bb
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/domain/common/MongoUtils.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright 2016 Sven Loesekann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.domain.common;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+
+public class MongoUtils {
+
+ public enum TimeFrame {
+ CURRENT("current"), TODAY("today"), SEVENDAYS("7days"), THIRTYDAYS("30days"), NINTYDAYS("90days"),
+ Month1("1month"), Month3("3month"), Month6("6month"), Year1("1year"), Year2("2year"), Year5("5year");
+
+ private TimeFrame(String value) {
+ this.value = value;
+ }
+
+ private String value;
+
+ public String getValue() {
+ return this.value;
+ }
+ };
+
+ public static final Map KEY_TO_TIMEFRAME = Collections.unmodifiableMap(new ConcurrentHashMap<>(
+ Stream.of(TimeFrame.values()).collect(Collectors.toMap(TimeFrame::getValue, tf -> tf))));
+
+ private static final Query buildQuery(Optional pair, boolean ascending, Optional begin,
+ int limit) {
+ limit = limit < 5000 ? limit : 5000;
+ Calendar cal = GregorianCalendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, -1);
+ Query query = new Query();
+ query.allowDiskUse(true);
+ query.limit(limit);
+ pair.ifPresent(myValue -> query.addCriteria(Criteria.where("pair").is(myValue)));
+ begin.ifPresentOrElse(myValue -> query.addCriteria(Criteria.where("createdAt").gt(myValue.getTime())),
+ () -> query.addCriteria(Criteria.where("createdAt").gt(cal.getTime())));
+ var myValue = ascending ? query.with(Sort.by("createdAt").ascending())
+ : query.with(Sort.by("createdAt").descending());
+ return query;
+ }
+
+ private static final Query buildQuery(Optional pair, boolean ascending, Optional begin) {
+ return buildQuery(pair, ascending, begin, 1000);
+ }
+
+ public static final Query build90DayQuery(Optional pair) {
+ Calendar cal = GregorianCalendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, -90);
+ return buildQuery(pair, true, Optional.of(cal));
+ }
+
+ public static final Query build30DayQuery(Optional pair) {
+ Calendar cal = GregorianCalendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, -30);
+ return buildQuery(pair, true, Optional.of(cal));
+ }
+
+ public static final Query build7DayQuery(Optional pair) {
+ Calendar cal = GregorianCalendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, -7);
+ return buildQuery(pair, true, Optional.of(cal));
+ }
+
+ public static final Query buildTimeFrameQuery(Optional pair, TimeFrame timeFrame, int limit) {
+ Calendar cal = GregorianCalendar.getInstance();
+ Query query = switch (timeFrame) {
+ case CURRENT -> buildCurrentQuery(pair);
+ case TODAY -> buildTodayQuery(pair);
+ case SEVENDAYS -> build7DayQuery(pair);
+ case THIRTYDAYS -> build30DayQuery(pair);
+ case NINTYDAYS -> build90DayQuery(pair);
+ case Month1 -> {
+ cal.add(Calendar.MONTH, -1);
+ yield buildQuery(pair, true, Optional.of(cal));
+ }
+ case Month3 -> {
+ cal.add(Calendar.MONTH, -3);
+ yield buildQuery(pair, true, Optional.of(cal));
+ }
+ case Month6 -> {
+ cal.add(Calendar.MONTH, -6);
+ yield buildQuery(pair, true, Optional.of(cal));
+ }
+ case Year1 -> {
+ cal.add(Calendar.YEAR, -1);
+ yield buildQuery(pair, true, Optional.of(cal));
+ }
+ case Year2 -> {
+ cal.add(Calendar.YEAR, -2);
+ yield buildQuery(pair, true, Optional.of(cal), limit);
+ }
+ case Year5 -> {
+ cal.add(Calendar.YEAR, -5);
+ yield buildQuery(pair, true, Optional.of(cal), limit);
+ }
+ default -> new Query();
+ };
+ return query;
+ }
+
+ public static final Query buildTimeFrameQuery(Optional pair, TimeFrame timeFrame) {
+ return buildTimeFrameQuery(pair, timeFrame, 1000);
+ }
+
+ public static final Query buildTodayQuery(Optional pair) {
+ return buildQuery(pair, true, Optional.empty());
+ }
+
+ public static final Query buildCurrentQuery(Optional pair) {
+ return buildQuery(pair, false, Optional.empty());
+ }
+
+ public static final boolean filterEvenMinutes(Date date) {
+ return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).getMinute() % 2 == 0;
+ }
+
+ public static final boolean filter10Minutes(Date date) {
+ return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).getMinute() % 10 == 0;
+ }
+}
diff --git a/src/main/java/ch/xxx/trader/PasswordEncryption.java b/backend/src/main/java/ch/xxx/trader/domain/common/PasswordEncryption.java
similarity index 67%
rename from src/main/java/ch/xxx/trader/PasswordEncryption.java
rename to backend/src/main/java/ch/xxx/trader/domain/common/PasswordEncryption.java
index ebc83a7b..669200a3 100644
--- a/src/main/java/ch/xxx/trader/PasswordEncryption.java
+++ b/backend/src/main/java/ch/xxx/trader/domain/common/PasswordEncryption.java
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-package ch.xxx.trader;
+package ch.xxx.trader.domain.common;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -31,27 +31,15 @@ public class PasswordEncryption {
public boolean authenticate(String attemptedPassword, String encryptedPassword, String salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
- // Encrypt the clear-text password using the same salt that was used to
- // encrypt the original password
String encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);
- // Authentication succeeds if encrypted password that the user entered
- // is equal to the stored hash
return encryptedPassword.equals(encryptedAttemptedPassword);
}
public String getEncryptedPassword(String password, String salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
- // PBKDF2 with SHA-1 as the hashing algorithm. Note that the NIST
- // specifically names SHA-1 as an acceptable hashing algorithm for PBKDF2
- String algorithm = "PBKDF2WithHmacSHA1";
- // SHA-1 generates 160 bit hashes, so that's what makes sense here
- int derivedKeyLength = 160;
- // Pick an iteration count that works for you. The NIST recommends at
- // least 1,000 iterations:
- // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
- // iOS 4.x reportedly uses 10,000:
- // http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/
+ String algorithm = "PBKDF2WithHmacSHA256";
+ int derivedKeyLength = 256;
int iterations = 20000;
char[] pwd = new String(password).toCharArray();
KeySpec spec = new PBEKeySpec(pwd, Base64.getDecoder().decode(salt), iterations, derivedKeyLength);
@@ -61,9 +49,7 @@ public String getEncryptedPassword(String password, String salt)
}
public String generateSalt() throws NoSuchAlgorithmException {
- // VERY important to use SecureRandom instead of just Random
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
- // Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5
byte[] salt = new byte[8];
random.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
diff --git a/src/angular/trader/src/app/common/quoteBf.ts b/backend/src/main/java/ch/xxx/trader/domain/common/Role.java
similarity index 71%
rename from src/angular/trader/src/app/common/quoteBf.ts
rename to backend/src/main/java/ch/xxx/trader/domain/common/Role.java
index a97412bc..ec7bd012 100644
--- a/src/angular/trader/src/app/common/quoteBf.ts
+++ b/backend/src/main/java/ch/xxx/trader/domain/common/Role.java
@@ -13,16 +13,16 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-export interface QuoteBf {
- _id: string;
- pair: string;
- createdAt: Date;
- mid: number;
- bid: number;
- ask: number;
- last_price: number;
- low: number;
- high: number;
- volume: number;
- timestamp: string;
-}
\ No newline at end of file
+package ch.xxx.trader.domain.common;
+
+import org.springframework.security.core.GrantedAuthority;
+
+public enum Role implements GrantedAuthority{
+ USERS, GUEST;
+
+ @Override
+ public String getAuthority() {
+ return this.name();
+ }
+
+}
diff --git a/backend/src/main/java/ch/xxx/trader/domain/common/StreamHelpers.java b/backend/src/main/java/ch/xxx/trader/domain/common/StreamHelpers.java
new file mode 100644
index 00000000..50acd0f5
--- /dev/null
+++ b/backend/src/main/java/ch/xxx/trader/domain/common/StreamHelpers.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2019 Sven Loesekann
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package ch.xxx.trader.domain.common;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+public class StreamHelpers {
+ public static Predicate distinctByKey(
+ Function super T, ?> keyExtractor) {
+
+ Map