diff --git a/.env-example b/.env-example index b3624f3..4608c81 100644 --- a/.env-example +++ b/.env-example @@ -1,6 +1 @@ -# -# You can generate your own key pair -# -PUBLIC_KEY='-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt8FNmx/af8Ul2eo+dwL/JjnC2cJGhjSMT0AmM3NpGEPNuBC7gPtaMiPMkreoZ9aL5wD9M/Cz1jWdQdj5fT1nxSDut+9Wm+stqAyF5pkw4hXN9Qqhi048sZsVsmqi8M4XJqOg2W2a9+CvQPU1jvSQuwJEEFoXfnPvtVs6aB/YcUoHNuBUlYGVjcZonkJz3JByfn5+q/gI4hsTXzHSuL81n6Uwr2cL8K9DbC5Q8jICK+p8rNZy3z9FuGFtt4f5MG5lVtI8TlTrzhoZ6mKsGaeV+6a6jSFAK4uunGpowfoOKO2aS3GBp6sHz3kEY2vWomr5qvN5z7YSJm0kmtA4fp77MkXuezVLPfjc0G26fTtOoreLKCgFd9YsfacX/Q4nVZv8K5lZ+qQBhX5fl+k3qa8NnDsyGmJnj0UPwbKkdRpRpWPfmc5cnVYkEaQZ/D/St0UvdiqaVUhPyEMMKAYMa7PbQe6L2MQmA3ZsECnbksn6ZF9KWkK/slqOzAhKVo/r7d9X1wT/uVt/b0fBriwGIkORtnQtRQFdslbjrUXDt2GWk3/tm9OHBn/g/jK+NK4TyTzuiXawo39gg/CWZ0MQFU6hLXdn15244Nef2mIWbJece6CLwF/Gn43+z+SuPop+zG1JmkReJ8FDiEjq8KRLNf9UHRFXITsKThsr5s/4+jBTPJkCAwEAAQ==-----END PUBLIC KEY-----' -PRIVATE_KEY='-----BEGIN PRIVATE KEY-----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-----END PRIVATE KEY-----' SPRING_PROFILES_ACTIVE=development \ No newline at end of file diff --git a/HealthCheck b/HealthCheck new file mode 100644 index 0000000..417c4be --- /dev/null +++ b/HealthCheck @@ -0,0 +1,27 @@ +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.Arrays; + +class HealthCheck { + + static final int OK = 0; + static final int NOK = -1; + + public static void main(String[] args) { + try { + System.exit(doHealthCheck(URI.create(Arrays.stream(args).findFirst().orElseThrow()))); + } catch (Exception ignored) { + // Just quietly exit with NOK + System.exit(NOK); + } + } + + private static int doHealthCheck(URI uri) throws IOException { + if (!(uri.toURL().openConnection() instanceof HttpURLConnection httpConnection)) { + return NOK; + } + + return HttpURLConnection.HTTP_OK == httpConnection.getResponseCode() ? OK : NOK; + } +} \ No newline at end of file diff --git a/README.md b/README.md index 55ca84e..427c200 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,6 @@ ### Specifying Environment Variables -There are two mandatory environment variables that you need to set before running the service: - -| Variable Name | Description | Remarks | -|---------------|------------------------------------------------------------------------|-------------------------------------| -| `PUBLIC_KEY` | The public key used to verify JWT authorization | RSA public key (2048 bits or more) | -| `PRIVATE_KEY` | The private key used to generate access tokens for authenticated users | RSA private key (2048 bits or more) | - -> [!TIP] -> -> You can visit [this link](https://travistidwell.com/jsencrypt/demo/) to generate a pair of RSA keys for testing purposes. - The `SPRING_PROFILES_ACTIVE` variable is optional, but you can set it to `development` for local development. You can either specify these environment variables in a `.env` file or set them directly in Run Configurations. @@ -51,11 +40,11 @@ An example `.env` file can be copied from [this file](/.env-example). ### Running the Required Containers -You can run [this script (Windows only)](/run-docker.cmd) and it will start the required containers for local development: PostgreSQL and Redis. +You can run [this script (Windows only)](/initialize-postgres-keycloak.cmd) and it will start the required containers for local development: PostgreSQL and KeyCloak. ## Running the Compose Stack -You can run [this script](/run-docker.cmd) and it will build the service image and start the containers for you. +You can run [this script(Windows only)](/run-docker-compose-stack.cmd) and it will build the service image and start the containers for you. # Additional Notes diff --git a/docker-compose.yml b/docker-compose.yml index 4ddb50f..c0c231b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,19 +8,17 @@ services: context: . dockerfile: ./Dockerfile ports: - - "8080:8080" + - "8088:8088" environment: - SPRING_PROFILES_ACTIVE=development - DATABASE_HOST=postgresql - DATABASE_USERNAME=postgres - - REDIS_HOST=redis - # Those keys are randomly generated, please replace them if you can - - PUBLIC_KEY=-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArEl0hMdt0ozeuilVp+5qfjWUdLieuB8F1LTgeYSfLg6W2EYLQ0mLl498h3fCOCnChWc+Rxb0PbUfPyO61pZvQp5822RD53t1Qvued9lmFti7HITlPjPYXMk2MPCMQAE7AR77VMdNZfiFUXqy9T+GKVmkrZe1c8Zu17Qtm4rjto1nbbHJDHB6FSfdWGHDdJeK696b8IaZ+Zu4nySTs5foTvfHZxNzemkOMsqStsWABTtkuK3vvfDJlFZUvZQSDSM+APqSfBmcdSa5dPSK8Pm5mRfpH//SYVk0XK6xKT4iuP1cXATmYtc1A05IHw+dAzzarbRK9uIRkV5wYa8D6WPRGP7jdOXDkFgqg9tUAOJ6gnp3OP2KotTAyzkfhtydTqEiOWsUITZ5eb2Q5FnLqV1pGqJXpOak/fw5U/kXorzPy4dM2Iwr1XQRjOTfWk9xzqBLXzSapwHCx1TSnXYtDbMDMprlGcNLtCm0MjzMnykqljRe0l3EGh44A14dx2PJVzaRVyj2+liLZTLgb89YFvwaR8gGaz01YXWUb3u3MAwr8tgmONjTcF+F9epfwB6OOad2mx2Y5svu7yc01vMP+5E5D/lkYIlUcKUk9CUlp1jZB3AqUOXFPVjgYRxGHc92/55fyMc4f23wqWj5FHsBvVljPKlhT/dUEH91IPh6YBKClMcCAwEAAQ==-----END PUBLIC KEY----- - - PRIVATE_KEY=-----BEGIN PRIVATE KEY-----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-----END PRIVATE KEY----- + - SERVER_PORT=8088 + - KEYCLOAK_HOST=http://keycloak:8080 depends_on: postgresql: condition: service_healthy - redis: + keycloak: condition: service_healthy postgresql: networks: @@ -44,29 +42,30 @@ services: interval: 10s timeout: 5s retries: 5 - redis: + keycloak: networks: - spring-base-network - image: 'redis:8.2.3-bookworm' - container_name: redis + image: quay.io/keycloak/keycloak:26.4 + container_name: standalone-keycloak + command: start-dev + environment: + - KC_BOOTSTRAP_ADMIN_USERNAME=admin + - KC_BOOTSTRAP_ADMIN_PASSWORD=123456 + - KC_HEALTH_ENABLED=true + - KC_METRICS_ENABLED=true + - KC_HOSTNAME=keycloak ports: - - "6379:6379" - volumes: - - redis-volume:/data + - "8080:8080" + - "9000:9000" healthcheck: - test: [ "CMD", "redis-cli", "ping" ] + test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { java.net.URI uri = java.net.URI.create(args[0]); System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)uri.toURL().openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live'] interval: 5s timeout: 5s - retries: 5 - command: - - redis-server - - --save 60 1 - - --loglevel warning + retries: 15 + start_period: 10s volumes: postgres-volume: name: postgres-volume - redis-volume: - name: redis-volume networks: spring-base-network: driver: bridge \ No newline at end of file diff --git a/initialize-postgres-keycloak.cmd b/initialize-postgres-keycloak.cmd new file mode 100644 index 0000000..d3d90b8 --- /dev/null +++ b/initialize-postgres-keycloak.cmd @@ -0,0 +1,119 @@ +@echo off +SETLOCAL EnableDelayedExpansion + +docker info >nul 2>&1 +if errorlevel 1 ( + echo Error: Docker daemon is not running. + echo Please start Docker Desktop or Docker service and run this script again. + exit /b 1 +) + +:: --- PostgreSQL Setup --- +SET PG_CONTAINER_NAME=standalone-postgresql +SET PG_VOLUME_NAME=postgres-volume +SET PG_COMMAND=docker run -d --name !PG_CONTAINER_NAME! -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=123456" -e "POSTGRES_DB=myspringdatabase" -p 5432:5432 -v !PG_VOLUME_NAME!:/var/lib/postgresql/data postgres:18.0-alpine3.22 + +echo Checking PostgreSQL container [%PG_CONTAINER_NAME%]... +docker ps -a | findstr /C:"!PG_CONTAINER_NAME!" >nul +if errorlevel 1 ( + echo Container [%PG_CONTAINER_NAME%] not existed, creating with volume [%PG_VOLUME_NAME%]... + !PG_COMMAND! +) else ( + docker ps | findstr /C:"!PG_CONTAINER_NAME!" >nul + if errorlevel 1 ( + echo Container with the same name [%PG_CONTAINER_NAME%] stopped, restarting... + docker start !PG_CONTAINER_NAME! + ) else ( + echo Container [%PG_CONTAINER_NAME%] is already running... + ) +) + +:: KEYCLOAK_REALM and CLIENT_ID should match the values in application.properties + +:: --- Keycloak Setup --- +set KEYCLOAK_IMAGE=quay.io/keycloak/keycloak:26.4 +set KEYCLOAK_CONTAINER=standalone-keycloak + +:: application-properties.security.realm-name +set KEYCLOAK_REALM=spring-base + +:: application-properties.security.client-name +set CLIENT_ID=spring-base-client + +set KEYCLOAK_OVERLORD=admin +set KEYCLOAK_ADMIN_PASSWORD=123456 + +echo Stopping and removing old Keycloak containers/images... +docker container rm -f %KEYCLOAK_CONTAINER% 2>nul + +echo Starting Keycloak container [%KEYCLOAK_CONTAINER%]... +docker run --name %KEYCLOAK_CONTAINER% --detach -p 8080:8080 -p 9000:9000 -e "KC_BOOTSTRAP_ADMIN_USERNAME=%KEYCLOAK_OVERLORD%" -e "KC_BOOTSTRAP_ADMIN_PASSWORD=%KEYCLOAK_ADMIN_PASSWORD%" -e "KC_HEALTH_ENABLED=true" -e "KC_METRICS_ENABLED=true" %KEYCLOAK_IMAGE% start-dev + +docker cp HealthCheck %KEYCLOAK_CONTAINER%:/tmp/HealthCheck.java + +:health_check +docker exec %KEYCLOAK_CONTAINER% sh -c "java /tmp/HealthCheck.java http://localhost:9000/health/live" +if !ERRORLEVEL!==0 ( + echo Keycloak is serviceable! + goto after_health_check +) else ( + echo Waiting for Keycloak to be healthy... + timeout /t 5 >nul + goto health_check +) + +:after_health_check + +set KCADM_PATH=/opt/keycloak/bin/kcadm.sh + +echo Configuring KCADM credentials... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% config credentials --server http://localhost:8080 --realm master --user %KEYCLOAK_OVERLORD% --password %KEYCLOAK_ADMIN_PASSWORD% + +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create realms -s realm=%KEYCLOAK_REALM% -s enabled=true + +echo Creating client [%CLIENT_ID%]... + +:: if anyone has any better idea to not use variable, let me know +set "CLIENT_CREATE_CMD=docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients -r %KEYCLOAK_REALM% -s clientId=%CLIENT_ID% -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i" +for /f "usebackq delims=" %%i in (`!CLIENT_CREATE_CMD!`) do ( + set CLIENT_UUID=%%i +) + +:: --- Role and User Setup --- +SET ROLE_ADMIN=ADMIN +SET ROLE_POWER_USER=POWER_USER +SET ROLE_USER=USER + +set ADMIN_USERNAME=administrator +set POWER_USER_USERNAME=power_user +set USER_USERNAME=user + +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_ADMIN% +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_POWER_USER% +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_USER% + +echo Creating admin user [%ADMIN_USERNAME%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%ADMIN_USERNAME% -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %ADMIN_USERNAME% --new-password 123456 + +echo Assigning client role [%ROLE_ADMIN%] to user [%ADMIN_USERNAME%] in client [%CLIENT_ID%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %ADMIN_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_ADMIN% +echo Created user [%ADMIN_USERNAME%] with password 123456 and client role [%ROLE_ADMIN%] in client [%CLIENT_ID%] + +echo Creating regular user [%USER_USERNAME%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%USER_USERNAME% -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %USER_USERNAME% --new-password 123456 + +echo Assigning client role [%ROLE_USER%] to user [%USER_USERNAME%] in client [%CLIENT_ID%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_USER% +echo Created user [%USER_USERNAME%] with password 123456 and client role [%ROLE_USER%] in client [%CLIENT_ID%] + +echo Creating power user [%POWER_USER_USERNAME%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%POWER_USER_USERNAME% -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %POWER_USER_USERNAME% --new-password 123456 + +echo Assigning client role [%ROLE_POWER_USER%] to user [%POWER_USER_USERNAME%] in client [%CLIENT_ID%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %POWER_USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_POWER_USER% +echo Created user [%POWER_USER_USERNAME%] with password 123456 and client role [%ROLE_POWER_USER%] in client [%CLIENT_ID%] + +ENDLOCAL \ No newline at end of file diff --git a/initialize-postgres-keycloak.sh b/initialize-postgres-keycloak.sh new file mode 100644 index 0000000..23f875e --- /dev/null +++ b/initialize-postgres-keycloak.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +# Check if Docker is running +docker info >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Error: Docker daemon is not running." + echo "Please start Docker and run this script again." + exit 1 +fi + +# --- PostgreSQL Setup --- +PG_CONTAINER_NAME="standalone-postgresql" +PG_VOLUME_NAME="postgres-volume" +PG_COMMAND="docker run -d --name ${PG_CONTAINER_NAME} -e \"POSTGRES_USER=postgres\" -e \"POSTGRES_PASSWORD=123456\" -e \"POSTGRES_DB=myspringdatabase\" -p 5432:5432 -v ${PG_VOLUME_NAME}:/var/lib/postgresql/data postgres:18.0-alpine3.22" + +echo "Checking PostgreSQL container [${PG_CONTAINER_NAME}]..." +if ! docker ps -a | grep -q "${PG_CONTAINER_NAME}"; then + echo "Container [${PG_CONTAINER_NAME}] not existed, creating with volume [${PG_VOLUME_NAME}]..." + eval "${PG_COMMAND}" +else + if ! docker ps | grep -q "${PG_CONTAINER_NAME}"; then + echo "Container with the same name [${PG_CONTAINER_NAME}] stopped, restarting..." + docker start "${PG_CONTAINER_NAME}" + else + echo "Container [${PG_CONTAINER_NAME}] is already running..." + fi +fi + +# KEYCLOAK_REALM and CLIENT_ID should match the values in application.properties +# --- Keycloak Setup --- +KEYCLOAK_IMAGE="quay.io/keycloak/keycloak:26.4" +KEYCLOAK_CONTAINER="standalone-keycloak" +# application-properties.security.realm-name +KEYCLOAK_REALM="spring-base" +# application-properties.security.client-name +CLIENT_ID="spring-base-client" +KEYCLOAK_OVERLORD="admin" +KEYCLOAK_ADMIN_PASSWORD="123456" + +echo "Stopping and removing old Keycloak containers/images..." +docker container rm -f "${KEYCLOAK_CONTAINER}" 2>/dev/null + +echo "Starting Keycloak container [${KEYCLOAK_CONTAINER}]..." +docker run --name "${KEYCLOAK_CONTAINER}" --detach -p 8080:8080 -p 9000:9000 -e "KC_BOOTSTRAP_ADMIN_USERNAME=${KEYCLOAK_OVERLORD}" -e "KC_BOOTSTRAP_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}" -e "KC_HEALTH_ENABLED=true" -e "KC_METRICS_ENABLED=true" "${KEYCLOAK_IMAGE}" start-dev + +docker cp HealthCheck "${KEYCLOAK_CONTAINER}":/tmp/HealthCheck.java + +# Health check loop +while true; do + docker exec "${KEYCLOAK_CONTAINER}" sh -c "java /tmp/HealthCheck.java http://localhost:9000/health/live" + if [ $? -eq 0 ]; then + echo "Keycloak is serviceable!" + break + else + echo "Waiting for Keycloak to be healthy..." + sleep 5 + fi +done + +KCADM_PATH="/opt/keycloak/bin/kcadm.sh" + +echo "Configuring KCADM credentials..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" config credentials --server http://localhost:8080 --realm master --user "${KEYCLOAK_OVERLORD}" --password "${KEYCLOAK_ADMIN_PASSWORD}" + +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create realms -s realm="${KEYCLOAK_REALM}" -s enabled=true + +echo "Creating client [${CLIENT_ID}]..." + +CLIENT_UUID=$(docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients -r "${KEYCLOAK_REALM}" -s clientId="${CLIENT_ID}" -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i) + +# --- Role and User Setup --- +ROLE_ADMIN="ADMIN" +ROLE_POWER_USER="POWER_USER" +ROLE_USER="USER" +ADMIN_USERNAME="administrator" +POWER_USER_USERNAME="power_user" +USER_USERNAME="user" + +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients/"${CLIENT_UUID}"/roles -r "${KEYCLOAK_REALM}" -s name="${ROLE_ADMIN}" +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients/"${CLIENT_UUID}"/roles -r "${KEYCLOAK_REALM}" -s name="${ROLE_POWER_USER}" +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients/"${CLIENT_UUID}"/roles -r "${KEYCLOAK_REALM}" -s name="${ROLE_USER}" + +echo "Creating admin user [${ADMIN_USERNAME}]..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create users -r "${KEYCLOAK_REALM}" -s username="${ADMIN_USERNAME}" -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" set-password -r "${KEYCLOAK_REALM}" --username "${ADMIN_USERNAME}" --new-password 123456 + +echo "Assigning client role [${ROLE_ADMIN}] to user [${ADMIN_USERNAME}] in client [${CLIENT_ID}]..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" add-roles -r "${KEYCLOAK_REALM}" --uusername "${ADMIN_USERNAME}" --cclientid "${CLIENT_ID}" --rolename "${ROLE_ADMIN}" +echo "Created user [${ADMIN_USERNAME}] with password 123456 and client role [${ROLE_ADMIN}] in client [${CLIENT_ID}]" + +echo "Creating regular user [${USER_USERNAME}]..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create users -r "${KEYCLOAK_REALM}" -s username="${USER_USERNAME}" -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" set-password -r "${KEYCLOAK_REALM}" --username "${USER_USERNAME}" --new-password 123456 + +echo "Assigning client role [${ROLE_USER}] to user [${USER_USERNAME}] in client [${CLIENT_ID}]..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" add-roles -r "${KEYCLOAK_REALM}" --uusername "${USER_USERNAME}" --cclientid "${CLIENT_ID}" --rolename "${ROLE_USER}" +echo "Created user [${USER_USERNAME}] with password 123456 and client role [${ROLE_USER}] in client [${CLIENT_ID}]" + +echo "Creating power user [${POWER_USER_USERNAME}]..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create users -r "${KEYCLOAK_REALM}" -s username="${POWER_USER_USERNAME}" -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" set-password -r "${KEYCLOAK_REALM}" --username "${POWER_USER_USERNAME}" --new-password 123456 + +echo "Assigning client role [${ROLE_POWER_USER}] to user [${POWER_USER_USERNAME}] in client [${CLIENT_ID}]..." +docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" add-roles -r "${KEYCLOAK_REALM}" --uusername "${POWER_USER_USERNAME}" --cclientid "${CLIENT_ID}" --rolename "${ROLE_POWER_USER}" +echo "Created user [${POWER_USER_USERNAME}] with password 123456 and client role [${ROLE_POWER_USER}] in client [${CLIENT_ID}]" \ No newline at end of file diff --git a/initialize-postgres-redis.cmd b/initialize-postgres-redis.cmd deleted file mode 100644 index d2ca468..0000000 --- a/initialize-postgres-redis.cmd +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -SETLOCAL EnableDelayedExpansion - -docker info >nul 2>&1 -if errorlevel 1 ( - echo Error: Docker daemon is not running. - echo Please start Docker Desktop or Docker service and run this script again. - pause - exit /b 1 -) - -SET PG_CONTAINER_NAME=standalone-postgresql -SET PG_VOLUME_NAME=postgres-volume -SET PG_COMMAND=docker run -d --name !PG_CONTAINER_NAME! -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=123456" -e "POSTGRES_DB=myspringdatabase" -p 5432:5432 -v !PG_VOLUME_NAME!:/var/lib/postgresql/data postgres:18.0-alpine3.22 - -echo Checking PostgreSQL container... -docker ps -a | findstr /C:"!PG_CONTAINER_NAME!" >nul - -if errorlevel 1 ( - echo Container [!PG_CONTAINER_NAME!] not existed, creating with volume [!PG_VOLUME_NAME!]... - !PG_COMMAND! -) else ( - docker ps | findstr /C:"!PG_CONTAINER_NAME!" >nul - if errorlevel 1 ( - echo Container with the same name [!PG_CONTAINER_NAME!] stopped, restarting... - docker start !PG_CONTAINER_NAME! - ) else ( - echo Container [!PG_CONTAINER_NAME!] is already running... - ) -) - -SET REDIS_CONTAINER_NAME=standalone-redis -SET REDIS_VOLUME_NAME=redis-volume -SET REDIS_COMMAND=docker run --detach --name !REDIS_CONTAINER_NAME! -v !REDIS_VOLUME_NAME!:/data -p 6379:6379 redis:8.2.3-bookworm redis-server --save 60 1 --loglevel warning - -echo Checking Redis container... -docker ps -a | findstr /C:"!REDIS_CONTAINER_NAME!" >nul - -if errorlevel 1 ( - echo Container [!REDIS_CONTAINER_NAME!] not existed, creating with volume [!REDIS_VOLUME_NAME!]... - !REDIS_COMMAND! -) else ( - docker ps | findstr /C:"!REDIS_CONTAINER_NAME!" >nul - if errorlevel 1 ( - echo Container with the same name [!REDIS_CONTAINER_NAME!] stopped, restarting... - docker start !REDIS_CONTAINER_NAME! - ) else ( - echo Container [!REDIS_CONTAINER_NAME!] is already running... - ) -) - -echo. -echo Currently running containers: -docker ps - -ENDLOCAL \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3349874..6bb9ab7 100644 --- a/pom.xml +++ b/pom.xml @@ -33,9 +33,6 @@ 2.8.14 - - 4.5.0 - 2.1.0 @@ -51,9 +48,6 @@ 2.0.2 - - 2.2.4 - 1.21.3 @@ -64,6 +58,9 @@ 3.20.0 + + + 4.0.0 @@ -99,15 +96,11 @@ org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-oauth2-resource-server org.springframework.boot - spring-boot-starter-cache - - - org.springframework.boot - spring-boot-starter-data-redis + spring-boot-starter-actuator org.springframework.boot @@ -174,14 +167,6 @@ - - - com.auth0 - java-jwt - ${java-jwt.version} - - - org.springdoc @@ -242,9 +227,9 @@ test - com.redis - testcontainers-redis - ${testcontainers-redis.version} + com.github.dasniko + testcontainers-keycloak + ${testcontainers-keycloak.version} test diff --git a/run-docker-compose-stack.cmd b/run-docker-compose-stack.cmd new file mode 100644 index 0000000..a982040 --- /dev/null +++ b/run-docker-compose-stack.cmd @@ -0,0 +1,73 @@ +@echo off +SETLOCAL EnableDelayedExpansion + +REM Check if Docker daemon is running +docker info >nul 2>&1 +if errorlevel 1 ( + echo Error: Docker daemon is not running. + echo Please start Docker Desktop or Docker service and run this script again. + exit /b 1 +) + +docker compose down +docker rmi --force spring-base:1.0.0 +docker compose up --detach + +SET KEYCLOAK_CONTAINER=standalone-keycloak +SET KCADM_PATH=/opt/keycloak/bin/kcadm.sh +SET KEYCLOAK_REALM=spring-base +SET CLIENT_ID=spring-base-client +SET KEYCLOAK_OVERLORD=admin +SET KEYCLOAK_ADMIN_PASSWORD=123456 + +echo Configuring KCADM credentials... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% config credentials --server http://localhost:8080 --realm master --user %KEYCLOAK_OVERLORD% --password %KEYCLOAK_ADMIN_PASSWORD% + +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create realms -s realm=%KEYCLOAK_REALM% -s enabled=true + +echo Creating client [%CLIENT_ID%]... + +:: if anyone has any better idea to not use variable, let me know +set "CLIENT_CREATE_CMD=docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients -r %KEYCLOAK_REALM% -s clientId=%CLIENT_ID% -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i" +for /f "usebackq delims=" %%i in (`!CLIENT_CREATE_CMD!`) do ( + set CLIENT_UUID=%%i +) + +:: --- Role and User Setup --- +SET ROLE_ADMIN=ADMIN +SET ROLE_POWER_USER=POWER_USER +SET ROLE_USER=USER + +set ADMIN_USERNAME=administrator +set POWER_USER_USERNAME=power_user +set USER_USERNAME=user + +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_ADMIN% +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_POWER_USER% +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_USER% + +echo Creating admin user [%ADMIN_USERNAME%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%ADMIN_USERNAME% -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %ADMIN_USERNAME% --new-password 123456 + +echo Assigning client role [%ROLE_ADMIN%] to user [%ADMIN_USERNAME%] in client [%CLIENT_ID%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %ADMIN_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_ADMIN% +echo Created user [%ADMIN_USERNAME%] with password 123456 and client role [%ROLE_ADMIN%] in client [%CLIENT_ID%] + +echo Creating regular user [%USER_USERNAME%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%USER_USERNAME% -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %USER_USERNAME% --new-password 123456 + +echo Assigning client role [%ROLE_USER%] to user [%USER_USERNAME%] in client [%CLIENT_ID%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_USER% +echo Created user [%USER_USERNAME%] with password 123456 and client role [%ROLE_USER%] in client [%CLIENT_ID%] + +echo Creating power user [%POWER_USER_USERNAME%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%POWER_USER_USERNAME% -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %POWER_USER_USERNAME% --new-password 123456 + +echo Assigning client role [%ROLE_POWER_USER%] to user [%POWER_USER_USERNAME%] in client [%CLIENT_ID%]... +docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %POWER_USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_POWER_USER% +echo Created user [%POWER_USER_USERNAME%] with password 123456 and client role [%ROLE_POWER_USER%] in client [%CLIENT_ID%] + +ENDLOCAL \ No newline at end of file diff --git a/run-docker-compose-stack.sh b/run-docker-compose-stack.sh new file mode 100644 index 0000000..bbcc5bf --- /dev/null +++ b/run-docker-compose-stack.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Check if Docker daemon is running +if ! docker info > /dev/null 2>&1; then + echo "Error: Docker daemon is not running." + echo "Please start Docker service and run this script again." + read -p "Press any key to continue..." + exit 1 +fi + +docker compose down +docker rmi --force spring-base:1.0.0 +docker compose up --detach + +KEYCLOAK_CONTAINER="standalone-keycloak" +KCADM_PATH="/opt/keycloak/bin/kcadm.sh" +KEYCLOAK_REALM="spring-base" +CLIENT_ID="spring-base-client" +KEYCLOAK_OVERLORD="admin" +KEYCLOAK_ADMIN_PASSWORD="123456" + +echo "Configuring KCADM credentials..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" config credentials --server http://localhost:8080 --realm master --user "$KEYCLOAK_OVERLORD" --password "$KEYCLOAK_ADMIN_PASSWORD" + +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create realms -s realm="$KEYCLOAK_REALM" -s enabled=true + +echo "Creating client [$CLIENT_ID]..." + +CLIENT_UUID=$(docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients -r "$KEYCLOAK_REALM" -s clientId="$CLIENT_ID" -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i) + +# --- Role and User Setup --- +ROLE_ADMIN="ADMIN" +ROLE_POWER_USER="POWER_USER" +ROLE_USER="USER" + +ADMIN_USERNAME="administrator" +POWER_USER_USERNAME="power_user" +USER_USERNAME="user" + +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients/"$CLIENT_UUID"/roles -r "$KEYCLOAK_REALM" -s name="$ROLE_ADMIN" +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients/"$CLIENT_UUID"/roles -r "$KEYCLOAK_REALM" -s name="$ROLE_POWER_USER" +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients/"$CLIENT_UUID"/roles -r "$KEYCLOAK_REALM" -s name="$ROLE_USER" + +echo "Creating admin user [$ADMIN_USERNAME]..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create users -r "$KEYCLOAK_REALM" -s username="$ADMIN_USERNAME" -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" set-password -r "$KEYCLOAK_REALM" --username "$ADMIN_USERNAME" --new-password 123456 + +echo "Assigning client role [$ROLE_ADMIN] to user [$ADMIN_USERNAME] in client [$CLIENT_ID]..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" add-roles -r "$KEYCLOAK_REALM" --uusername "$ADMIN_USERNAME" --cclientid "$CLIENT_ID" --rolename "$ROLE_ADMIN" +echo "Created user [$ADMIN_USERNAME] with password 123456 and client role [$ROLE_ADMIN] in client [$CLIENT_ID]" + +echo "Creating regular user [$USER_USERNAME]..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create users -r "$KEYCLOAK_REALM" -s username="$USER_USERNAME" -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" set-password -r "$KEYCLOAK_REALM" --username "$USER_USERNAME" --new-password 123456 + +echo "Assigning client role [$ROLE_USER] to user [$USER_USERNAME] in client [$CLIENT_ID]..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" add-roles -r "$KEYCLOAK_REALM" --uusername "$USER_USERNAME" --cclientid "$CLIENT_ID" --rolename "$ROLE_USER" +echo "Created user [$USER_USERNAME] with password 123456 and client role [$ROLE_USER] in client [$CLIENT_ID]" + +echo "Creating power user [$POWER_USER_USERNAME]..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create users -r "$KEYCLOAK_REALM" -s username="$POWER_USER_USERNAME" -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" set-password -r "$KEYCLOAK_REALM" --username "$POWER_USER_USERNAME" --new-password 123456 + +echo "Assigning client role [$ROLE_POWER_USER] to user [$POWER_USER_USERNAME] in client [$CLIENT_ID]..." +docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" add-roles -r "$KEYCLOAK_REALM" --uusername "$POWER_USER_USERNAME" --cclientid "$CLIENT_ID" --rolename "$ROLE_POWER_USER" +echo "Created user [$POWER_USER_USERNAME] with password 123456 and client role [$ROLE_POWER_USER] in client [$CLIENT_ID]" \ No newline at end of file diff --git a/run-docker.cmd b/run-docker.cmd deleted file mode 100644 index c457268..0000000 --- a/run-docker.cmd +++ /dev/null @@ -1,14 +0,0 @@ -@echo off - -REM Check if Docker daemon is running -docker info >nul 2>&1 -if errorlevel 1 ( - echo Error: Docker daemon is not running. - echo Please start Docker Desktop or Docker service and run this script again. - pause - exit /b 1 -) - -docker compose down -docker rmi --force spring-base -docker compose up --detach \ No newline at end of file diff --git a/src/main/java/com/vulinh/configuration/AsyncSpringEventsConfiguration.java b/src/main/java/com/vulinh/configuration/AsyncSpringEventsConfiguration.java deleted file mode 100644 index 7e27d8e..0000000 --- a/src/main/java/com/vulinh/configuration/AsyncSpringEventsConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.vulinh.configuration; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.system.JavaVersion; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.event.ApplicationEventMulticaster; -import org.springframework.context.event.SimpleApplicationEventMulticaster; -import org.springframework.core.task.TaskExecutor; -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -@Slf4j -@Configuration -public class AsyncSpringEventsConfiguration { - - static final String THREAD_NAME_PREFIX = "event_executor_"; - - @Bean - public ApplicationEventMulticaster applicationEventMulticaster() { - var eventMulticaster = new SimpleApplicationEventMulticaster(); - - var taskExecutor = getTaskExecutor(); - - eventMulticaster.setTaskExecutor(taskExecutor); - - log.info( - "Configured ApplicationEventMulticaster with async task executor: {}", - taskExecutor.getClass().getCanonicalName()); - - return eventMulticaster; - } - - // Use virtual threads only if Java version is 21 - // Not that it is necessary, but a safeguard nonetheless - static TaskExecutor getTaskExecutor() { - if (JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE)) { - return new VirtualThreadTaskExecutor(THREAD_NAME_PREFIX); - } - - log.info("Virtual Threads not supported, using ThreadPoolTaskExecutor..."); - - var executor = new ThreadPoolTaskExecutor(); - - executor.setThreadNamePrefix(THREAD_NAME_PREFIX); - - return executor; - } -} diff --git a/src/main/java/com/vulinh/configuration/CustomAuthenticationManager.java b/src/main/java/com/vulinh/configuration/CustomAuthenticationManager.java deleted file mode 100644 index dee2357..0000000 --- a/src/main/java/com/vulinh/configuration/CustomAuthenticationManager.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.vulinh.configuration; - -import com.vulinh.configuration.data.CustomAuthentication; -import com.vulinh.data.dto.carrier.DecodedJwtPayloadCarrier; -import com.vulinh.data.mapper.UserMapper; -import com.vulinh.data.repository.UserRepository; -import com.vulinh.service.sessions.UserSessionService; -import lombok.RequiredArgsConstructor; -import org.springframework.lang.NonNull; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.InternalAuthenticationServiceException; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class CustomAuthenticationManager implements AuthenticationManager { - - final UserRepository userRepository; - - final UserSessionService userSessionService; - - @Override - @Transactional - @NonNull - public CustomAuthentication authenticate(Authentication authentication) { - if (!(authentication.getPrincipal() - instanceof DecodedJwtPayloadCarrier(var userId, var sessionId))) { - throw new InternalAuthenticationServiceException( - "Invalid authentication principal: %s".formatted(authentication)); - } - - var userDTO = - UserMapper.INSTANCE.toBasicUserDTO( - userRepository.findActiveUser(userId), - userSessionService.findUserSession(userId, sessionId)); - - return new CustomAuthentication(userDTO); - } -} diff --git a/src/main/java/com/vulinh/configuration/PasswordEncoderConfiguration.java b/src/main/java/com/vulinh/configuration/PasswordEncoderConfiguration.java deleted file mode 100644 index 07cf7e0..0000000 --- a/src/main/java/com/vulinh/configuration/PasswordEncoderConfiguration.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.vulinh.configuration; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -@Configuration -@Slf4j -public class PasswordEncoderConfiguration { - - @Bean - public PasswordEncoder passwordEncoder() { - log.info("Using BCryptPassword as password encoder"); - - return new BCryptPasswordEncoder(); - } -} diff --git a/src/main/java/com/vulinh/configuration/SecurityConfiguration.java b/src/main/java/com/vulinh/configuration/SecurityConfiguration.java index bad3eb0..f5ce763 100644 --- a/src/main/java/com/vulinh/configuration/SecurityConfiguration.java +++ b/src/main/java/com/vulinh/configuration/SecurityConfiguration.java @@ -3,21 +3,14 @@ import module java.base; import com.vulinh.configuration.data.ApplicationProperties; +import com.vulinh.configuration.data.ApplicationProperties.SecurityProperties; import com.vulinh.data.constant.UserRole; -import com.vulinh.exception.AuthorizationException; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.service.token.AccessTokenValidator; -import com.vulinh.utils.security.Auth0Utils; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import com.vulinh.utils.JwtUtils; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.lang.NonNull; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.config.Customizer; @@ -26,18 +19,11 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails; import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue; -import org.springframework.util.AntPathMatcher; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.servlet.HandlerExceptionResolver; @Configuration @EnableWebSecurity @@ -45,23 +31,20 @@ @Slf4j public class SecurityConfiguration { - static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); - - static final UserRole ROLE_ADMIN = UserRole.ADMIN; + static final String ROLE_ADMIN_NAME = UserRole.ADMIN.name(); final ApplicationProperties applicationProperties; - final HandlerExceptionResolver handlerExceptionResolver; - final AccessTokenValidator accessTokenValidator; - final CustomAuthenticationManager customAuthenticationManager; - @Bean @SneakyThrows public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) { + var security = applicationProperties.security(); + return httpSecurity .headers( - shc -> - shc.xssProtection( + headersConfigurer -> + headersConfigurer + .xssProtection( xssConfig -> xssConfig.headerValue(HeaderValue.ENABLED_MODE_BLOCK)) .contentSecurityPolicy(cps -> cps.policyDirectives("script-src 'self'"))) .csrf(AbstractHttpConfigurer::disable) @@ -69,14 +52,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) { .sessionManagement( sessionManagementConfigurer -> sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(this::configureAuthorizeHttpRequestCustomizer) - .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(createCorsFilter(), UsernamePasswordAuthenticationFilter.class) - .exceptionHandling( - exceptionHandlingCustomizer -> - exceptionHandlingCustomizer - .accessDeniedHandler(this::handleSecurityException) - .authenticationEntryPoint(this::handleSecurityException)) + .authorizeHttpRequests( + authorizeHttpRequestsCustomizer -> + configureAuthorizeHttpRequestCustomizer(authorizeHttpRequestsCustomizer, security)) + .cors(corsConfigurer -> corsConfigurer.configurationSource(createCorsFilter())) + .oauth2ResourceServer( + oAuth2ResourceServerProperties -> + oAuth2ResourceServerProperties.jwt( + jwtConfigurer -> + jwtConfigurer.jwtAuthenticationConverter( + jwt -> + JwtUtils.parseAuthoritiesByCustomClaims( + jwt, security.clientName())))) .build(); } @@ -85,7 +72,7 @@ public RoleHierarchy roleHierarchy() { return RoleHierarchyImpl.fromHierarchy(toHierarchyPhrase()); } - CorsFilter createCorsFilter() { + CorsConfigurationSource createCorsFilter() { var config = new CorsConfiguration(); config.setAllowCredentials(true); @@ -97,14 +84,13 @@ CorsFilter createCorsFilter() { source.registerCorsConfiguration("/**", config); - return new CorsFilter(source); + return source; } - void configureAuthorizeHttpRequestCustomizer( + static void configureAuthorizeHttpRequestCustomizer( AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry - authorizeHttpRequestsCustomizer) { - var securityProperties = applicationProperties.security(); - + authorizeHttpRequestsCustomizer, + SecurityProperties securityProperties) { for (var verbUrl : securityProperties.noAuthenticatedVerbUrls()) { authorizeHttpRequestsCustomizer.requestMatchers(verbUrl.method(), verbUrl.url()).permitAll(); } @@ -112,30 +98,18 @@ void configureAuthorizeHttpRequestCustomizer( for (var privilegeVerbUrl : securityProperties.highPrivilegeVerbUrls()) { authorizeHttpRequestsCustomizer .requestMatchers(privilegeVerbUrl.method(), privilegeVerbUrl.url()) - .hasAuthority(ROLE_ADMIN.name()); + .hasAuthority(ROLE_ADMIN_NAME); } authorizeHttpRequestsCustomizer .requestMatchers(securityProperties.noAuthenticatedUrls().toArray(String[]::new)) .permitAll() .requestMatchers(securityProperties.highPrivilegeUrls().toArray(String[]::new)) - .hasAuthority(ROLE_ADMIN.name()) + .hasAuthority(ROLE_ADMIN_NAME) .anyRequest() .authenticated(); } - void handleSecurityException( - HttpServletRequest request, HttpServletResponse response, Throwable authException) { - handlerExceptionResolver.resolveException( - request, - response, - null, - AuthorizationException.invalidAuthorization( - "Invalid user authorization", - ServiceErrorCode.MESSAGE_INVALID_AUTHORIZATION, - authException)); - } - static String toHierarchyPhrase() { var sortedRoles = Arrays.stream(UserRole.values()) @@ -159,81 +133,4 @@ static String toHierarchyPhrase() { return result.toString(); } - - // Temporary solution to avoid filter being called twice for every request - class JwtFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) { - try { - var header = request.getHeader(HttpHeaders.AUTHORIZATION); - - if (header != null) { - var jwtPayload = - accessTokenValidator.validateAccessToken(Auth0Utils.parseBearerToken(header)); - - var authentication = - customAuthenticationManager.authenticate( - new PreAuthenticatedAuthenticationToken(jwtPayload, null)); - - var customAuthentication = - authentication.addDetails( - new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails( - request, authentication.getAuthorities())); - - SecurityContextHolder.getContext().setAuthentication(customAuthentication); - } - - filterChain.doFilter(request, response); - } catch (Exception exception) { - handlerExceptionResolver.resolveException(request, response, null, exception); - } - } - - @Override - protected boolean shouldNotFilter(@NonNull HttpServletRequest request) { - var requestURI = request.getRequestURI(); - - var securityProperties = applicationProperties.security(); - - return securityProperties.noAuthenticatedUrls().stream() - .anyMatch( - antUrl -> { - var result = ANT_PATH_MATCHER.match(antUrl, requestURI); - - if (result) { - log.debug( - "Request URI [{}] matched against ant pattern [{}]", requestURI, antUrl); - } - - return result; - }) - || securityProperties.noAuthenticatedVerbUrls().stream() - .anyMatch( - verbAntUrl -> { - var requestMethod = request.getMethod(); - - var antUrl = verbAntUrl.url(); - var antMethod = verbAntUrl.method(); - - var result = - antMethod.name().equals(requestMethod) - && ANT_PATH_MATCHER.match(antUrl, requestURI); - - if (result) { - log.debug( - "Request URI [{} {}] matched against ant pattern [{} {}]", - requestMethod, - requestURI, - antMethod, - antUrl); - } - - return result; - }); - } - } } diff --git a/src/main/java/com/vulinh/configuration/cache/FromUserSessionEntityKeyGenerator.java b/src/main/java/com/vulinh/configuration/cache/FromUserSessionEntityKeyGenerator.java deleted file mode 100644 index cedea4b..0000000 --- a/src/main/java/com/vulinh/configuration/cache/FromUserSessionEntityKeyGenerator.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vulinh.configuration.cache; - -import module java.base; - -import com.vulinh.data.entity.UserSession; -import com.vulinh.data.entity.ids.UserSessionId; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; - -@Component(FromUserSessionEntityKeyGenerator.BEAN_NAME) -@Slf4j -public class FromUserSessionEntityKeyGenerator implements KeyGenerator { - - public static final String BEAN_NAME = "fromUserSessionEntity"; - - @Override - @NonNull - public UserSessionId generate(@NonNull Object target, @NonNull Method method, Object... params) { - var obj = params[0]; - - if (!(obj instanceof UserSession session)) { - throw new ClassCastException( - "Expected class %s, got %s instead".formatted(UserSession.class, obj.getClass())); - } - - var userSessionId = session.getId(); - - log.debug("{}#{} | key = {}", method.getDeclaringClass(), method.getName(), userSessionId); - - return userSessionId; - } -} diff --git a/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java b/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java index 7746834..1b0268d 100644 --- a/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java +++ b/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java @@ -7,24 +7,18 @@ import org.springframework.http.HttpMethod; @ConfigurationProperties(prefix = "application-properties") -public record ApplicationProperties( - SecurityProperties security, SchedulingTaskProperties schedule) { +public record ApplicationProperties(SecurityProperties security) { // Testability @Builder public record SecurityProperties( - String publicKey, - String privateKey, List noAuthenticatedUrls, List noAuthenticatedVerbUrls, List highPrivilegeUrls, List highPrivilegeVerbUrls, - Duration jwtDuration, - Duration refreshJwtDuration, - Duration passwordResetCodeDuration, - String issuer) {} - - public record SchedulingTaskProperties(String cleanExpiredUserSessions) {} + String issuerUri, + String realmName, + String clientName) {} public record VerbUrl(HttpMethod method, String url) {} } diff --git a/src/main/java/com/vulinh/configuration/data/CustomAuthentication.java b/src/main/java/com/vulinh/configuration/data/CustomAuthentication.java deleted file mode 100644 index 4f5959e..0000000 --- a/src/main/java/com/vulinh/configuration/data/CustomAuthentication.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.vulinh.configuration.data; - -import module java.base; - -import com.vulinh.data.dto.response.UserBasicResponse; -import com.vulinh.data.dto.response.data.RoleData; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -@Getter -@EqualsAndHashCode(callSuper = true) -public class CustomAuthentication extends AbstractAuthenticationToken { - - @Serial private static final long serialVersionUID = 8271426933575028085L; - - final UserBasicResponse principal; - - public CustomAuthentication(UserBasicResponse userBasicResponse) { - super( - userBasicResponse.userRoles().stream() - .map(RoleData::id) - .map(String::valueOf) - .map(SimpleGrantedAuthority::new) - .toList()); - - principal = userBasicResponse; - setAuthenticated(true); - } - - public CustomAuthentication addDetails(Object detail) { - setDetails(detail); - - return this; - } - - @Override - public Object getCredentials() { - return null; - } -} diff --git a/src/main/java/com/vulinh/controller/api/AuthAPI.java b/src/main/java/com/vulinh/controller/api/AuthAPI.java deleted file mode 100644 index b23b87b..0000000 --- a/src/main/java/com/vulinh/controller/api/AuthAPI.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.vulinh.controller.api; - -import module java.base; - -import com.vulinh.data.constant.CommonConstant; -import com.vulinh.data.constant.EndpointConstant; -import com.vulinh.data.constant.EndpointConstant.AuthEndpoint; -import com.vulinh.data.dto.carrier.TokenResponse; -import com.vulinh.data.dto.request.PasswordChangeRequest; -import com.vulinh.data.dto.request.RefreshTokenRequest; -import com.vulinh.data.dto.request.UserLoginRequest; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.SingleUserResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RequestMapping(EndpointConstant.ENDPOINT_AUTH) -@Tag( - name = "Authentication controller", - description = - "Public controller that allows user login, user registration and user confirmation") -public interface AuthAPI { - - @PostMapping(AuthEndpoint.LOGIN) - @Operation( - summary = "User login", - description = "Login with username and password", - requestBody = - @io.swagger.v3.oas.annotations.parameters.RequestBody( - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "username": "admin", - "password": "12345678" - } - """))), - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_OK, - description = "Authentication successfully", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0000", - "displayMessage": "Success", - "data": { - "accessToken": "your-access-token", - "issuedAt": "ISO Date and Time", - "expiration": "ISO Date and Time" - } - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_UNAUTHORIZED, - description = "Invalid credentials", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0401", - "displayMessage": "Invalid credentials" - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - GenericResponse login(@RequestBody UserLoginRequest userLoginRequest); - - @PostMapping(AuthEndpoint.REGISTER) - @Operation( - summary = "User Registration", - description = "Register a new user", - requestBody = - @io.swagger.v3.oas.annotations.parameters.RequestBody( - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "username": "username", - "password": "12345678", - "email": "email@company.com", - "fullName": "Name" - } - """))), - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_CREATED, - description = "User created", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0000", - "displayMessage": "Success", - "data": { - "id": "user-uuid-here", - "username": "username", - "fullName": "Name", - "email": "email@company.com", - "createdDate": "ISO Date and Time", - "updatedDate": "ISO Date and Time" - } - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_BAD_REQUEST, - description = CommonConstant.MESSAGE_USER_FIELD_INVALID, - content = - @Content( - examples = - @ExampleObject( - value = CommonConstant.MESSAGE_USER_CREATION_VALIDATION_FAILED))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - @ResponseStatus(HttpStatus.CREATED) - GenericResponse register( - @RequestBody UserRegistrationRequest userRegistrationRequest); - - @GetMapping(AuthEndpoint.CONFIRM_USER) - @Operation( - summary = "Confirm user registration", - description = "Confirm the user registration via a public link", - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_OK, - description = "User registered successfully", - content = @Content(examples = @ExampleObject(value = CommonConstant.MESSAGE_SUCCESS))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_NOT_FOUND, - description = "Invalid user or confirmation code", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0405", - "displayMessage": "Invalid user confirmation" - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - ResponseEntity> confirmUser( - @RequestParam UUID userId, @RequestParam String code); - - @PatchMapping(AuthEndpoint.CHANGE_PASSWORD) - ResponseEntity changePassword( - @RequestBody PasswordChangeRequest passwordChangeRequest, - HttpServletRequest httpServletRequest); - - @PostMapping(AuthEndpoint.REFRESH_TOKEN) - GenericResponse refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest); - - @DeleteMapping(AuthEndpoint.LOG_OUT) - ResponseEntity logout( - @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorization); -} diff --git a/src/main/java/com/vulinh/controller/api/BcryptPublicAPI.java b/src/main/java/com/vulinh/controller/api/BcryptPublicAPI.java deleted file mode 100644 index 5957195..0000000 --- a/src/main/java/com/vulinh/controller/api/BcryptPublicAPI.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.vulinh.controller.api; - -import com.vulinh.data.constant.CommonConstant; -import com.vulinh.data.constant.EndpointConstant; -import com.vulinh.data.dto.request.BCryptPasswordGenerationRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.PasswordResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; - -@RequestMapping(EndpointConstant.ENDPOINT_PASSWORD) -@Tag( - name = "Password Generator", - description = "Public controller that helps generate BCrypt encoded password") -public interface BcryptPublicAPI { - - @PostMapping(EndpointConstant.BcryptEndpoint.GENERATE) - @Operation( - summary = "BCrypt Encoded Password Generator", - description = "Generate encoded password from a raw string (8 characters or more)", - requestBody = - @io.swagger.v3.oas.annotations.parameters.RequestBody( - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "rawPassword": "Raw password" - } - """))), - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_OK, - description = "BCrypt encoded password generated", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0000", - "displayMessage": "Success", - "data": { - "rawPassword": "Raw password", - "encodedPassword": "Bcrypt encoded password" - } - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_BAD_REQUEST, - description = "Password length is less than 8 characters", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "code": "M0002", - "message": "Invalid password" - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - GenericResponse generateEncodedPassword( - @RequestBody BCryptPasswordGenerationRequest bcryptPasswordGenerationRequest); -} diff --git a/src/main/java/com/vulinh/controller/api/UserAPI.java b/src/main/java/com/vulinh/controller/api/UserAPI.java deleted file mode 100644 index e1683e0..0000000 --- a/src/main/java/com/vulinh/controller/api/UserAPI.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.vulinh.controller.api; - -import module java.base; - -import com.vulinh.data.constant.CommonConstant; -import com.vulinh.data.constant.EndpointConstant; -import com.vulinh.data.constant.EndpointConstant.UserEndpoint; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.request.UserSearchRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.SingleUserResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RequestMapping(EndpointConstant.ENDPOINT_USER) -@Tag( - name = "User management controller", - description = "Support various user management operations") -public interface UserAPI { - - @PostMapping(UserEndpoint.CREATE_USER) - @ResponseStatus(HttpStatus.CREATED) - @Operation( - summary = "User Direct Creation", - description = "Directly create an active user (`ADMIN` only)", - requestBody = - @io.swagger.v3.oas.annotations.parameters.RequestBody( - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "username": "username", - "password": "password", - "email": "email@site.com", - "fullName": "Full name", - "userRoles": [ - "ADMIN", - "POWER_USER", - "USER" - ] - } - """))), - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_CREATED, - description = "User created", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0000", - "displayMessage": "Success", - "data": { - "id": "user-uuid-here", - "username": "Username", - "fullName": "Full name", - "email": "mail@site.com", - "isActive": true, - "createdDate": "ISO Date and Time", - "updatedDate": "ISO Date and Time", - "userRoles": [ - { - "id": "ROLE_NAME", - "superiority": 0 - } - ] - } - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_BAD_REQUEST, - description = CommonConstant.MESSAGE_USER_FIELD_INVALID, - content = - @Content( - examples = - @ExampleObject( - value = CommonConstant.MESSAGE_USER_CREATION_VALIDATION_FAILED))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - GenericResponse createUser( - @RequestBody UserRegistrationRequest userRegistrationRequest); - - @DeleteMapping(UserEndpoint.DELETE_USER + "/{id}") - @Operation( - summary = "User Deletion", - description = "Delete a user (require `ADMIN` privilege)", - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_FORBIDDEN, - description = "Users cannot delete themselves", - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0010", - "displayMessage": "Cannot delete self" - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - ResponseEntity deleteUser(@PathVariable UUID id); - - @GetMapping(UserEndpoint.SEARCH) - @Operation( - summary = "Search user via identities and roles", - description = "Search user via identity (id, username, email or full name) and roles", - parameters = { - @Parameter( - in = ParameterIn.QUERY, - name = "identity", - description = "Identity to search (id, username, email or full name)"), - @Parameter( - in = ParameterIn.QUERY, - name = "roles", - description = "List of roles to search (separated by comma character)") - }, - responses = { - @ApiResponse( - responseCode = CommonConstant.MESSAGE_OK, - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "errorCode": "M0000", - "displayMessage": "Success", - "data": { - "content": [ - { - "id": "user-uuid-here", - "username": "username", - "fullName": "Full Name", - "email": "email@company.com", - "isActive": true, - "createdDate": "ISO Date and Time", - "updatedDate": "ISO Date and Time", - "userRoles": [ - { - "id": "ROLE_ID", - "superiority": 0 - } - ] - } - ], - "pageable": { - "pageNumber": 0, - "pageSize": 20, - "sort": { - "empty": true, - "sorted": false, - "unsorted": true - }, - "offset": 0, - "paged": true, - "unpaged": false - }, - "last": true, - "totalElements": 1, - "totalPages": 1, - "first": true, - "size": 20, - "number": 0, - "sort": { - "empty": true, - "sorted": false, - "unsorted": true - }, - "numberOfElements": 1, - "empty": false - } - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_FORBIDDEN, - description = CommonConstant.MESSAGE_NO_PERMISSION, - content = - @Content( - examples = - @ExampleObject( - value = - """ - { - "code": "M0403", - "message": "Invalid authorization info" - } - """))), - @ApiResponse( - responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE, - description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY, - content = - @Content( - examples = - @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR))) - }) - GenericResponse> search( - @ParameterObject UserSearchRequest userSearchRequest, @ParameterObject Pageable pageable); -} diff --git a/src/main/java/com/vulinh/controller/impl/AuthController.java b/src/main/java/com/vulinh/controller/impl/AuthController.java deleted file mode 100644 index 5e3f92d..0000000 --- a/src/main/java/com/vulinh/controller/impl/AuthController.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.vulinh.controller.impl; - -import module java.base; - -import com.vulinh.controller.api.AuthAPI; -import com.vulinh.data.dto.carrier.TokenResponse; -import com.vulinh.data.dto.request.PasswordChangeRequest; -import com.vulinh.data.dto.request.RefreshTokenRequest; -import com.vulinh.data.dto.request.UserLoginRequest; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.GenericResponse.ResponseCreator; -import com.vulinh.data.dto.response.SingleUserResponse; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.service.auth.AuthService; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -public class AuthController implements AuthAPI { - - final AuthService authService; - - @Override - public GenericResponse login(UserLoginRequest userLoginRequest) { - return ResponseCreator.success(authService.login(userLoginRequest)); - } - - @Override - public GenericResponse register( - UserRegistrationRequest userRegistrationRequest) { - return ResponseCreator.success(authService.registerUser(userRegistrationRequest)); - } - - @Override - public ResponseEntity> confirmUser(UUID userId, String code) { - boolean isUserConfirmed = authService.confirmUser(userId, code); - - return isUserConfirmed - ? ResponseEntity.ok(ResponseCreator.success()) - : ResponseEntity.badRequest() - .body(ResponseCreator.toError(ServiceErrorCode.MESSAGE_INVALID_CONFIRMATION)); - } - - @Override - public ResponseEntity changePassword( - PasswordChangeRequest passwordChangeRequest, HttpServletRequest httpServletRequest) { - authService.changePassword(passwordChangeRequest); - - return ResponseEntity.ok().build(); - } - - @Override - public GenericResponse refreshToken(RefreshTokenRequest refreshTokenRequest) { - return ResponseCreator.success(authService.refreshToken(refreshTokenRequest)); - } - - @Override - public ResponseEntity logout(String authorization) { - return ResponseEntity.status( - authService.logout(authorization) ? HttpStatus.OK : HttpStatus.NO_CONTENT) - .build(); - } -} diff --git a/src/main/java/com/vulinh/controller/impl/BcryptPublicController.java b/src/main/java/com/vulinh/controller/impl/BcryptPublicController.java deleted file mode 100644 index a88c026..0000000 --- a/src/main/java/com/vulinh/controller/impl/BcryptPublicController.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.vulinh.controller.impl; - -import com.vulinh.controller.api.BcryptPublicAPI; -import com.vulinh.data.dto.request.BCryptPasswordGenerationRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.GenericResponse.ResponseCreator; -import com.vulinh.data.dto.response.PasswordResponse; -import com.vulinh.exception.ValidationException; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.service.user.UserValidationService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@Slf4j -public class BcryptPublicController implements BcryptPublicAPI { - - final PasswordEncoder passwordEncoder; - - @Override - public GenericResponse generateEncodedPassword( - BCryptPasswordGenerationRequest bcryptPasswordGenerationRequest) { - var rawPassword = bcryptPasswordGenerationRequest.rawPassword(); - - if (StringUtils.isBlank(rawPassword) - || rawPassword.length() < UserValidationService.PASSWORD_MINIMUM_LENGTH) { - throw ValidationException.validationException( - "Invalid password", - ServiceErrorCode.MESSAGE_INVALID_PASSWORD, - UserValidationService.PASSWORD_MINIMUM_LENGTH); - } - - var encodedPassword = passwordEncoder.encode(rawPassword); - - log.info("\nRaw password: {}\nEncoded password: {}", rawPassword, encodedPassword); - - return ResponseCreator.success( - PasswordResponse.builder() - .rawPassword(rawPassword) - .encodedPassword(encodedPassword) - .build()); - } -} diff --git a/src/main/java/com/vulinh/controller/impl/UserController.java b/src/main/java/com/vulinh/controller/impl/UserController.java deleted file mode 100644 index af89c88..0000000 --- a/src/main/java/com/vulinh/controller/impl/UserController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.vulinh.controller.impl; - -import module java.base; - -import com.vulinh.controller.api.UserAPI; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.request.UserSearchRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.GenericResponse.ResponseCreator; -import com.vulinh.data.dto.response.SingleUserResponse; -import com.vulinh.service.user.UserService; -import com.vulinh.utils.ResponseUtils; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -public class UserController implements UserAPI { - - final UserService userService; - - @Override - public GenericResponse createUser( - UserRegistrationRequest userRegistrationRequest) { - return ResponseCreator.success(userService.createUser(userRegistrationRequest)); - } - - @Override - public ResponseEntity deleteUser(UUID id) { - return ResponseUtils.returnOkOrNoContent(userService.delete(id)); - } - - @Override - public GenericResponse> search( - UserSearchRequest userSearchRequest, Pageable pageable) { - return ResponseCreator.success(userService.search(userSearchRequest, pageable)); - } -} diff --git a/src/main/java/com/vulinh/data/constant/CacheConstant.java b/src/main/java/com/vulinh/data/constant/CacheConstant.java deleted file mode 100644 index c5806ae..0000000 --- a/src/main/java/com/vulinh/data/constant/CacheConstant.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vulinh.data.constant; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class CacheConstant { - - public static final String USER_SESSION_CACHE = "user-session-cache"; -} diff --git a/src/main/java/com/vulinh/data/constant/CommonConstant.java b/src/main/java/com/vulinh/data/constant/CommonConstant.java index fce4129..d562986 100644 --- a/src/main/java/com/vulinh/data/constant/CommonConstant.java +++ b/src/main/java/com/vulinh/data/constant/CommonConstant.java @@ -9,24 +9,10 @@ public class CommonConstant { public static final String MESSAGE_OK = "200"; - public static final String MESSAGE_CREATED = "201"; - public static final String MESSAGE_BAD_REQUEST = "400"; public static final String MESSAGE_UNAUTHORIZED = "401"; - public static final String MESSAGE_FORBIDDEN = "403"; - public static final String MESSAGE_NOT_FOUND = "404"; public static final String MESSAGE_INTERNAL_SERVER_CODE = "500"; public static final String MESSAGE_INTERNAL_SERVER_SUMMARY = "Unknown server error"; - public static final String MESSAGE_USER_FIELD_INVALID = "Field validations failed"; - public static final String MESSAGE_NO_PERMISSION = "Insufficient privileges"; - - public static final String MESSAGE_SUCCESS = - """ - { - "code": "M0000", - "message": "Success" - } - """; public static final String MESSAGE_INTERNAL_SERVER_ERROR = """ @@ -35,13 +21,6 @@ public class CommonConstant { "message": "Internal Server error, please contact the development team!" } """; - public static final String MESSAGE_USER_CREATION_VALIDATION_FAILED = - """ - { - "code": "error code", - "message": "error message" - } - """; public static final UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); diff --git a/src/main/java/com/vulinh/data/constant/EndpointConstant.java b/src/main/java/com/vulinh/data/constant/EndpointConstant.java index d3c510a..4759ce7 100644 --- a/src/main/java/com/vulinh/data/constant/EndpointConstant.java +++ b/src/main/java/com/vulinh/data/constant/EndpointConstant.java @@ -6,9 +6,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class EndpointConstant { - public static final String ENDPOINT_AUTH = "/auth"; - public static final String ENDPOINT_USER = "/user"; - public static final String ENDPOINT_PASSWORD = "/password"; public static final String ENDPOINT_POST = "/post"; public static final String ENDPOINT_CATEGORY = "/category"; public static final String ENDPOINT_FREE = "/free"; @@ -16,26 +13,6 @@ public class EndpointConstant { public static final String COMMON_SEARCH_ENDPOINT = "/search"; public static final String ENDPOINT_COMMENT = "/comment"; - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class AuthEndpoint { - - public static final String LOGIN = "/login"; - public static final String REGISTER = "/register"; - public static final String CONFIRM_USER = "/confirm-user"; - public static final String CHANGE_PASSWORD = "/change-password"; - public static final String REFRESH_TOKEN = "/refresh-token"; - public static final String LOG_OUT = "/logout"; - } - - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class UserEndpoint { - - public static final String CREATE_USER = "/create-user"; - public static final String DELETE_USER = "/delete-user"; - public static final String DETAILS = "/details"; - public static final String SEARCH = COMMON_SEARCH_ENDPOINT; - } - @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class PostEndpoint { @@ -48,12 +25,6 @@ public static class PostEndpoint { public static final String REVISION_ENDPOINT = "/revisions"; } - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class BcryptEndpoint { - - public static final String GENERATE = "/generate"; - } - @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class CategoryEndpoint { diff --git a/src/main/java/com/vulinh/data/constant/TokenType.java b/src/main/java/com/vulinh/data/constant/TokenType.java deleted file mode 100644 index e373e96..0000000 --- a/src/main/java/com/vulinh/data/constant/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.vulinh.data.constant; - -public enum TokenType { - ACCESS_TOKEN, - REFRESH_TOKEN -} diff --git a/src/main/java/com/vulinh/data/constant/UserRole.java b/src/main/java/com/vulinh/data/constant/UserRole.java index 2710f9d..6ca5b0c 100644 --- a/src/main/java/com/vulinh/data/constant/UserRole.java +++ b/src/main/java/com/vulinh/data/constant/UserRole.java @@ -12,8 +12,7 @@ public enum UserRole { ADMIN(Integer.MAX_VALUE), POWER_USER(Integer.MAX_VALUE - 1), - USER(0), - INVALID(Integer.MIN_VALUE); + USER(0); // The higher the value, the more "superior" a role is final int superiority; diff --git a/src/main/java/com/vulinh/data/dto/carrier/AccessTokenCarrier.java b/src/main/java/com/vulinh/data/dto/carrier/AccessTokenCarrier.java deleted file mode 100644 index 4eb583f..0000000 --- a/src/main/java/com/vulinh/data/dto/carrier/AccessTokenCarrier.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.vulinh.data.dto.carrier; - -import module java.base; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record AccessTokenCarrier( - TokenResponse tokenResponse, UUID userId, UUID sessionId, Instant refreshTokenExpirationDate) - implements Serializable {} diff --git a/src/main/java/com/vulinh/data/dto/carrier/DecodedJwtPayloadCarrier.java b/src/main/java/com/vulinh/data/dto/carrier/DecodedJwtPayloadCarrier.java deleted file mode 100644 index ca24348..0000000 --- a/src/main/java/com/vulinh/data/dto/carrier/DecodedJwtPayloadCarrier.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vulinh.data.dto.carrier; - -import module java.base; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record DecodedJwtPayloadCarrier(UUID userId, UUID sessionId) implements Serializable {} diff --git a/src/main/java/com/vulinh/data/dto/carrier/RefreshTokenCarrier.java b/src/main/java/com/vulinh/data/dto/carrier/RefreshTokenCarrier.java deleted file mode 100644 index 383f4be..0000000 --- a/src/main/java/com/vulinh/data/dto/carrier/RefreshTokenCarrier.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.vulinh.data.dto.carrier; - -import module java.base; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record RefreshTokenCarrier(String refreshToken, Instant expirationDate) - implements Serializable {} diff --git a/src/main/java/com/vulinh/data/dto/carrier/TokenResponse.java b/src/main/java/com/vulinh/data/dto/carrier/TokenResponse.java deleted file mode 100644 index ab7382a..0000000 --- a/src/main/java/com/vulinh/data/dto/carrier/TokenResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.vulinh.data.dto.carrier; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record TokenResponse(String accessToken, String refreshToken) {} diff --git a/src/main/java/com/vulinh/data/dto/event/UserRegistrationEvent.java b/src/main/java/com/vulinh/data/dto/event/UserRegistrationEvent.java deleted file mode 100644 index 48e0752..0000000 --- a/src/main/java/com/vulinh/data/dto/event/UserRegistrationEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.vulinh.data.dto.event; - -import com.vulinh.data.entity.Users; -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record UserRegistrationEvent(Users user) {} diff --git a/src/main/java/com/vulinh/data/dto/projection/AuthorProjection.java b/src/main/java/com/vulinh/data/dto/projection/AuthorProjection.java deleted file mode 100644 index 6756db2..0000000 --- a/src/main/java/com/vulinh/data/dto/projection/AuthorProjection.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vulinh.data.dto.projection; - -import com.vulinh.data.base.UUIDIdentifiable; - -public interface AuthorProjection extends UUIDIdentifiable { - - String getUsername(); - - String getFullName(); -} diff --git a/src/main/java/com/vulinh/data/dto/projection/PrefetchPostProjection.java b/src/main/java/com/vulinh/data/dto/projection/PrefetchPostProjection.java index 5a498c7..1c57282 100644 --- a/src/main/java/com/vulinh/data/dto/projection/PrefetchPostProjection.java +++ b/src/main/java/com/vulinh/data/dto/projection/PrefetchPostProjection.java @@ -13,7 +13,7 @@ public interface PrefetchPostProjection extends UUIDIdentifiable, DateTimeAudita String getSlug(); - AuthorProjection getAuthor(); + UUID getAuthorId(); CategoryProjection getCategory(); diff --git a/src/main/java/com/vulinh/data/dto/request/BCryptPasswordGenerationRequest.java b/src/main/java/com/vulinh/data/dto/request/BCryptPasswordGenerationRequest.java deleted file mode 100644 index c2084c3..0000000 --- a/src/main/java/com/vulinh/data/dto/request/BCryptPasswordGenerationRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.vulinh.data.dto.request; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record BCryptPasswordGenerationRequest(String rawPassword) {} diff --git a/src/main/java/com/vulinh/data/dto/request/PasswordChangeRequest.java b/src/main/java/com/vulinh/data/dto/request/PasswordChangeRequest.java deleted file mode 100644 index 8964cfd..0000000 --- a/src/main/java/com/vulinh/data/dto/request/PasswordChangeRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.vulinh.data.dto.request; - -import module java.base; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record PasswordChangeRequest(String oldPassword, String newPassword) - implements Serializable {} diff --git a/src/main/java/com/vulinh/data/dto/request/RefreshTokenRequest.java b/src/main/java/com/vulinh/data/dto/request/RefreshTokenRequest.java deleted file mode 100644 index 273c5a9..0000000 --- a/src/main/java/com/vulinh/data/dto/request/RefreshTokenRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.vulinh.data.dto.request; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record RefreshTokenRequest(String refreshToken) {} diff --git a/src/main/java/com/vulinh/data/dto/request/UserLoginRequest.java b/src/main/java/com/vulinh/data/dto/request/UserLoginRequest.java deleted file mode 100644 index 7b261e0..0000000 --- a/src/main/java/com/vulinh/data/dto/request/UserLoginRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.vulinh.data.dto.request; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record UserLoginRequest(String username, String password) {} diff --git a/src/main/java/com/vulinh/data/dto/request/UserRegistrationRequest.java b/src/main/java/com/vulinh/data/dto/request/UserRegistrationRequest.java deleted file mode 100644 index f8b2d61..0000000 --- a/src/main/java/com/vulinh/data/dto/request/UserRegistrationRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.vulinh.data.dto.request; - -import module java.base; - -import com.vulinh.data.constant.UserRole; -import com.vulinh.utils.TextSanitizer; -import lombok.Builder; -import lombok.With; -import org.apache.commons.lang3.StringUtils; - -@With -@Builder -public record UserRegistrationRequest( - String username, String password, String fullName, String email, Collection userRoles) { - - public UserRegistrationRequest { - userRoles = userRoles == null ? Set.of(UserRole.USER.name()) : userRoles; - fullName = TextSanitizer.sanitize(StringUtils.normalizeSpace(fullName)); - } -} diff --git a/src/main/java/com/vulinh/data/dto/request/UserSearchRequest.java b/src/main/java/com/vulinh/data/dto/request/UserSearchRequest.java deleted file mode 100644 index 2e0b16d..0000000 --- a/src/main/java/com/vulinh/data/dto/request/UserSearchRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.vulinh.data.dto.request; - -import module java.base; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record UserSearchRequest(String identity, Collection roles) { - - public UserSearchRequest { - roles = Optional.ofNullable(roles).orElse(Collections.emptySet()); - } -} diff --git a/src/main/java/com/vulinh/data/dto/response/BasicPostResponse.java b/src/main/java/com/vulinh/data/dto/response/BasicPostResponse.java index f1c5fe0..08bad63 100644 --- a/src/main/java/com/vulinh/data/dto/response/BasicPostResponse.java +++ b/src/main/java/com/vulinh/data/dto/response/BasicPostResponse.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.vulinh.data.base.RecordDateTimeAuditable; import com.vulinh.data.base.RecordUuidIdentifiable; -import com.vulinh.data.dto.response.data.AuthorData; import com.vulinh.data.dto.response.data.TagData; import lombok.Builder; import lombok.With; @@ -21,7 +20,7 @@ public record BasicPostResponse( String slug, Instant createdDate, Instant updatedDate, - AuthorData author, + UUID authorId, CategoryResponse category, Collection tags) implements RecordUuidIdentifiable, RecordDateTimeAuditable {} diff --git a/src/main/java/com/vulinh/data/dto/response/PasswordResponse.java b/src/main/java/com/vulinh/data/dto/response/PasswordResponse.java deleted file mode 100644 index 1ccbda2..0000000 --- a/src/main/java/com/vulinh/data/dto/response/PasswordResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.vulinh.data.dto.response; - -import lombok.Builder; -import lombok.With; - -@With -@Builder -public record PasswordResponse(String rawPassword, String encodedPassword) {} diff --git a/src/main/java/com/vulinh/data/dto/response/SingleCommentResponse.java b/src/main/java/com/vulinh/data/dto/response/SingleCommentResponse.java index 89241bc..5289708 100644 --- a/src/main/java/com/vulinh/data/dto/response/SingleCommentResponse.java +++ b/src/main/java/com/vulinh/data/dto/response/SingleCommentResponse.java @@ -14,7 +14,5 @@ public record SingleCommentResponse( String content, Instant createdDate, Instant updatedDate, - String username, - String fullName, Boolean isEdited) implements RecordUuidIdentifiable, RecordDateTimeAuditable {} diff --git a/src/main/java/com/vulinh/data/dto/response/SinglePostResponse.java b/src/main/java/com/vulinh/data/dto/response/SinglePostResponse.java index b7d13fc..11520ba 100644 --- a/src/main/java/com/vulinh/data/dto/response/SinglePostResponse.java +++ b/src/main/java/com/vulinh/data/dto/response/SinglePostResponse.java @@ -3,7 +3,6 @@ import module java.base; import com.vulinh.data.base.RecordUuidIdentifiable; -import com.vulinh.data.dto.response.data.AuthorData; import com.vulinh.data.dto.response.data.TagData; import lombok.Builder; import lombok.With; @@ -18,7 +17,7 @@ public record SinglePostResponse( String postContent, Instant createdDate, Instant updatedDate, - AuthorData author, + UUID authorId, CategoryResponse category, List tags) implements RecordUuidIdentifiable, Serializable {} diff --git a/src/main/java/com/vulinh/data/dto/response/SingleUserResponse.java b/src/main/java/com/vulinh/data/dto/response/SingleUserResponse.java deleted file mode 100644 index 4641869..0000000 --- a/src/main/java/com/vulinh/data/dto/response/SingleUserResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vulinh.data.dto.response; - -import module java.base; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.vulinh.data.base.RecordDateTimeAuditable; -import com.vulinh.data.base.RecordUuidIdentifiable; -import com.vulinh.data.dto.response.data.RoleData; -import lombok.*; - -@With -@Builder -@JsonInclude(Include.NON_NULL) -public record SingleUserResponse( - UUID id, - String username, - String fullName, - String email, - Boolean isActive, - Instant createdDate, - Instant updatedDate, - Collection userRoles) - implements RecordUuidIdentifiable, RecordDateTimeAuditable {} diff --git a/src/main/java/com/vulinh/data/dto/response/UserBasicResponse.java b/src/main/java/com/vulinh/data/dto/response/UserBasicResponse.java index 7eb036c..b51e88b 100644 --- a/src/main/java/com/vulinh/data/dto/response/UserBasicResponse.java +++ b/src/main/java/com/vulinh/data/dto/response/UserBasicResponse.java @@ -3,7 +3,7 @@ import module java.base; import com.vulinh.data.base.RecordUuidIdentifiable; -import com.vulinh.data.dto.response.data.RoleData; +import com.vulinh.data.constant.UserRole; import lombok.Builder; import lombok.With; @@ -11,11 +11,7 @@ @Builder public record UserBasicResponse( UUID id, - UUID sessionId, String username, - String fullName, String email, - Instant createdDate, - Instant updatedDate, - Collection userRoles) + Collection userRoles) implements RecordUuidIdentifiable, Serializable {} diff --git a/src/main/java/com/vulinh/data/dto/response/data/RoleData.java b/src/main/java/com/vulinh/data/dto/response/data/RoleData.java deleted file mode 100644 index 42c69ec..0000000 --- a/src/main/java/com/vulinh/data/dto/response/data/RoleData.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.vulinh.data.dto.response.data; - -import module java.base; - -import com.vulinh.data.base.RecordIdentifiable; -import com.vulinh.data.constant.UserRole; -import lombok.*; - -@With -@Builder -public record RoleData(UserRole id, int superiority) - implements RecordIdentifiable, Serializable {} diff --git a/src/main/java/com/vulinh/data/entity/Comment.java b/src/main/java/com/vulinh/data/entity/Comment.java index f85457a..6de92a3 100644 --- a/src/main/java/com/vulinh/data/entity/Comment.java +++ b/src/main/java/com/vulinh/data/entity/Comment.java @@ -39,8 +39,5 @@ public class Comment extends UuidJpaEntity implements DateTimeAuditable { @Column(name = "post_id") UUID postId; - @ToString.Exclude - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "created_by") - Users createdBy; + UUID createdBy; } diff --git a/src/main/java/com/vulinh/data/entity/Post.java b/src/main/java/com/vulinh/data/entity/Post.java index dd81345..4c47b5d 100644 --- a/src/main/java/com/vulinh/data/entity/Post.java +++ b/src/main/java/com/vulinh/data/entity/Post.java @@ -40,9 +40,7 @@ public class Post extends UuidJpaEntity implements DateTimeAuditable { @LastModifiedBy UUID updatedBy; - @ManyToOne(fetch = FetchType.LAZY) - @ToString.Exclude - Users author; + UUID authorId; @ManyToOne(fetch = FetchType.LAZY) @ToString.Exclude diff --git a/src/main/java/com/vulinh/data/entity/UserSession.java b/src/main/java/com/vulinh/data/entity/UserSession.java deleted file mode 100644 index 45568c3..0000000 --- a/src/main/java/com/vulinh/data/entity/UserSession.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.vulinh.data.entity; - -import module java.base; - -import com.vulinh.data.base.AbstractIdentifiable; -import com.vulinh.data.base.DateTimeAuditable; -import com.vulinh.data.entity.ids.UserSessionId; -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import lombok.*; -import lombok.experimental.Accessors; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -@Entity -@EntityListeners(AuditingEntityListener.class) -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@ToString -@Builder -@Accessors(chain = true) -public class UserSession extends AbstractIdentifiable implements DateTimeAuditable { - - @Serial private static final long serialVersionUID = -7459401877851544734L; - - @EmbeddedId UserSessionId id; - - @CreatedDate Instant createdDate; - - @LastModifiedDate Instant updatedDate; - - Instant expirationDate; -} diff --git a/src/main/java/com/vulinh/data/entity/Users.java b/src/main/java/com/vulinh/data/entity/Users.java deleted file mode 100644 index 34179d0..0000000 --- a/src/main/java/com/vulinh/data/entity/Users.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vulinh.data.entity; - -import module java.base; - -import com.vulinh.data.base.DateTimeAuditable; -import com.vulinh.data.base.UuidJpaEntity; -import jakarta.persistence.*; -import lombok.*; -import lombok.experimental.Accessors; -import org.hibernate.annotations.UuidGenerator; -import org.hibernate.annotations.UuidGenerator.Style; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -@Entity -@EntityListeners(AuditingEntityListener.class) -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@ToString -@Builder -@Accessors(chain = true) -public class Users extends UuidJpaEntity implements DateTimeAuditable { - - @Serial private static final long serialVersionUID = -2867497192634401616L; - - @Id - @UuidGenerator(style = Style.TIME) - UUID id; - - String username; - String fullName; - String email; - - @ToString.Exclude String password; - - @Builder.Default Boolean isActive = false; - - @ToString.Exclude String passwordResetCode; - - @ToString.Exclude Instant passwordResetCodeExpiration; - - @ToString.Exclude String userRegistrationCode; - - @CreatedDate Instant createdDate; - - @LastModifiedDate Instant updatedDate; - - @ManyToMany - @JoinTable( - name = "user_role_mapping", - joinColumns = @JoinColumn(name = "user_id"), - inverseJoinColumns = @JoinColumn(name = "role_id")) - @ToString.Exclude - Set userRoles; -} diff --git a/src/main/java/com/vulinh/data/entity/ids/UserSessionId.java b/src/main/java/com/vulinh/data/entity/ids/UserSessionId.java deleted file mode 100644 index af2ff9b..0000000 --- a/src/main/java/com/vulinh/data/entity/ids/UserSessionId.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vulinh.data.entity.ids; - -import module java.base; - -import jakarta.persistence.Embeddable; - -@Embeddable -public record UserSessionId(UUID userId, UUID sessionId) implements Serializable { - - public static UserSessionId of(UUID userId, UUID sessionId) { - return new UserSessionId(userId, sessionId); - } -} diff --git a/src/main/java/com/vulinh/data/mapper/CommentMapper.java b/src/main/java/com/vulinh/data/mapper/CommentMapper.java index 1468298..977c667 100644 --- a/src/main/java/com/vulinh/data/mapper/CommentMapper.java +++ b/src/main/java/com/vulinh/data/mapper/CommentMapper.java @@ -6,7 +6,6 @@ import com.vulinh.data.entity.Comment; import com.vulinh.data.entity.CommentRevision; import com.vulinh.data.entity.RevisionType; -import com.vulinh.data.entity.Users; import com.vulinh.data.entity.ids.CommentRevisionId; import org.mapstruct.Builder; import org.mapstruct.Mapper; @@ -22,7 +21,7 @@ public interface CommentMapper { @Mapping(target = "updatedDate", ignore = true) @Mapping(target = "id", ignore = true) @Mapping(target = "createdDate", ignore = true) - Comment fromNewComment(NewCommentRequest newComment, Users createdBy, UUID postId); + Comment fromNewComment(NewCommentRequest newComment, UUID createdBy, UUID postId); @Mapping(target = "id", expression = "java(createTransientId(comment))") @Mapping(target = "revisionCreatedBy", ignore = true) diff --git a/src/main/java/com/vulinh/data/mapper/PostMapper.java b/src/main/java/com/vulinh/data/mapper/PostMapper.java index 21ee3b8..0b0ce2a 100644 --- a/src/main/java/com/vulinh/data/mapper/PostMapper.java +++ b/src/main/java/com/vulinh/data/mapper/PostMapper.java @@ -33,7 +33,7 @@ public interface PostMapper extends EntityDTOMapper { @Mapping(target = "updatedBy", ignore = true) @Mapping(target = "tags", source = "tags") @Mapping(target = "comments", ignore = true) - Post toEntity(PostCreationRequest dto, Users author, Category category, Collection tags); + Post toEntity(PostCreationRequest dto, UUID authorId, Category category, Collection tags); @Mapping( target = "id", @@ -42,13 +42,11 @@ public interface PostMapper extends EntityDTOMapper { target = "tags", expression = "java(post.getTags().stream().map(Tag::getDisplayName).collect(Collectors.joining(\",\")))") - @Mapping(target = "authorId", source = "post.author.id") @Mapping(target = "categoryId", source = "post.category.id") @Mapping(target = "revisionCreatedDate", ignore = true) @Mapping(target = "revisionCreatedBy", ignore = true) PostRevision toPostRevision(Post post, RevisionType revisionType); - @Mapping(target = "author", ignore = true) @Mapping(target = "id", ignore = true) @Mapping(target = "createdDate", ignore = true) @Mapping(target = "updatedDate", ignore = true) @@ -66,7 +64,7 @@ void merge( @Mapping(target = "updatedBy", ignore = true) @Mapping(target = "createdDate", ignore = true) @Mapping(target = "updatedDate", ignore = true) - @Mapping(target = "author", ignore = true) + @Mapping(target = "authorId", ignore = true) @Mapping(target = "comments", ignore = true) @Mapping(target = "tags", source = "tags") @Mapping( diff --git a/src/main/java/com/vulinh/data/mapper/UserMapper.java b/src/main/java/com/vulinh/data/mapper/UserMapper.java deleted file mode 100644 index 1f80930..0000000 --- a/src/main/java/com/vulinh/data/mapper/UserMapper.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.vulinh.data.mapper; - -import module java.base; - -import com.vulinh.data.base.EntityDTOMapper; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.response.SingleUserResponse; -import com.vulinh.data.dto.response.UserBasicResponse; -import com.vulinh.data.entity.UserSession; -import com.vulinh.data.entity.Users; -import org.mapstruct.*; -import org.mapstruct.factory.Mappers; - -@Mapper( - builder = @Builder(disableBuilder = true), - unmappedTargetPolicy = ReportingPolicy.IGNORE, - imports = Collectors.class) -public interface UserMapper extends EntityDTOMapper { - - UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); - - @Mapping(target = "isActive", ignore = true) - @Mapping(target = "userRoles", ignore = true) - SingleUserResponse toUserDTO(Users users); - - @Mapping(target = "sessionId", source = "userSession.id.sessionId") - @Mapping(target = "id", source = "user.id") - @Mapping(target = "createdDate", source = "user.createdDate") - @Mapping(target = "updatedDate", source = "user.updatedDate") - UserBasicResponse toBasicUserDTO(Users user, UserSession userSession); - - @Mapping(target = "updatedDate", ignore = true) - @Mapping(target = "passwordResetCode", ignore = true) - @Mapping(target = "isActive", ignore = true) - @Mapping(target = "id", ignore = true) - @Mapping(target = "createdDate", ignore = true) - @Mapping(target = "userRegistrationCode", source = "userRegistrationCode") - @Mapping(target = "userRoles", ignore = true) - @Mapping( - target = "username", - expression = "java(userRegistrationRequest.username().toLowerCase())") - @Mapping(target = "email", expression = "java(userRegistrationRequest.email().toLowerCase())") - @Mapping(target = "passwordResetCodeExpiration", ignore = true) - Users toUserWithRegistrationCode( - UserRegistrationRequest userRegistrationRequest, String userRegistrationCode); - - @Mapping(target = "userRoles", ignore = true) - @Mapping( - target = "username", - expression = "java(userRegistrationRequest.username().toLowerCase())") - @Mapping(target = "email", expression = "java(userRegistrationRequest.email().toLowerCase())") - @Mapping(target = "userRegistrationCode", ignore = true) - @Mapping(target = "updatedDate", ignore = true) - @Mapping(target = "passwordResetCodeExpiration", ignore = true) - @Mapping(target = "passwordResetCode", ignore = true) - @Mapping(target = "isActive", ignore = true) - @Mapping(target = "id", ignore = true) - @Mapping(target = "createdDate", ignore = true) - Users toUser(UserRegistrationRequest userRegistrationRequest); -} diff --git a/src/main/java/com/vulinh/data/repository/RoleRepository.java b/src/main/java/com/vulinh/data/repository/RoleRepository.java deleted file mode 100644 index a07bc8f..0000000 --- a/src/main/java/com/vulinh/data/repository/RoleRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.vulinh.data.repository; - -import com.vulinh.data.base.BaseRepository; -import com.vulinh.data.constant.UserRole; -import com.vulinh.data.entity.Roles; -import org.springframework.stereotype.Repository; - -@Repository -public interface RoleRepository extends BaseRepository {} diff --git a/src/main/java/com/vulinh/data/repository/UserRepository.java b/src/main/java/com/vulinh/data/repository/UserRepository.java deleted file mode 100644 index 8c59cf3..0000000 --- a/src/main/java/com/vulinh/data/repository/UserRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vulinh.data.repository; - -import module java.base; - -import com.vulinh.data.base.BaseRepository; -import com.vulinh.data.entity.Users; -import com.vulinh.exception.InvalidCredentialsException; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserRepository extends BaseRepository { - - // username is unique - Optional findByUsernameAndIsActiveIsTrue(String username); - - Optional findByIdAndIsActiveIsTrue(UUID id); - - boolean existsByUsernameIgnoreCaseOrEmailIgnoreCase(String username, String email); - - boolean existsByIdAndIsActiveIsTrue(UUID id); - - default Users findActiveUser(UUID id) { - return findByIdAndIsActiveIsTrue(id) - .orElseThrow(InvalidCredentialsException::invalidCredentialsException); - } -} diff --git a/src/main/java/com/vulinh/data/repository/UserSessionRepository.java b/src/main/java/com/vulinh/data/repository/UserSessionRepository.java deleted file mode 100644 index bf6c119..0000000 --- a/src/main/java/com/vulinh/data/repository/UserSessionRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vulinh.data.repository; - -import module java.base; - -import com.vulinh.configuration.cache.FromUserSessionEntityKeyGenerator; -import com.vulinh.data.base.BaseRepository; -import com.vulinh.data.constant.CacheConstant; -import com.vulinh.data.entity.UserSession; -import com.vulinh.data.entity.ids.UserSessionId; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserSessionRepository extends BaseRepository { - - @Override - @NonNull - @CachePut( - cacheNames = CacheConstant.USER_SESSION_CACHE, - keyGenerator = FromUserSessionEntityKeyGenerator.BEAN_NAME) - S save(@NonNull S entity); - - @Override - @NonNull - @Cacheable(cacheNames = CacheConstant.USER_SESSION_CACHE) - Optional findById(@NonNull UserSessionId userSessionId); - - @Override - @CacheEvict( - cacheNames = CacheConstant.USER_SESSION_CACHE, - keyGenerator = FromUserSessionEntityKeyGenerator.BEAN_NAME) - void delete(@NonNull UserSession userSession); -} diff --git a/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java b/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java index 78d73f5..ee857d6 100644 --- a/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java @@ -2,7 +2,6 @@ import module java.base; -import com.auth0.jwt.exceptions.JWTVerificationException; import com.vulinh.data.dto.response.GenericResponse; import com.vulinh.data.dto.response.GenericResponse.ResponseCreator; import com.vulinh.locale.LocalizationSupport; @@ -58,13 +57,6 @@ GenericResponse handleConfigurationException( return stackTraceAndReturn("Configuration error", securityConfigurationException); } - @ExceptionHandler(InvalidCredentialsException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - GenericResponse handleInvalidCredentialsException( - InvalidCredentialsException invalidCredentialsException) { - return logAndReturn(invalidCredentialsException); - } - @ExceptionHandler(NoSuchPermissionException.class) @ResponseStatus(HttpStatus.FORBIDDEN) GenericResponse handleNoSuchPermissionException( @@ -119,15 +111,6 @@ GenericResponse handleTypeMismatchException(TypeMismatchException typeMi .orElse("unknown or empty type"))); } - @ExceptionHandler(JWTVerificationException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - GenericResponse handleJWTVerificationException( - JWTVerificationException jwtVerificationException) { - log.info("Invalid JWT token: {}", jwtVerificationException.getMessage()); - - return ResponseCreator.toError(AuthorizationException.invalidAuthorization()); - } - static GenericResponse badRequestBody(String additionalMessage) { return GenericResponse.builder() .errorCode(ServiceErrorCode.MESSAGE_INVALID_BODY_REQUEST.getErrorCode()) diff --git a/src/main/java/com/vulinh/exception/InvalidCredentialsException.java b/src/main/java/com/vulinh/exception/InvalidCredentialsException.java deleted file mode 100644 index 1a1cf15..0000000 --- a/src/main/java/com/vulinh/exception/InvalidCredentialsException.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vulinh.exception; - -import module java.base; - -import com.vulinh.data.base.ApplicationError; -import com.vulinh.locale.ServiceErrorCode; - -/// Exception thrown when a user's authentication attempt fails due to invalid credentials. -/// -/// This exception is typically thrown during login processes when provided credentials (such as -/// username/password or tokens) do not match expected values in the system. It extends -/// [ApplicationException] and returns an HTTP 401 UNAUTHORIZED status code. -public class InvalidCredentialsException extends ApplicationException { - - @Serial private static final long serialVersionUID = -4482845426503935711L; - - /// Creates a new [InvalidCredentialsException] with a predefined message and error code. - /// - /// @param args Variable arguments that will be used for message formatting - /// @return A new [InvalidCredentialsException] instance with default error message - public static InvalidCredentialsException invalidCredentialsException(Object... args) { - return new InvalidCredentialsException( - "Invalid user credentials", ServiceErrorCode.MESSAGE_INVALID_CREDENTIALS, args); - } - - /// Constructs a new [InvalidCredentialsException] with specified error details. - /// - /// @param message The detail message describing the error - /// @param applicationError The application-specific error code - /// @param args Variable arguments that will be used for message formatting - InvalidCredentialsException(String message, ApplicationError applicationError, Object... args) { - super(message, applicationError, null, args); - } -} diff --git a/src/main/java/com/vulinh/factory/UserRegistrationEventFactory.java b/src/main/java/com/vulinh/factory/UserRegistrationEventFactory.java deleted file mode 100644 index a87eeff..0000000 --- a/src/main/java/com/vulinh/factory/UserRegistrationEventFactory.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vulinh.factory; - -import com.vulinh.data.dto.event.UserRegistrationEvent; -import com.vulinh.data.entity.Users; - -@SuppressWarnings("java:S6548") -public enum UserRegistrationEventFactory { - INSTANCE; - - public UserRegistrationEvent fromUser(Users users) { - return new UserRegistrationEvent(users); - } -} diff --git a/src/main/java/com/vulinh/locale/ServiceErrorCode.java b/src/main/java/com/vulinh/locale/ServiceErrorCode.java index 3d89dc9..c08c3c1 100644 --- a/src/main/java/com/vulinh/locale/ServiceErrorCode.java +++ b/src/main/java/com/vulinh/locale/ServiceErrorCode.java @@ -9,15 +9,6 @@ public enum ServiceErrorCode implements ApplicationError { MESSAGE_SUCCESS("M0000"), MESSAGE_INTERNAL_ERROR("M9999"), - MESSAGE_INVALID_USERNAME("M9001"), - MESSAGE_INVALID_PASSWORD("M9002"), - MESSAGE_INVALID_EMAIL("M9003"), - MESSAGE_USER_OR_EMAIL_EXISTED("M9004"), - MESSAGE_INVALID_NEW_PASSWORD("M9005"), - MESSAGE_INVALID_CONFIRMATION("M9006"), - MESSAGE_PASSWORD_MISMATCHED("M9007"), - MESSAGE_SAME_OLD_PASSWORD("M9008"), - MESSAGE_NO_SELF_DESTRUCTION("M9009"), MESSAGE_INVALID_ENTITY_ID("M0404"), MESSAGE_POST_INVALID_TITLE("M1000"), @@ -33,16 +24,9 @@ public enum ServiceErrorCode implements ApplicationError { MESSAGE_INVALID_CATEGORY_SLUG("M3002"), MESSAGE_DEFAULT_CATEGORY_IMMORTAL("M3003"), - MESSAGE_INVALID_CREDENTIALS("M9101"), MESSAGE_INVALID_AUTHORIZATION("M9102"), - MESSAGE_CREDENTIALS_EXPIRED("M9103"), - MESSAGE_INVALID_PUBLIC_KEY_CONFIG("M9104"), - MESSAGE_INVALID_PRIVATE_KEY_CONFIG("M9105"), MESSAGE_INVALID_BODY_REQUEST("M9106"), - MESSAGE_INVALID_OWNER_OR_NO_RIGHT("M9107"), - MESSAGE_INVALID_TOKEN_TYPE("M9108"), - MESSAGE_INVALID_SESSION("M9109"), - MESSAGE_INVALID_TOKEN("M9110"); + MESSAGE_INVALID_OWNER_OR_NO_RIGHT("M9107"); final String errorCode; } diff --git a/src/main/java/com/vulinh/schedule/CleanExpiredUserSessionService.java b/src/main/java/com/vulinh/schedule/CleanExpiredUserSessionService.java deleted file mode 100644 index 55d92ed..0000000 --- a/src/main/java/com/vulinh/schedule/CleanExpiredUserSessionService.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.vulinh.schedule; - -import module java.base; - -import com.vulinh.data.constant.CacheConstant; -import com.vulinh.data.entity.QUserSession; -import com.vulinh.data.entity.UserSession; -import com.vulinh.data.repository.UserSessionRepository; -import com.vulinh.utils.PredicateBuilder; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.CacheManager; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.scheduling.support.CronExpression; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/// This class is just a simple example to demonstrate Spring Boot's scheduling capabilities; in a microservices -/// architecture, scheduling tasks should be handled by a dedicated service separate from the main business logic -/// service. -@Component -@RequiredArgsConstructor -@Slf4j -public class CleanExpiredUserSessionService { - - static final int ITEMS_PER_BATCH = 10; - - final UserSessionRepository userSessionRepository; - - final CacheManager cacheManager; - - final SchedulingTaskSupportService schedulingTaskSupportService; - - @PostConstruct - public void info() { - var cronExpression = schedulingTaskSupportService.cleanExpiredUserSessionsExpression(); - - log.info( - "Expired user session cleaning bean enabled, cron expression: {}, next recent execution will be at {}", - cronExpression, - CronExpression.parse(cronExpression).next(LocalDateTime.now())); - } - - @Scheduled(cron = "#{schedulingTaskSupportService.cleanExpiredUserSessionsExpression()}") - @Transactional - public void cleanExpiredUserSessions() { - log.info("Started cleaning expired user sessions..."); - - var qUserSession = QUserSession.userSession; - - var userSessions = - userSessionRepository.findAll( - qUserSession.expirationDate.lt(Instant.now()), - PageRequest.of( - 0, - ITEMS_PER_BATCH, - Sort.by(Order.asc(PredicateBuilder.getFieldName(qUserSession.expirationDate))))); - - userSessionRepository.deleteAllInBatch(userSessions); - - Optional.ofNullable(cacheManager.getCache(CacheConstant.USER_SESSION_CACHE)) - .ifPresent( - userSessionCache -> - userSessions.stream() - .map(UserSession::getId) - .forEach(userSessionCache::evictIfPresent)); - } -} diff --git a/src/main/java/com/vulinh/schedule/SchedulingTaskSupportService.java b/src/main/java/com/vulinh/schedule/SchedulingTaskSupportService.java deleted file mode 100644 index bc99804..0000000 --- a/src/main/java/com/vulinh/schedule/SchedulingTaskSupportService.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vulinh.schedule; - -import com.vulinh.configuration.data.ApplicationProperties; -import com.vulinh.utils.springcron.HourExpression; -import com.vulinh.utils.springcron.MinuteExpression; -import com.vulinh.utils.springcron.dto.ExpressionObject; -import com.vulinh.utils.springcron.dto.HourExpressionObject; -import com.vulinh.utils.springcron.dto.MinuteExpressionObject; -import com.vulinh.utils.springcron.dto.SpringCronGeneratorDTO; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class SchedulingTaskSupportService { - - final ApplicationProperties applicationProperties; - - public String cleanExpiredUserSessionsExpression() { - var expression = applicationProperties.schedule().cleanExpiredUserSessions(); - - if (StringUtils.isBlank(expression)) { - return SpringCronGeneratorDTO.builder() - .second(ExpressionObject.NO_CARE) - .minute(MinuteExpressionObject.of(MinuteExpression.EVERY_MINUTE)) - .hour(HourExpressionObject.of(HourExpression.EVERY_HOUR_BETWEEN, 1, 5)) - .build() - .toCronExpression(); - } - - return expression; - } -} diff --git a/src/main/java/com/vulinh/service/auth/AuthService.java b/src/main/java/com/vulinh/service/auth/AuthService.java deleted file mode 100644 index 6ee156f..0000000 --- a/src/main/java/com/vulinh/service/auth/AuthService.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.vulinh.service.auth; - -import module java.base; - -import com.vulinh.configuration.data.ApplicationProperties; -import com.vulinh.data.dto.carrier.TokenResponse; -import com.vulinh.data.dto.request.PasswordChangeRequest; -import com.vulinh.data.dto.request.RefreshTokenRequest; -import com.vulinh.data.dto.request.UserLoginRequest; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.response.SingleUserResponse; -import com.vulinh.data.dto.response.UserBasicResponse; -import com.vulinh.data.mapper.UserMapper; -import com.vulinh.data.repository.UserRepository; -import com.vulinh.exception.AuthorizationException; -import com.vulinh.exception.InvalidCredentialsException; -import com.vulinh.exception.ValidationException; -import com.vulinh.factory.UserRegistrationEventFactory; -import com.vulinh.factory.ValidatorStepFactory; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.service.auth.PasswordValidationService.PasswordChangeRule; -import com.vulinh.service.sessions.UserSessionService; -import com.vulinh.service.token.AccessTokenGenerator; -import com.vulinh.service.token.AccessTokenValidator; -import com.vulinh.service.token.RefreshTokenValidator; -import com.vulinh.service.user.UserValidationService; -import com.vulinh.utils.SecurityUtils; -import com.vulinh.utils.security.Auth0Utils; -import com.vulinh.utils.validator.ValidatorChain; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class AuthService { - - static final ValidatorStepFactory VALIDATOR_STEP_FACTORY = ValidatorStepFactory.INSTANCE; - - static final UserMapper USER_MAPPER = UserMapper.INSTANCE; - - final ApplicationProperties applicationProperties; - - final UserRepository userRepository; - - final PasswordEncoder passwordEncoder; - - final UserValidationService userValidationService; - final PasswordValidationService passwordValidationService; - final UserSessionService userSessionService; - - final AccessTokenValidator accessTokenValidator; - final RefreshTokenValidator refreshTokenValidator; - final AccessTokenGenerator accessTokenGenerator; - - final ApplicationEventPublisher applicationEventPublisher; - - public TokenResponse login(UserLoginRequest userLoginRequest) { - var now = Instant.now(); - - return userRepository - .findByUsernameAndIsActiveIsTrue(userLoginRequest.username()) - .filter( - matchedUser -> - UserValidationService.isPasswordMatched( - userLoginRequest, matchedUser, passwordEncoder)) - .map(user -> accessTokenGenerator.generateAccessToken(user.getId(), UUID.randomUUID(), now)) - .map(userSessionService::createUserSession) - .orElseThrow(InvalidCredentialsException::invalidCredentialsException); - } - - // Normal user that requires confirmation - @Transactional - public SingleUserResponse registerUser(UserRegistrationRequest userRegistrationRequest) { - userValidationService.validateUserCreation(userRegistrationRequest); - - var registrationWithEncodedPassword = - userRegistrationRequest.withPassword( - passwordEncoder.encode(userRegistrationRequest.password())); - - var newUser = - userRepository.save( - USER_MAPPER.toUserWithRegistrationCode( - registrationWithEncodedPassword, UUID.randomUUID().toString())); - - applicationEventPublisher.publishEvent(UserRegistrationEventFactory.INSTANCE.fromUser(newUser)); - - return USER_MAPPER.toUserDTO(newUser); - } - - // Confirm normal user - @Transactional - public boolean confirmUser(UUID userId, String code) { - if (userId == null || StringUtils.isBlank(code)) { - return false; - } - - return userRepository - .findById(userId) - .filter(Predicate.not(UserValidationService::isActive)) - .filter(matchedUser -> UserValidationService.isRegistrationCodeMatched(code, matchedUser)) - .map(matchedUser -> matchedUser.setIsActive(true).setUserRegistrationCode(null)) - .map(userRepository::save) - .isPresent(); - } - - @Transactional - public void changePassword(PasswordChangeRequest passwordChangeRequest) { - ValidatorChain.start() - .addValidator(PasswordChangeRule.values()) - .executeValidation(passwordChangeRequest); - - var userEntity = - SecurityUtils.getUserDTO() - .map(UserBasicResponse::id) - .flatMap(userRepository::findByIdAndIsActiveIsTrue) - .orElseThrow(AuthorizationException::invalidAuthorization); - - ValidatorChain.start() - .addValidator( - VALIDATOR_STEP_FACTORY.build( - passwordValidationService.isOldPasswordMatched(userEntity), - ServiceErrorCode.MESSAGE_PASSWORD_MISMATCHED, - "Invalid old password"), - VALIDATOR_STEP_FACTORY.build( - passwordValidationService.isNewPasswordNotMatched(userEntity), - ServiceErrorCode.MESSAGE_SAME_OLD_PASSWORD, - "New password cannot be the same as old password")) - .executeValidation(passwordChangeRequest); - - userRepository.save( - userEntity.setPassword(passwordEncoder.encode(passwordChangeRequest.newPassword()))); - } - - // TODO: Use ID to check the existence of entities - @Transactional - public TokenResponse refreshToken(RefreshTokenRequest refreshTokenRequest) { - var refreshToken = refreshTokenRequest.refreshToken(); - - if (StringUtils.isBlank(refreshToken)) { - throw ValidationException.validationException( - "Empty refresh token", ServiceErrorCode.MESSAGE_INVALID_TOKEN); - } - - var decodedJwtPayload = refreshTokenValidator.validateRefreshToken(refreshToken); - - var user = userRepository.findActiveUser(decodedJwtPayload.userId()); - - var userSession = - userSessionService.findUserSession(user.getId(), decodedJwtPayload.sessionId()); - - var issuedAt = Instant.now(); - - userSessionService.updateUserSession( - userSession, issuedAt.plus(applicationProperties.security().refreshJwtDuration())); - - var tokenResult = - accessTokenGenerator - .generateAccessToken( - decodedJwtPayload.userId(), decodedJwtPayload.sessionId(), issuedAt) - .tokenResponse(); - - return TokenResponse.builder() - .accessToken(tokenResult.accessToken()) - .refreshToken(tokenResult.refreshToken()) - .build(); - } - - // BAD, yes, I am validating the JWT directly, but I am too lazy - // to refactor the validation utils, who cares? - @Transactional - public boolean logout(String authorization) { - var jwtPayload = - accessTokenValidator.validateAccessToken(Auth0Utils.parseBearerToken(authorization)); - - var userId = jwtPayload.userId(); - - if (!userRepository.existsByIdAndIsActiveIsTrue(userId)) { - log.info("User ID {} did not exist", userId); - return false; - } - - var userSession = userSessionService.findUserSession(userId, jwtPayload.sessionId()); - - userSessionService.deleteUserSession(userSession); - - return true; - } -} diff --git a/src/main/java/com/vulinh/service/auth/PasswordValidationService.java b/src/main/java/com/vulinh/service/auth/PasswordValidationService.java deleted file mode 100644 index af09b17..0000000 --- a/src/main/java/com/vulinh/service/auth/PasswordValidationService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.vulinh.service.auth; - -import module java.base; - -import com.vulinh.data.dto.request.PasswordChangeRequest; -import com.vulinh.data.entity.Users; -import com.vulinh.factory.ValidatorStepFactory; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.service.user.UserValidationService; -import com.vulinh.utils.validator.NoArgsValidatorStep; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class PasswordValidationService { - - static final int PASSWORD_MIN_LENGTH = 8; - - final PasswordEncoder passwordEncoder; - - public Predicate isOldPasswordMatched(Users user) { - return dto -> passwordEncoder.matches(dto.oldPassword(), user.getPassword()); - } - - public Predicate isNewPasswordNotMatched(Users user) { - return Predicate.not(dto -> passwordEncoder.matches(dto.newPassword(), user.getPassword())); - } - - @RequiredArgsConstructor - @Getter - public enum PasswordChangeRule implements NoArgsValidatorStep { - RULE_NO_BLANK_PASSWORD( - ValidatorStepFactory.noBlankField(PasswordChangeRequest::oldPassword), - ServiceErrorCode.MESSAGE_INVALID_PASSWORD, - "Blank old password is not allowed"), - RULE_NO_BLANK_NEW_PASSWORD( - ValidatorStepFactory.noBlankField(PasswordChangeRequest::newPassword), - ServiceErrorCode.MESSAGE_INVALID_NEW_PASSWORD, - "Blank new password is not allowed"), - RULE_LONG_ENOUGH_NEW_PASSWORD( - ValidatorStepFactory.atLeastLength( - PasswordChangeRequest::newPassword, UserValidationService.PASSWORD_MINIMUM_LENGTH), - ServiceErrorCode.MESSAGE_INVALID_NEW_PASSWORD, - "New password must have more than %s characters".formatted(PASSWORD_MIN_LENGTH)) { - - @Override - public Integer[] getArgs() { - return new Integer[] {PASSWORD_MIN_LENGTH}; - } - }; - - final Predicate predicate; - final ServiceErrorCode applicationError; - final String exceptionMessage; - } -} diff --git a/src/main/java/com/vulinh/service/comment/CommentFetchingService.java b/src/main/java/com/vulinh/service/comment/CommentFetchingService.java index e9d8a89..368a4bc 100644 --- a/src/main/java/com/vulinh/service/comment/CommentFetchingService.java +++ b/src/main/java/com/vulinh/service/comment/CommentFetchingService.java @@ -51,7 +51,6 @@ JPAQuery buildFetchQuery(UUID postId, Pageable pageable) var qComment = QComment.comment; var qCommentCreatedDate = qComment.createdDate; var qCommentRevision = QCommentRevision.commentRevision; - var qCommentAuthor = qComment.createdBy; var qCommentId = qComment.id; var select = @@ -61,8 +60,6 @@ JPAQuery buildFetchQuery(UUID postId, Pageable pageable) qComment.content, qCommentCreatedDate, qComment.updatedDate, - qCommentAuthor.username, - qCommentAuthor.fullName, new CaseBuilder() .when( getQueryFactory() diff --git a/src/main/java/com/vulinh/service/comment/CommentService.java b/src/main/java/com/vulinh/service/comment/CommentService.java index 9618424..b31aec1 100644 --- a/src/main/java/com/vulinh/service/comment/CommentService.java +++ b/src/main/java/com/vulinh/service/comment/CommentService.java @@ -9,7 +9,6 @@ import com.vulinh.data.entity.RevisionType; import com.vulinh.data.mapper.CommentMapper; import com.vulinh.data.repository.CommentRepository; -import com.vulinh.data.repository.UserRepository; import com.vulinh.exception.AuthorizationException; import com.vulinh.utils.SecurityUtils; import lombok.RequiredArgsConstructor; @@ -22,7 +21,6 @@ @RequiredArgsConstructor public class CommentService { - final UserRepository userRepository; final CommentRepository commentRepository; final NewCommentValidationService commentValidationService; @@ -36,7 +34,6 @@ public CommentResponse addComment(UUID postId, NewCommentRequest newCommentReque var createdBy = SecurityUtils.getUserDTO() .map(UserBasicResponse::id) - .flatMap(userRepository::findByIdAndIsActiveIsTrue) .orElseThrow(AuthorizationException::invalidAuthorization); var persistedComment = diff --git a/src/main/java/com/vulinh/service/comment/NewCommentValidationService.java b/src/main/java/com/vulinh/service/comment/NewCommentValidationService.java index 66d9af8..f0aceb3 100644 --- a/src/main/java/com/vulinh/service/comment/NewCommentValidationService.java +++ b/src/main/java/com/vulinh/service/comment/NewCommentValidationService.java @@ -42,10 +42,10 @@ public Comment validateEditComment(NewCommentRequest newCommentRequest, UUID com var createdBy = comment.getCreatedBy(); - if (!Objects.equals(createdBy.getId(), user.id())) { + if (!Objects.equals(createdBy, user.id())) { throw NoSuchPermissionException.noSuchPermissionException( "User [%s] cannot edit comment [%s] which belonged to user [%s]" - .formatted(user.username(), comment.getId(), createdBy.getUsername()), + .formatted(user.username(), comment.getId(), createdBy), ServiceErrorCode.MESSAGE_INVALID_OWNER_OR_NO_RIGHT); } diff --git a/src/main/java/com/vulinh/service/eventlistener/CustomEventListener.java b/src/main/java/com/vulinh/service/eventlistener/CustomEventListener.java deleted file mode 100644 index db10209..0000000 --- a/src/main/java/com/vulinh/service/eventlistener/CustomEventListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.vulinh.service.eventlistener; - -import com.vulinh.data.constant.EndpointConstant; -import com.vulinh.data.constant.EndpointConstant.AuthEndpoint; -import com.vulinh.data.dto.event.UserRegistrationEvent; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -@Slf4j -public class CustomEventListener { - - @EventListener - public void listenUserRegistrationEvent(UserRegistrationEvent userRegistrationEvent) { - var user = userRegistrationEvent.user(); - - log.debug( - """ - {} - - Sending email to {} - - Registering link: {}{}?userId={}&code={} - """, - Thread.currentThread(), - user.getEmail(), - EndpointConstant.ENDPOINT_AUTH, - AuthEndpoint.CONFIRM_USER, - user.getId(), - user.getUserRegistrationCode()); - } -} diff --git a/src/main/java/com/vulinh/service/post/PostCreationService.java b/src/main/java/com/vulinh/service/post/PostCreationService.java index 1f14a0c..9cd484d 100644 --- a/src/main/java/com/vulinh/service/post/PostCreationService.java +++ b/src/main/java/com/vulinh/service/post/PostCreationService.java @@ -5,7 +5,6 @@ import com.vulinh.data.entity.Post; import com.vulinh.data.mapper.PostMapper; import com.vulinh.data.repository.PostRepository; -import com.vulinh.data.repository.UserRepository; import com.vulinh.exception.AuthorizationException; import com.vulinh.service.category.CategoryService; import com.vulinh.service.tag.TagService; @@ -25,7 +24,6 @@ public class PostCreationService { final TagService tagService; final PostRepository postRepository; - final UserRepository userRepository; final CategoryService categoryService; @Transactional @@ -34,10 +32,9 @@ public Post createPost(PostCreationRequest postCreationRequest) { var actualCreationDTO = PostUtils.getActualDTO(postCreationRequest); - var author = + var authorId = SecurityUtils.getUserDTO() .map(UserBasicResponse::id) - .flatMap(userRepository::findById) .orElseThrow(AuthorizationException::invalidAuthorization); var categoryId = postCreationRequest.categoryId(); @@ -46,6 +43,6 @@ public Post createPost(PostCreationRequest postCreationRequest) { var tags = tagService.parseTags(postCreationRequest); - return postRepository.save(POST_MAPPER.toEntity(actualCreationDTO, author, category, tags)); + return postRepository.save(POST_MAPPER.toEntity(actualCreationDTO, authorId, category, tags)); } } diff --git a/src/main/java/com/vulinh/service/post/PostValidationService.java b/src/main/java/com/vulinh/service/post/PostValidationService.java index 133dd36..234f31c 100644 --- a/src/main/java/com/vulinh/service/post/PostValidationService.java +++ b/src/main/java/com/vulinh/service/post/PostValidationService.java @@ -5,7 +5,6 @@ import com.vulinh.data.constant.UserRole; import com.vulinh.data.dto.request.PostCreationRequest; import com.vulinh.data.dto.response.UserBasicResponse; -import com.vulinh.data.dto.response.data.RoleData; import com.vulinh.data.entity.Post; import com.vulinh.exception.NoSuchPermissionException; import com.vulinh.factory.ValidatorStepFactory; @@ -33,27 +32,25 @@ public void validateModifyingPermission(UserBasicResponse userDTO, Post post) { if (!(PostValidationService.isOwner(userDTO, post) || PostValidationService.isPowerUser(userDTO))) { throw NoSuchPermissionException.noSuchPermissionException( - "Invalid author or no permission to edit", + "Invalid authorId or no permission to edit", ServiceErrorCode.MESSAGE_INVALID_OWNER_OR_NO_RIGHT); } } public static boolean isOwner(UserBasicResponse userDTO, Post post) { var userId = userDTO.id(); - var postAuthor = post.getAuthor(); - var postAuthorId = postAuthor.getId(); + var postAuthorId = post.getAuthorId(); var result = Objects.equals(userId, postAuthorId); if (!result) { log.debug( - "User {} ({}) is not the owner of post ({}) {} (actual owner: {} ({}))", + "User {} ({}) is not the owner of post ({}) {} (actual owner: {})", userId, userDTO.username(), post.getId(), post.getTitle(), - postAuthorId, - postAuthor.getUsername()); + postAuthorId); } return result; @@ -62,7 +59,7 @@ public static boolean isOwner(UserBasicResponse userDTO, Post post) { public static boolean isPowerUser(UserBasicResponse userDTO) { var maximumRole = userDTO.userRoles().stream() - .mapToInt(RoleData::superiority) + .mapToInt(UserRole::superiority) .max() .orElse(UserRole.USER.superiority()); @@ -73,7 +70,7 @@ public static boolean isPowerUser(UserBasicResponse userDTO) { "User {} ({}) with role {} is not a power user!", userDTO.id(), userDTO.username(), - userDTO.userRoles().stream().map(RoleData::id).collect(Collectors.toSet())); + userDTO.userRoles().stream().map(UserRole::name).collect(Collectors.toSet())); } return result; diff --git a/src/main/java/com/vulinh/service/sessions/UserSessionService.java b/src/main/java/com/vulinh/service/sessions/UserSessionService.java deleted file mode 100644 index ec2f34c..0000000 --- a/src/main/java/com/vulinh/service/sessions/UserSessionService.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.vulinh.service.sessions; - -import module java.base; - -import com.vulinh.data.dto.carrier.AccessTokenCarrier; -import com.vulinh.data.dto.carrier.TokenResponse; -import com.vulinh.data.entity.UserSession; -import com.vulinh.data.entity.ids.UserSessionId; -import com.vulinh.data.repository.UserSessionRepository; -import com.vulinh.exception.AuthorizationException; -import com.vulinh.locale.ServiceErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class UserSessionService { - - final UserSessionRepository userSessionRepository; - - @Transactional - public TokenResponse createUserSession(AccessTokenCarrier container) { - userSessionRepository.save( - UserSession.builder() - .id(UserSessionId.of(container.userId(), container.sessionId())) - .expirationDate(container.refreshTokenExpirationDate()) - .build()); - - return container.tokenResponse(); - } - - @Transactional - public void updateUserSession(UserSession userSession, Instant expirationDate) { - userSessionRepository.save(userSession.setExpirationDate(expirationDate)); - } - - public UserSession findUserSession(UUID userId, UUID sessionId) { - var userSessionId = UserSessionId.of(userId, sessionId); - - return userSessionRepository - .findById(userSessionId) - .orElseThrow( - () -> - AuthorizationException.invalidAuthorization( - "Session ID %s for user ID %s did not exist or has been invalidated" - .formatted(userSessionId.sessionId(), userSessionId.userId()), - ServiceErrorCode.MESSAGE_INVALID_SESSION)); - } - - public void deleteUserSession(UserSession userSession) { - userSessionRepository.delete(userSession); - } -} diff --git a/src/main/java/com/vulinh/service/token/AccessTokenGenerator.java b/src/main/java/com/vulinh/service/token/AccessTokenGenerator.java deleted file mode 100644 index b1b5e6e..0000000 --- a/src/main/java/com/vulinh/service/token/AccessTokenGenerator.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.vulinh.service.token; - -import module java.base; - -import com.vulinh.configuration.data.ApplicationProperties; -import com.vulinh.data.constant.TokenType; -import com.vulinh.data.dto.carrier.AccessTokenCarrier; -import com.vulinh.data.dto.carrier.TokenResponse; -import com.vulinh.data.entity.ids.UserSessionId; -import com.vulinh.utils.security.Auth0Utils; -import lombok.RequiredArgsConstructor; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AccessTokenGenerator { - - final ApplicationProperties securityConfigProperties; - - final RefreshTokenGenerator refreshTokenGenerator; - - @NonNull - public AccessTokenCarrier generateAccessToken(UUID userId, UUID sessionId, Instant issuedAt) { - var userSessionId = UserSessionId.of(userId, sessionId); - - var refreshTokenContainer = refreshTokenGenerator.generateRefreshToken(userSessionId, issuedAt); - - var securityProperties = securityConfigProperties.security(); - - return AccessTokenCarrier.builder() - .tokenResponse( - TokenResponse.builder() - .accessToken( - Auth0Utils.buildTokenCommonParts( - securityProperties, userSessionId, issuedAt, TokenType.ACCESS_TOKEN) - .withIssuedAt(issuedAt) - .sign(Auth0Utils.getAlgorithm(securityProperties))) - .refreshToken(refreshTokenContainer.refreshToken()) - .build()) - .userId(userId) - .sessionId(sessionId) - .refreshTokenExpirationDate(refreshTokenContainer.expirationDate()) - .build(); - } -} diff --git a/src/main/java/com/vulinh/service/token/AccessTokenValidator.java b/src/main/java/com/vulinh/service/token/AccessTokenValidator.java deleted file mode 100644 index 4ac0198..0000000 --- a/src/main/java/com/vulinh/service/token/AccessTokenValidator.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.vulinh.service.token; - -import com.vulinh.data.constant.TokenType; -import com.vulinh.data.dto.carrier.DecodedJwtPayloadCarrier; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AccessTokenValidator { - - final TokenValidator tokenValidator; - - @NonNull - public DecodedJwtPayloadCarrier validateAccessToken(String accessToken) { - return tokenValidator.validateToken(accessToken, TokenType.ACCESS_TOKEN); - } -} diff --git a/src/main/java/com/vulinh/service/token/RefreshTokenGenerator.java b/src/main/java/com/vulinh/service/token/RefreshTokenGenerator.java deleted file mode 100644 index df8b53e..0000000 --- a/src/main/java/com/vulinh/service/token/RefreshTokenGenerator.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vulinh.service.token; - -import module java.base; - -import com.vulinh.configuration.data.ApplicationProperties; -import com.vulinh.data.constant.TokenType; -import com.vulinh.data.dto.carrier.RefreshTokenCarrier; -import com.vulinh.data.entity.ids.UserSessionId; -import com.vulinh.utils.security.Auth0Utils; -import lombok.RequiredArgsConstructor; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RefreshTokenGenerator { - - final ApplicationProperties applicationProperties; - - @NonNull - public RefreshTokenCarrier generateRefreshToken(UserSessionId userSessionId, Instant issuedAt) { - var securityProperties = applicationProperties.security(); - - var ttl = securityProperties.refreshJwtDuration(); - - return RefreshTokenCarrier.builder() - .expirationDate(issuedAt.plus(ttl)) - .refreshToken( - Auth0Utils.buildTokenCommonParts( - securityProperties, userSessionId, issuedAt, TokenType.REFRESH_TOKEN) - .sign(Auth0Utils.getAlgorithm(securityProperties))) - .build(); - } -} diff --git a/src/main/java/com/vulinh/service/token/RefreshTokenValidator.java b/src/main/java/com/vulinh/service/token/RefreshTokenValidator.java deleted file mode 100644 index 2bc8204..0000000 --- a/src/main/java/com/vulinh/service/token/RefreshTokenValidator.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.vulinh.service.token; - -import com.vulinh.data.constant.TokenType; -import com.vulinh.data.dto.carrier.DecodedJwtPayloadCarrier; -import lombok.RequiredArgsConstructor; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RefreshTokenValidator { - - final TokenValidator tokenValidator; - - @NonNull - public DecodedJwtPayloadCarrier validateRefreshToken(String refreshToken) { - return tokenValidator.validateToken(refreshToken, TokenType.REFRESH_TOKEN); - } -} diff --git a/src/main/java/com/vulinh/service/token/TokenValidator.java b/src/main/java/com/vulinh/service/token/TokenValidator.java deleted file mode 100644 index 899b88c..0000000 --- a/src/main/java/com/vulinh/service/token/TokenValidator.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vulinh.service.token; - -import module java.base; - -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.exceptions.TokenExpiredException; -import com.vulinh.configuration.data.ApplicationProperties; -import com.vulinh.data.constant.TokenType; -import com.vulinh.data.dto.carrier.DecodedJwtPayloadCarrier; -import com.vulinh.exception.AuthorizationException; -import com.vulinh.exception.SecurityConfigurationException; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.utils.security.Auth0Utils; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -class TokenValidator { - - final ApplicationProperties securityConfigProperties; - - public DecodedJwtPayloadCarrier validateToken(String refreshToken, TokenType expectedTokenType) { - try { - var decodedJWT = - Auth0Utils.getJwtVerifier(securityConfigProperties.security()).verify(refreshToken); - - var actualTokenType = - TokenType.valueOf(Auth0Utils.claimAsString(decodedJWT, Auth0Utils.TOKEN_TYPE)); - - if (actualTokenType != expectedTokenType) { - throw AuthorizationException.invalidAuthorization( - "Expected token type %s, but actual token was %s" - .formatted(TokenType.REFRESH_TOKEN, actualTokenType), - ServiceErrorCode.MESSAGE_INVALID_TOKEN_TYPE); - } - - return DecodedJwtPayloadCarrier.builder() - .userId(UUID.fromString(Auth0Utils.claimAsString(decodedJWT, Auth0Utils.USER_ID_CLAIM))) - .sessionId( - UUID.fromString(Auth0Utils.claimAsString(decodedJWT, Auth0Utils.SESSION_ID_CLAIM))) - .build(); - } catch (JWTDecodeException jwtDecodeException) { - throw AuthorizationException.invalidAuthorization( - "Invalid token format", ServiceErrorCode.MESSAGE_INVALID_TOKEN, jwtDecodeException); - } catch (TokenExpiredException tokenExpiredException) { - throw AuthorizationException.invalidAuthorization( - "Access token expired", - ServiceErrorCode.MESSAGE_CREDENTIALS_EXPIRED, - tokenExpiredException); - } catch (IllegalArgumentException illegalArgumentException) { - throw SecurityConfigurationException.configurationException( - "Invalid public key", - ServiceErrorCode.MESSAGE_INVALID_PUBLIC_KEY_CONFIG, - illegalArgumentException); - } - } -} diff --git a/src/main/java/com/vulinh/service/user/UserService.java b/src/main/java/com/vulinh/service/user/UserService.java deleted file mode 100644 index bee7167..0000000 --- a/src/main/java/com/vulinh/service/user/UserService.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.vulinh.service.user; - -import module java.base; - -import com.querydsl.core.types.Predicate; -import com.vulinh.data.constant.UserRole; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.request.UserSearchRequest; -import com.vulinh.data.dto.response.SingleUserResponse; -import com.vulinh.data.entity.*; -import com.vulinh.data.mapper.UserMapper; -import com.vulinh.data.repository.RoleRepository; -import com.vulinh.data.repository.UserRepository; -import com.vulinh.exception.NoSuchPermissionException; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.utils.PageableQueryService; -import com.vulinh.utils.PredicateBuilder; -import com.vulinh.utils.SecurityUtils; -import lombok.RequiredArgsConstructor; -import org.springframework.lang.NonNull; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class UserService - implements PageableQueryService { - - final UserMapper USER_MAPPER = UserMapper.INSTANCE; - - final UserRepository userRepository; - final RoleRepository roleRepository; - - final PasswordEncoder passwordEncoder; - - final UserValidationService userValidationService; - - @Transactional - public SingleUserResponse createUser(UserRegistrationRequest userRegistrationRequest) { - userValidationService.validateUserCreation(userRegistrationRequest); - - var transientUser = - USER_MAPPER.toUser( - userRegistrationRequest.withPassword( - passwordEncoder.encode(userRegistrationRequest.password()))); - - var rawRoleNames = UserRole.fromRawRole(userRegistrationRequest.userRoles()); - - transientUser - .setIsActive(true) - .setUserRoles( - Set.copyOf( - roleRepository.findAllById( - rawRoleNames.isEmpty() ? Set.of(UserRole.USER) : rawRoleNames))); - - return USER_MAPPER.toDto(userRepository.save(transientUser)); - } - - @Transactional - public boolean delete(UUID id) { - SecurityUtils.getUserDTO() - .filter(user -> user.id().equals(id)) - .ifPresent( - ignored -> { - throw NoSuchPermissionException.noSuchPermissionException( - "Cannot delete self", ServiceErrorCode.MESSAGE_NO_SELF_DESTRUCTION); - }); - - return userRepository - .findById(id) - .map( - found -> { - userRepository.delete(found); - - return true; - }) - .isPresent(); - } - - /* - * Pageable user query implementation - */ - - @Override - @NonNull - public SingleUserResponse toDto(@NonNull Users entity) { - return USER_MAPPER.toDto(entity); - } - - @Override - public Predicate toPredicate(@NonNull UserSearchRequest searchCriteria) { - var identity = searchCriteria.identity(); - var qUser = QUsers.users; - - var specification = - PredicateBuilder.or( - PredicateBuilder.likeIgnoreCase(qUser.id, identity), - PredicateBuilder.likeIgnoreCase(qUser.username, identity), - PredicateBuilder.likeIgnoreCase(qUser.email, identity), - PredicateBuilder.likeIgnoreCase(qUser.fullName, identity)); - - var searchRoles = UserRole.fromRawRole(searchCriteria.roles()); - - if (!searchRoles.isEmpty()) { - var roles = - roleRepository.findAllById(searchRoles).stream() - .map(Roles::getId) - .collect(Collectors.toSet()); - - specification = - PredicateBuilder.and( - specification, - roles.isEmpty() ? PredicateBuilder.never() : qUser.userRoles.any().id.in(roles)); - } - - return specification; - } - - @Override - @NonNull - public UserRepository getDslRepository() { - return userRepository; - } -} diff --git a/src/main/java/com/vulinh/service/user/UserValidationService.java b/src/main/java/com/vulinh/service/user/UserValidationService.java deleted file mode 100644 index 3231beb..0000000 --- a/src/main/java/com/vulinh/service/user/UserValidationService.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.vulinh.service.user; - -import module java.base; - -import com.sanctionco.jmail.JMail; -import com.vulinh.data.base.ApplicationError; -import com.vulinh.data.dto.request.UserLoginRequest; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.entity.Users; -import com.vulinh.data.repository.UserRepository; -import com.vulinh.factory.ValidatorStepFactory; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.utils.validator.NoArgsValidatorStep; -import com.vulinh.utils.validator.ValidatorChain; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.BooleanUtils; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -@Slf4j -public class UserValidationService { - - public static final int PASSWORD_MINIMUM_LENGTH = 8; - public static final int USERNAME_MAX_LENGTH = 200; - - final UserRepository userRepository; - - public static boolean isActive(Users matcherUser) { - var result = BooleanUtils.isTrue(matcherUser.getIsActive()); - - log.debug( - "User {} - {} is {}", - matcherUser.getId(), - matcherUser.getUsername(), - result ? "active" : "not active"); - - return result; - } - - public static boolean isPasswordMatched( - UserLoginRequest userLoginRequest, Users matchedUser, PasswordEncoder passwordEncoder) { - var result = passwordEncoder.matches(userLoginRequest.password(), matchedUser.getPassword()); - - if (!result) { - log.debug( - "Invalid password for user {} - {}", matchedUser.getId(), matchedUser.getUsername()); - } - - return result; - } - - public static boolean isRegistrationCodeMatched(String code, Users matchedUser) { - var result = code.equals(matchedUser.getUserRegistrationCode()); - - if (!result) { - log.debug("Invalid registration code for user {}", matchedUser.getUsername()); - } - - return result; - } - - public static boolean isEmailAvailable(UserRegistrationRequest dto) { - var email = dto.email(); - - var result = JMail.strictValidator().isValid(email); - - if (!result) { - log.debug("Email {} is invalid", email); - } - - return result; - } - - public static boolean isUsernameValid(UserRegistrationRequest dto) { - // Assuming the username is not blank - var username = dto.username(); - - var firstCharacter = username.charAt(0); - - if (Character.isDigit(firstCharacter) || isUnderscoreOrDot(firstCharacter)) { - log.debug("Username {} with invalid first character ({})", username, firstCharacter); - - return false; - } - - var lastCharacter = username.charAt(username.length() - 1); - - if (isUnderscoreOrDot(lastCharacter)) { - log.debug("Username {} with invalid last character ({})", username, lastCharacter); - - return false; - } - - var charArray = username.toCharArray(); - - // First and last characters are already checked - for (int index = 1; index < charArray.length - 1; index++) { - var character = charArray[index]; - - if (!(Character.isLetterOrDigit(character) || isUnderscoreOrDot(character))) { - log.debug( - "Username {} with invalid character ({}) at position {}", username, character, index); - - return false; - } - } - - return true; - } - - public void validateUserCreation(UserRegistrationRequest userRegistrationRequest) { - ValidatorChain.start() - .addValidator(UserRule.values()) - .addValidator(userAvailabilityValidation(userRegistrationRequest)) - .executeValidation(userRegistrationRequest); - } - - NoArgsValidatorStep userAvailabilityValidation( - UserRegistrationRequest userRegistrationRequest) { - return new NoArgsValidatorStep<>() { - - @Override - public Predicate getPredicate() { - return Predicate.not( - _ -> - userRepository.existsByUsernameIgnoreCaseOrEmailIgnoreCase( - userRegistrationRequest.username(), userRegistrationRequest.email())); - } - - @Override - public ApplicationError getApplicationError() { - return ServiceErrorCode.MESSAGE_USER_OR_EMAIL_EXISTED; - } - - @Override - public String getExceptionMessage() { - return "Username [%s] or email [%s] already existed!" - .formatted(userRegistrationRequest.username(), userRegistrationRequest.email()); - } - }; - } - - static boolean isUnderscoreOrDot(char character) { - return character == '_' || character == '.'; - } - - @Getter - @RequiredArgsConstructor - public enum UserRule implements NoArgsValidatorStep { - USER_NO_BLANK_USERNAME( - ValidatorStepFactory.noBlankField(UserRegistrationRequest::username), - ServiceErrorCode.MESSAGE_INVALID_USERNAME, - "Blank username is not allowed"), - USER_LONG_ENOUGH_USERNAME( - ValidatorStepFactory.noExceededLength( - UserRegistrationRequest::username, USERNAME_MAX_LENGTH), - ServiceErrorCode.MESSAGE_INVALID_USERNAME, - "Username exceeded %s characters".formatted(USERNAME_MAX_LENGTH)) { - - @Override - public Object[] getArgs() { - return new Integer[] {USERNAME_MAX_LENGTH}; - } - }, - USER_VALID_USERNAME( - UserValidationService::isUsernameValid, - ServiceErrorCode.MESSAGE_INVALID_USERNAME, - "Username must contain only letters, digits, dot, and underscore; must not start with a digit, underscore, or dot; and must not end with an underscore or dot."), - USER_NO_BLANK_PASSWORD( - ValidatorStepFactory.noBlankField(UserRegistrationRequest::password), - ServiceErrorCode.MESSAGE_INVALID_PASSWORD, - "Blank password is not allowed"), - USER_PASSWORD_LONG( - ValidatorStepFactory.atLeastLength( - UserRegistrationRequest::password, PASSWORD_MINIMUM_LENGTH), - ServiceErrorCode.MESSAGE_INVALID_PASSWORD, - "Password has to be %s characters or more".formatted(PASSWORD_MINIMUM_LENGTH)) { - - @Override - public Object[] getArgs() { - return new Integer[] {PASSWORD_MINIMUM_LENGTH}; - } - }, - USER_NO_BLANK_EMAIL( - ValidatorStepFactory.noBlankField(UserRegistrationRequest::email), - ServiceErrorCode.MESSAGE_INVALID_EMAIL, - "Blank email is not allowed"), - USER_NO_INVALID_EMAIL( - UserValidationService::isEmailAvailable, - ServiceErrorCode.MESSAGE_INVALID_EMAIL, - "Wrong email format"); - - final Predicate predicate; - final ServiceErrorCode applicationError; - final String exceptionMessage; - } -} diff --git a/src/main/java/com/vulinh/utils/JwtUtils.java b/src/main/java/com/vulinh/utils/JwtUtils.java new file mode 100644 index 0000000..6d9ded0 --- /dev/null +++ b/src/main/java/com/vulinh/utils/JwtUtils.java @@ -0,0 +1,44 @@ +package com.vulinh.utils; + +import module java.base; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.BadJwtException; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JwtUtils { + + static final String AUTHORIZED_PARTY_CLAIM = "azp"; + static final String RESOURCE_ACCESS_CLAIM = "resource_access"; + + @SuppressWarnings("unchecked") + public static AbstractAuthenticationToken parseAuthoritiesByCustomClaims( + Jwt jwt, @NonNull String clientName) { + if (!clientName.equals(jwt.getClaim(AUTHORIZED_PARTY_CLAIM))) { + throw new BadJwtException("Invalid authorized party"); + } + + if (!jwt.hasClaim(RESOURCE_ACCESS_CLAIM)) { + throw new BadJwtException("Missing resource access claim"); + } + + var resourceAccess = jwt.getClaimAsMap(RESOURCE_ACCESS_CLAIM); + + if (!resourceAccess.containsKey(clientName)) { + throw new BadJwtException("Missing client name claim"); + } + + var clientRoleContainer = (Map) resourceAccess.get(clientName); + + var roles = (List) clientRoleContainer.getOrDefault("roles", Collections.emptyList()); + + return new JwtAuthenticationToken( + jwt, roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet())); + } +} diff --git a/src/main/java/com/vulinh/utils/RSAUtils.java b/src/main/java/com/vulinh/utils/RSAUtils.java deleted file mode 100644 index 50e1afe..0000000 --- a/src/main/java/com/vulinh/utils/RSAUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.vulinh.utils; - -import module java.base; - -import com.vulinh.exception.SecurityConfigurationException; -import com.vulinh.locale.ServiceErrorCode; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; - -/// Utility class for handling RSA key operations. -/// -/// This class provides methods to generate [RSAPublicKey] and [RSAPrivateKey] instances from their string -/// representations. It can handle raw Base64 encoded keys as well as keys in PEM format (with headers, footers, and -/// line breaks). -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class RSAUtils { - - static final KeyFactory RSA_KEY_FACTORY; - - static { - try { - RSA_KEY_FACTORY = KeyFactory.getInstance("RSA"); - } catch (NoSuchAlgorithmException e) { - // Should never happen in a proper JDK - throw new ExceptionInInitializerError(e); - } - } - - static final String[] PUBLIC_PEM_HEADER_FOOTER = { - "-----BEGIN PUBLIC KEY-----", - "-----END PUBLIC KEY-----", - "-----BEGIN RSA PUBLIC KEY-----", - "-----END RSA PUBLIC KEY-----" - }; - - static final String[] PRIVATE_PEM_HEADER_FOOTER = { - "-----BEGIN PRIVATE KEY-----", - "-----END PRIVATE KEY-----", - "-----BEGIN RSA PRIVATE KEY-----", - "-----END RSA PRIVATE KEY-----" - }; - - /// Generates an [RSAPublicKey] from its string representation. - /// - /// This method takes a raw public key string, which can be in PEM format (e.g., containing - /// `-----BEGIN PUBLIC KEY-----` headers/footers) or a simple Base64 encoded string. It strips any PEM - /// headers/footers and whitespace before decoding and generating the key. - /// - /// @param rawPublicKey The string representation of the public key. - /// @return The generated [RSAPublicKey]. - /// @throws SecurityConfigurationException if the provided key string is invalid, malformed, or not an RSA public key. - public static RSAPublicKey generatePublicKey(String rawPublicKey) { - // Remove header and footer if they are present - // Also remove any whitespace character (\n, \r, space) - var refinedPublicKey = - StringUtils.deleteWhitespace(stripRawKey(rawPublicKey, PUBLIC_PEM_HEADER_FOOTER)); - - if (!(generateRSAPublicKey(refinedPublicKey) instanceof RSAPublicKey rsaPublicKey)) { - throw SecurityConfigurationException.configurationException( - "Not an instance of RSAPublicKey", ServiceErrorCode.MESSAGE_INVALID_PUBLIC_KEY_CONFIG); - } - - return rsaPublicKey; - } - - /// Generates an [RSAPrivateKey] from its string representation. - /// - /// This method takes a raw private key string, which can be in PEM format (e.g., containing - /// `-----BEGIN PRIVATE KEY-----` headers/footers) or a simple Base64 encoded string. It strips any PEM - /// headers/footers and whitespace before decoding and generating the key. - /// - /// @param rawPrivateKey The string representation of the private key. - /// @return The generated [RSAPrivateKey]. - /// @throws SecurityConfigurationException if the provided key string is invalid, malformed, or not an RSA private - /// key. - public static RSAPrivateKey generatePrivateKey(String rawPrivateKey) { - // Remove header and footer if they are present - // Also remove any whitespace character (\n, \r, space) - var refinedPrivateKey = - StringUtils.deleteWhitespace(stripRawKey(rawPrivateKey, PRIVATE_PEM_HEADER_FOOTER)); - - if (!(generateRSAPrivateKey(refinedPrivateKey) instanceof RSAPrivateKey rsaPrivateKey)) { - throw SecurityConfigurationException.configurationException( - "Not an instance of RSAPrivateKey", ServiceErrorCode.MESSAGE_INVALID_PRIVATE_KEY_CONFIG); - } - - return rsaPrivateKey; - } - - static String stripRawKey(String rawKey, String[] partsToRemove) { - var atomicCounter = new AtomicInteger(0); - - return rawKey - .replace(partsToRemove[atomicCounter.getAndIncrement()], StringUtils.EMPTY) - .replace(partsToRemove[atomicCounter.getAndIncrement()], StringUtils.EMPTY) - .replace(partsToRemove[atomicCounter.getAndIncrement()], StringUtils.EMPTY) - .replace(partsToRemove[atomicCounter.getAndIncrement()], StringUtils.EMPTY); - } - - static PublicKey generateRSAPublicKey(String refinedPublicKey) { - try { - return RSA_KEY_FACTORY.generatePublic( - new X509EncodedKeySpec(Base64.getDecoder().decode(refinedPublicKey))); - } catch (Exception exception) { - throw SecurityConfigurationException.configurationException( - "Invalid public key configuration", - ServiceErrorCode.MESSAGE_INVALID_PUBLIC_KEY_CONFIG, - exception); - } - } - - static PrivateKey generateRSAPrivateKey(String refinedPrivateKey) { - try { - return RSA_KEY_FACTORY.generatePrivate( - new PKCS8EncodedKeySpec(Base64.getDecoder().decode(refinedPrivateKey))); - } catch (Exception exception) { - throw SecurityConfigurationException.configurationException( - "Invalid private key configuration", - ServiceErrorCode.MESSAGE_INVALID_PRIVATE_KEY_CONFIG, - exception); - } - } -} diff --git a/src/main/java/com/vulinh/utils/SecurityUtils.java b/src/main/java/com/vulinh/utils/SecurityUtils.java index 9b6aa51..41b658d 100644 --- a/src/main/java/com/vulinh/utils/SecurityUtils.java +++ b/src/main/java/com/vulinh/utils/SecurityUtils.java @@ -2,13 +2,15 @@ import module java.base; -import com.vulinh.configuration.data.CustomAuthentication; +import com.vulinh.data.constant.UserRole; import com.vulinh.data.dto.response.UserBasicResponse; import com.vulinh.exception.AuthorizationException; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.springframework.lang.NonNull; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; /* * Those methods must be used only after the controller level, where the authentication process has been completed. @@ -17,10 +19,27 @@ public class SecurityUtils { public static Optional getUserDTO() { - return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .filter(CustomAuthentication.class::isInstance) - .map(CustomAuthentication.class::cast) - .map(CustomAuthentication::getPrincipal); + var authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (!(authentication instanceof JwtAuthenticationToken jat)) { + return Optional.empty(); + } + + var token = jat.getToken(); + + var basicResponse = + UserBasicResponse.builder() + .id(UUID.fromString(token.getSubject())) + .username(token.getClaimAsString("preferred_username")) + .email(token.getClaimAsString("email")) + .userRoles( + jat.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(UserRole::valueOf) + .collect(Collectors.toSet())) + .build(); + + return Optional.of(basicResponse); } @NonNull diff --git a/src/main/java/com/vulinh/utils/security/Auth0Utils.java b/src/main/java/com/vulinh/utils/security/Auth0Utils.java deleted file mode 100644 index a640c8c..0000000 --- a/src/main/java/com/vulinh/utils/security/Auth0Utils.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.vulinh.utils.security; - -import module java.base; - -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTCreator; -import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.jwt.interfaces.JWTVerifier; -import com.vulinh.configuration.data.ApplicationProperties.SecurityProperties; -import com.vulinh.data.constant.TokenType; -import com.vulinh.data.entity.ids.QUserSessionId; -import com.vulinh.data.entity.ids.UserSessionId; -import com.vulinh.exception.AuthorizationException; -import com.vulinh.locale.ServiceErrorCode; -import com.vulinh.utils.PredicateBuilder; -import com.vulinh.utils.RSAUtils; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class Auth0Utils { - - public static final String USER_ID_CLAIM = - PredicateBuilder.getFieldName(QUserSessionId.userSessionId.userId); - - public static final String SESSION_ID_CLAIM = - PredicateBuilder.getFieldName(QUserSessionId.userSessionId.sessionId); - - public static final String TOKEN_TYPE = "tokenType"; - - static final AtomicReference RSA_ALGORITHM = new AtomicReference<>(); - static final AtomicReference JWT_VERIFIER = new AtomicReference<>(); - - public static JWTCreator.Builder buildTokenCommonParts( - SecurityProperties securityProperties, - UserSessionId userSessionId, - Instant issuedAt, - TokenType tokenType) { - return JWT.create() - .withIssuer(securityProperties.issuer()) - .withExpiresAt(issuedAt.plus(securityProperties.jwtDuration())) - .withClaim(USER_ID_CLAIM, String.valueOf(userSessionId.userId())) - .withClaim(SESSION_ID_CLAIM, String.valueOf(userSessionId.sessionId())) - .withClaim(TOKEN_TYPE, tokenType.name()); - } - - public static String claimAsString(DecodedJWT decodedJWT, String claimName) { - var claimNode = decodedJWT.getClaim(claimName); - - if (claimNode.isMissing() || claimNode.isNull()) { - throw AuthorizationException.invalidAuthorization( - "Claim %s is missing".formatted(claimName), - ServiceErrorCode.MESSAGE_INVALID_AUTHORIZATION); - } - - return claimNode.asString(); - } - - public static String parseBearerToken(String token) { - if (StringUtils.isBlank(token)) { - throw AuthorizationException.invalidAuthorization( - "Session ID %s for user ID %s did not exist or has been invalidated", - ServiceErrorCode.MESSAGE_INVALID_SESSION); - } - - return token.startsWith("Bearer") ? token.substring(7) : token; - } - - public static Algorithm getAlgorithm(SecurityProperties securityProperties) { - var current = RSA_ALGORITHM.get(); - - if (current == null) { - var created = - Algorithm.RSA512( - RSAUtils.generatePublicKey(securityProperties.publicKey()), - RSAUtils.generatePrivateKey(securityProperties.privateKey())); - - return RSA_ALGORITHM.compareAndSet(null, created) ? created : RSA_ALGORITHM.get(); - } - - return current; - } - - public static JWTVerifier getJwtVerifier(SecurityProperties securityProperties) { - var current = JWT_VERIFIER.get(); - - if (current == null) { - var created = - JWT.require(getAlgorithm(securityProperties)) - .withIssuer(securityProperties.issuer()) - .build(); - - return JWT_VERIFIER.compareAndSet(null, created) ? created : JWT_VERIFIER.get(); - } - - return current; - } -} diff --git a/src/main/resources/META-INF/jpa-named-queries.properties b/src/main/resources/META-INF/jpa-named-queries.properties index fd5ad30..620ac49 100644 --- a/src/main/resources/META-INF/jpa-named-queries.properties +++ b/src/main/resources/META-INF/jpa-named-queries.properties @@ -38,6 +38,6 @@ find-prefetched-posts=select \ p.slug as slug, \ p.createdDate as createdDate, \ p.updatedDate as updatedDate, \ - p.author as author, \ + p.authorId as authorId, \ p.category as category \ from Post p \ No newline at end of file diff --git a/src/main/resources/application-development.yaml b/src/main/resources/application-development.yaml index 2645e00..7615263 100644 --- a/src/main/resources/application-development.yaml +++ b/src/main/resources/application-development.yaml @@ -1,13 +1,15 @@ -logging.level.com.vulinh: - configuration.cache.Common: DEBUG - service: - taxcalculator.TaxService: DEBUG - user.UserValidationService: DEBUG - post: - PostValidationService: DEBUG - PostEditValidationService: DEBUG - category.CategoryService: DEBUG - eventlistener.CustomEventListener: DEBUG - configuration: - com.vulinh.configuration.SecurityConfiguration: DEBUG - cache.FromUserSessionEntityKeyGenerator: DEBUG \ No newline at end of file +logging.level: + com.vulinh: + configuration.cache.Common: DEBUG + service: + taxcalculator.TaxService: DEBUG + user.UserValidationService: DEBUG + post: + PostValidationService: DEBUG + PostEditValidationService: DEBUG + category.CategoryService: DEBUG + eventlistener.CustomEventListener: DEBUG + configuration: + com.vulinh.configuration.SecurityConfiguration: DEBUG + cache.FromUserSessionEntityKeyGenerator: DEBUG + org.springframework.security.oauth2: TRACE \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 849d7d7..e1f9208 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -3,12 +3,11 @@ # application-properties: security: - issuer: ${SECURITY_ISSUER:spring-base-service} - # - # Go to https://www.devglan.com/online-tools/rsa-encryption-decryption to generate your own key - # - public-key: ${PUBLIC_KEY} - private-key: ${PRIVATE_KEY} + realm-name: ${KEYCLOAK_REALM:spring-base} + client-name: ${KEYCLOAK_CLIENT:spring-base-client} + auth-server: ${KEYCLOAK_HOST:http://localhost:8080} + # noinspection HttpUrlsUsage + issuer-uri: ${application-properties.security.auth-server}/realms/${application-properties.security.realm-name} no-authenticated-verb-urls: - { method: GET, url: /post/** } - { method: GET, url: /category/search/** } @@ -37,38 +36,25 @@ application-properties: - /user/search/** high-privilege-verb-urls: - { method: DELETE, url: /category/** } - jwt-duration: ${JWT_DURATION:5m} - refresh-jwt-duration: ${REFRESH_JWT_DURATION:1d} - password-reset-code-duration: ${PASSWORD_RESET_CODE_DURATION:1h} - schedule: - clean-expired-user-sessions: ${CLEAN_EXPIRED_SESSIONS_CRON:0 0/1 1-5 * * *} # Runs every minute between 1:00 AM and 5:59 AM daily # # End of application properties # server: - port: ${PORT:8080} + port: ${SERVER_PORT:8088} shutdown: GRACEFUL - # Require configuration on the server to enable HTTP/2 - # You can try https://brytecode.be/articles/using-http-2-in-your-spring-boot-application/ - http2.enabled: ${SERVER_SSL_ENABLED:true} - servlet.application-display-name: ${application.name} spring: # Make your default spring boot logging colorful (if you are using Community version lmao) output.ansi.enabled: ALWAYS application.name: spring-base - data.redis: - repositories.enabled: false - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - client-type: lettuce datasource: url: jdbc:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:myspringdatabase} username: ${DATABASE_USERNAME:postgres} password: ${DATABASE_PASSWORD:123456} - # END: change this to fit your computer's configuration - liquibase: - enabled: ${LIQUIBASE_ENABLED:true} + liquibase.enabled: ${LIQUIBASE_ENABLED:true} threads.virtual.enabled: true # Make use of Virtual Threads if possible + security.oauth2.resourceserver.jwt: + issuer-uri: ${application-properties.security.issuer-uri} + audiences: account decorator.datasource.p6spy: custom-appender-class: com.vulinh.MyP6SpyLogging logging: CUSTOM diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 9b4182f..9ab67c4 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -46,4 +46,10 @@ databaseChangeLog: relativeToChangelogFile: true - include: file: sql/0015-create-trigram-indexes.sql + relativeToChangelogFile: true + - include: + file: sql/0016-remove-user-relation.sql + relativeToChangelogFile: true + - include: + file: sql/0017-housecleaning.sql relativeToChangelogFile: true \ No newline at end of file diff --git a/src/main/resources/db/changelog/sql/0016-remove-user-relation.sql b/src/main/resources/db/changelog/sql/0016-remove-user-relation.sql new file mode 100644 index 0000000..2329ea2 --- /dev/null +++ b/src/main/resources/db/changelog/sql/0016-remove-user-relation.sql @@ -0,0 +1,5 @@ +--liquibase formatted sql +--changeset vulinh:20251127-0000 +ALTER TABLE post DROP CONSTRAINT post_users_fk; +ALTER TABLE post DROP CONSTRAINT post_users_updated_by_fk; +ALTER TABLE comment DROP CONSTRAINT comment_user_fk; diff --git a/src/main/resources/db/changelog/sql/0017-housecleaning.sql b/src/main/resources/db/changelog/sql/0017-housecleaning.sql new file mode 100644 index 0000000..5517635 --- /dev/null +++ b/src/main/resources/db/changelog/sql/0017-housecleaning.sql @@ -0,0 +1,9 @@ +--liquibase formatted sql +--changeset vulinh:20251127-0001 +ALTER TABLE user_role_mapping DROP CONSTRAINT user_role_mapping_roles_fk; +ALTER TABLE user_role_mapping DROP CONSTRAINT user_role_mapping_users_fk; + +DROP TABLE IF EXISTS "users"; +DROP TABLE IF EXISTS user_session; +DROP TABLE IF EXISTS roles; +DROP TABLE IF EXISTS user_role_mapping; \ No newline at end of file diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 95cf2ac..abd58a0 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -6,28 +6,9 @@ M9999=Internal Server error, please contact the development team! # # Authorization errors # -M9101=Invalid user credentials M9102=Invalid authorization info -M9103=User credentials expired -M9104=Invalid private key configuration (issuer) -M9105=Invalid public key configuration (validator) M9106=Invalid request body (%s) M9107=Invalid author or no permission to edit -M9108=Invalid token type -M9109=Invalid user session (not existed or has been invalidated) -M9110=Invalid token -# -# User creation errors -# -M9001=Invalid username -M9002=Invalid password (password has to be %s characters or more) -M9003=Invalid email -M9004=User or email already existed -M9005=Invalid new password (password has to be %s characters or more) -M9006=Invalid user confirmation -M9007=Old password did not match -M9008=New password cannot be the same as old password -M9009=Cannot delete self # # Post creation # diff --git a/src/main/resources/i18n/messages_vi.properties b/src/main/resources/i18n/messages_vi.properties index 1bf4339..69fe8cc 100644 --- a/src/main/resources/i18n/messages_vi.properties +++ b/src/main/resources/i18n/messages_vi.properties @@ -6,28 +6,9 @@ M9999=\u0110\u00E3 c\u00F3 l\u1ED7i x\u1EA3y ra, vui l\u00F2ng li\u00EAn h\u1EC7 # # Authorization errors # -M9101=Th\u00F4ng tin \u0111\u0103ng nh\u1EADp sai M9102=Th\u00F4ng tin x\u00E1c th\u1EF1c kh\u00F4ng h\u1EE3p l\u1EC7 -M9103=Th\u00F4ng tin x\u00E1c th\u1EF1c \u0111\u00E3 h\u1EBFt h\u1EA1n -M9104=C\u1EA5u h\u00ECnh cung c\u1EA5p ch\u1EE9ng th\u01B0 kh\u00F4ng h\u1EE3p l\u1EC7 -M9105=c\u1EA5u h\u00ECnh x\u00E1c minh ch\u1EE9ng th\u01B0 kh\u00F4ng h\u1EE3p l\u1EC7 M9106=B\u1EA3n tin kh\u00F4ng h\u1EE3p l\u1EC7 (%s) M9107=Kh\u00F4ng ph\u1EA3i t\u00E1c gi\u1EA3 ho\u1EB7c kh\u00F4ng c\u00F3 quy\u1EC1n -M9108=Sai lo\u1EA1i token -M9109=Phi\u00EAn \u0111\u0103ng nh\u1EADp kh\u00F4ng h\u1EE3p l\u1EC7 (kh\u00F4ng t\u1ED3n t\u1EA1i ho\u1EB7c h\u1EBFt h\u1EA1n) -M9110=Token kh\u00F4ng h\u1EE3p l\u1EC7 -# -# User creation errors -# -M9001=Username kh\u00F4ng h\u1EE3p l\u1EC7 -M9002=M\u1EADt kh\u1EA9u kh\u00F4ng h\u1EE3p l\u1EC7 (m\u1EADt kh\u1EA9u ph\u1EA3i d\u00E0i h\u01A1n %s k\u00FD t\u1EF1) -M9003=Email kh\u00F4ng h\u1EE3p l\u1EC7 -M9004=User ho\u1EB7c email \u0111\u00E3 t\u1ED3n t\u1EA1i -M9005=M\u1EADt kh\u1EA9u m\u1EDBi kh\u00F4ng h\u1EE3p l\u1EC7 (m\u1EADt kh\u1EA9u ph\u1EA3i d\u00E0i h\u01A1n %s k\u00FD t\u1EF1) -M9006=X\u00E1c nh\u1EADn ng\u01B0\u1EDDi d\u00F9ng m\u1EDBi kh\u00F4ng h\u1EE3p l\u1EC7 -M9007=Sai m\u1EADt kh\u1EA9u c\u0169 -M9008=M\u1EADt kh\u1EA9u m\u1EDBi v\u00E0 c\u0169 kh\u00F4ng \u0111\u01B0\u1EE3c gi\u1ED1ng nhau -M9009=Kh\u00F4ng th\u1EC3 x\u00F3a ch\u00EDnh m\u00ECnh # # Post creation # diff --git a/src/test/java/com/vulinh/Constants.java b/src/test/java/com/vulinh/Constants.java new file mode 100644 index 0000000..a849d1e --- /dev/null +++ b/src/test/java/com/vulinh/Constants.java @@ -0,0 +1,22 @@ +package com.vulinh; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Constants { + + // spring.datasource.username + public static final String POSTGRES_USERNAME = "postgres"; + + // spring.datasource.password + public static final String POSTGRES_PASSWORD = "123456"; + + public static final String MOCK_UUID = "1234567890abcdef1234567890abcdef"; + public static final String MOCK_SLUG = "test-title-%s".formatted(MOCK_UUID); + public static final String COMMON_PASSWORD = "123456"; + public static final String KC_ADMIN_USERNAME = "administrator"; + public static final String KC_ADMIN_PASSWORD = "administrator"; + public static final String TEST_ADMIN = "admin"; + public static final String TEST_POWER_USER = "power_user"; +} diff --git a/src/test/java/com/vulinh/configuration/RestClientConfiguration.java b/src/test/java/com/vulinh/configuration/RestClientConfiguration.java new file mode 100644 index 0000000..b90186c --- /dev/null +++ b/src/test/java/com/vulinh/configuration/RestClientConfiguration.java @@ -0,0 +1,22 @@ +package com.vulinh.configuration; + +import com.vulinh.configuration.data.ApplicationProperties; +import com.vulinh.keycloak.KeycloakAuthExchange; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +@Configuration +public class RestClientConfiguration { + + @Bean + public KeycloakAuthExchange keycloakAuth(ApplicationProperties applicationProperties) { + return HttpServiceProxyFactory.builderFor( + RestClientAdapter.create( + RestClient.builder().baseUrl(applicationProperties.security().issuerUri()).build())) + .build() + .createClient(KeycloakAuthExchange.class); + } +} diff --git a/src/main/java/com/vulinh/data/dto/response/data/AuthorData.java b/src/test/java/com/vulinh/data/AuthorData.java similarity index 85% rename from src/main/java/com/vulinh/data/dto/response/data/AuthorData.java rename to src/test/java/com/vulinh/data/AuthorData.java index d1f2f8b..e4233bc 100644 --- a/src/main/java/com/vulinh/data/dto/response/data/AuthorData.java +++ b/src/test/java/com/vulinh/data/AuthorData.java @@ -1,4 +1,4 @@ -package com.vulinh.data.dto.response.data; +package com.vulinh.data; import module java.base; diff --git a/src/test/java/com/vulinh/data/KeycloakTokenResponse.java b/src/test/java/com/vulinh/data/KeycloakTokenResponse.java new file mode 100644 index 0000000..4e4f19f --- /dev/null +++ b/src/test/java/com/vulinh/data/KeycloakTokenResponse.java @@ -0,0 +1,7 @@ +package com.vulinh.data; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KeycloakTokenResponse( + @JsonProperty("access_token") String accessToken, + @JsonProperty("refresh_token") String refreshToken) {} diff --git a/src/test/java/com/vulinh/it/IntegrationTestBase.java b/src/test/java/com/vulinh/it/IntegrationTestBase.java index 2a78717..55115b0 100644 --- a/src/test/java/com/vulinh/it/IntegrationTestBase.java +++ b/src/test/java/com/vulinh/it/IntegrationTestBase.java @@ -1,26 +1,19 @@ package com.vulinh.it; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.core.type.TypeReference; -import com.redis.testcontainers.RedisContainer; -import com.vulinh.data.constant.EndpointConstant; -import com.vulinh.data.constant.EndpointConstant.AuthEndpoint; -import com.vulinh.data.dto.carrier.TokenResponse; -import com.vulinh.data.dto.request.UserLoginRequest; -import com.vulinh.data.dto.response.GenericResponse; +import com.vulinh.Constants; +import com.vulinh.configuration.data.ApplicationProperties; +import com.vulinh.keycloak.KeycloakAuthExchange; import com.vulinh.utils.HealthCheckCommand; import com.vulinh.utils.JsonUtils; +import dasniko.testcontainers.keycloak.KeycloakContainer; import lombok.Getter; -import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.testcontainers.junit.jupiter.Container; @@ -34,57 +27,35 @@ @Getter public abstract class IntegrationTestBase { - // spring.datasource.username - protected static final String POSTGRES_USERNAME = "postgres"; - - // spring.datasource.password - protected static final String POSTGRES_PASSWORD = "123456"; - - protected static final String MOCK_UUID = "1234567890abcdef1234567890abcdef"; - - protected static final String MOCK_SLUG = "test-title-%s".formatted(MOCK_UUID); - - protected static final String COMMON_PASSWORD = "12345678"; - @Container protected static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer("postgres:18.0-alpine3.22") - .withUsername(POSTGRES_USERNAME) - .withPassword(POSTGRES_PASSWORD) - .waitingFor(HealthCheckCommand.POSTGRESQL.shellStrategyHealthCheck(POSTGRES_USERNAME)); + .withUsername(Constants.POSTGRES_USERNAME) + .withPassword(Constants.POSTGRES_PASSWORD) + .waitingFor( + HealthCheckCommand.POSTGRESQL.shellStrategyHealthCheck(Constants.POSTGRES_USERNAME)); @Container - protected static final RedisContainer REDIS_CONTAINER = - new RedisContainer("redis:8.2.3-bookworm") - .withCommand("redis-server", "--save", "60", "1") - .waitingFor(HealthCheckCommand.REDIS.shellStrategyHealthCheck()); + protected static final KeycloakContainer KEYCLOAK_CONTAINER = + new KeycloakContainer("quay.io/keycloak/keycloak:26.4") + .withAdminUsername(Constants.KC_ADMIN_USERNAME) + .withAdminPassword(Constants.KC_ADMIN_PASSWORD) + .waitingFor(HealthCheckCommand.KEYCLOAK.shellStrategyHealthCheck()); @Autowired private MockMvc mockMvc; - // Get the f*** out of my face, checked exceptions - @SneakyThrows - protected String getAccessToken(String username) { - var adminLoginResult = - mockMvc - .perform( - postWithEndpointAndPayload( - EndpointConstant.ENDPOINT_AUTH + AuthEndpoint.LOGIN, - UserLoginRequest.builder() - .username(username) - .password(COMMON_PASSWORD) - .build())) - .andExpect(status().isOk()) - .andReturn(); + @Autowired private ApplicationProperties applicationProperties; - assertNotNull(adminLoginResult); + @Autowired private KeycloakAuthExchange keycloakAuthExchange; - var response = adminLoginResult.getResponse(); - - assertNotNull(response); - - return JsonUtils.toObject( - response.getContentAsString(), new TypeReference>() {}) - .data() + // Get the f*** out of my face, checked exceptions + protected String getAccessToken(String username) { + return keycloakAuthExchange + .getToken( + "password", + applicationProperties.security().clientName(), + username, + Constants.COMMON_PASSWORD) .accessToken(); } @@ -94,9 +65,4 @@ protected static MockHttpServletRequestBuilder postWithEndpointAndPayload( .contentType(MediaType.APPLICATION_JSON) .content(JsonUtils.toMinimizedJSON(payload)); } - - // Recall this method to get the renewed JDBC url for each test class - protected static void reinitializeConnectionPropertiesInternal(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", POSTGRESQL_CONTAINER::getJdbcUrl); - } } diff --git a/src/test/java/com/vulinh/it/PostCreationIT.java b/src/test/java/com/vulinh/it/PostCreationIT.java index 39b077d..035f2fb 100644 --- a/src/test/java/com/vulinh/it/PostCreationIT.java +++ b/src/test/java/com/vulinh/it/PostCreationIT.java @@ -8,6 +8,7 @@ import module java.base; import com.fasterxml.jackson.core.type.TypeReference; +import com.vulinh.Constants; import com.vulinh.data.constant.EndpointConstant; import com.vulinh.data.dto.request.NewCommentRequest; import com.vulinh.data.dto.request.PostCreationRequest; @@ -24,13 +25,13 @@ import com.vulinh.data.repository.PostRepository; import com.vulinh.locale.ServiceErrorCode; import com.vulinh.utils.JsonUtils; +import com.vulinh.utils.KeycloakInitializationUtils; import com.vulinh.utils.post.NoDashedUUIDGenerator; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -52,9 +53,6 @@ class PostCreationIT extends IntegrationTestBase { static final String COMMENT_CONTENT = "This is a comment"; static final String EDITED_COMMENT_CONTENT = "Edited Comment"; - static final String ADMIN_USER = "admin"; - static final String POWER_USER = "power_user"; - // Shared across tests static UUID COMMENT_ID; static Long REVISION_NUMBER; @@ -65,13 +63,24 @@ class PostCreationIT extends IntegrationTestBase { @Autowired CommentRepository commentRepository; @Autowired CommentRevisionRepository commentRevisionRepository; + @DynamicPropertySource + static void reinitializeJdbcUrl(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", POSTGRESQL_CONTAINER::getJdbcUrl); + registry.add( + "application-properties.security.auth-server", KEYCLOAK_CONTAINER::getAuthServerUrl); + } + @Test @Transactional @Commit @Order(0) @SneakyThrows void testCreatePost() { - var accessToken = getAccessToken(ADMIN_USER); + // Ugly hack to get it runs ONLY ONCE + KeycloakInitializationUtils.initializeKeycloak( + getApplicationProperties().security(), KEYCLOAK_CONTAINER); + + var accessToken = getAccessToken(Constants.TEST_ADMIN); var postCreationResult = createPostRequest( @@ -94,8 +103,7 @@ void testCreatePost() { assertEquals(TITLE, data.title()); assertEquals(EXCERPT, data.excerpt()); - assertEquals(ADMIN_USER, data.author().username()); - assertEquals(MOCK_SLUG, data.slug()); + assertEquals(Constants.MOCK_SLUG, data.slug()); } @Test @@ -108,8 +116,7 @@ public void testVerifyPostCreation() { assertEquals(TITLE, post.getTitle()); assertEquals(EXCERPT, post.getExcerpt()); assertEquals("Test blank", post.getPostContent()); - assertEquals(MOCK_SLUG, post.getSlug()); - assertEquals(ADMIN_USER, post.getAuthor().getUsername()); + assertEquals(Constants.MOCK_SLUG, post.getSlug()); assertTrue( TAGS.containsAll(post.getTags().stream().map(Tag::getDisplayName).toList())); }, @@ -126,7 +133,7 @@ void testCreateComment() { POST_ID = postId; - var accessToken = getAccessToken(POWER_USER); + var accessToken = getAccessToken(Constants.TEST_POWER_USER); var newCommentResult = getMockMvc() @@ -134,7 +141,7 @@ void testCreateComment() { postWithEndpointAndPayload( "%s/%s".formatted(EndpointConstant.ENDPOINT_COMMENT, postId), NewCommentRequest.builder().content(COMMENT_CONTENT).build()) - .header(HttpHeaders.AUTHORIZATION, accessToken)) + .header(HttpHeaders.AUTHORIZATION, bearerToken(accessToken))) .andExpect(status().isCreated()) .andReturn(); @@ -150,6 +157,10 @@ void testCreateComment() { assertEquals(postId, response.postId()); } + private static @NotNull String bearerToken(String accessToken) { + return "Bearer %s".formatted(accessToken); + } + @Test @Transactional(readOnly = true) @Order(3) @@ -163,7 +174,6 @@ void testVerifyComment() { .ifPresentOrElse( comment -> { assertEquals(COMMENT_CONTENT, comment.getContent()); - assertEquals(POWER_USER, comment.getCreatedBy().getUsername()); assertEquals(POST_ID, comment.getPostId()); }, () -> fail("Comment not found")); @@ -192,7 +202,9 @@ void testEditComment() { "%s/%s".formatted(EndpointConstant.ENDPOINT_COMMENT, "{commentId}"), COMMENT_ID) .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, getAccessToken(POWER_USER)) + .header( + HttpHeaders.AUTHORIZATION, + bearerToken(getAccessToken(Constants.TEST_POWER_USER))) .content( JsonUtils.toMinimizedJSON( NewCommentRequest.builder().content(EDITED_COMMENT_CONTENT).build()))) @@ -229,7 +241,6 @@ void testVerifyEditedComment() { .ifPresentOrElse( comment -> { assertEquals(EDITED_COMMENT_CONTENT, comment.getContent()); - assertEquals(POWER_USER, comment.getCreatedBy().getUsername()); assertEquals(POST_ID, comment.getPostId()); }, () -> fail("Edited comment not found")); @@ -252,23 +263,18 @@ private MvcResult createPostRequest(PostCreationRequest postCreationRequest, Str // Return a fixed UUID for testing mockNoDashUUID .when(() -> NoDashedUUIDGenerator.createNonDashedUUID(any())) - .thenReturn(MOCK_UUID); + .thenReturn(Constants.MOCK_UUID); return getMockMvc() .perform( postWithEndpointAndPayload(EndpointConstant.ENDPOINT_POST, postCreationRequest) - .header(HttpHeaders.AUTHORIZATION, accessToken)) + .header(HttpHeaders.AUTHORIZATION, bearerToken(accessToken))) .andExpect(status().isCreated()) .andReturn(); } } private Optional findCreatedPost() { - return postRepository.findOne(QPost.post.slug.eq(MOCK_SLUG)); - } - - @DynamicPropertySource - static void reinitializeJdbcUrl(DynamicPropertyRegistry registry) { - reinitializeConnectionPropertiesInternal(registry); + return postRepository.findOne(QPost.post.slug.eq(Constants.MOCK_SLUG)); } } diff --git a/src/test/java/com/vulinh/it/UserRegistrationFlowIT.java b/src/test/java/com/vulinh/it/UserRegistrationFlowIT.java deleted file mode 100644 index ada2886..0000000 --- a/src/test/java/com/vulinh/it/UserRegistrationFlowIT.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.vulinh.it; - -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.vulinh.data.constant.EndpointConstant; -import com.vulinh.data.constant.EndpointConstant.AuthEndpoint; -import com.vulinh.data.dto.request.UserRegistrationRequest; -import com.vulinh.data.dto.response.GenericResponse; -import com.vulinh.data.dto.response.SingleUserResponse; -import com.vulinh.data.entity.QUsers; -import com.vulinh.data.entity.Users; -import com.vulinh.data.repository.UserRepository; -import com.vulinh.utils.JsonUtils; -import java.util.Optional; -import lombok.SneakyThrows; -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.Commit; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.springframework.transaction.annotation.Transactional; - -@TestMethodOrder(OrderAnnotation.class) -class UserRegistrationFlowIT extends IntegrationTestBase { - - static final String TEST_USERNAME = "linh.nguyen"; - - @Autowired UserRepository userRepository; - - @Order(0) - @Test - @Transactional - @Commit - @SneakyThrows - void testRegisterUser() { - var registerUserResult = - getMockMvc() - .perform( - postWithEndpointAndPayload( - EndpointConstant.ENDPOINT_AUTH + AuthEndpoint.REGISTER, - UserRegistrationRequest.builder() - .username(TEST_USERNAME) - .password("12345678") - .fullName("Linh Nguyen") - .email("linh.nguyen@email.com") - .build())) - .andExpect(status().isCreated()) - .andReturn(); - - assertNotNull(registerUserResult); - - var response = registerUserResult.getResponse(); - - assertNotNull(response); - - var userRegistrationResponse = - JsonUtils.toObject( - response.getContentAsString(), - new TypeReference>() {}) - .data(); - - var userId = userRegistrationResponse.id(); - - userRepository - .findById(userId) - .ifPresentOrElse( - user -> { - assertNotNull(user.getUserRegistrationCode()); - assertFalse(user.getIsActive()); - }, - () -> fail("User not found")); - } - - @Order(1) - @Test - @Transactional - @Commit - void testConfirmUser() { - findUserByUsername() - .ifPresentOrElse( - user -> { - try { - getMockMvc() - .perform( - get(EndpointConstant.ENDPOINT_AUTH + AuthEndpoint.CONFIRM_USER) - .param("userId", String.valueOf(user.getId())) - .param("code", user.getUserRegistrationCode())) - .andExpect(status().isOk()); - } catch (Exception e) { - fail("Exception occurred: " + e.getMessage()); - } - }, - () -> fail("User not found")); - } - - @Order(2) - @Test - @Transactional(readOnly = true) - void testVerifyConfirmedUser() { - findUserByUsername() - .ifPresentOrElse( - user -> { - assertNull(user.getUserRegistrationCode()); - assertTrue(user.getIsActive()); - }, - () -> fail("User not found")); - } - - private Optional findUserByUsername() { - return userRepository.findOne(QUsers.users.username.eq(TEST_USERNAME)); - } - - @DynamicPropertySource - static void reinitializeConnectionProperties(DynamicPropertyRegistry registry) { - reinitializeConnectionPropertiesInternal(registry); - } -} diff --git a/src/test/java/com/vulinh/keycloak/KeycloakAuthExchange.java b/src/test/java/com/vulinh/keycloak/KeycloakAuthExchange.java new file mode 100644 index 0000000..ff74802 --- /dev/null +++ b/src/test/java/com/vulinh/keycloak/KeycloakAuthExchange.java @@ -0,0 +1,20 @@ +package com.vulinh.keycloak; + +import com.vulinh.data.KeycloakTokenResponse; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; + +@HttpExchange +public interface KeycloakAuthExchange { + + @PostExchange( + url = "/protocol/openid-connect/token", + contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KeycloakTokenResponse getToken( + @RequestPart("grant_type") String grantType, + @RequestPart("client_id") String clientId, + @RequestPart("username") String username, + @RequestPart("password") String password); +} diff --git a/src/test/java/com/vulinh/service/token/AccessTokenGeneratorTest.java b/src/test/java/com/vulinh/service/token/AccessTokenGeneratorTest.java deleted file mode 100644 index 1f4a574..0000000 --- a/src/test/java/com/vulinh/service/token/AccessTokenGeneratorTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.vulinh.service.token; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import module java.base; - -import com.vulinh.configuration.data.ApplicationProperties; -import com.vulinh.configuration.data.ApplicationProperties.SecurityProperties; -import com.vulinh.data.constant.CommonConstant; -import com.vulinh.data.dto.carrier.RefreshTokenCarrier; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class AccessTokenGeneratorTest { - - @Mock ApplicationProperties securityConfigProperties; - @Mock RefreshTokenGenerator refreshTokenGenerator; - - @InjectMocks AccessTokenGenerator accessTokenGenerator; - - @Test - @Disabled - void testGenerateAccessToken() { - when(refreshTokenGenerator.generateRefreshToken(any(), any())) - .thenReturn( - RefreshTokenCarrier.builder() - .refreshToken("refreshToken") - .expirationDate(Instant.EPOCH) - .build()); - - when(securityConfigProperties.security()) - .thenReturn( - SecurityProperties.builder() - .privateKey( - """ - -----BEGIN RSA PRIVATE KEY----- - MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUdgQ4wThZR0qC1aNFoRX9dl68m91szEDJmEDMqIkTz42lo5LFWCQKarrTYbOVhWADHfSi4EFmiBRCZUjYynuNLQqHIZFWDLNEkarnN0rMlnYzkDCqVbpRRM34YfYgQCQjqNJlqs7KXc0zbB3d2U7/SLKxizej5ztCJqGn9vQrsxGk0KUFX0xxjZe8xd0t/292ok1JmSAwsdOQ9o5hoANY9z/cCmSnHhP/kfS4Dii7MJRaz7qeyr0679QdpOqcJrALLoTs3FJOKlVmEJ1DeTIgtukWBGt9xPkziR0SNbv7Csr8AwLu4GHOhSx8hCd9cDKuxLn1CowjnYTdCJXf7PDjAgMBAAECggEAPKEJB2EsQV30x21L0Hztl40F7/DSuU94VY8bPswBgiPCmjgZlDNY5ZgbhGLnKo4LHhiYTTqNr0K59VCN/z+ZDmqCDJnprZKmPbUL/jtrHwL47DIDkTgxmSt3U6Aw6ncjWQG+OMELjfhCrJ/3ze1Le9I1HDFMSXudD32SuCni9+z2oTaJ8s1WFtheCHKLSXlfNqnax8R8hQR4lBVKpWVDrisj+eXrSoeS6XitQTj9ICANjVeGwK1Wfum1Jt0lVMAtaV9A5co01wYw/T3L8FUdIQ1jU6ySZFuzkrgg4/N/VwL3ggOjQFF/IRQwA+WJs45tn/4mxlXJLF5Gb8novO++EQKBgQD1CQoq8pTFYEbRIjJNK2F43gzbbMap2KzSWKkZ4CGO9E0YsWZ9apMZeSaBJlPHKt67Pi62R/biy1m+lYiuS1tbncN4mBSZgsgF++zaO1prNRxHA2wKclraSIC+fV/nelN5MTrF4eNftek+BTtsQo1E/v9qnj0isY6qguhIjYDslQKBgQDd99PLHhuF8UtO7cs5Hb0GxnPccgkTMSiLmhEUUmg8xXwod2f1k2XA/gHLbDDx7vjuzXDOIWMLhNrvjWPSuZJiSUWQ0gOBCd7711W1UG9or/Fphm16kwmlYw5z0xyw2e7HFoub6Wppypg/f/774QG9D0qRxpnzSQ4hT8znvh2RlwKBgQCVHT0syZazTlWKKy9FOuMENMzKMzXqYks5bm7pqjWB0zWfk0V1iQefdtRxv6s4BuSoOb0ffEfH2Evy6PjWaFFePXGYz6Opj6a9zYNjgr8Rgq6EoJZ8/P5A2+JNCer06MInfEfx5/cAZalc7r4ssYtas3snnMhDdp4FMci9bi9IyQKBgAwFKK3+MmVdfMOIcxHjv2HHi2yrrDwi1FxC+pvMHqLz2tZiKPoOglsiJjy63ier1kUwUOSIwFFWX3jLglVeAURbTW4bQV9ShoXC0nxgH7helsctJW6W2dXf+F9jVlFpa9nSKbtGt6GE/BusNcW0GKEBW/tq8tlO4noBVUpTbEx/AoGBAIKSPtwjERXeeyskScv8QkulX7SB19QW9PKEpEn1YjOHETuj22Vb//XAj7eYvNsxkMngTbU+vmE8vdzZpjjyT70HVCrZ1cvqZDj6BzERoqnlqdmAbkLTsI/XgZGH5vBpYOJfNpH5ZMZqSHW3mn5U2SPQcrHm8EsHOyRZDrTAmWr5 - -----END RSA PRIVATE KEY----- - """) - .publicKey( - """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1HYEOME4WUdKgtWjRaEV/XZevJvdbMxAyZhAzKiJE8+NpaOSxVgkCmq602GzlYVgAx30ouBBZogUQmVI2Mp7jS0KhyGRVgyzRJGq5zdKzJZ2M5AwqlW6UUTN+GH2IEAkI6jSZarOyl3NM2wd3dlO/0iysYs3o+c7Qiahp/b0K7MRpNClBV9McY2XvMXdLf9vdqJNSZkgMLHTkPaOYaADWPc/3Apkpx4T/5H0uA4ouzCUWs+6nsq9Ou/UHaTqnCawCy6E7NxSTipVZhCdQ3kyILbpFgRrfcT5M4kdEjW7+wrK/AMC7uBhzoUsfIQnfXAyrsS59QqMI52E3QiV3+zw4wIDAQAB - -----END PUBLIC KEY----- - """) - .jwtDuration(Duration.ofHours(1L)) - .build()); - - var actualToken = - accessTokenGenerator.generateAccessToken( - CommonConstant.NIL_UUID, CommonConstant.NIL_UUID, Instant.EPOCH); - - assertEquals( - "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOm51bGwsImV4cCI6MzYwMCwidXNlcklkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwic2Vzc2lvbklkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwidG9rZW5UeXBlIjoiQUNDRVNTX1RPS0VOIiwiaWF0IjowfQ.iYK0CL37jIJUzJu1hR921y1ZxyvoROlxN2a9umUfBhf7kED9JmmjvpByzjMPyaEh7pSDq_Vcvh7GdEcnqZCbEdDHS8zvHD5ihjQdx1NMNy7wrAeHR7ynRU8RujwuyazgSq9rg3HsATp9kFj6px6kJtCjPdHz7EJEt6Q7eOyakGdKxTZiG3u-4wzP7Ni-RAK5FS-MUCeevBES8s9Sd72y0EIAU3_nbjfyqnWxu6okEYAlA8UutU3AW_EBxGSw1jUKE0plJJtGWsER7Hwr5Vhz_U8JiLdt32bP48g2--IrgBEDECPY_rKjM_jrAZxi7TUo82s41vky-ZiQsSM7zBB4rQ", - actualToken.tokenResponse().accessToken()); - } -} diff --git a/src/test/java/com/vulinh/service/user/UserValidationServiceTest.java b/src/test/java/com/vulinh/service/user/UserValidationServiceTest.java deleted file mode 100644 index 2110b72..0000000 --- a/src/test/java/com/vulinh/service/user/UserValidationServiceTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vulinh.service.user; - -import static org.junit.jupiter.api.Assertions.*; - -import com.vulinh.data.dto.request.UserRegistrationRequest; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; - -class UserValidationServiceTest { - - @ParameterizedTest - @ValueSource(strings = {"john_doe", "john.doe", "JohnDoe123", "john_doe.123"}) - void testValidUsername(String username) { - assertTrue(UserValidationService.isUsernameValid(mockUserRegistrationRequest(username))); - } - - @ParameterizedTest - @CsvSource({ - // Invalid first character - "_johndoe", - ".johndoe", - "1johndoe", - // Invalid last character - "johndoe_", - "johndoe.", - // Invalid user - "john#doe", - "john doe", - "john$doe" - }) - void testInvalidUsername(String username) { - assertFalse(UserValidationService.isUsernameValid(mockUserRegistrationRequest(username))); - } - - private static UserRegistrationRequest mockUserRegistrationRequest(String username) { - return UserRegistrationRequest.builder().username(username).build(); - } -} diff --git a/src/test/java/com/vulinh/utils/EquivalenceTest.java b/src/test/java/com/vulinh/utils/EquivalenceTest.java index 57649cb..35fcf71 100644 --- a/src/test/java/com/vulinh/utils/EquivalenceTest.java +++ b/src/test/java/com/vulinh/utils/EquivalenceTest.java @@ -4,7 +4,7 @@ import static org.junit.jupiter.api.Assertions.*; -import com.vulinh.data.dto.response.data.AuthorData; +import com.vulinh.data.AuthorData; import com.vulinh.utils.Equivalence.Creator; import com.vulinh.utils.Equivalence.EqualityDeepness; import lombok.Getter; diff --git a/src/test/java/com/vulinh/utils/HealthCheckCommand.java b/src/test/java/com/vulinh/utils/HealthCheckCommand.java index 5a0734a..105153c 100644 --- a/src/test/java/com/vulinh/utils/HealthCheckCommand.java +++ b/src/test/java/com/vulinh/utils/HealthCheckCommand.java @@ -7,7 +7,12 @@ @RequiredArgsConstructor public enum HealthCheckCommand { POSTGRESQL("pg_isready -U %s"), - REDIS("redis-cli -a %s ping"); + // Weird health check, but it is because there is no way to do Keycloak healthcheck + // Because Keycloak image is based on alpine + KEYCLOAK( + """ + [ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { java.net.URI uri = java.net.URI.create(args[0]); System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)uri.toURL().openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live + """); final String shellCommand; diff --git a/src/test/java/com/vulinh/utils/KeycloakInitializationUtils.java b/src/test/java/com/vulinh/utils/KeycloakInitializationUtils.java new file mode 100644 index 0000000..0656365 --- /dev/null +++ b/src/test/java/com/vulinh/utils/KeycloakInitializationUtils.java @@ -0,0 +1,78 @@ +package com.vulinh.utils; + +import com.vulinh.Constants; +import com.vulinh.configuration.data.ApplicationProperties; +import com.vulinh.data.constant.UserRole; +import dasniko.testcontainers.keycloak.KeycloakContainer; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +public class KeycloakInitializationUtils { + + static final String KC_ADM_SHELL = "/opt/keycloak/bin/kcadm.sh"; + + @SneakyThrows + public static void initializeKeycloak( + ApplicationProperties.SecurityProperties security, KeycloakContainer keycloakContainer) { + var clientIdMap = new AtomicReference(); + + var replacementMap = generateReplacementMap(security); + + var commands = KeycloakShellCommandUtils.readKeycloakExecCommands(replacementMap); + + for (var command : commands) { + var possibleUuid = clientIdMap.get(); + + if (possibleUuid != null) { + command = + Arrays.stream(command) + .map(s -> s.replace(wrapByBrackets("CLIENT_UUID"), possibleUuid)) + .toArray(String[]::new); + } + + var result = keycloakContainer.execInContainer(command); + + var singleCommand = String.join(" ", command); + + var output = + StringUtils.defaultIfBlank( + StringUtils.defaultIfBlank(result.getStdout(), result.getStderr()), singleCommand) + .replace("\r", StringUtils.EMPTY) + .replace("\n", StringUtils.EMPTY); + + log.info(output); + + if (singleCommand.contains("create clients -r")) { + clientIdMap.set(output); + } + } + } + + private static Map generateReplacementMap( + ApplicationProperties.SecurityProperties security) { + return Map.ofEntries( + Map.entry("KC_ADMIN_USERNAME", Constants.KC_ADMIN_USERNAME), + Map.entry("KC_ADMIN_PASSWORD", Constants.KC_ADMIN_PASSWORD), + Map.entry("KEYCLOAK_REALM", security.realmName()), + Map.entry("CLIENT_ID", security.clientName()), + Map.entry("ROLE_ADMIN", UserRole.ADMIN.name()), + Map.entry("ROLE_POWER_USER", UserRole.POWER_USER.name()), + Map.entry("ADMIN_USERNAME", Constants.TEST_ADMIN), + Map.entry("POWER_USER_USERNAME", Constants.TEST_POWER_USER), + Map.entry("COMMON_PASSWORD", Constants.COMMON_PASSWORD), + Map.entry("KC_ADM_SHELL", KC_ADM_SHELL)); + } + + public static @NotNull String wrapByBrackets(String variable) { + return "{{%s}}".formatted(variable); + } +} diff --git a/src/test/java/com/vulinh/utils/KeycloakShellCommandUtils.java b/src/test/java/com/vulinh/utils/KeycloakShellCommandUtils.java new file mode 100644 index 0000000..1ec0946 --- /dev/null +++ b/src/test/java/com/vulinh/utils/KeycloakShellCommandUtils.java @@ -0,0 +1,36 @@ +package com.vulinh.utils; + +import module java.base; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.io.ClassPathResource; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class KeycloakShellCommandUtils { + + @SneakyThrows + public static List readKeycloakExecCommands(Map replacementMap) { + try (var lines = Files.lines(new ClassPathResource("keycloak-exec.txt").getFile().toPath())) { + return lines + .filter(Predicate.not(StringUtils::isBlank)) + .map(StringUtils::normalizeSpace) + .map( + command -> { + for (var entry : replacementMap.entrySet()) { + command = + command.replace( + KeycloakInitializationUtils.wrapByBrackets(entry.getKey()), + entry.getValue()); + } + + return command.split("\\s+"); + }) + .toList(); + } + } +} diff --git a/src/test/java/com/vulinh/utils/OrderedObjectTest.java b/src/test/java/com/vulinh/utils/OrderedObjectTest.java index 9eb0765..38c8317 100644 --- a/src/test/java/com/vulinh/utils/OrderedObjectTest.java +++ b/src/test/java/com/vulinh/utils/OrderedObjectTest.java @@ -1,9 +1,9 @@ package com.vulinh.utils; -import module java.base; - import static org.junit.jupiter.api.Assertions.*; +import module java.base; + import com.vulinh.utils.OrderedObject.NullsOrder; import com.vulinh.utils.OrderedObject.Order; import com.vulinh.utils.OrderedObject.Wrapper; diff --git a/src/test/java/com/vulinh/utils/PredicateBuilderTest.java b/src/test/java/com/vulinh/utils/PredicateBuilderTest.java deleted file mode 100644 index 5da0bdd..0000000 --- a/src/test/java/com/vulinh/utils/PredicateBuilderTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vulinh.utils; - -import static org.junit.jupiter.api.Assertions.*; - -import com.vulinh.data.entity.QUsers; -import org.junit.jupiter.api.Test; - -class PredicateBuilderTest { - - @Test - void testGetFieldName() { - var expected = "username"; - - assertEquals(expected, PredicateBuilder.getFieldName(QUsers.users.username)); - } -} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml deleted file mode 100644 index d21f1d0..0000000 --- a/src/test/resources/application-test.yaml +++ /dev/null @@ -1,6 +0,0 @@ -application-properties: - security: - issuer: ${SECURITY_ISSUER:spring-base-service} - public-key: '-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt8FNmx/af8Ul2eo+dwL/JjnC2cJGhjSMT0AmM3NpGEPNuBC7gPtaMiPMkreoZ9aL5wD9M/Cz1jWdQdj5fT1nxSDut+9Wm+stqAyF5pkw4hXN9Qqhi048sZsVsmqi8M4XJqOg2W2a9+CvQPU1jvSQuwJEEFoXfnPvtVs6aB/YcUoHNuBUlYGVjcZonkJz3JByfn5+q/gI4hsTXzHSuL81n6Uwr2cL8K9DbC5Q8jICK+p8rNZy3z9FuGFtt4f5MG5lVtI8TlTrzhoZ6mKsGaeV+6a6jSFAK4uunGpowfoOKO2aS3GBp6sHz3kEY2vWomr5qvN5z7YSJm0kmtA4fp77MkXuezVLPfjc0G26fTtOoreLKCgFd9YsfacX/Q4nVZv8K5lZ+qQBhX5fl+k3qa8NnDsyGmJnj0UPwbKkdRpRpWPfmc5cnVYkEaQZ/D/St0UvdiqaVUhPyEMMKAYMa7PbQe6L2MQmA3ZsECnbksn6ZF9KWkK/slqOzAhKVo/r7d9X1wT/uVt/b0fBriwGIkORtnQtRQFdslbjrUXDt2GWk3/tm9OHBn/g/jK+NK4TyTzuiXawo39gg/CWZ0MQFU6hLXdn15244Nef2mIWbJece6CLwF/Gn43+z+SuPop+zG1JmkReJ8FDiEjq8KRLNf9UHRFXITsKThsr5s/4+jBTPJkCAwEAAQ==-----END PUBLIC KEY-----' - private-key: '-----BEGIN PRIVATE KEY-----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-----END PRIVATE KEY-----' -spring.data.redis.client-type: jedis \ No newline at end of file diff --git a/src/test/resources/keycloak-exec.txt b/src/test/resources/keycloak-exec.txt new file mode 100644 index 0000000..d0abf6d --- /dev/null +++ b/src/test/resources/keycloak-exec.txt @@ -0,0 +1,11 @@ +{{KC_ADM_SHELL}} config credentials --server http://localhost:8080 --realm master --user {{KC_ADMIN_USERNAME}} --password {{KC_ADMIN_PASSWORD}} +{{KC_ADM_SHELL}} create realms -s realm="{{KEYCLOAK_REALM}}" -s enabled=true +{{KC_ADM_SHELL}} create clients -r {{KEYCLOAK_REALM}} -s clientId={{CLIENT_ID}} -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i +{{KC_ADM_SHELL}} create clients/{{CLIENT_UUID}}/roles -r {{KEYCLOAK_REALM}} -s name={{ROLE_ADMIN}} +{{KC_ADM_SHELL}} create clients/{{CLIENT_UUID}}/roles -r {{KEYCLOAK_REALM}} -s name={{ROLE_POWER_USER}} +{{KC_ADM_SHELL}} create users -r {{KEYCLOAK_REALM}} -s username={{ADMIN_USERNAME}} -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User +{{KC_ADM_SHELL}} set-password -r {{KEYCLOAK_REALM}} --username {{ADMIN_USERNAME}} --new-password {{COMMON_PASSWORD}} +{{KC_ADM_SHELL}} add-roles -r {{KEYCLOAK_REALM}} --uusername {{ADMIN_USERNAME}} --cclientid {{CLIENT_ID}} --rolename {{ROLE_ADMIN}} +{{KC_ADM_SHELL}} create users -r {{KEYCLOAK_REALM}} -s username={{POWER_USER_USERNAME}} -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User +{{KC_ADM_SHELL}} set-password -r {{KEYCLOAK_REALM}} --username {{POWER_USER_USERNAME}} --new-password {{COMMON_PASSWORD}} +{{KC_ADM_SHELL}} add-roles -r {{KEYCLOAK_REALM}} --uusername {{POWER_USER_USERNAME}} --cclientid {{CLIENT_ID}} --rolename {{ROLE_POWER_USER}} \ No newline at end of file