diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..4d074ee63
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,24 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.c text
+*.h text
+*.java text
+*.scala text
+*.sbt text
+*.py text
+
+# Declare files that will always have CR line endings on checkout
+*.sh text eol=lf
+*.template text eol=lf
+*.yml text eol=lf
+*.sql text eol=lf
+
+# Declare files that will always have CRLF line endings on checkout.
+*.sln text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
diff --git a/.github/workflows/actions/build-check/action.yml b/.github/workflows/actions/build-check/action.yml
new file mode 100644
index 000000000..093df8d1b
--- /dev/null
+++ b/.github/workflows/actions/build-check/action.yml
@@ -0,0 +1,27 @@
+name: Action > Build
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/cached-dependencies
+ - name: Build javascript app (ui)
+ run: |
+ source "$HOME/.nvm/nvm.sh"
+ cd $JEMPI_APP_PATH/JeMPI_UI
+ yarn install --frozen-lockfile
+ yarn build
+ shell: bash
+ - name: Build Scala Apps
+ run: |
+ set -eo pipefail
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ cd $JEMPI_APP_PATH/JeMPI_EM_Scala
+ sbt clean assembly
+ shell: bash
+ - name: Build Java App
+ run: |
+ set -eo pipefail
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ cd $JEMPI_APP_PATH
+ mvn clean package
+ shell: bash
\ No newline at end of file
diff --git a/.github/workflows/actions/build-deploy-images/action.yml b/.github/workflows/actions/build-deploy-images/action.yml
new file mode 100644
index 000000000..1620c1765
--- /dev/null
+++ b/.github/workflows/actions/build-deploy-images/action.yml
@@ -0,0 +1,25 @@
+name: Build and Deploy Images
+inputs:
+ docker-push-tag:
+ required: false
+ image-build-tag:
+ required: true
+ docker-host:
+ required: true
+ docker-username:
+ required: true
+ docker-password:
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/docker-images-build
+ with:
+ image-build-tag: ${{ inputs.image-build-tag }}
+ - uses: ./.github/workflows/actions/docker-images-deploy
+ with:
+ image-build-tag: ${{ inputs.image-build-tag }}
+ docker-push-tag: ${{ inputs.docker-push-tag }}
+ docker-username: ${{ inputs.docker-username }}
+ docker-password: ${{ inputs.docker-password }}
+ docker-host: ${{ inputs.docker-host }}
\ No newline at end of file
diff --git a/.github/workflows/actions/cached-dependencies/action.yml b/.github/workflows/actions/cached-dependencies/action.yml
new file mode 100644
index 000000000..729162fe6
--- /dev/null
+++ b/.github/workflows/actions/cached-dependencies/action.yml
@@ -0,0 +1,15 @@
+name: Action > CacheDependencies
+runs:
+ using: 'composite'
+ steps:
+ - name: Cache SDKMan Install
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.sdkman
+ ~/.nvm
+ ~/.npm
+ ~/.cache/yarn
+ $GITHUB_WORKSPACE/JeMPI_Apps/JeMPI_UI/node_modules
+ # Using the prepare file as it contains all the version of the dependencies
+ key: build-dependencies-${{ hashFiles('**/.github/workflows/actions/prepare/action.yml', '**/yarn.lock') }}
\ No newline at end of file
diff --git a/.github/workflows/actions/docker-images-build/action.yml b/.github/workflows/actions/docker-images-build/action.yml
new file mode 100644
index 000000000..80d4417e2
--- /dev/null
+++ b/.github/workflows/actions/docker-images-build/action.yml
@@ -0,0 +1,22 @@
+name: Action > Docker Images Build
+inputs:
+ image-build-tag:
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/cached-dependencies
+ - uses: ./.github/workflows/actions/load-conf-env
+ - name: Build Docker Images
+ run: |
+ set -eo pipefail
+ source "$HOME/.nvm/nvm.sh"
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ source $GITHUB_WORKSPACE/devops/linux/docker/conf.env
+ source $GITHUB_WORKSPACE/devops/linux/docker/conf/images/conf-app-images.sh
+ pushd $GITHUB_WORKSPACE/JeMPI_Apps
+ source ./build-all-ci.sh "${{ inputs.image-build-tag }}"
+ popd
+ docker image ls
+ shell: bash
+
\ No newline at end of file
diff --git a/.github/workflows/actions/docker-images-deploy/action.yml b/.github/workflows/actions/docker-images-deploy/action.yml
new file mode 100644
index 000000000..d09bbd33e
--- /dev/null
+++ b/.github/workflows/actions/docker-images-deploy/action.yml
@@ -0,0 +1,24 @@
+name: Deploy Docker Images
+inputs:
+ image-build-tag:
+ required: true
+ docker-push-tag:
+ required: false
+ docker-host:
+ required: true
+ docker-username:
+ required: true
+ docker-password:
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - run: |
+ set -eo pipefail
+ source $GITHUB_WORKFLOW_FOLDER/actions/docker-images-deploy/deployDockerImages.sh \
+ "${{ inputs.image-build-tag }}" \
+ "${{ inputs.docker-push-tag }}" \
+ "${{ inputs.docker-host }}" \
+ "${{ inputs.docker-username }}" \
+ "${{ inputs.docker-password }}"
+ shell: bash
\ No newline at end of file
diff --git a/.github/workflows/actions/docker-images-deploy/deployDockerImages.sh b/.github/workflows/actions/docker-images-deploy/deployDockerImages.sh
new file mode 100644
index 000000000..6614a4d9e
--- /dev/null
+++ b/.github/workflows/actions/docker-images-deploy/deployDockerImages.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+original_tag=$1
+push_tag=$2
+registry_url=$3
+username=$4
+password=$5
+
+if [ -z "$registry_url" ] || [ -z "$username" ] || [ -z "$password" ]; then
+ echo "Docker host details not set. Skipping deploying"
+ exit 0
+fi
+
+
+if [ -z "$push_tag" ]; then
+ push_tag=$original_tag
+fi
+
+if ! docker login "$registry_url" -u "$username" -p "$password"; then
+ echo "Failed to authenticate with Docker registry. Cannot push."
+ exit 1
+fi
+
+
+IMAGE_LIST=$(docker image ls --filter "reference=*:$original_tag" --format "{{.Repository}}:{{.Tag}}")
+
+for IMAGE in $IMAGE_LIST; do
+ IFS=':' read -a image_details <<< "$IMAGE"
+ push_tag_url="$registry_url/$username/${image_details[0]}:$push_tag"
+
+ echo "Pushing image: $IMAGE to '$push_tag_url'"
+
+ docker tag "$IMAGE" $push_tag_url
+ docker push $push_tag_url
+done
\ No newline at end of file
diff --git a/.github/workflows/actions/docker-images-save/action.yml b/.github/workflows/actions/docker-images-save/action.yml
new file mode 100644
index 000000000..67eeaa403
--- /dev/null
+++ b/.github/workflows/actions/docker-images-save/action.yml
@@ -0,0 +1,21 @@
+name: Action > Docker Images Build
+inputs:
+ image-build-tag:
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/cached-dependencies
+ - uses: ./.github/workflows/actions/load-conf-env
+ - name: Build Docker Save
+ run: |
+ set -eo pipefail
+ source $GITHUB_WORKFLOW_FOLDER/actions/docker-images-save/saveImages.sh "${{ inputs.image-build-tag }}" "./.github/workflows/actions/docker-images-save/docker-images"
+ shell: bash
+ - uses: actions/upload-artifact@v4
+ with:
+ name: docker-images-${{ inputs.image-build-tag }}
+ path: |
+ ./.github/workflows/actions/docker-images-save/docker-images/
+ retention-days: 2
+
\ No newline at end of file
diff --git a/.github/workflows/actions/docker-images-save/saveImages.sh b/.github/workflows/actions/docker-images-save/saveImages.sh
new file mode 100644
index 000000000..8ea7f8934
--- /dev/null
+++ b/.github/workflows/actions/docker-images-save/saveImages.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+images_path="$2"
+
+if [ ! -d "$images_path" ]; then
+ mkdir -p "$images_path"
+fi
+
+IMAGE_LIST=$(docker image ls --filter "reference=*:$1" --format "{{.Repository}}:{{.Tag}}")
+
+for IMAGE in $IMAGE_LIST; do
+ IFS=':' read -a image_details <<< "$IMAGE"
+ echo "Saving image: $IMAGE to '$images_path/${image_details[0]}.${image_details[1]}.tar'"
+ docker save -o "$images_path/${image_details[0]}.${image_details[1]}.tar" "$IMAGE"
+done
\ No newline at end of file
diff --git a/.github/workflows/actions/install-node/action.yml b/.github/workflows/actions/install-node/action.yml
new file mode 100644
index 000000000..478096231
--- /dev/null
+++ b/.github/workflows/actions/install-node/action.yml
@@ -0,0 +1,24 @@
+name: Install Node
+inputs:
+ node-version:
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - name: Install Nvm
+ shell: bash
+ run: |
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
+ source "$HOME/.nvm/nvm.sh"
+ nvm --version
+ - name: Install node ${{ inputs.node-version }}
+ run: |
+ source "$HOME/.nvm/nvm.sh"
+ nvm install ${{ inputs.node-version }}
+ shell: bash
+ - name: Install UI packages
+ run: |
+ source "$HOME/.nvm/nvm.sh"
+ cd $JEMPI_APP_PATH/JeMPI_UI
+ yarn install --frozen-lockfile
+ shell: bash
\ No newline at end of file
diff --git a/.github/workflows/actions/install-sdkman/action.yml b/.github/workflows/actions/install-sdkman/action.yml
new file mode 100644
index 000000000..6d9f5c9e4
--- /dev/null
+++ b/.github/workflows/actions/install-sdkman/action.yml
@@ -0,0 +1,10 @@
+name: Install SDKMan
+runs:
+ using: 'composite'
+ steps:
+ - name: Install SDKMan
+ shell: bash
+ run: |
+ curl -s "https://get.sdkman.io" | bash
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ sdk version
\ No newline at end of file
diff --git a/.github/workflows/actions/lint/action.yml b/.github/workflows/actions/lint/action.yml
new file mode 100644
index 000000000..998169353
--- /dev/null
+++ b/.github/workflows/actions/lint/action.yml
@@ -0,0 +1,20 @@
+name: Action > Lint
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/cached-dependencies
+ - name: Running javascript linter
+ run: |
+ source "$HOME/.nvm/nvm.sh"
+ cd $JEMPI_APP_PATH/JeMPI_UI
+ yarn install --frozen-lockfile
+ yarn lint && yarn format
+ shell: bash
+ - name: Running java linter
+ run: |
+ set -eo pipefail
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ source $GITHUB_WORKFLOW_FOLDER/actions/lint/mvn_linter.sh $JEMPI_APP_PATH
+ shell: bash
+
\ No newline at end of file
diff --git a/.github/workflows/actions/lint/mvn_linter.sh b/.github/workflows/actions/lint/mvn_linter.sh
new file mode 100644
index 000000000..8db1e2392
--- /dev/null
+++ b/.github/workflows/actions/lint/mvn_linter.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+cd "$1"
+
+for dir in */; do
+ dir="${dir%/}"
+ if [ -f "$dir/pom.xml" ]; then
+ echo "Running Checkstyle for $dir ..."
+ mvn -f "$dir/pom.xml" checkstyle:check -Dcheckstyle.suppressions.location="$dir/checkstyle/suppression.xml"
+ fi
+done
diff --git a/.github/workflows/actions/load-conf-env/action.yml b/.github/workflows/actions/load-conf-env/action.yml
new file mode 100644
index 000000000..2df5fca08
--- /dev/null
+++ b/.github/workflows/actions/load-conf-env/action.yml
@@ -0,0 +1,12 @@
+name: Action > Load Conf Env
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Load Conf Env
+ run: |
+ pushd $GITHUB_WORKSPACE/devops/linux/docker/conf/env
+ ./create-env-linux-high-1.sh
+ popd
+ source $GITHUB_WORKSPACE/devops/linux/docker/conf.env
+ shell: bash
diff --git a/.github/workflows/actions/prepare/action.yml b/.github/workflows/actions/prepare/action.yml
new file mode 100644
index 000000000..b3f986242
--- /dev/null
+++ b/.github/workflows/actions/prepare/action.yml
@@ -0,0 +1,33 @@
+name: Prepare
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/cached-dependencies
+ id: cache-dependencies
+ - if: ${{ steps.cache-dependencies.outputs.cache-hit != 'true' }}
+ name: Set up Node
+ uses: ./.github/workflows/actions/install-node
+ with:
+ node-version: 20
+ - if: ${{ steps.cache-dependencies.outputs.cache-hit != 'true' }}
+ name: Set up SDKMan
+ uses: ./.github/workflows/actions/install-sdkman
+ - if: ${{ steps.cache-dependencies.outputs.cache-hit != 'true' }}
+ name: Set up Java
+ uses: ./.github/workflows/actions/sdkman-installer
+ with:
+ candidate: java
+ version: '21.0.1-tem'
+ - if: ${{ steps.cache-dependencies.outputs.cache-hit != 'true' }}
+ name: Set up Maven
+ uses: ./.github/workflows/actions/sdkman-installer
+ with:
+ candidate: maven
+ version: '3.9.5'
+ - if: ${{ steps.cache-dependencies.outputs.cache-hit != 'true' }}
+ name: Set Scala Build Tools
+ uses: ./.github/workflows/actions/sdkman-installer
+ with:
+ candidate: sbt
+ version: '1.9.8'
\ No newline at end of file
diff --git a/.github/workflows/actions/sdkman-installer/action.yml b/.github/workflows/actions/sdkman-installer/action.yml
new file mode 100644
index 000000000..53d01cff4
--- /dev/null
+++ b/.github/workflows/actions/sdkman-installer/action.yml
@@ -0,0 +1,15 @@
+name: SDKMan Installer
+inputs:
+ candidate:
+ required: true
+ version:
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - name: Installing ${{ inputs.candidate }} (version ${{ inputs.version }})
+ shell: bash
+ run: |
+ echo "$HOME/.sdkman/bin/sdkman-init.sh"
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ sdk install ${{ inputs.candidate }} ${{ inputs.version }}
diff --git a/.github/workflows/actions/smoke-test/action.yml b/.github/workflows/actions/smoke-test/action.yml
new file mode 100644
index 000000000..69699655b
--- /dev/null
+++ b/.github/workflows/actions/smoke-test/action.yml
@@ -0,0 +1,7 @@
+name: Action > Smoke Test
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
\ No newline at end of file
diff --git a/.github/workflows/actions/test/action.yml b/.github/workflows/actions/test/action.yml
new file mode 100644
index 000000000..d5ec01713
--- /dev/null
+++ b/.github/workflows/actions/test/action.yml
@@ -0,0 +1,20 @@
+name: Action > Test
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: ./.github/workflows/actions/cached-dependencies
+ - name: Testing Java Apps
+ run: |
+ set -eo pipefail
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
+ cd $JEMPI_APP_PATH
+ mvn clean test
+ shell: bash
+ - name: Testing javascript app (ui)
+ run: |
+ source "$HOME/.nvm/nvm.sh"
+ cd $JEMPI_APP_PATH/JeMPI_UI
+ yarn install --frozen-lockfile
+ yarn run test -- --watchAll=false
+ shell: bash
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index e03bd26f3..000000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: JeMPI Maven Build
-
-on:
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up JDK 17
- uses: actions/setup-java@v3
- with:
- java-version: '17'
- distribution: 'temurin'
- cache: maven
- - name: Build with Maven
- run: mvn -B package --file ./JeMPI_Apps/pom.xml
diff --git a/.github/workflows/deploy-images-dockerhub.yml b/.github/workflows/deploy-images-dockerhub.yml
new file mode 100644
index 000000000..92b0c7293
--- /dev/null
+++ b/.github/workflows/deploy-images-dockerhub.yml
@@ -0,0 +1,54 @@
+name: Deploy Images to DockerHub
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Tag to use'
+ required: true
+ type: string
+
+env:
+ GITHUB_WORKFLOW_FOLDER: ./.github/workflows
+ JEMPI_APP_PATH: ./JeMPI_Apps
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ prepare:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/prepare
+ build-deploy-images:
+ runs-on: ubuntu-22.04
+ needs: [prepare]
+ steps:
+ - uses: actions/checkout@v4
+ - id: validate-tag
+ run: |
+
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
+ echo "Can only do a manual deployment on main / master. Exiting."
+ exit 1
+ fi
+
+ git fetch --tags
+ if git rev-parse -q --verify "refs/tags/${{ inputs.tag }}" > /dev/null; then
+ echo "image-build-tag=$(git rev-parse --abbrev-ref HEAD)-$(git log -1 --pretty=format:%h)" >> $GITHUB_OUTPUT
+ echo "docker-push-tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
+ else
+ echo "The tag '${{ inputs.tag }}' does not exist on the branch '$GITHUB_REF_NAME'"
+ exit 1
+ fi
+
+ - uses: ./.github/workflows/actions/build-deploy-images
+ with:
+ image-build-tag: ${{ steps.validate-tag.outputs.image-build-tag }}
+ docker-push-tag: ${{ steps.validate-tag.outputs.docker-push-tag }}
+ docker-host: "docker.io"
+ docker-username: ${{ secrets.DOCKER_HUB_USER_NAME }}
+ docker-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/entry-on-merge.yml b/.github/workflows/entry-on-merge.yml
new file mode 100644
index 000000000..22dbfae9c
--- /dev/null
+++ b/.github/workflows/entry-on-merge.yml
@@ -0,0 +1,39 @@
+name: OnMerge
+
+on:
+ pull_request:
+ branches:
+ - 'dev'
+ - 'main'
+ - 'master'
+ types:
+ - closed
+
+env:
+ GITHUB_WORKFLOW_FOLDER: ./.github/workflows
+ JEMPI_APP_PATH: ./JeMPI_Apps
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ prepare:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/prepare
+ build-deploy-images:
+ runs-on: ubuntu-22.04
+ needs: [prepare]
+ steps:
+ - uses: actions/checkout@v4
+ - id: get-image-build-tag
+ run: echo "image-build-tag=$(git rev-parse --abbrev-ref HEAD)-$(git log -1 --pretty=format:%h)" >> $GITHUB_OUTPUT
+ - uses: ./.github/workflows/actions/build-deploy-images
+ with:
+ image-build-tag: ${{ steps.get-image-build-tag.outputs.image-build-tag }}
+ docker-host: ${{ vars.DOCKER_LOCAL_HOST_NAME }}
+ docker-username: ${{ secrets.DOCKER_LOCAL_USER_NAME }}
+ docker-password: ${{ secrets.DOCKER_LOCAL_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/entry-on-pull-request.yml b/.github/workflows/entry-on-pull-request.yml
new file mode 100644
index 000000000..9729c6ab7
--- /dev/null
+++ b/.github/workflows/entry-on-pull-request.yml
@@ -0,0 +1,49 @@
+name: OnPullRequest
+
+on:
+ pull_request:
+ branches:
+ - 'dev'
+ - 'main'
+ - 'master'
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ GITHUB_WORKFLOW_FOLDER: ./.github/workflows
+ JEMPI_APP_PATH: ./JeMPI_Apps
+
+jobs:
+ prepare:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/prepare
+ lint-check:
+ needs: [prepare]
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/lint
+ build-check:
+ needs: [lint-check]
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/build-check
+ test:
+ needs: [build-check]
+ runs-on: ubuntu-22.04
+ continue-on-error: true # TODO: Uncomment this out once tests are in a better state - ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/test
+ smoke-test:
+ needs: [test]
+ runs-on: ubuntu-22.04
+ continue-on-error: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/smoke-test
\ No newline at end of file
diff --git a/.github/workflows/entry-on-release.yml b/.github/workflows/entry-on-release.yml
new file mode 100644
index 000000000..2a8f298bd
--- /dev/null
+++ b/.github/workflows/entry-on-release.yml
@@ -0,0 +1,39 @@
+name: OnRelease
+
+on:
+ release:
+ branches:
+ - 'main'
+ - 'master'
+ types: [published]
+
+env:
+ GITHUB_WORKFLOW_FOLDER: ./.github/workflows
+ JEMPI_APP_PATH: ./JeMPI_Apps
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ prepare:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/prepare
+ build-deploy-images:
+ runs-on: ubuntu-22.04
+ needs: [prepare]
+ steps:
+ - uses: actions/checkout@v4
+ - id: validate-tag
+ run: |
+ echo "image-build-tag=$(git rev-parse --abbrev-ref HEAD)-$(git log -1 --pretty=format:%h)" >> $GITHUB_OUTPUT
+ echo "docker-push-tag=$GITHUB_REF_NAME" >> $GITHUB_OUTPUT
+ - uses: ./.github/workflows/actions/build-deploy-images
+ with:
+ image-build-tag: ${{ steps.validate-tag.outputs.image-build-tag }}
+ docker-push-tag: ${{ steps.validate-tag.outputs.docker-push-tag }}
+ docker-host: "docker.io"
+ docker-username: ${{ secrets.DOCKER_HUB_USER_NAME }}
+ docker-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/jempiUI.yml b/.github/workflows/jempiUI.yml
deleted file mode 100644
index e91bdac9d..000000000
--- a/.github/workflows/jempiUI.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-on:
- pull_request:
- branches:
- - dev
- paths:
- - "JeMPI_Apps/JeMPI_UI/**"
- push:
- branches:
- - dev
- paths:
- - 'JeMPI_Apps/JeMPI_UI/**'
-jobs:
- common-setup:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Code
- uses: actions/checkout@v2
-
- - name: Setup Node.js
- uses: actions/setup-node@v2
- with:
- node-version: "18"
-
- - name: Install Yarn Package Manager
- run: npm install -g yarn
-
- - name: Change Directory
- run: cd JeMPI_Apps/JeMPI_UI
-
- - name: Install Dependencies
- run: yarn install --frozen-lockfile
-
- lint-and-format:
- name: Lint and Format
- needs: common-setup
- runs-on: ubuntu-latest
- steps:
- - name: Run Formatter/Linter
- run: yarn lint && yarn format
-
- build:
- name: Build
- needs: common-setup
- runs-on: ubuntu-latest
- steps:
- - name: Build
- run: yarn build
diff --git a/.github/workflows/save-docker-images.yml b/.github/workflows/save-docker-images.yml
new file mode 100644
index 000000000..dfafe0b88
--- /dev/null
+++ b/.github/workflows/save-docker-images.yml
@@ -0,0 +1,43 @@
+name: Save Docker Images
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Tag to use (defaults to [branch]-[commit])'
+ required: false
+ type: string
+
+env:
+ GITHUB_WORKFLOW_FOLDER: ./.github/workflows
+ JEMPI_APP_PATH: ./JeMPI_Apps
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ prepare:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/workflows/actions/prepare
+ save-docker-images:
+ runs-on: ubuntu-22.04
+ needs: [prepare]
+ steps:
+ - uses: actions/checkout@v4
+ - id: get-image-build-tag
+ run: |
+ user_tag=${{ inputs.tag }}
+ if [ ! -z "$user_tag" ]; then
+ echo "image-build-tag=$user_tag" >> $GITHUB_OUTPUT
+ else
+ echo "image-build-tag=$(git rev-parse --abbrev-ref HEAD)-$(git log -1 --pretty=format:%h)" >> $GITHUB_OUTPUT
+ fi
+ - uses: ./.github/workflows/actions/docker-images-build
+ with:
+ image-build-tag: ${{ steps.get-image-build-tag.outputs.image-build-tag }}
+ - uses: ./.github/workflows/actions/docker-images-save
+ with:
+ image-build-tag: ${{ steps.get-image-build-tag.outputs.image-build-tag }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ec7af3b03..4ab1743ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,7 @@
sandbox
my-notes
.Rproj.user
+.settings
+.classpath
+.project
+.metals
\ No newline at end of file
diff --git a/JeMPI_Apps/.scalafmt.conf b/JeMPI_Apps/.scalafmt.conf
new file mode 100644
index 000000000..259f078cf
--- /dev/null
+++ b/JeMPI_Apps/.scalafmt.conf
@@ -0,0 +1,2 @@
+version = 3.7.17
+runner.dialect = scala213
\ No newline at end of file
diff --git a/JeMPI_Apps/JeMPI_API/docker/Dockerfile b/JeMPI_Apps/JeMPI_API/docker/Dockerfile
index 7aff0c8bd..02219df10 100644
--- a/JeMPI_Apps/JeMPI_API/docker/Dockerfile
+++ b/JeMPI_Apps/JeMPI_API/docker/Dockerfile
@@ -6,7 +6,7 @@ ADD API-1.0-SNAPSHOT-spring-boot.jar /app/app.jar
RUN printf "#!/bin/bash\n\
cd /app\n\
-java -server --enable-preview -XX:MaxRAMPercentage=80 -XX:+UseZGC -jar /app/app.jar\n" > /entrypoint.sh
+java -server -XX:MaxRAMPercentage=80 -jar /app/app.jar\n" > /entrypoint.sh
RUN chmod +x /entrypoint.sh
diff --git a/JeMPI_Apps/JeMPI_API/pom.xml b/JeMPI_Apps/JeMPI_API/pom.xml
index c2a4b1b54..d34add8d2 100644
--- a/JeMPI_Apps/JeMPI_API/pom.xml
+++ b/JeMPI_Apps/JeMPI_API/pom.xml
@@ -216,15 +216,15 @@
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- 17
- 17
- --enable-preview
-
-
+
+
+
+
+
+
+
+
+
diff --git a/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/AppConfig.java b/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/AppConfig.java
index 350388b82..859286aea 100644
--- a/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/AppConfig.java
+++ b/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/AppConfig.java
@@ -14,21 +14,21 @@ public final class AppConfig {
private static final Logger LOGGER = LogManager.getLogger(AppConfig.class);
private static final Config SYSTEM_PROPERTIES = ConfigFactory.systemProperties();
private static final Config SYSTEM_ENVIRONMENT = ConfigFactory.systemEnvironment();
- public static final Config CONFIG = new Builder()
- .withSystemEnvironment()
- .withSystemProperties()
- .withOptionalRelativeFile("/conf/server.production.conf")
- .withOptionalRelativeFile("/conf/server.staging.conf")
- .withOptionalRelativeFile("/conf/server.test.conf")
- .withResource("application.local.conf")
- .withResource("application.conf")
- .build();
+ public static final Config CONFIG = new Builder().withSystemEnvironment()
+ .withSystemProperties()
+ .withOptionalRelativeFile("/conf/server.production.conf")
+ .withOptionalRelativeFile("/conf/server.staging.conf")
+ .withOptionalRelativeFile("/conf/server.test.conf")
+ .withResource("application.local.conf")
+ .withResource("application.conf")
+ .build();
public static final String POSTGRESQL_IP = CONFIG.getString("POSTGRESQL_IP");
public static final Integer POSTGRESQL_PORT = CONFIG.getInt("POSTGRESQL_PORT");
public static final String POSTGRESQL_USER = CONFIG.getString("POSTGRESQL_USER");
public static final String POSTGRESQL_PASSWORD = CONFIG.getString("POSTGRESQL_PASSWORD");
- public static final String POSTGRESQL_DATABASE = CONFIG.getString("POSTGRESQL_DATABASE");
+ public static final String POSTGRESQL_NOTIFICATIONS_DB = CONFIG.getString("POSTGRESQL_NOTIFICATIONS_DB");
+ public static final String POSTGRESQL_AUDIT_DB = CONFIG.getString("POSTGRESQL_AUDIT_DB");
public static final String KAFKA_BOOTSTRAP_SERVERS = CONFIG.getString("KAFKA_BOOTSTRAP_SERVERS");
public static final String KAFKA_APPLICATION_ID = CONFIG.getString("KAFKA_APPLICATION_ID");
private static final String[] DGRAPH_ALPHA_HOSTS = CONFIG.getString("DGRAPH_HOSTS").split(",");
@@ -42,18 +42,23 @@ public final class AppConfig {
public static final String LINKER_IP = CONFIG.getString("LINKER_IP");
public static final Integer LINKER_HTTP_PORT = CONFIG.getInt("LINKER_HTTP_PORT");
+
+ public static final String CONTROLLER_IP = CONFIG.getString("CONTROLLER_IP");
+ public static final Integer CONTROLLER_HTTP_PORT = CONFIG.getInt("CONTROLLER_HTTP_PORT");
public static final Integer API_HTTP_PORT = CONFIG.getInt("API_HTTP_PORT");
public static final Level GET_LOG_LEVEL = Level.toLevel(CONFIG.getString("LOG4J2_LEVEL"));
+
+ private AppConfig() {
+ }
+
public static String[] getDGraphHosts() {
return DGRAPH_ALPHA_HOSTS;
}
+
public static int[] getDGraphPorts() {
return DGRAPH_ALPHA_PORTS;
}
- private AppConfig() {
- }
-
private static class Builder {
private Config conf = ConfigFactory.empty();
@@ -80,7 +85,9 @@ Builder withSystemEnvironment() {
Builder withResource(final String resource) {
Config resourceConfig = ConfigFactory.parseResources(resource);
- String empty = resourceConfig.entrySet().isEmpty() ? " contains no values" : "";
+ String empty = resourceConfig.entrySet().isEmpty()
+ ? " contains no values"
+ : "";
conf = conf.withFallback(resourceConfig);
LOGGER.info("Loaded config file from resource ({}){}", resource, empty);
return this;
diff --git a/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/API.java b/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/API.java
index a0ac242df..9dac6fab1 100644
--- a/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/API.java
+++ b/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/API.java
@@ -16,7 +16,7 @@
public final class API {
private static final Logger LOGGER = LogManager.getLogger(API.class);
- private static final String CONFIG_RESOURCE_FILE_NAME = "/config-api.json";
+ private static final String CONFIG_RESOURCE_FILE_NAME = "config-api.json";
private final JsonFieldsConfig jsonFieldsConfig = new JsonFieldsConfig(CONFIG_RESOURCE_FILE_NAME);
private HttpServer httpServer;
@@ -35,25 +35,20 @@ public static void main(final String[] args) {
public Behavior create() {
return Behaviors.setup(context -> {
- ActorRef backEnd =
- context.spawn(BackEnd.create(AppConfig.GET_LOG_LEVEL,
- AppConfig.getDGraphHosts(),
- AppConfig.getDGraphPorts(),
- AppConfig.POSTGRESQL_IP,
- AppConfig.POSTGRESQL_PORT,
- AppConfig.POSTGRESQL_USER,
- AppConfig.POSTGRESQL_PASSWORD,
- AppConfig.POSTGRESQL_DATABASE,
- AppConfig.KAFKA_BOOTSTRAP_SERVERS,
- "CLIENT_ID_API-" + UUID.randomUUID()),
- "BackEnd");
+ ActorRef backEnd = context.spawn(BackEnd.create(AppConfig.GET_LOG_LEVEL,
+ AppConfig.getDGraphHosts(),
+ AppConfig.getDGraphPorts(),
+ AppConfig.POSTGRESQL_IP,
+ AppConfig.POSTGRESQL_PORT,
+ AppConfig.POSTGRESQL_USER,
+ AppConfig.POSTGRESQL_PASSWORD,
+ AppConfig.POSTGRESQL_NOTIFICATIONS_DB,
+ AppConfig.POSTGRESQL_AUDIT_DB,
+ AppConfig.KAFKA_BOOTSTRAP_SERVERS,
+ "CLIENT_ID_API-" + UUID.randomUUID()), "BackEnd");
context.watch(backEnd);
httpServer = HttpServer.create();
- httpServer.open("0.0.0.0",
- AppConfig.API_HTTP_PORT,
- context.getSystem(),
- backEnd,
- jsonFieldsConfig.jsonFields);
+ httpServer.open("0.0.0.0", AppConfig.API_HTTP_PORT, context.getSystem(), backEnd, jsonFieldsConfig.jsonFields);
return Behaviors.receive(Void.class).onSignal(Terminated.class, sig -> {
httpServer.close(context.getSystem());
return Behaviors.stopped();
diff --git a/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/HttpServer.java b/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/HttpServer.java
index 6d42e48b8..bb1b55a2c 100644
--- a/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/HttpServer.java
+++ b/JeMPI_Apps/JeMPI_API/src/main/java/org/jembi/jempi/api/HttpServer.java
@@ -4,8 +4,11 @@
import akka.actor.typed.ActorSystem;
import akka.http.javadsl.Http;
import akka.http.javadsl.ServerBinding;
+import akka.http.javadsl.model.HttpEntity;
import akka.http.javadsl.model.StatusCodes;
import akka.http.javadsl.server.AllDirectives;
+import akka.http.javadsl.server.ExceptionHandler;
+import akka.http.javadsl.server.RejectionHandler;
import akka.http.javadsl.server.Route;
import ch.megard.akka.http.cors.javadsl.settings.CorsSettings;
import org.apache.logging.log4j.LogManager;
@@ -15,12 +18,9 @@
import org.jembi.jempi.libapi.BackEnd;
import org.jembi.jempi.libapi.Routes;
import org.jembi.jempi.shared.models.GlobalConstants;
-import org.jembi.jempi.shared.models.RecordType;
import java.util.concurrent.CompletionStage;
-import java.util.regex.Pattern;
-import static akka.http.javadsl.server.PathMatchers.segment;
import static ch.megard.akka.http.cors.javadsl.CorsDirectives.cors;
public final class HttpServer extends AllDirectives {
@@ -55,94 +55,39 @@ public void close(final ActorSystem actorSystem) {
.thenAccept(unbound -> actorSystem.terminate()); // and shutdown when done
}
-
- private Route createJeMPIRoutes(
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final String jsonFields) {
- return concat(post(() -> concat(path(GlobalConstants.SEGMENT_POST_UPDATE_NOTIFICATION,
- () -> Routes.postUpdateNotification(actorSystem, backEnd)),
- path(segment(GlobalConstants.SEGMENT_POST_SIMPLE_SEARCH).slash(segment(Pattern.compile(
- "^(golden|patient)$"))),
- type -> Routes.postSimpleSearch(actorSystem,
- backEnd,
- type.equals("golden")
- ? RecordType.GoldenRecord
- : RecordType.Interaction)),
- path(segment(GlobalConstants.SEGMENT_POST_CUSTOM_SEARCH).slash(segment(Pattern.compile(
- "^(golden|patient)$"))),
- type -> Routes.postCustomSearch(actorSystem,
- backEnd,
- type.equals("golden")
- ? RecordType.GoldenRecord
- : RecordType.Interaction)),
- path(GlobalConstants.SEGMENT_POST_UPLOAD_CSV_FILE,
- () -> Routes.postUploadCsvFile(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_PROXY_POST_CALCULATE_SCORES,
- () -> Routes.proxyPostCalculateScores(AppConfig.LINKER_IP,
- AppConfig.LINKER_HTTP_PORT,
- http)),
- path(GlobalConstants.SEGMENT_POST_FILTER_GIDS,
- () -> Routes.postFilterGids(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_PROXY_CR_REGISTER,
- () -> Routes.postCrRegister(AppConfig.LINKER_IP, AppConfig.LINKER_HTTP_PORT, http)),
- path(GlobalConstants.SEGMENT_PROXY_CR_FIND,
- () -> Routes.postCrFind(AppConfig.LINKER_IP, AppConfig.LINKER_HTTP_PORT, http)),
- path(GlobalConstants.SEGMENT_PROXY_CR_CANDIDATES,
- () -> Routes.postCrCandidates(AppConfig.LINKER_IP, AppConfig.LINKER_HTTP_PORT, http)),
- path(GlobalConstants.SEGMENT_POST_FILTER_GIDS_WITH_INTERACTION_COUNT,
- () -> Routes.postFilterGidsWithInteractionCount(actorSystem, backEnd)))),
- patch(() -> concat(path(segment(GlobalConstants.SEGMENT_PATCH_GOLDEN_RECORD).slash(segment(Pattern.compile(
- "^[A-z0-9]+$"))), gid -> Routes.patchGoldenRecord(actorSystem, backEnd, gid)),
- path(GlobalConstants.SEGMENT_PATCH_IID_NEW_GID_LINK,
- () -> Routes.patchIidNewGidLink(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_PATCH_IID_GID_LINK,
- () -> Routes.patchIidGidLink(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_PROXY_CR_UPDATE_FIELDS,
- () -> Routes.patchCrUpdateFields(AppConfig.LINKER_IP, AppConfig.LINKER_HTTP_PORT, http)))),
- get(() -> concat(path(GlobalConstants.SEGMENT_COUNT_GOLDEN_RECORDS,
- () -> Routes.countGoldenRecords(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_COUNT_INTERACTIONS,
- () -> Routes.countInteractions(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_COUNT_RECORDS,
- () -> Routes.countRecords(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_GIDS_ALL,
- () -> Routes.getGidsAll(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_GIDS_PAGED,
- () -> Routes.getGidsPaged(actorSystem, backEnd)),
- path(segment(GlobalConstants.SEGMENT_GET_INTERACTION).slash(segment(Pattern.compile(
- "^[A-z0-9]+$"))), iid -> Routes.getInteraction(actorSystem, backEnd, iid)),
- path(segment(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORD).slash(segment(Pattern.compile(
- "^[A-z0-9]+$"))), gid -> Routes.getExpandedGoldenRecord(actorSystem, backEnd, gid)),
- path(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORDS_USING_PARAMETER_LIST,
- () -> Routes.getExpandedGoldenRecordsUsingParameterList(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORDS_USING_CSV,
- () -> Routes.getExpandedGoldenRecordsFromUsingCSV(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_EXPANDED_INTERACTIONS_USING_CSV,
- () -> Routes.getExpandedInteractionsUsingCSV(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_GOLDEN_RECORD_AUDIT_TRAIL,
- () -> Routes.getGoldenRecordAuditTrail(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_INTERACTION_AUDIT_TRAIL,
- () -> Routes.getInteractionAuditTrail(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_NOTIFICATIONS,
- () -> Routes.getNotifications(actorSystem, backEnd)),
- path(segment(GlobalConstants.SEGMENT_GET_INTERACTION).slash(segment(Pattern.compile(
- "^[A-z0-9]+$"))), iid -> Routes.getInteraction(actorSystem, backEnd, iid)),
- path(segment(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORD).slash(segment(Pattern.compile(
- "^[A-z0-9]+$"))), gid -> Routes.getExpandedGoldenRecord(actorSystem, backEnd, gid)),
- path(GlobalConstants.SEGMENT_GET_FIELDS_CONFIG, () -> complete(StatusCodes.OK, jsonFields)),
- path(GlobalConstants.SEGMENT_PROXY_GET_CANDIDATES_WITH_SCORES,
- () -> Routes.proxyGetCandidatesWithScore(AppConfig.LINKER_IP,
- AppConfig.LINKER_HTTP_PORT,
- http)))));
- }
-
- Route createCorsRoutes(
+ public Route createCorsRoutes(
final ActorSystem actorSystem,
final ActorRef backEnd,
final String jsonFields) {
final var settings = CorsSettings.create(AppConfig.CONFIG);
- return cors(settings, () -> pathPrefix("JeMPI", () -> createJeMPIRoutes(actorSystem, backEnd, jsonFields)));
+
+ final RejectionHandler rejectionHandler = RejectionHandler.defaultHandler().mapRejectionResponse(response -> {
+ if (response.entity() instanceof HttpEntity.Strict) {
+ String message = ((HttpEntity.Strict) response.entity()).getData().utf8String();
+ LOGGER.warn(String.format("Request was rejected. Reason: %s", message));
+ }
+
+ return response;
+ });
+
+ final ExceptionHandler exceptionHandler = ExceptionHandler.newBuilder().match(Exception.class, x -> {
+ LOGGER.error("An exception occurred while executing the Route", x);
+ return complete(StatusCodes.INTERNAL_SERVER_ERROR, "An exception occurred, see server logs for details");
+ }).build();
+
+ return cors(settings,
+ () -> pathPrefix("JeMPI",
+ () -> concat(Routes.createCoreAPIRoutes(actorSystem,
+ backEnd,
+ jsonFields,
+ AppConfig.LINKER_IP,
+ AppConfig.LINKER_HTTP_PORT,
+ AppConfig.CONTROLLER_IP,
+ AppConfig.CONTROLLER_HTTP_PORT,
+ http),
+ path(GlobalConstants.SEGMENT_GET_FIELDS_CONFIG,
+ () -> complete(StatusCodes.OK, jsonFields))))).seal(rejectionHandler,
+ exceptionHandler);
}
}
diff --git a/JeMPI_Apps/JeMPI_API_KC/docker/Dockerfile b/JeMPI_Apps/JeMPI_API_KC/docker/Dockerfile
index 938e16308..20a470f3c 100644
--- a/JeMPI_Apps/JeMPI_API_KC/docker/Dockerfile
+++ b/JeMPI_Apps/JeMPI_API_KC/docker/Dockerfile
@@ -6,7 +6,7 @@ ADD API_KC-1.0-SNAPSHOT-spring-boot.jar /app/app.jar
RUN printf "#!/bin/bash\n\
cd /app\n\
-java -server --enable-preview -XX:MaxRAMPercentage=80 -XX:+UseZGC -jar /app/app.jar\n" > /entrypoint.sh
+java -server -XX:MaxRAMPercentage=80 -jar /app/app.jar\n" > /entrypoint.sh
RUN chmod +x /entrypoint.sh
diff --git a/JeMPI_Apps/JeMPI_API_KC/pom.xml b/JeMPI_Apps/JeMPI_API_KC/pom.xml
index 253194802..7eee2e845 100644
--- a/JeMPI_Apps/JeMPI_API_KC/pom.xml
+++ b/JeMPI_Apps/JeMPI_API_KC/pom.xml
@@ -230,9 +230,9 @@
org.apache.maven.plugins
maven-compiler-plugin
- 17
- 17
- --enable-preview
+ ${java.version}
+ ${java.version}
+
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/AppConfig.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/AppConfig.java
index 3f62c4e67..79d621c49 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/AppConfig.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/AppConfig.java
@@ -14,20 +14,21 @@ public final class AppConfig {
private static final Logger LOGGER = LogManager.getLogger(AppConfig.class);
private static final Config SYSTEM_PROPERTIES = ConfigFactory.systemProperties();
private static final Config SYSTEM_ENVIRONMENT = ConfigFactory.systemEnvironment();
- public static final Config CONFIG = new Builder()
- .withSystemEnvironment()
- .withSystemProperties()
- .withOptionalRelativeFile("/conf/server.production.conf")
- .withOptionalRelativeFile("/conf/server.staging.conf")
- .withOptionalRelativeFile("/conf/server.test.conf")
- .withResource("application.local.conf")
- .withResource("application.conf")
- .build();
+ public static final Config CONFIG = new Builder().withSystemEnvironment()
+ .withSystemProperties()
+ .withOptionalRelativeFile("/conf/server.production.conf")
+ .withOptionalRelativeFile("/conf/server.staging.conf")
+ .withOptionalRelativeFile("/conf/server.test.conf")
+ .withResource("application.local.conf")
+ .withResource("application.conf")
+ .build();
public static final String POSTGRESQL_IP = CONFIG.getString("POSTGRESQL_IP");
public static final Integer POSTGRESQL_PORT = CONFIG.getInt("POSTGRESQL_PORT");
public static final String POSTGRESQL_USER = CONFIG.getString("POSTGRESQL_USER");
public static final String POSTGRESQL_PASSWORD = CONFIG.getString("POSTGRESQL_PASSWORD");
- public static final String POSTGRESQL_DATABASE = CONFIG.getString("POSTGRESQL_DATABASE");
+ public static final String POSTGRESQL_USERS_DB = CONFIG.getString("POSTGRESQL_USERS_DB");
+ public static final String POSTGRESQL_NOTIFICATIONS_DB = CONFIG.getString("POSTGRESQL_NOTIFICATIONS_DB");
+ public static final String POSTGRESQL_AUDIT_DB = CONFIG.getString("POSTGRESQL_AUDIT_DB");
public static final String KAFKA_BOOTSTRAP_SERVERS = CONFIG.getString("KAFKA_BOOTSTRAP_SERVERS");
public static final String KAFKA_APPLICATION_ID = CONFIG.getString("KAFKA_APPLICATION_ID");
private static final String[] DGRAPH_ALPHA_HOSTS = CONFIG.getString("DGRAPH_HOSTS").split(",");
@@ -41,6 +42,9 @@ public final class AppConfig {
public static final Integer API_KC_HTTP_PORT = CONFIG.getInt("API_KC_HTTP_PORT");
public static final String LINKER_IP = CONFIG.getString("LINKER_IP");
public static final Integer LINKER_HTTP_PORT = CONFIG.getInt("LINKER_HTTP_PORT");
+
+ public static final String CONTROLLER_IP = CONFIG.getString("CONTROLLER_IP");
+ public static final Integer CONTROLLER_HTTP_PORT = CONFIG.getInt("CONTROLLER_HTTP_PORT");
public static final String SESSION_SECRET = CONFIG.getString("JEMPI_SESSION_SECRET");
public static final Level GET_LOG_LEVEL = Level.toLevel(CONFIG.getString("LOG4J2_LEVEL"));
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/APIKC.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/APIKC.java
index 8704c9204..78e4fd6ba 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/APIKC.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/APIKC.java
@@ -6,6 +6,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jembi.jempi.AppConfig;
+import org.jembi.jempi.api.httpServer.HttpServer;
import org.jembi.jempi.libapi.BackEnd;
import org.jembi.jempi.libapi.JsonFieldsConfig;
@@ -14,7 +15,7 @@
public final class APIKC {
private static final Logger LOGGER = LogManager.getLogger(APIKC.class);
- private static final String CONFIG_RESOURCE_FILE_NAME = "/config-api.json";
+ private static final String CONFIG_RESOURCE_FILE_NAME = "config-api.json";
private final JsonFieldsConfig jsonFieldsConfig = new JsonFieldsConfig(CONFIG_RESOURCE_FILE_NAME);
private HttpServer httpServer;
@@ -41,7 +42,8 @@ public Behavior create() {
AppConfig.POSTGRESQL_PORT,
AppConfig.POSTGRESQL_USER,
AppConfig.POSTGRESQL_PASSWORD,
- AppConfig.POSTGRESQL_DATABASE,
+ AppConfig.POSTGRESQL_NOTIFICATIONS_DB,
+ AppConfig.POSTGRESQL_AUDIT_DB,
AppConfig.KAFKA_BOOTSTRAP_SERVERS,
"CLIENT_ID_API_KC-" + UUID.randomUUID()),
"BackEnd");
@@ -54,12 +56,9 @@ public Behavior create() {
final DispatcherSelector selector = DispatcherSelector.fromConfig("akka.actor.default-dispatcher");
final MessageDispatcher dispatcher = (MessageDispatcher) system.dispatchers().lookup(selector);
httpServer = new HttpServer(dispatcher);
- httpServer.open("0.0.0.0",
- AppConfig.API_KC_HTTP_PORT,
- context.getSystem(),
- backEnd,
- jsonFieldsConfig.jsonFields);
+ httpServer.open("0.0.0.0", AppConfig.API_KC_HTTP_PORT, context.getSystem(), backEnd, jsonFieldsConfig.jsonFields);
return Behaviors.receive(Void.class).onSignal(Terminated.class, sig -> {
+ LOGGER.info("API Server Terminated. Reason {}", sig);
httpServer.close(context.getSystem());
return Behaviors.stopped();
}).build();
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/HttpServer.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/HttpServer.java
deleted file mode 100644
index dd12708b3..000000000
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/HttpServer.java
+++ /dev/null
@@ -1,356 +0,0 @@
-package org.jembi.jempi.api;
-
-import akka.actor.typed.ActorRef;
-import akka.actor.typed.ActorSystem;
-import akka.dispatch.MessageDispatcher;
-import akka.http.javadsl.Http;
-import akka.http.javadsl.ServerBinding;
-import akka.http.javadsl.marshallers.jackson.Jackson;
-import akka.http.javadsl.model.HttpResponse;
-import akka.http.javadsl.model.StatusCodes;
-import akka.http.javadsl.server.Route;
-import ch.megard.akka.http.cors.javadsl.settings.CorsSettings;
-import com.softwaremill.session.*;
-import com.softwaremill.session.javadsl.HttpSessionAwareDirectives;
-import com.softwaremill.session.javadsl.InMemoryRefreshTokenStorage;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.config.Configurator;
-import org.jembi.jempi.AppConfig;
-import org.jembi.jempi.libapi.Ask;
-import org.jembi.jempi.libapi.BackEnd;
-import org.jembi.jempi.libapi.Routes;
-import org.jembi.jempi.shared.models.GlobalConstants;
-import org.jembi.jempi.shared.models.RecordType;
-import org.keycloak.adapters.KeycloakDeployment;
-import org.keycloak.adapters.ServerRequest;
-import org.keycloak.adapters.rotation.AdapterTokenVerifier;
-import org.keycloak.common.VerificationException;
-import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.AccessTokenResponse;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.regex.Pattern;
-
-import static akka.http.javadsl.server.PathMatchers.segment;
-import static ch.megard.akka.http.cors.javadsl.CorsDirectives.cors;
-import static com.softwaremill.session.javadsl.SessionTransports.CookieST;
-
-final class HttpServer extends HttpSessionAwareDirectives {
-
- private static final Logger LOGGER = LogManager.getLogger(HttpServer.class);
-
- private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(UserSession.getSerializer());
- // in-memory refresh token storage
- private static final RefreshTokenStorage REFRESH_TOKEN_STORAGE = new InMemoryRefreshTokenStorage<>() {
- @Override
- public void log(final String msg) {
- LOGGER.info(msg);
- }
- };
- private final Refreshable refreshable;
- private final SetSessionTransport sessionTransport;
- private CompletionStage binding = null;
- private AkkaAdapterConfig keycloakConfig = null;
- private KeycloakDeployment keycloak = null;
-
- private Http http = null;
-
- HttpServer(final MessageDispatcher dispatcher) {
- super(new SessionManager<>(SessionConfig.defaultConfig(AppConfig.SESSION_SECRET), BASIC_ENCODER));
-
- // use Refreshable for sessions, which needs to be refreshed or OneOff otherwise
- // using Refreshable, a refresh token is set in form of a cookie or a custom header
- refreshable = new Refreshable<>(getSessionManager(), REFRESH_TOKEN_STORAGE, dispatcher);
-
- // set the session transport - based on Cookies (or Headers)
- sessionTransport = CookieST;
-
- ClassLoader classLoader = getClass().getClassLoader();
- InputStream keycloakConfigStream = classLoader.getResourceAsStream("/keycloak.json");
- keycloakConfig = AkkaKeycloakDeploymentBuilder.loadAdapterConfig(keycloakConfigStream);
- keycloak = AkkaKeycloakDeploymentBuilder.build(keycloakConfig);
- }
-
- public void close(final ActorSystem actorSystem) {
- binding.thenCompose(ServerBinding::unbind) // trigger unbinding from the port
- .thenAccept(unbound -> actorSystem.terminate()); // and shutdown when done
- }
-
- public void open(
- final String httpServerHost,
- final int httpPort,
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final String jsonFields) {
- Configurator.setLevel(this.getClass(), AppConfig.GET_LOG_LEVEL);
- http = Http.get(actorSystem);
- binding = http.newServerAt(httpServerHost, httpPort)
- .bind(this.createCorsRoutes(actorSystem, backEnd, jsonFields));
- LOGGER.info("Server online at http://{}:{}", httpServerHost, httpPort);
- }
-
- private Route patchGoldenRecord(
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final String gid) {
- return requiredSession(refreshable, sessionTransport, session -> {
- if (session != null) {
- LOGGER.info("Current session: {}", session.getEmail());
- return Routes.patchGoldenRecord(actorSystem, backEnd, gid);
- }
- LOGGER.info("No active session");
- return complete(StatusCodes.FORBIDDEN);
- });
- }
-
- private Route getExpandedGoldenRecord(
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final String gid) {
- return requiredSession(refreshable,
- sessionTransport,
- session -> Routes.getExpandedGoldenRecord(actorSystem, backEnd, gid));
- }
-
- private Route getInteraction(
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final String iid) {
- return requiredSession(refreshable,
- sessionTransport,
- session -> Routes.getInteraction(actorSystem, backEnd, iid));
- }
-
-// private Route routeGetPatientResource(
-// final ActorSystem actorSystem,
-// final ActorRef backEnd,
-// final String patientResourceId) {
-// return onComplete(askFindPatientResource(actorSystem, backEnd, patientResourceId),
-// result -> result.isSuccess()
-// ? result.get()
-// .patientResource()
-// .mapLeft(this::mapError)
-// .fold(error -> error,
-// patientResource -> complete(StatusCodes.OK,
-// patientResource
-// ))
-// : complete(StatusCodes.IM_A_TEAPOT));
-// }
-
-// private Route routeSessionGetPatientResource(
-// final ActorSystem actorSystem,
-// final ActorRef backEnd,
-// final String patientResourceId) {
-// return requiredSession(refreshable, sessionTransport, session -> Routes.routeGetPatientResource(actorSystem, backEnd,
-// patientResourceId));
-// }
-
- private User loginWithKeycloakHandler(final OAuthCodeRequestPayload payload) {
- LOGGER.debug("loginWithKeycloak");
- LOGGER.debug("Logging in {}", payload);
- try {
- // Exchange code for a token from Keycloak
- AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(keycloak, payload.code(),
- keycloakConfig.getRedirectUri(),
- payload.sessionId());
- LOGGER.debug("Token Exchange succeeded!");
-
- String tokenString = tokenResponse.getToken();
- String idTokenString = tokenResponse.getIdToken();
-
- AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString,
- keycloak);
- LOGGER.debug("Token Verification succeeded!");
- AccessToken token = tokens.getAccessToken();
- LOGGER.debug("Is user already registered?");
- String email = token.getEmail();
- User user = PsqlQueries.getUserByEmail(email);
- if (user == null) {
- // Register new user
- LOGGER.debug("User registration ... {}", email);
- User newUser = User.buildUserFromToken(token);
- user = PsqlQueries.registerUser(newUser);
- }
- LOGGER.debug("User has signed in : {}", user.getEmail());
- return user;
- } catch (VerificationException e) {
- LOGGER.error("failed verification of token: {}", e.getMessage());
- } catch (ServerRequest.HttpFailure failure) {
- LOGGER.error("failed to turn code into token");
- LOGGER.error("status from server: {}", failure.getStatus());
- if (failure.getError() != null && !failure.getError().trim().isEmpty()) {
- LOGGER.error(failure.getLocalizedMessage(), failure);
- }
- } catch (IOException e) {
- LOGGER.error("failed to turn code into token", e);
- }
- return null;
- }
-
- private CompletionStage askLoginWithKeycloak(
- final OAuthCodeRequestPayload body) {
- CompletionStage stage = CompletableFuture.completedFuture(loginWithKeycloakHandler(body));
- return stage.thenApply(response -> response);
- }
-
- private Route routeLoginWithKeycloakRequest(final CheckHeader checkHeader) {
- return entity(
- Jackson.unmarshaller(OAuthCodeRequestPayload.class),
- obj -> onComplete(askLoginWithKeycloak(obj), response -> {
- if (response.isSuccess()) {
- final var user = response.get();
- if (user != null) {
- return setSession(refreshable,
- sessionTransport,
- new UserSession(user),
- () -> setNewCsrfToken(checkHeader,
- () -> complete(StatusCodes.OK, user, Jackson.marshaller())));
- } else {
- return complete(StatusCodes.FORBIDDEN);
- }
- } else {
- return complete(StatusCodes.IM_A_TEAPOT);
- }
- }));
- }
-
- private Route routeCurrentUser() {
- return requiredSession(refreshable, sessionTransport, session -> {
- if (session != null) {
- LOGGER.info("Current session: {}", session.getEmail());
- return complete(StatusCodes.OK, session, Jackson.marshaller());
- }
- LOGGER.info("No active session");
- return complete(StatusCodes.FORBIDDEN);
- });
- }
-
- private Route routeLogout() {
- return requiredSession(refreshable,
- sessionTransport,
- session -> invalidateSession(refreshable, sessionTransport, () -> extractRequestContext(ctx -> {
- LOGGER.info("Logging out {}", session.getUsername());
- return onSuccess(() -> ctx.completeWith(HttpResponse.create()),
- routeResult -> complete("success"));
- })));
- }
-
- private Route postUploadCsvFile(
- final ActorSystem actorSystem,
- final ActorRef backEnd) {
- return withSizeLimit(1024L * 1024L * 10L,
- () -> requiredSession(refreshable, sessionTransport, session -> {
- if (session != null) {
- LOGGER.info("Current session: {}", session.getEmail());
- return storeUploadedFile("csv",
- info -> {
- try {
- return File.createTempFile("import-", ".csv");
- } catch (Exception e) {
- LOGGER.error("error", e);
- return null;
- }
- },
- (info, file) -> onComplete(Ask.postUploadCsvFile(actorSystem, backEnd,
- info, file),
- response -> response.isSuccess()
- ? complete(StatusCodes.OK)
- : complete(StatusCodes.IM_A_TEAPOT)));
- }
- LOGGER.info("No active session");
- return complete(StatusCodes.FORBIDDEN);
- }));
- }
-
- private Route routeCustomSearch(
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final RecordType recordType) {
- return requiredSession(refreshable, sessionTransport, session -> {
- LOGGER.info("Custom search on {}", recordType);
- // Simple search for golden records
- return Routes.postCustomSearch(actorSystem, backEnd, recordType);
- });
- }
-
- private Route createJeMPIRoutes(
- final ActorSystem actorSystem,
- final ActorRef backEnd) {
- return concat(post(() -> concat(path(GlobalConstants.SEGMENT_POST_UPDATE_NOTIFICATION,
- () -> Routes.postUpdateNotification(actorSystem, backEnd)),
- path(segment(GlobalConstants.SEGMENT_POST_SIMPLE_SEARCH).slash(segment(Pattern.compile(
- "^(golden|patient)$"))), type -> {
- final var t = type.equals("golden")
- ? RecordType.GoldenRecord
- : RecordType.Interaction;
- return Routes.postSimpleSearch(actorSystem, backEnd, t);
- }),
- path(segment(GlobalConstants.SEGMENT_POST_CUSTOM_SEARCH).slash(segment(Pattern.compile(
- "^(golden|patient)$"))), type -> {
- final var t = type.equals("golden")
- ? RecordType.GoldenRecord
- : RecordType.Interaction;
- return this.routeCustomSearch(actorSystem, backEnd, t);
- }),
- path(GlobalConstants.SEGMENT_PROXY_GET_CANDIDATES_WITH_SCORES, () -> Routes.proxyGetCandidatesWithScore(AppConfig.LINKER_IP, AppConfig.LINKER_HTTP_PORT, http)),
- path(GlobalConstants.SEGMENT_POST_UPLOAD_CSV_FILE,
- () -> this.postUploadCsvFile(actorSystem, backEnd)))),
- patch(() -> concat(path(segment(GlobalConstants.SEGMENT_PATCH_GOLDEN_RECORD).slash(segment(Pattern.compile(
- "^[A-z0-9]+$"))), gid -> this.patchGoldenRecord(actorSystem, backEnd, gid)),
- path(GlobalConstants.SEGMENT_PATCH_IID_NEW_GID_LINK,
- () -> Routes.patchIidNewGidLink(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_PATCH_IID_GID_LINK,
- () -> Routes.patchIidGidLink(actorSystem, backEnd)))),
- get(() -> concat(
- path(GlobalConstants.SEGMENT_CURRENT_USER, this::routeCurrentUser),
- path(GlobalConstants.SEGMENT_LOGOUT, this::routeLogout),
- path(GlobalConstants.SEGMENT_COUNT_GOLDEN_RECORDS,
- () -> Routes.countGoldenRecords(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_COUNT_INTERACTIONS,
- () -> Routes.countInteractions(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_COUNT_RECORDS, () -> Routes.countRecords(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_GIDS_ALL, () -> Routes.getGidsAll(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_GIDS_PAGED, () -> Routes.getGidsPaged(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORDS_USING_PARAMETER_LIST,
- () -> Routes.getExpandedGoldenRecordsUsingParameterList(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORDS_USING_CSV,
- () -> Routes.getExpandedGoldenRecordsFromUsingCSV(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_EXPANDED_INTERACTIONS_USING_CSV,
- () -> Routes.getExpandedInteractionsUsingCSV(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_GET_NOTIFICATIONS,
- () -> Routes.getNotifications(actorSystem, backEnd)),
- path(GlobalConstants.SEGMENT_PROXY_GET_CANDIDATES_WITH_SCORES,
- () -> Routes.proxyGetCandidatesWithScore(AppConfig.LINKER_IP, AppConfig.LINKER_HTTP_PORT, http)),
- path(segment(GlobalConstants.SEGMENT_GET_INTERACTION).slash(segment(Pattern.compile("^[A-z0-9]+$"))),
- iid -> this.getInteraction(actorSystem, backEnd, iid)),
- path(segment(GlobalConstants.SEGMENT_GET_EXPANDED_GOLDEN_RECORD).slash(
- segment(Pattern.compile("^[A-z0-9]+$"))),
- gid -> this.getExpandedGoldenRecord(actorSystem, backEnd, gid)))));
- }
-
- Route createCorsRoutes(
- final ActorSystem actorSystem,
- final ActorRef backEnd,
- final String jsonFields) {
- final var settings = CorsSettings.create(AppConfig.CONFIG);
- final CheckHeader checkHeader = new CheckHeader<>(getSessionManager());
- return cors(
- settings,
- () -> randomTokenCsrfProtection(
- checkHeader,
- () -> pathPrefix("JeMPI",
- () -> concat(
- createJeMPIRoutes(actorSystem, backEnd),
- post(() -> path(GlobalConstants.SEGMENT_VALIDATE_OAUTH,
- () -> routeLoginWithKeycloakRequest(checkHeader))),
- get(() -> path(GlobalConstants.SEGMENT_GET_FIELDS_CONFIG,
- () -> setNewCsrfToken(checkHeader,
- () -> complete(StatusCodes.OK, jsonFields))))))));
- }
-
-}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/PsqlQueries.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/PsqlQueries.java
deleted file mode 100644
index b99bc1200..000000000
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/PsqlQueries.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.jembi.jempi.api;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.jembi.jempi.AppConfig;
-
-import java.sql.*;
-
-final class PsqlQueries {
- private static final Logger LOGGER = LogManager.getLogger(PsqlQueries.class);
- private static final String URL = String.format("jdbc:postgresql://%s:%d/%s",
- AppConfig.POSTGRESQL_IP,
- AppConfig.POSTGRESQL_PORT,
- AppConfig.POSTGRESQL_DATABASE);
-
- private PsqlQueries() {
- }
-
- static User getUserByEmail(final String email) {
- try (Connection conn = DriverManager.getConnection(URL, AppConfig.POSTGRESQL_USER, AppConfig.POSTGRESQL_PASSWORD);
- Statement stmt = conn.createStatement()) {
- ResultSet rs = stmt.executeQuery("select * from users where email = '" + email + "'");
- if (rs.next()) {
- return new User(
- rs.getString("id"),
- rs.getString("username"),
- rs.getString("email"),
- rs.getString("family_name"),
- rs.getString("given_name")
- );
- }
- } catch (SQLException e) {
- LOGGER.error(e.getLocalizedMessage(), e);
- }
- return null;
- }
-
- static User registerUser(final User user) {
- String sql = "INSERT INTO users (given_name, family_name, email, username) VALUES"
- + "('" + user.getGivenName() + "', '" + user.getFamilyName() + "', '" + user.getEmail() + "', '" + user.getUsername() + "')";
- try (Connection conn = DriverManager.getConnection(URL, AppConfig.POSTGRESQL_USER, AppConfig.POSTGRESQL_PASSWORD);
- Statement statement = conn.createStatement()) {
- statement.executeUpdate(sql);
- LOGGER.info("Registered a new user");
- } catch (SQLException e) {
- LOGGER.error(e.getLocalizedMessage(), e);
- }
- return getUserByEmail(user.getEmail());
- }
-
-}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/HttpServer.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/HttpServer.java
new file mode 100644
index 000000000..82a842a4a
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/HttpServer.java
@@ -0,0 +1,98 @@
+package org.jembi.jempi.api.httpServer;
+
+import akka.actor.typed.ActorRef;
+import akka.actor.typed.ActorSystem;
+import akka.dispatch.MessageDispatcher;
+import akka.http.javadsl.Http;
+import akka.http.javadsl.ServerBinding;
+import akka.http.javadsl.model.HttpEntity;
+import akka.http.javadsl.model.StatusCodes;
+import akka.http.javadsl.server.ExceptionHandler;
+import akka.http.javadsl.server.RejectionHandler;
+import akka.http.javadsl.server.Route;
+import ch.megard.akka.http.cors.javadsl.settings.CorsSettings;
+import com.softwaremill.session.javadsl.HttpSessionAwareDirectives;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.jembi.jempi.AppConfig;
+import org.jembi.jempi.api.httpServer.httpServerRoutes.RoutesEntries;
+import org.jembi.jempi.api.user.UserSession;
+import org.jembi.jempi.libapi.BackEnd;
+
+import java.util.concurrent.CompletionStage;
+
+import static ch.megard.akka.http.cors.javadsl.CorsDirectives.cors;
+
+public final class HttpServer extends HttpSessionAwareDirectives {
+ private static final Logger LOGGER = LogManager.getLogger(HttpServer.class);
+ private CompletionStage binding = null;
+
+ private ActorSystem actorSystem;
+ private ActorRef backEnd;
+ private String jsonFields;
+ private Http akkaHttpServer = null;
+
+ public HttpServer(final MessageDispatcher dispatcher) {
+ super(new HttpServerSessionManager(dispatcher));
+ }
+
+ public void close(final ActorSystem actorSystem) {
+ binding.thenCompose(ServerBinding::unbind) // trigger unbinding from the port
+ .thenAccept(unbound -> actorSystem.terminate()); // and shutdown when done
+ }
+
+ public void open(
+ final String httpServerHost,
+ final int httpPort,
+ final ActorSystem actorSystem,
+ final ActorRef backEnd,
+ final String jsonFields) {
+
+ this.actorSystem = actorSystem;
+ this.backEnd = backEnd;
+ this.jsonFields = jsonFields;
+ Configurator.setLevel(this.getClass(), AppConfig.GET_LOG_LEVEL);
+
+ akkaHttpServer = Http.get(actorSystem);
+ binding = akkaHttpServer.newServerAt(httpServerHost, httpPort).bind(this.createCorsRoutes());
+ LOGGER.info("Server online at http://{}:{}", httpServerHost, httpPort);
+ }
+
+ public ActorSystem getActorSystem() {
+ return actorSystem;
+ }
+
+ public Http getAkkaHttpServer() {
+ return akkaHttpServer;
+ }
+
+ public String getJsonFields() {
+ return jsonFields;
+ }
+
+ public ActorRef getBackEnd() {
+ return backEnd;
+ }
+
+ Route createCorsRoutes() {
+ final RejectionHandler rejectionHandler = RejectionHandler.defaultHandler().mapRejectionResponse(response -> {
+ if (response.entity() instanceof HttpEntity.Strict) {
+ String message = ((HttpEntity.Strict) response.entity()).getData().utf8String();
+ LOGGER.warn(String.format("Request was rejected. Reason: %s", message));
+ }
+
+ return response;
+ });
+ final ExceptionHandler exceptionHandler = ExceptionHandler.newBuilder().match(Exception.class, x -> {
+ LOGGER.error("An exception occurred while executing the Route", x);
+ return complete(StatusCodes.INTERNAL_SERVER_ERROR, "An exception occurred. Please see server logs for details");
+ }).build();
+
+
+ return cors(CorsSettings.create(AppConfig.CONFIG),
+ () -> pathPrefix("JeMPI", () -> new RoutesEntries(this).getRouteEntries())).seal(rejectionHandler,
+ exceptionHandler);
+ }
+
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/HttpServerSessionManager.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/HttpServerSessionManager.java
new file mode 100644
index 000000000..fa2963db0
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/HttpServerSessionManager.java
@@ -0,0 +1,43 @@
+package org.jembi.jempi.api.httpServer;
+
+import akka.dispatch.MessageDispatcher;
+import com.softwaremill.session.*;
+import com.softwaremill.session.javadsl.InMemoryRefreshTokenStorage;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jembi.jempi.AppConfig;
+import org.jembi.jempi.api.user.UserSession;
+
+import static com.softwaremill.session.javadsl.SessionTransports.HeaderST;
+
+public final class HttpServerSessionManager extends SessionManager {
+ private static final Logger LOGGER = LogManager.getLogger(HttpServerSessionManager.class);
+ private static final SessionEncoder BASIC_ENCODER = new BasicSessionEncoder<>(UserSession.getSerializer());
+ // in-memory refresh token storage
+ private static final RefreshTokenStorage REFRESH_TOKEN_STORAGE = new InMemoryRefreshTokenStorage<>() {
+ @Override
+ public void log(final String msg) {
+ LOGGER.info(msg);
+ }
+ };
+ private final Refreshable refreshable;
+ private final SetSessionTransport sessionTransport;
+
+ public HttpServerSessionManager(final MessageDispatcher dispatcher) {
+ super(SessionConfig.defaultConfig(AppConfig.SESSION_SECRET), BASIC_ENCODER);
+ // use Refreshable for sessions, which needs to be refreshed or OneOff otherwise
+ // using Refreshable, a refresh token is set in form of a cookie or a custom header
+ refreshable = new Refreshable<>(this, REFRESH_TOKEN_STORAGE, dispatcher);
+
+ // set the session transport - based on Cookies (or Headers)
+ sessionTransport = HeaderST;
+ }
+
+ public Refreshable getRefreshable() {
+ return refreshable;
+ }
+
+ public SetSessionTransport getSessionTransport() {
+ return sessionTransport;
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/ApiHttpServerRouteEntries.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/ApiHttpServerRouteEntries.java
new file mode 100644
index 000000000..d15c83408
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/ApiHttpServerRouteEntries.java
@@ -0,0 +1,38 @@
+package org.jembi.jempi.api.httpServer.httpServerRoutes;
+
+import akka.http.javadsl.model.StatusCodes;
+import akka.http.javadsl.server.Route;
+import com.softwaremill.session.CheckHeader;
+import org.jembi.jempi.api.httpServer.HttpServer;
+import org.jembi.jempi.api.httpServer.HttpServerSessionManager;
+import org.jembi.jempi.api.user.UserSession;
+import org.jembi.jempi.libapi.httpServer.HttpServerRouteEntries;
+
+import static akka.http.javadsl.server.Directives.complete;
+
+public abstract class ApiHttpServerRouteEntries extends HttpServerRouteEntries {
+ protected HttpServerSessionManager sessionManager;
+ protected CheckHeader checkHeader;
+
+ public ApiHttpServerRouteEntries(final HttpServer ihttpServer) {
+ super(ihttpServer);
+ sessionManager = (HttpServerSessionManager) this.httpServer.getSessionManager();
+ checkHeader = new CheckHeader<>(sessionManager);
+ }
+
+ /**
+ * @param routes
+ * @return
+ */
+ protected Route requireSession(final Route routes) {
+ return this.httpServer.requiredSession(sessionManager.getRefreshable(), sessionManager.getSessionTransport(), session -> {
+ if (session != null) {
+ return routes;
+ }
+ return complete(StatusCodes.FORBIDDEN);
+ });
+ }
+
+ @Override
+ public abstract Route getRouteEntries();
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/RoutesEntries.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/RoutesEntries.java
new file mode 100644
index 000000000..929259c27
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/RoutesEntries.java
@@ -0,0 +1,30 @@
+package org.jembi.jempi.api.httpServer.httpServerRoutes;
+
+import akka.http.javadsl.server.Route;
+import org.jembi.jempi.AppConfig;
+import org.jembi.jempi.api.httpServer.HttpServer;
+import org.jembi.jempi.api.httpServer.httpServerRoutes.routes.UserRoutes;
+import org.jembi.jempi.libapi.Routes;
+
+import static akka.http.javadsl.server.Directives.concat;
+
+public final class RoutesEntries extends ApiHttpServerRouteEntries {
+ public RoutesEntries(final HttpServer ihttpServer) {
+ super(ihttpServer);
+ }
+
+ @Override
+ public Route getRouteEntries() {
+
+ return concat(new UserRoutes(this.httpServer).getRouteEntries(),
+ requireSession(Routes.createCoreAPIRoutes(this.httpServer.getActorSystem(),
+ this.httpServer.getBackEnd(),
+ this.httpServer.getJsonFields(),
+ AppConfig.LINKER_IP,
+ AppConfig.LINKER_HTTP_PORT,
+ AppConfig.CONTROLLER_IP,
+ AppConfig.CONTROLLER_HTTP_PORT,
+ this.httpServer.getAkkaHttpServer())));
+
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/routes/UserRoutes.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/routes/UserRoutes.java
new file mode 100644
index 000000000..0f1490250
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/httpServer/httpServerRoutes/routes/UserRoutes.java
@@ -0,0 +1,89 @@
+package org.jembi.jempi.api.httpServer.httpServerRoutes.routes;
+
+import akka.http.javadsl.marshallers.jackson.Jackson;
+import akka.http.javadsl.model.HttpResponse;
+import akka.http.javadsl.model.StatusCodes;
+import akka.http.javadsl.server.Route;
+import com.softwaremill.session.CheckHeader;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jembi.jempi.api.httpServer.HttpServer;
+import org.jembi.jempi.api.httpServer.httpServerRoutes.ApiHttpServerRouteEntries;
+import org.jembi.jempi.api.keyCloak.KeyCloakAuthenticator;
+import org.jembi.jempi.api.keyCloak.OAuthCodeRequestPayload;
+import org.jembi.jempi.api.user.UserSession;
+import org.jembi.jempi.shared.models.GlobalConstants;
+
+import static akka.http.javadsl.server.Directives.*;
+
+public final class UserRoutes extends ApiHttpServerRouteEntries {
+ private static final Logger LOGGER = LogManager.getLogger(UserRoutes.class);
+ private final KeyCloakAuthenticator keyCloakAuthenticator;
+
+ public UserRoutes(final HttpServer ihttpServer) {
+ super(ihttpServer);
+ keyCloakAuthenticator = new KeyCloakAuthenticator();
+ }
+
+ private Route routeLoginWithKeycloakRequest(final CheckHeader checkHeader) {
+
+ return entity(Jackson.unmarshaller(OAuthCodeRequestPayload.class),
+ obj -> onComplete(keyCloakAuthenticator.askLoginWithKeycloak(obj), response -> {
+ if (response.isSuccess()) {
+ final var user = response.get();
+ if (user != null) {
+ return this.httpServer.setSession(sessionManager.getRefreshable(),
+ sessionManager.getSessionTransport(),
+ new UserSession(user),
+ () -> this.httpServer.setNewCsrfToken(checkHeader,
+ () -> complete(StatusCodes.OK,
+ user,
+ Jackson.marshaller())));
+ } else {
+ return complete(StatusCodes.FORBIDDEN);
+ }
+ } else {
+ return complete(StatusCodes.IM_A_TEAPOT);
+ }
+ }));
+ }
+
+ private Route routeCurrentUser() {
+ return this.httpServer.optionalSession(sessionManager.getRefreshable(), sessionManager.getSessionTransport(), session -> {
+ if (session.isPresent()) {
+ LOGGER.info("Current session: {}", session.get().getUsername());
+ return complete(StatusCodes.OK, session, Jackson.marshaller());
+ }
+ LOGGER.info("No active session");
+ return complete(StatusCodes.OK, "");
+ });
+ }
+
+ private Route routeLogout() {
+ return this.httpServer.requiredSession(sessionManager.getRefreshable(),
+ sessionManager.getSessionTransport(),
+ session -> this.httpServer.invalidateSession(sessionManager.getRefreshable(),
+ sessionManager.getSessionTransport(),
+ () -> extractRequestContext(ctx -> {
+ LOGGER.info("Logging out {}",
+ session.getUsername());
+ return onSuccess(() -> ctx.completeWith(
+ HttpResponse.create()),
+ routeResult -> complete(
+ "success"));
+ })));
+ }
+
+ @Override
+ public Route getRouteEntries() {
+ return concat(post(() -> path(GlobalConstants.SEGMENT_VALIDATE_OAUTH, () -> routeLoginWithKeycloakRequest(checkHeader))),
+ get(() -> concat(path(GlobalConstants.SEGMENT_GET_FIELDS_CONFIG,
+ () -> httpServer.setNewCsrfToken(checkHeader,
+ () -> complete(StatusCodes.OK,
+ httpServer.getJsonFields()))),
+ path(GlobalConstants.SEGMENT_CURRENT_USER, this::routeCurrentUser),
+ path(GlobalConstants.SEGMENT_LOGOUT, this::routeLogout)))
+
+ );
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/AkkaAdapterConfig.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/AkkaAdapterConfig.java
similarity index 51%
rename from JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/AkkaAdapterConfig.java
rename to JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/AkkaAdapterConfig.java
index 16e2fe5dc..e93d94d57 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/AkkaAdapterConfig.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/AkkaAdapterConfig.java
@@ -1,4 +1,4 @@
-package org.jembi.jempi.api;
+package org.jembi.jempi.api.keyCloak;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@@ -9,16 +9,20 @@
import java.util.TreeMap;
-@JsonPropertyOrder({"realm", "realm-public-key", "auth-server-url", "redirect-uri", "ssl-required", "resource",
- "public-client", "credentials", "use-resource-role-mappings", "enable-cors", "cors-max-age",
- "cors-allowed-methods", "cors-exposed-headers", "expose-token", "bearer-only", "autodetect-bearer-only",
- "connection-pool-size", "socket-timeout-millis", "connection-ttl-millis", "connection-timeout-millis",
- "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore",
- "client-keystore-password", "client-key-password", "always-refresh-token", "register-node-at-startup",
- "register-node-period", "token-store", "adapter-state-cookie-path", "principal-attribute", "proxy-url",
- "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests",
- "public-key-cache-ttl", "policy-enforcer", "ignore-oauth-query-parameter", "verify-token-audience"})
-final class AkkaAdapterConfig extends AdapterConfig {
+@JsonPropertyOrder(
+ {"realm", "realm-public-key", "auth-server-url", "redirect-uri", "ssl-required", "resource", "public-client",
+ "credentials", "use-resource-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods",
+ "cors-exposed-headers", "expose-token", "bearer-only", "autodetect-bearer-only", "connection-pool-size",
+ "socket-timeout-millis", "connection-ttl-millis", "connection-timeout-millis", "allow-any-hostname",
+ "disable-trust-manager", "truststore", "truststore" + "-password", "client-keystore", "client-keystore-password",
+ "client-key-password", "always-refresh-token", "register-node-at-startup", "register-node-period", "token-store",
+ "adapter-state-cookie-path", "principal-attribute", "proxy-url", "turn-off-change-session-id-on-login",
+ "token-minimum-time-to-live", "min-time-between-jwks-requests", "public-key-cache-ttl", "policy-enforcer",
+ "ignore-oauth-query-parameter", "verify-token-audience"})
+public final class AkkaAdapterConfig extends AdapterConfig {
+ @JsonProperty("frontend-kc-url")
+ private String frontendKcUri;
+
@JsonProperty("redirect-uri")
private String redirectUri;
@@ -50,6 +54,10 @@ public Map getCredentials() {
return credentials;
}
+ String getFrontendKcUri() {
+ return EnvUtil.replace(this.frontendKcUri);
+ }
+
String getRedirectUri() {
return EnvUtil.replace(this.redirectUri);
}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/AkkaKeycloakDeploymentBuilder.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/AkkaKeycloakDeploymentBuilder.java
similarity index 92%
rename from JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/AkkaKeycloakDeploymentBuilder.java
rename to JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/AkkaKeycloakDeploymentBuilder.java
index 3b9acfc0a..f5dbc22b4 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/AkkaKeycloakDeploymentBuilder.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/AkkaKeycloakDeploymentBuilder.java
@@ -1,4 +1,4 @@
-package org.jembi.jempi.api;
+package org.jembi.jempi.api.keyCloak;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -10,7 +10,7 @@
import java.io.IOException;
import java.io.InputStream;
-class AkkaKeycloakDeploymentBuilder extends KeycloakDeploymentBuilder {
+public class AkkaKeycloakDeploymentBuilder extends KeycloakDeploymentBuilder {
protected AkkaKeycloakDeploymentBuilder() {
}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/KeyCloakAdapterTokenVerifier.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/KeyCloakAdapterTokenVerifier.java
new file mode 100644
index 000000000..e23b2345b
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/KeyCloakAdapterTokenVerifier.java
@@ -0,0 +1,77 @@
+package org.jembi.jempi.api.keyCloak;
+
+import org.keycloak.TokenVerifier;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.rotation.AdapterTokenVerifier;
+import org.keycloak.adapters.rotation.PublicKeyLocator;
+import org.keycloak.common.VerificationException;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.JsonWebToken;
+
+import java.security.PublicKey;
+
+
+// Code taken from the super class org.keycloak.adapters.rotation.AdapterTokenVerifier
+// Since they are static methods we need to redeclare them here
+// The reason for override is that within JeMPI keycloak has 2 adddress which it is accessed from
+// 1) The frontend url (KC_FRONTEND_URL) which the ui uses, and 2) the backend url (KC_API_URL), which the api uses
+// of which the default verification assumes the address are the same.
+// This change also us to use the appropiate url when verifying the tokenss
+
+public final class KeyCloakAdapterTokenVerifier extends AdapterTokenVerifier {
+
+ public static VerifiedTokens verifyTokens(
+ final String accessTokenString,
+ final String idTokenString,
+ final KeycloakDeployment deployment,
+ final AkkaAdapterConfig keycloakConfig) throws VerificationException {
+ TokenVerifier tokenVerifier =
+ createVerifier(accessTokenString, deployment, true, AccessToken.class, keycloakConfig);
+ AccessToken accessToken = tokenVerifier.verify().getToken();
+
+ if (idTokenString != null) {
+ IDToken idToken = TokenVerifier.create(idTokenString, IDToken.class).getToken();
+ TokenVerifier idTokenVerifier = TokenVerifier.createWithoutSignature(idToken);
+
+ idTokenVerifier.audience(deployment.getResourceName());
+ idTokenVerifier.issuedFor(deployment.getResourceName());
+
+ idTokenVerifier.verify();
+ return new VerifiedTokens(accessToken, idToken);
+ } else {
+ return new VerifiedTokens(accessToken, null);
+ }
+ }
+
+ private static PublicKey getPublicKey(
+ final String kid,
+ final KeycloakDeployment deployment) throws VerificationException {
+ PublicKeyLocator pkLocator = deployment.getPublicKeyLocator();
+
+ PublicKey publicKey = pkLocator.getPublicKey(kid, deployment);
+ if (publicKey == null) {
+ throw new VerificationException("Didn't find publicKey for specified kid");
+ }
+
+ return publicKey;
+ }
+
+ public static TokenVerifier createVerifier(
+ final String tokenString,
+ final KeycloakDeployment deployment,
+ final boolean withDefaultChecks,
+ final Class tokenClass,
+ final AkkaAdapterConfig keycloakConfig) throws VerificationException {
+ TokenVerifier tokenVerifier = TokenVerifier.create(tokenString, tokenClass);
+
+ tokenVerifier.withDefaultChecks()
+ .realmUrl(String.format("%s/realms/%s", keycloakConfig.getFrontendKcUri(), deployment.getRealm()));
+
+ String kid = tokenVerifier.getHeader().getKeyId();
+ PublicKey publicKey = getPublicKey(kid, deployment);
+ tokenVerifier.publicKey(publicKey);
+
+ return tokenVerifier;
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/KeyCloakAuthenticator.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/KeyCloakAuthenticator.java
new file mode 100644
index 000000000..1ce60680b
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/KeyCloakAuthenticator.java
@@ -0,0 +1,80 @@
+package org.jembi.jempi.api.keyCloak;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jembi.jempi.api.persistance.postgres.queries.UserQueries;
+import org.jembi.jempi.api.user.User;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.common.VerificationException;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public final class KeyCloakAuthenticator {
+
+ private static final Logger LOGGER = LogManager.getLogger(KeyCloakAuthenticator.class);
+ private final KeycloakDeployment keycloak;
+ private final AkkaAdapterConfig keycloakConfig;
+ private final UserQueries userQueries;
+
+ public KeyCloakAuthenticator() {
+ ClassLoader classLoader = getClass().getClassLoader();
+ InputStream keycloakConfigStream = classLoader.getResourceAsStream("keycloak.json");
+ keycloakConfig = AkkaKeycloakDeploymentBuilder.loadAdapterConfig(keycloakConfigStream);
+ keycloak = AkkaKeycloakDeploymentBuilder.build(keycloakConfig);
+ userQueries = new UserQueries();
+ }
+
+ private User loginWithKeycloakHandler(final OAuthCodeRequestPayload payload) {
+ LOGGER.debug("loginWithKeycloak");
+ LOGGER.debug("Logging in {}", payload);
+ try {
+ // Exchange code for a token from Keycloak
+ AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(keycloak,
+ payload.code(),
+ keycloakConfig.getRedirectUri(),
+ payload.sessionId());
+ LOGGER.debug("Token Exchange succeeded!");
+
+ String tokenString = tokenResponse.getToken();
+ String idTokenString = tokenResponse.getIdToken();
+
+ KeyCloakAdapterTokenVerifier.VerifiedTokens tokens =
+ KeyCloakAdapterTokenVerifier.verifyTokens(tokenString, idTokenString, keycloak, keycloakConfig);
+ LOGGER.debug("Token Verification succeeded!");
+ AccessToken token = tokens.getAccessToken();
+ LOGGER.debug("Is user already registered?");
+ String username = token.getPreferredUsername();
+ User user = userQueries.getUser(username);
+ if (user == null) {
+ // Register new user
+ LOGGER.debug("User registration ... {}", username);
+ User newUser = User.buildUserFromToken(token);
+ user = userQueries.registerUser(newUser);
+ }
+ LOGGER.debug("User has signed in : {}", username);
+ return user;
+ } catch (VerificationException e) {
+ LOGGER.error("failed verification of token: {}", e.getMessage());
+ } catch (ServerRequest.HttpFailure failure) {
+ LOGGER.error("failed to turn code into token");
+ LOGGER.error("status from server: {}", failure.getStatus());
+ if (failure.getError() != null && !failure.getError().trim().isEmpty()) {
+ LOGGER.error(failure.getLocalizedMessage(), failure);
+ }
+ } catch (IOException e) {
+ LOGGER.error("failed to turn code into token", e);
+ }
+ return null;
+ }
+
+ public CompletionStage askLoginWithKeycloak(final OAuthCodeRequestPayload body) {
+ CompletionStage stage = CompletableFuture.completedFuture(loginWithKeycloakHandler(body));
+ return stage.thenApply(response -> response);
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/OAuthCodeRequestPayload.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/OAuthCodeRequestPayload.java
similarity index 79%
rename from JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/OAuthCodeRequestPayload.java
rename to JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/OAuthCodeRequestPayload.java
index c909e9bec..218390638 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/OAuthCodeRequestPayload.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/keyCloak/OAuthCodeRequestPayload.java
@@ -1,10 +1,10 @@
-package org.jembi.jempi.api;
+package org.jembi.jempi.api.keyCloak;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
-record OAuthCodeRequestPayload(
+public record OAuthCodeRequestPayload(
@JsonProperty("code") String code,
@JsonProperty("state") String state,
@JsonProperty("session_state") String sessionId) {
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/persistance/postgres/QueryRunner.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/persistance/postgres/QueryRunner.java
new file mode 100644
index 000000000..bebe1658b
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/persistance/postgres/QueryRunner.java
@@ -0,0 +1,71 @@
+package org.jembi.jempi.api.persistance.postgres;
+
+import org.jembi.jempi.AppConfig;
+
+import java.sql.*;
+
+public class QueryRunner {
+
+ private static final String URL = String.format("jdbc:postgresql://%s:%d/%s",
+ AppConfig.POSTGRESQL_IP,
+ AppConfig.POSTGRESQL_PORT,
+ AppConfig.POSTGRESQL_USERS_DB);
+
+ protected final Connection establishConnection() throws SQLException {
+ return DriverManager.getConnection(URL, AppConfig.POSTGRESQL_USER, AppConfig.POSTGRESQL_PASSWORD);
+ }
+
+ /**
+ * @param sqlQuery
+ * @param statementUpdater
+ * @param runner
+ * @param
+ * @return
+ * @throws SQLException
+ */
+ public T executor(
+ final String sqlQuery,
+ final ExceptionalConsumer statementUpdater,
+ final ExceptionalFunction runner) throws SQLException {
+ try (Connection connection = establishConnection()) {
+ PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery);
+ statementUpdater.accept(preparedStatement);
+ return runner.apply(preparedStatement);
+
+ }
+ }
+
+ /**
+ * @param sqlQuery
+ * @param statementUpdater
+ * @return
+ * @throws SQLException
+ */
+ public ResultSet executeQuery(
+ final String sqlQuery,
+ final ExceptionalConsumer statementUpdater) throws SQLException {
+ return executor(sqlQuery, statementUpdater, PreparedStatement::executeQuery);
+ }
+
+ /**
+ * @param sqlQuery
+ * @param statementUpdater
+ * @return
+ * @throws SQLException
+ */
+ public int executeUpdate(
+ final String sqlQuery,
+ final ExceptionalConsumer statementUpdater) throws SQLException {
+ return executor(sqlQuery, statementUpdater, PreparedStatement::executeUpdate);
+ }
+
+ @FunctionalInterface
+ public interface ExceptionalConsumer {
+ void accept(T t) throws E;
+ }
+
+ @FunctionalInterface
+ public interface ExceptionalFunction {
+ R apply(T t) throws E;
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/persistance/postgres/queries/UserQueries.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/persistance/postgres/queries/UserQueries.java
new file mode 100644
index 000000000..53178a339
--- /dev/null
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/persistance/postgres/queries/UserQueries.java
@@ -0,0 +1,79 @@
+package org.jembi.jempi.api.persistance.postgres.queries;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jembi.jempi.api.persistance.postgres.QueryRunner;
+import org.jembi.jempi.api.user.User;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+
+public final class UserQueries extends QueryRunner {
+
+ private static final Logger LOGGER = LogManager.getLogger(UserQueries.class);
+
+ public User getUser(final String username) {
+ return this.getUser("username", username);
+ }
+
+ public User getUser(
+ final String field,
+ final String value) {
+ try {
+
+ ResultSet rs = executeQuery(String.format("SELECT * FROM users where %s = ?", field), preparedStatement -> {
+ preparedStatement.setString(1, value);
+ });
+
+ if (rs.next()) {
+ return new User(rs.getString("id"),
+ rs.getString("username"),
+ rs.getString("email"),
+ rs.getString("family_name"),
+ rs.getString("given_name"));
+ }
+ } catch (SQLException e) {
+ LOGGER.error(e.getLocalizedMessage(), e);
+ }
+
+ return null;
+
+ }
+
+ public User registerUser(final User user) {
+
+ try {
+ executeUpdate("INSERT INTO users (given_name, family_name, email, username) VALUES (?, ?, ?, ?)", preparedStatement -> {
+ String givenName = user.getGivenName();
+ String familyName = user.getFamilyName();
+ String email = user.getEmail();
+ String username = user.getUsername();
+
+ preparedStatement.setString(1,
+ givenName == null
+ ? ""
+ : givenName);
+ preparedStatement.setString(2,
+ familyName == null
+ ? ""
+ : familyName);
+ preparedStatement.setString(3,
+ email == null
+ ? ""
+ : email);
+ preparedStatement.setString(4,
+ username == null
+ ? ""
+ : username);
+ });
+
+ LOGGER.info("Registered a new user");
+ return getUser(user.getUsername());
+ } catch (SQLException e) {
+ LOGGER.error(e.getLocalizedMessage(), e);
+ }
+
+ return null;
+ }
+}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/User.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/user/User.java
similarity index 65%
rename from JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/User.java
rename to JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/user/User.java
index f0caac314..0c46e7ba3 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/User.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/user/User.java
@@ -1,15 +1,15 @@
-package org.jembi.jempi.api;
+package org.jembi.jempi.api.user;
import org.keycloak.representations.AccessToken;
-class User {
+public class User {
private String id;
private String username;
private String email;
private String familyName;
private String givenName;
- User(
+ public User(
final String id,
final String username,
final String email,
@@ -22,23 +22,21 @@ class User {
this.setGivenName(givenName);
}
- static User buildUserFromToken(final AccessToken token) {
+ public static User buildUserFromToken(final AccessToken token) {
String familyName = token.getFamilyName();
String givenName = token.getGivenName();
- return new User(
- null,
- token.getPreferredUsername(),
- token.getEmail(),
- familyName != null
- ? familyName
- : "",
- givenName != null
- ? givenName
- : ""
- );
+ return new User(null,
+ token.getPreferredUsername(),
+ token.getEmail(),
+ familyName != null
+ ? familyName
+ : "",
+ givenName != null
+ ? givenName
+ : "");
}
- String getUsername() {
+ public String getUsername() {
return username;
}
@@ -54,7 +52,7 @@ void setId(final String id) {
this.id = id;
}
- String getEmail() {
+ public String getEmail() {
return email;
}
@@ -62,7 +60,7 @@ void setEmail(final String email) {
this.email = email;
}
- String getFamilyName() {
+ public String getFamilyName() {
return familyName;
}
@@ -70,7 +68,7 @@ void setFamilyName(final String familyName) {
this.familyName = familyName;
}
- String getGivenName() {
+ public String getGivenName() {
return givenName;
}
diff --git a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/UserSession.java b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/user/UserSession.java
similarity index 58%
rename from JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/UserSession.java
rename to JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/user/UserSession.java
index 9957c02f6..044758bea 100644
--- a/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/UserSession.java
+++ b/JeMPI_Apps/JeMPI_API_KC/src/main/java/org/jembi/jempi/api/user/UserSession.java
@@ -1,4 +1,4 @@
-package org.jembi.jempi.api;
+package org.jembi.jempi.api.user;
import com.softwaremill.session.converters.MapConverters;
import scala.collection.immutable.Map;
@@ -8,7 +8,7 @@
import java.util.HashMap;
-class UserSession extends User {
+public class UserSession extends User {
/**
* This session serializer converts a session type into a value (always a String type). The first two arguments are just
@@ -18,8 +18,8 @@ class UserSession extends User {
* in the com.softwaremill.session.SessionSerializer companion object, like stringToString and mapToString, just to name a
* few.
*/
- private static final UserSessionSerializer SERIALIZER = new UserSessionSerializer(
- (JFunction1>) user -> {
+ private static final UserSessionSerializer SERIALIZER =
+ new UserSessionSerializer((JFunction1>) user -> {
final java.util.Map m = new HashMap<>();
m.put("id", user.getId());
m.put("email", user.getEmail());
@@ -28,21 +28,18 @@ class UserSession extends User {
m.put("familyName", user.getFamilyName());
return MapConverters.toImmutableMap(m);
},
- (JFunction1