├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── release.yml └── workflows │ ├── 1.dev-test.yml │ ├── 2.dev-deploy.yml │ ├── 3.rc-test-deploy.yml │ ├── 4.prod-test-deploy.yml │ └── codeql.yml ├── .gitignore ├── .mvn ├── jvm.config └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Makefile ├── README.md ├── docker-compose.yaml ├── lombok.config ├── mvnw ├── mvnw.cmd ├── opentelemetry ├── default.properties ├── dev.properties └── opentelemetry-javaagent.jar ├── pom.xml ├── postman ├── API.postman_collection.json ├── E2E.postman_collection.json └── dev.postman_environment.json ├── scripts ├── configs │ ├── postgres │ │ └── conf │ │ │ └── conf.d │ │ │ └── postgresql.conf │ └── rabbitmq-definition.json ├── create_hotfix.sh ├── create_merge_failed.sh ├── create_release.sh └── dumps │ └── keycloak.sql └── src ├── main ├── java │ └── com │ │ └── mycompany │ │ └── microservice │ │ └── api │ │ ├── ApiApplication.java │ │ ├── clients │ │ ├── http │ │ │ ├── DefaultRestTemplate.java │ │ │ ├── WebhookSiteHttpClient.java │ │ │ └── WebhookSiteUrlEnum.java │ │ └── slack │ │ │ ├── BaseSlackClient.java │ │ │ └── SlackAlertClient.java │ │ ├── constants │ │ ├── AppCompanySlug.java │ │ ├── AppConstants.java │ │ ├── AppHeaders.java │ │ ├── AppUrls.java │ │ └── JWTClaims.java │ │ ├── controllers │ │ ├── backoffice │ │ │ └── BackOfficeController.java │ │ ├── internal │ │ │ ├── CacheInternalApiController.java │ │ │ └── actuator │ │ │ │ └── WebMvcPreStopHookEndpoint.java │ │ ├── management │ │ │ ├── ApikeyManagementController.java │ │ │ ├── CompanyManagementController.java │ │ │ └── base │ │ │ │ └── BaseManagementController.java │ │ ├── platform │ │ │ ├── api │ │ │ │ └── PlatformApiController.java │ │ │ ├── mobile │ │ │ │ └── PlatformMobileController.java │ │ │ └── web │ │ │ │ └── PlatformWebController.java │ │ └── pubic │ │ │ └── PublicController.java │ │ ├── entities │ │ ├── ApiKey.java │ │ ├── Company.java │ │ └── base │ │ │ └── BaseEntity.java │ │ ├── enums │ │ └── UserRolesEnum.java │ │ ├── exceptions │ │ ├── BadRequestException.java │ │ ├── GlobalExceptionHandler.java │ │ ├── InternalServerErrorException.java │ │ ├── NotAllowedException.java │ │ ├── NotAuthorizedException.java │ │ ├── ResourceNotFoundException.java │ │ └── RootException.java │ │ ├── facades │ │ └── AuthFacade.java │ │ ├── infra │ │ ├── advices │ │ │ └── ResponseHeaderAdvice.java │ │ ├── auditors │ │ │ └── AuditorConfig.java │ │ ├── auth │ │ │ ├── converters │ │ │ │ └── KeycloakJwtConverter.java │ │ │ ├── jwt │ │ │ │ └── ClaimValidator.java │ │ │ └── providers │ │ │ │ ├── ApiKeyAuthentication.java │ │ │ │ ├── ApiKeyAuthenticationFilter.java │ │ │ │ └── ApiKeyAuthenticationProvider.java │ │ ├── filters │ │ │ ├── AddCredsToMDCFilter.java │ │ │ ├── AddNginxReqIdToMDCFilter.java │ │ │ └── RateLimitFilter.java │ │ ├── interceptors │ │ │ ├── InterceptorConfig.java │ │ │ ├── LogSlowResponseTimeInterceptor.java │ │ │ └── TimeExecutionInterceptor.java │ │ ├── profiling │ │ │ └── PyroscopeConfiguration.java │ │ ├── ratelimit │ │ │ ├── DefaultRateLimit.java │ │ │ └── base │ │ │ │ └── BaseRateLimit.java │ │ └── security │ │ │ └── SecurityConfiguration.java │ │ ├── listeners │ │ └── EntityTransactionLogListener.java │ │ ├── mappers │ │ ├── ApiKeyMapper.java │ │ ├── CompanyMapper.java │ │ ├── annotations │ │ │ └── ToEntity.java │ │ └── base │ │ │ └── ManagementBaseMapper.java │ │ ├── rabbitmq │ │ ├── configs │ │ │ ├── RabbitApplicationStartupListener.java │ │ │ └── RabbitConfig.java │ │ ├── listeners │ │ │ └── EventListener.java │ │ └── publishers │ │ │ └── EventPublisher.java │ │ ├── repositories │ │ ├── ApikeyRepository.java │ │ └── CompanyRepository.java │ │ ├── requests │ │ └── management │ │ │ ├── CreateApiKeyManagementRequest.java │ │ │ ├── CreateCompanyManagementRequest.java │ │ │ ├── UpdateApiKeyManagementRequest.java │ │ │ └── UpdateCompanyManagementRequest.java │ │ ├── responses │ │ ├── management │ │ │ ├── ApikeyManagementResponse.java │ │ │ └── CompanyManagementResponse.java │ │ └── shared │ │ │ ├── ApiErrorDetails.java │ │ │ ├── ApiListPaginationSimple.java │ │ │ ├── ApiListPaginationSuccess.java │ │ │ └── ApiListSuccess.java │ │ ├── services │ │ ├── ApiKeyService.java │ │ ├── CompanyService.java │ │ ├── LocalCacheManagerService.java │ │ ├── WebhookSiteService.java │ │ └── base │ │ │ └── BaseService.java │ │ └── utils │ │ ├── CryptoUtils.java │ │ ├── JsonUtils.java │ │ ├── LogUtils.java │ │ ├── UrlUtils.java │ │ └── WebClientUtils.java └── resources │ ├── META-INF │ └── native-image │ │ └── logback-config.json │ ├── application-dev.yml │ ├── application.yml │ ├── db │ └── migration │ │ ├── postgresql │ │ └── V1.1.1__Create_tables.sql │ │ └── replication │ │ ├── V2.1.1__Create_replication.sql │ │ └── V2.1.1__Create_replication.sql.conf │ └── logback-spring.xml └── test ├── java └── com │ └── mycompany │ └── microservice │ └── api │ ├── BaseIntegrationTest.java │ ├── controllers │ ├── backoffice │ │ └── AuthorizationBackOfficeControllerIT.java │ ├── internal │ │ └── AuthorizationInternalControllerIT.java │ ├── management │ │ └── AuthorizationManagementControllerIT.java │ └── platform │ │ ├── api │ │ └── AuthorizationPlatformApiControllerIT.java │ │ ├── mobile │ │ └── AuthorizationPlatformMobileControllerIT.java │ │ └── web │ │ └── AuthorizationPlatformWebControllerIT.java │ ├── facades │ └── AuthFacadeTest.java │ ├── junit │ └── ParallelizableTest.java │ ├── testutils │ ├── builders │ │ ├── ApiKeyBuilder.java │ │ ├── CompanyBuilder.java │ │ └── JwtBuilder.java │ └── configs │ │ └── TestContainersConfig.java │ └── utils │ ├── CryptoUtilsTest.java │ ├── JsonUtilsTest.java │ └── LogUtilsTest.java └── resources ├── application-test.yml ├── junit-platform.properties ├── logback-test.xml └── testcontainers ├── rabbitmq-definition.json └── rabbitmq.conf /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Jojoooo1 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | ###### _Short description of your task_ 4 | 5 | # Task 6 | 7 | ###### _Link to your task(s) in Jira_ 8 | 9 | # Test 10 | 11 | - ###### _A short description to test your PR_ 12 | 13 | # PR Dependencies 14 | 15 | - None 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | target-branch: "develop" 8 | - package-ecosystem: "maven" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | target-branch: "develop" 13 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-changelog 5 | 6 | categories: 7 | - title: ':warning: Update considerations and deprecations' 8 | labels: 9 | - 'warn/api-change' 10 | - 'warn/behavior-change' 11 | - 'warn/blocker' 12 | - 'warn/deprecation' 13 | - 'warn/regression' 14 | 15 | - title: ':rocket: New features and improvements' 16 | labels: 17 | - 'feature' 18 | - 'enhancement' 19 | - title: ':lady_beetle: Bug fixes' 20 | labels: 21 | - 'fix' 22 | - 'bugfix' 23 | - 'bug' 24 | - title: ':hammer: Build/Test Dependency Upgrades' 25 | labels: 26 | - 'dependencies' 27 | - 'maintenance' 28 | 29 | - title: ':book: Documentation, Tests and Build' 30 | labels: 31 | - 'documentation' 32 | - 'test' 33 | - 'chore' 34 | 35 | - title: ':question: Other Changes' 36 | labels: 37 | - '*' 38 | -------------------------------------------------------------------------------- /.github/workflows/1.dev-test.yml: -------------------------------------------------------------------------------- 1 | name: "run tests" 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize, reopened ] 6 | branches: [ develop ] 7 | 8 | concurrency: 9 | group: ci-dev-test-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | dev-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | # 1. Setup 17 | - uses: actions/checkout@v4 18 | - name: Set up JDK 19 | uses: actions/setup-java@v4.5.0 20 | with: 21 | distribution: 'liberica' 22 | java-version: '21' 23 | cache: 'maven' 24 | 25 | # 2. Run tests 26 | - name: Run Unit & Integration Tests 27 | run: mvn clean verify --no-transfer-progress 28 | 29 | # 3. Notify if fails 30 | -------------------------------------------------------------------------------- /.github/workflows/2.dev-deploy.yml: -------------------------------------------------------------------------------- 1 | name: "deploy to dev" 2 | 3 | # Note: Execute when feature branch is merge into develop this CI will be executed. 4 | 5 | on: 6 | pull_request: 7 | types: [ closed ] 8 | branches: [ develop ] 9 | 10 | env: 11 | IMAGE_NAME: api 12 | 13 | concurrency: 14 | group: ci-deploy-dev-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | dev-deploy: 19 | runs-on: ubuntu-latest 20 | environment: dev 21 | permissions: # Necessary for workload identity provider 22 | contents: 'read' 23 | id-token: 'write' 24 | 25 | if: github.event.pull_request.merged == true 26 | steps: 27 | # 1. Setup 28 | - uses: actions/checkout@v4 29 | - name: Set up JDK 30 | uses: actions/setup-java@v4.5.0 31 | with: 32 | distribution: 'liberica' 33 | java-version: '21' 34 | cache: 'maven' 35 | 36 | # 2. Sets & print variables 37 | - name: Sets variables 38 | id: variables 39 | run: | 40 | # 1. Set vars 41 | IMAGE_REGISTRY="us-docker.pkg.dev/${{ secrets.PROJECT_ID }}/cloud-diplomats/${{ env.IMAGE_NAME }}" 42 | IMAGE_TAG="dev-${{ github.run_number }}" 43 | 44 | # 3. Set vars as env 45 | echo "IMAGE_REGISTRY=$IMAGE_REGISTRY" >> $GITHUB_ENV 46 | echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV 47 | 48 | - name: Print variables 49 | run: | 50 | echo "IMAGE_TAG=$IMAGE_TAG" 51 | echo "IMAGE_REGISTRY=$IMAGE_REGISTRY" 52 | 53 | # # 3. Auth 54 | # - name: Auth via Workload Identity Federation 55 | # id: auth 56 | # uses: google-github-actions/auth@v2.1.2 57 | # with: 58 | # workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} 59 | # service_account: ${{ secrets.SERVICE_ACCOUNT }} # impersonated SA 60 | # 61 | # # 4. Setup gcloud & configure docker to use gcloud 62 | # - name: Setup gcloud 63 | # uses: google-github-actions/setup-gcloud@v2.1.0 64 | # with: 65 | # project_id: ${{ secrets.PROJECT_ID }} 66 | # - name: Setup docker to authenticate via gcloud 67 | # run: gcloud --quiet auth configure-docker us-docker.pkg.dev 68 | # 69 | # # 5. Build image 70 | # - name: Build image 71 | # run: mvn clean package -DskipTests spring-boot:build-image --no-transfer-progress -Dspring-boot.build-image.imageName=$IMAGE_REGISTRY:$IMAGE_TAG 72 | # 73 | # # 6. Push image 74 | # - name: Push image 75 | # run: docker push $IMAGE_REGISTRY:$IMAGE_TAG 76 | # 77 | # # 7. Notify if fails 78 | # # - name: Notify slack fail 79 | # # if: failure() 80 | # # env: 81 | # # SLACK_BOT_TOKEN: ${{ secrets.SLACK_NOTIFICATIONS_BOT_TOKEN }} 82 | # # uses: voxmedia/github-action-slack-notify-build@v1 83 | # # with: 84 | # # channel: app-alerts 85 | # # status: FAILED 86 | # # color: danger -------------------------------------------------------------------------------- /.github/workflows/3.rc-test-deploy.yml: -------------------------------------------------------------------------------- 1 | name: "run tests & deploy to test" 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize, reopened ] 6 | branches: [ main ] 7 | 8 | env: 9 | IMAGE_NAME: api 10 | 11 | concurrency: 12 | group: ci-deploy-rc-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | rc-test: 17 | if: contains(toJSON(github.head_ref), 'release/') || contains(toJSON(github.head_ref), 'hotfix/') 18 | runs-on: ubuntu-latest 19 | steps: 20 | # 1. Setup 21 | - uses: actions/checkout@v4 22 | - name: Set up JDK 23 | uses: actions/setup-java@v4.5.0 24 | with: 25 | distribution: 'liberica' 26 | java-version: '21' 27 | cache: 'maven' 28 | 29 | # 2. Test 30 | - name: Run Unit & Integration Tests 31 | run: mvn clean verify --no-transfer-progress 32 | 33 | rc-deploy: 34 | needs: [ rc-test ] 35 | runs-on: ubuntu-latest 36 | environment: test 37 | permissions: # Necessary for workload identity provider 38 | contents: 'read' 39 | id-token: 'write' 40 | 41 | steps: 42 | # 1. Setup 43 | - uses: actions/checkout@v4 44 | - name: Set up JDK 45 | uses: actions/setup-java@v4.5.0 46 | with: 47 | distribution: 'liberica' 48 | java-version: '21' 49 | cache: 'maven' 50 | 51 | # 2. Sets & print variables 52 | - name: Sets variables 53 | id: variables 54 | run: | 55 | git fetch --prune --prune-tags origin 56 | 57 | # 1. Get tags 58 | LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") 59 | TAG_LIST=($(echo $LATEST_TAG | tr '.' ' ')) 60 | [[ "${#TAG_LIST[@]}" -ne 2 ]] && echo "$RELEASE_VERSION is not a valid version" && exit 1 61 | 62 | # 2. Set release version 63 | if [[ "$GITHUB_HEAD_REF" == release* ]] 64 | then 65 | RELEASE_VERSION=$(( TAG_LIST[0] + 1 )).0; 66 | else 67 | RELEASE_VERSION=${TAG_LIST[0]}.$(( TAG_LIST[1] + 1)); 68 | fi 69 | 70 | # 3. Set vars 71 | IMAGE_REGISTRY="us-docker.pkg.dev/${{ secrets.PROJECT_ID }}/cloud-diplomats/${{ env.IMAGE_NAME }}" 72 | IMAGE_TAG=${RELEASE_VERSION}-$(git rev-parse --short=4 HEAD)-rc 73 | 74 | # 4. Set vars as env 75 | echo "IMAGE_REGISTRY=$IMAGE_REGISTRY" >> $GITHUB_ENV 76 | echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV 77 | 78 | - name: Print variables 79 | run: | 80 | echo "IMAGE_TAG=$IMAGE_TAG" 81 | echo "IMAGE_REGISTRY=$IMAGE_REGISTRY" 82 | 83 | # 3. Auth 84 | - name: Auth via Workload Identity Federation 85 | id: auth 86 | uses: google-github-actions/auth@v2.1.7 87 | with: 88 | workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} 89 | service_account: ${{ secrets.SERVICE_ACCOUNT }} # impersonated SA 90 | 91 | # 4. Setup gcloud & configure docker to use gcloud 92 | - name: Setup gcloud 93 | uses: google-github-actions/setup-gcloud@v2.1.2 94 | with: 95 | project_id: ${{ secrets.PROJECT_ID }} 96 | - name: Setup docker to authenticate via gcloud 97 | run: gcloud --quiet auth configure-docker us-docker.pkg.dev 98 | 99 | # 5. Build image 100 | - name: Build image 101 | run: mvn clean package -DskipTests spring-boot:build-image --no-transfer-progress -Dspring-boot.build-image.imageName=$IMAGE_REGISTRY:$IMAGE_TAG 102 | 103 | # 6. Push image 104 | - name: Push image 105 | run: docker push $IMAGE_REGISTRY:$IMAGE_TAG 106 | 107 | # 7. Notify if failss 108 | # - name: Notify slack fail 109 | # if: failure() 110 | # env: 111 | # SLACK_BOT_TOKEN: ${{ secrets.SLACK_NOTIFICATIONS_BOT_TOKEN }} 112 | # uses: voxmedia/github-action-slack-notify-build@v1 113 | # with: 114 | # channel: app-alerts 115 | # status: FAILED 116 | # color: danger -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "run codeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: 'ubuntu-latest' 13 | timeout-minutes: 360 14 | permissions: 15 | # required for all workflows 16 | security-events: write 17 | 18 | # only required for workflows in private repositories 19 | actions: read 20 | contents: read 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | - name: Set up JDK 26 | uses: actions/setup-java@v4.5.0 27 | with: 28 | distribution: 'liberica' 29 | java-version: '21' 30 | cache: 'maven' 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: java 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: "/language:java" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jojoooo1/spring-boot-api/28e1c0ce550b6a23bb6adac2e225d892cdfaab20/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | .PHONY: test install run help 3 | 4 | DB_CONTAINER="api-database" 5 | DB_NAME="api" 6 | DB_SUPERUSER="postgres" 7 | DB_USER="user" 8 | DB_PASS="password" 9 | 10 | DB_CONTAINER_KC="keycloak-database" 11 | DB_NAME_KC="keycloak" 12 | DB_USER_KC="user" 13 | DB_PASS_KC="password" 14 | 15 | help: ## Show this help message. 16 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 17 | 18 | test: ## Execute all test 19 | @mvn clean verify 20 | 21 | run-api: ## Run API with maven 22 | @mvn clean spring-boot:run -Dspring.profiles.active=dev 23 | 24 | start-api-with-docker-image: ## Run API with docker (don't forget to build the image locally before) 25 | @docker run --net host -e SPRING_PROFILES_ACTIVE="dev" -e SPRING_DATASOURCE_URL="jdbc:postgresql://localhost:5432/api?stringtype=unspecified&reWriteBatchedInserts=true" api:0.0.1-SNAPSHOT 26 | 27 | start-api: ## Run API with docker compose 28 | @docker compose up -d 29 | @docker compose logs -f api 30 | 31 | start-infra: ## Run required infrastructure with docker compose 32 | $(MAKE) kill start-database start-keycloak start-rabbitmq 33 | 34 | restart-infra: ## Reset and start required infrastructure with docker compose 35 | $(MAKE) start-database start-keycloak start-rabbitmq 36 | 37 | start-all: ## Run all containers with docker compose 38 | $(MAKE) start-infra start-api 39 | 40 | restart-all: ## Restart containers with docker compose 41 | @docker compose stop api # used to rebuild API after modification 42 | @docker compose up -d 43 | @docker compose logs -f api 44 | 45 | start-database: ## Run api database 46 | @docker compose up -d ${DB_CONTAINER} --wait 47 | # Set db_user as superuser to allow replication slot creation within migration 48 | @docker exec -i ${DB_CONTAINER} psql "host=localhost dbname=${DB_NAME} user=${DB_SUPERUSER} password=${DB_PASS}" --command "ALTER USER \"${DB_USER}\" WITH SUPERUSER;" 49 | 50 | kill-database: ## Kill api database 51 | @docker compose rm -sf ${DB_CONTAINER} 52 | @docker volume rm -f api_database 53 | 54 | start-keycloak : ## Run keycloak 55 | @docker compose up -d ${DB_CONTAINER_KC} --wait 56 | @cat ./scripts/dumps/keycloak.sql | docker exec -i ${DB_CONTAINER_KC} psql "host=localhost dbname=${DB_NAME_KC} user=${DB_USER_KC} password=${DB_PASS_KC}" 57 | @docker compose up -d keycloak 58 | 59 | kill-keycloak : ## Kill keycloak 60 | @docker compose rm -sf keycloak ${DB_CONTAINER_KC} 61 | @docker volume rm -f api_database_kc 62 | 63 | start-rabbitmq : ## Run rabbitmq 64 | @docker compose up -d rabbitmq 65 | 66 | kill-rabbitmq : ## Kill rabbitmq 67 | @docker compose rm -sf rabbitmq 68 | @docker volume rm -f api_rabbitmq 69 | 70 | kill: ## Kill and reset project 71 | @docker compose down 72 | @mvn install -DskipTests 73 | $(MAKE) kill-database kill-keycloak kill-rabbitmq 74 | 75 | release: ## Create release 76 | ./scripts/create_release.sh 77 | 78 | hotfix: ## Create hotfix 79 | ./scripts/create_hotfix.sh 80 | 81 | create-merge-failed: ## Create failed merge PR 82 | ./scripts/create_merge_failed.sh 83 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | # After using this maven container you need to re-install maven using "mvn install -DskipTests" 5 | api: 6 | container_name: api 7 | image: maven:3.9.6-eclipse-temurin-21 8 | user: '1000' 9 | command: 10 | [ 11 | 'mvn', 12 | 'clean', 13 | 'spring-boot:run', 14 | '-Duser.home=/var/maven' 15 | ] 16 | network_mode: host 17 | depends_on: 18 | api-database: 19 | condition: service_healthy 20 | rabbitmq: 21 | condition: service_healthy 22 | environment: 23 | MAVEN_CONFIG: /var/maven/.m2 24 | SPRING_PROFILES_ACTIVE: dev 25 | SPRING_CLOUD_KUBERNETES_ENABLED: "false" 26 | SPRING_DATASOURCE_URL: "jdbc:postgresql://localhost:5432/api?stringtype=unspecified&reWriteBatchedInserts=true" 27 | volumes: 28 | - "$PWD:/usr/src/workdir" 29 | - ~/.m2:/var/maven/.m2 30 | working_dir: /usr/src/workdir 31 | healthcheck: 32 | test: 33 | [ 34 | 'CMD-SHELL', 35 | 'curl --fail --silent localhost:8080/actuator/health | grep UP || exit 1' 36 | ] 37 | interval: 30s 38 | timeout: 5s 39 | retries: 5 40 | start_period: 30s 41 | 42 | api-database: 43 | container_name: api-database 44 | image: bitnami/postgresql:16.3.0 45 | network_mode: host 46 | environment: 47 | POSTGRESQL_DATABASE: api 48 | POSTGRESQL_POSTGRES_PASSWORD: password 49 | POSTGRESQL_USERNAME: user 50 | POSTGRESQL_PASSWORD: password 51 | volumes: 52 | # Config is injected to activate logical replication 53 | - ./scripts/configs/postgres/conf:/bitnami/postgresql/conf 54 | - api_database:/bitnami/postgresql 55 | healthcheck: 56 | test: [ 'CMD-SHELL', 'pg_isready -U postgres' ] 57 | interval: 5s 58 | timeout: 5s 59 | retries: 10 60 | 61 | rabbitmq: 62 | container_name: api-rabbitmq 63 | image: bitnami/rabbitmq:3.13.2 64 | network_mode: host 65 | healthcheck: 66 | test: rabbitmq-diagnostics -q ping 67 | interval: 5s 68 | timeout: 5s 69 | retries: 10 70 | environment: 71 | RABBITMQ_LOAD_DEFINITIONS: "true" 72 | # definition is set with -> user: password 73 | RABBITMQ_DEFINITIONS_FILE: /etc/rabbitmq/definitions.json 74 | RABBITMQ_PLUGINS: rabbitmq_shovel rabbitmq_shovel_management rabbitmq_delayed_message_exchange 75 | RABBITMQ_COMMUNITY_PLUGINS: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.12.0/rabbitmq_delayed_message_exchange-3.12.0.ez 76 | volumes: 77 | - ./scripts/configs/rabbitmq-definition.json:/etc/rabbitmq/definitions.json 78 | - api_rabbitmq:/bitnami/rabbitmq/mnesia 79 | 80 | keycloak: 81 | container_name: keycloak 82 | image: bitnami/keycloak:24.0.4 83 | # network_mode: host 84 | environment: 85 | KEYCLOAK_ADMIN_USER: admin 86 | KEYCLOAK_ADMIN_PASSWORD: password 87 | 88 | KEYCLOAK_DATABASE_HOST: keycloak-database 89 | KEYCLOAK_DATABASE_PORT: 5432 90 | 91 | KEYCLOAK_DATABASE_NAME: keycloak 92 | KEYCLOAK_DATABASE_USER: user 93 | KEYCLOAK_DATABASE_PASSWORD: password 94 | ports: 95 | - "8000:8080" 96 | 97 | keycloak-database: 98 | container_name: keycloak-database 99 | image: bitnami/postgresql:16.3.0 100 | environment: 101 | POSTGRESQL_DATABASE: keycloak 102 | POSTGRESQL_USERNAME: user 103 | POSTGRESQL_PASSWORD: password 104 | ports: 105 | - "5434:5432" 106 | healthcheck: 107 | test: [ 'CMD-SHELL', 'pg_isready -U postgres' ] 108 | interval: 5s 109 | timeout: 5s 110 | retries: 10 111 | volumes: 112 | - api_database_kc:/bitnami/postgresql 113 | 114 | volumes: 115 | api_database: 116 | name: api_database 117 | api_database_kc: 118 | name: api_database_kc 119 | api_rabbitmq: 120 | name: api_rabbitmq -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | lombok.anyConstructor.suppressConstructorProperties = true 3 | lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier 4 | lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value 5 | lombok.copyableAnnotations += org.springframework.context.annotation.Lazy 6 | -------------------------------------------------------------------------------- /opentelemetry/default.properties: -------------------------------------------------------------------------------- 1 | # https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md 2 | # https://opentelemetry.io/docs/languages/java/automatic/configuration/ 3 | 4 | # Version 2.4.0 5 | 6 | otel.javaagent.enabled=true 7 | otel.javaagent.logging=application 8 | 9 | otel.metrics.exporter=otlp 10 | otel.traces.exporter=otlp 11 | otel.logs.exporter=none 12 | 13 | otel.propagators=tracecontext, baggage 14 | 15 | otel.exporter.otlp.protocol=grpc 16 | # otel.exporter.otlp.endpoint=http://localhost:4317 # set per environment 17 | 18 | otel.instrumentation.jdbc-datasource.enabled=true 19 | 20 | otel.instrumentation.common.enduser.enabled=true 21 | otel.instrumentation.common.enduser.id.enabled=true 22 | otel.instrumentation.common.enduser.role.enabled=true 23 | otel.instrumentation.common.enduser.scope.enabled=true 24 | 25 | # https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md 26 | # https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources 27 | # remove resource labels: 28 | # container.id: ContainerResourceProvider 29 | # host.arch, host.name: HostResourceProvider 30 | # os.type os.description: OsResourceProvider 31 | # process.command_args process.executable.path process.pid: ProcessResourceProvider 32 | # process.runtime.description process.runtime.name process.runtime.version: ProcessRuntimeResourceProvider 33 | 34 | # otel.java.disabled.resource.providers=io.opentelemetry.instrumentation.resources.ContainerResourceProvider,io.opentelemetry.instrumentation.resources.HostResourceProvider,io.opentelemetry.instrumentation.resources.OsResourceProvider,io.opentelemetry.instrumentation.resources.ProcessResourceProvider,io.opentelemetry.instrumentation.resources.ProcessRuntimeResourceProvider 35 | -------------------------------------------------------------------------------- /opentelemetry/dev.properties: -------------------------------------------------------------------------------- 1 | # https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md 2 | # https://opentelemetry.io/docs/languages/java/automatic/configuration/ 3 | 4 | # Version 2.4.0 5 | 6 | otel.javaagent.enabled=true 7 | otel.javaagent.logging=application 8 | 9 | otel.metrics.exporter=none 10 | otel.traces.exporter=none 11 | otel.logs.exporter=none 12 | 13 | otel.propagators=tracecontext, baggage 14 | 15 | otel.exporter.otlp.protocol=grpc 16 | otel.exporter.otlp.endpoint=http://localhost:4317 17 | 18 | otel.instrumentation.jdbc-datasource.enabled=true 19 | 20 | otel.instrumentation.common.enduser.enabled=true 21 | otel.instrumentation.common.enduser.id.enabled=true 22 | otel.instrumentation.common.enduser.role.enabled=true 23 | otel.instrumentation.common.enduser.scope.enabled=true -------------------------------------------------------------------------------- /opentelemetry/opentelemetry-javaagent.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jojoooo1/spring-boot-api/28e1c0ce550b6a23bb6adac2e225d892cdfaab20/opentelemetry/opentelemetry-javaagent.jar -------------------------------------------------------------------------------- /postman/dev.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0aa2104f-e67a-4949-90d1-066492dec4f8", 3 | "name": "dev", 4 | "values": [ 5 | { 6 | "key": "host", 7 | "value": "http://localhost:8080", 8 | "type": "default", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "", 13 | "value": "", 14 | "type": "default", 15 | "enabled": false 16 | }, 17 | { 18 | "key": "keycloak_host", 19 | "value": "http://localhost:8000", 20 | "type": "default", 21 | "enabled": true 22 | }, 23 | { 24 | "key": "keycloak_realm_name", 25 | "value": "api", 26 | "type": "default", 27 | "enabled": true 28 | }, 29 | { 30 | "key": "keycloak_client", 31 | "value": "api-cli", 32 | "type": "default", 33 | "enabled": true 34 | }, 35 | { 36 | "key": "keycloak_client_secret", 37 | "value": "lpbLD2EboyHXU4wlx1qULsxt69Xo0ybz", 38 | "type": "default", 39 | "enabled": true 40 | }, 41 | { 42 | "key": "", 43 | "value": "", 44 | "type": "default", 45 | "enabled": false 46 | }, 47 | { 48 | "key": "management_apikey", 49 | "value": "mgmt-apikey", 50 | "type": "default", 51 | "enabled": true 52 | }, 53 | { 54 | "key": "internal_apikey", 55 | "value": "internal-apikey", 56 | "type": "default", 57 | "enabled": true 58 | }, 59 | { 60 | "key": "platform_apikey", 61 | "value": "platform-apikey", 62 | "type": "default", 63 | "enabled": true 64 | }, 65 | { 66 | "key": "", 67 | "value": "", 68 | "type": "default", 69 | "enabled": false 70 | }, 71 | { 72 | "key": "platform_user_username", 73 | "value": "platform_user", 74 | "type": "default", 75 | "enabled": true 76 | }, 77 | { 78 | "key": "platform_admin_username", 79 | "value": "platform_admin", 80 | "type": "default", 81 | "enabled": true 82 | }, 83 | { 84 | "key": "platform_user_password", 85 | "value": "password", 86 | "type": "default", 87 | "enabled": true 88 | }, 89 | { 90 | "key": "platform_admin_password", 91 | "value": "password", 92 | "type": "default", 93 | "enabled": true 94 | }, 95 | { 96 | "key": "", 97 | "value": "", 98 | "type": "default", 99 | "enabled": false 100 | }, 101 | { 102 | "key": "back_office_user_username", 103 | "value": "back_office_user", 104 | "type": "default", 105 | "enabled": true 106 | }, 107 | { 108 | "key": "back_office_admin_username", 109 | "value": "back_office_admin", 110 | "type": "default", 111 | "enabled": true 112 | }, 113 | { 114 | "key": "back_office_user_password", 115 | "value": "password", 116 | "type": "default", 117 | "enabled": true 118 | }, 119 | { 120 | "key": "back_office_admin_password", 121 | "value": "password", 122 | "type": "default", 123 | "enabled": true 124 | }, 125 | { 126 | "key": "", 127 | "value": "access_token", 128 | "type": "default", 129 | "enabled": false 130 | }, 131 | { 132 | "key": "management_user_username", 133 | "value": "management_user", 134 | "type": "default", 135 | "enabled": true 136 | }, 137 | { 138 | "key": "management_admin_username", 139 | "value": "management_admin", 140 | "type": "default", 141 | "enabled": true 142 | }, 143 | { 144 | "key": "management_user_password", 145 | "value": "password", 146 | "type": "default", 147 | "enabled": true 148 | }, 149 | { 150 | "key": "management_admin_password", 151 | "value": "password", 152 | "type": "default", 153 | "enabled": true 154 | }, 155 | { 156 | "key": "", 157 | "value": "", 158 | "type": "default", 159 | "enabled": false 160 | }, 161 | { 162 | "key": "access_token_platform_user", 163 | "value": "", 164 | "type": "default", 165 | "enabled": true 166 | }, 167 | { 168 | "key": "access_token_platform_admin", 169 | "value": "", 170 | "type": "default", 171 | "enabled": true 172 | }, 173 | { 174 | "key": "access_token_back_office_user", 175 | "value": "", 176 | "type": "default", 177 | "enabled": true 178 | }, 179 | { 180 | "key": "access_token_back_office_admin", 181 | "value": "", 182 | "type": "default", 183 | "enabled": true 184 | }, 185 | { 186 | "key": "access_token_management_user", 187 | "value": "", 188 | "type": "default", 189 | "enabled": true 190 | }, 191 | { 192 | "key": "access_token_management_admin", 193 | "value": "", 194 | "type": "default", 195 | "enabled": true 196 | }, 197 | { 198 | "key": "", 199 | "value": "", 200 | "type": "default", 201 | "enabled": false 202 | }, 203 | { 204 | "key": "entity_id", 205 | "value": "", 206 | "type": "default", 207 | "enabled": true 208 | }, 209 | { 210 | "key": "", 211 | "value": "", 212 | "type": "default", 213 | "enabled": false 214 | } 215 | ], 216 | "_postman_variable_scope": "environment", 217 | "_postman_exported_at": "2023-11-23T12:39:58.100Z", 218 | "_postman_exported_using": "Postman/10.20.3" 219 | } -------------------------------------------------------------------------------- /scripts/configs/rabbitmq-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "rabbit_version": "3.12.4", 3 | "rabbitmq_version": "3.12.4", 4 | "product_name": "RabbitMQ", 5 | "product_version": "3.12.4", 6 | "users": [ 7 | { 8 | "name": "user", 9 | "password_hash": "uRpW/Oo8IwVY9z/V2i48TfsRXlrkrDM8yi+gdte4m49Snm+A", 10 | "hashing_algorithm": "rabbit_password_hashing_sha256", 11 | "tags": [ 12 | "administrator" 13 | ], 14 | "limits": {} 15 | } 16 | ], 17 | "vhosts": [ 18 | { 19 | "name": "/" 20 | } 21 | ], 22 | "permissions": [ 23 | { 24 | "user": "user", 25 | "vhost": "/", 26 | "configure": ".*", 27 | "write": ".*", 28 | "read": ".*" 29 | } 30 | ], 31 | "topic_permissions": [], 32 | "parameters": [], 33 | "global_parameters": [ 34 | { 35 | "name": "internal_cluster_id", 36 | "value": "rabbitmq-cluster-id-7RK4AQm26a2tDE2NFh2zdw" 37 | } 38 | ], 39 | "policies": [], 40 | "queues": [ 41 | { 42 | "name": "event", 43 | "vhost": "/", 44 | "durable": true, 45 | "auto_delete": false, 46 | "arguments": { 47 | "x-max-length": 100000, 48 | "x-overflow": "reject-publish", 49 | "x-queue-type": "quorum" 50 | } 51 | }, 52 | { 53 | "name": "webhook", 54 | "vhost": "/", 55 | "durable": true, 56 | "auto_delete": false, 57 | "arguments": { 58 | "x-max-length": 100000, 59 | "x-overflow": "reject-publish", 60 | "x-queue-type": "quorum" 61 | } 62 | } 63 | ], 64 | "exchanges": [ 65 | { 66 | "name": "outbound", 67 | "vhost": "/", 68 | "type": "x-delayed-message", 69 | "durable": true, 70 | "auto_delete": false, 71 | "internal": false, 72 | "arguments": { 73 | "x-delayed-type": "direct" 74 | } 75 | }, 76 | { 77 | "name": "inbound", 78 | "vhost": "/", 79 | "type": "direct", 80 | "durable": true, 81 | "auto_delete": false, 82 | "internal": false, 83 | "arguments": {} 84 | } 85 | ], 86 | "bindings": [ 87 | { 88 | "source": "inbound", 89 | "vhost": "/", 90 | "destination": "event", 91 | "destination_type": "queue", 92 | "routing_key": "to_inbound_event", 93 | "arguments": {} 94 | }, 95 | { 96 | "source": "outbound", 97 | "vhost": "/", 98 | "destination": "webhook", 99 | "destination_type": "queue", 100 | "routing_key": "to_outbound_webhook", 101 | "arguments": {} 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /scripts/create_hotfix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # exit on first error (used for return) 3 | 4 | message() { 5 | echo -e "\n######################################################################" 6 | echo "# $1" 7 | echo "######################################################################" 8 | } 9 | 10 | getHotfixReleaseVersion() { 11 | # 1. Create array based on LATEST_TAG 12 | LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") # gets tags across all branches, not just the current branch 13 | TAG_LIST=($(echo "$LATEST_TAG" | tr '.' ' ')) 14 | 15 | # 2. Exit if invalid version 16 | [[ "${#TAG_LIST[@]}" -ne 2 ]] && echo "$LATEST_TAG is not a valid version" && exit 1 17 | 18 | # 3. Calculate release version 19 | V_MINOR=${TAG_LIST[0]} 20 | V_PATCH=$(( TAG_LIST[1] + 1 )) 21 | RELEASE_VERSION=${V_MINOR}.${V_PATCH} 22 | } 23 | 24 | message ">>> Starting hotfix" 25 | 26 | [[ ! -x "$(command -v gh)" ]] && echo "gh not found, you need to install github CLI" && exit 1 27 | 28 | gh auth status 29 | 30 | # 1. Make sure branch is set to main 31 | [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]] && echo "ERROR: Checkout to main" && exit 1 32 | 33 | # 2. Make sure branch is clean 34 | [[ $(git status --porcelain) ]] && echo "ERROR: The branch is not clean, commit your changes before creating the release" && exit 1 35 | 36 | message ">>> Pulling main" 37 | git pull origin main 38 | message ">>> Pulling tags" 39 | git fetch --prune --tags 40 | 41 | getHotfixReleaseVersion 42 | 43 | message ">>> Hotfix: $RELEASE_VERSION" 44 | 45 | # 3. Start hotfix 46 | read -r -p "What is the name of the branch you want to create (should start with hotfix/): " BRANCH_NAME 47 | [[ $BRANCH_NAME != hotfix/* ]] && echo "'$BRANCH_NAME' is invalid, it should start with 'hotfix/')" && exit 1 48 | 49 | read -r -p "Are you sure you want to create the branch '$BRANCH_NAME' [Y/n]: " RESPONSE 50 | if [[ $RESPONSE =~ ^([yY][eE][sS]|[yY])$ ]]; then 51 | 52 | message ">>>>> Creating branch '$BRANCH_NAME' from main..." 53 | git checkout -b "$BRANCH_NAME" main 54 | git commit --allow-empty -m "Hotfix - $RELEASE_VERSION" 55 | git push origin "$BRANCH_NAME" 56 | gh pr create --base main --head "$BRANCH_NAME" --title "Hotfix - $RELEASE_VERSION" --fill 57 | 58 | else 59 | 60 | message "Action cancelled exiting..." 61 | exit 1 62 | 63 | fi 64 | -------------------------------------------------------------------------------- /scripts/create_merge_failed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # exit on first error (used for return 3 | 4 | message() { 5 | echo -e "\n######################################################################" 6 | echo "# $1" 7 | echo "######################################################################" 8 | } 9 | 10 | LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") # gets tags across all branches, not just the current branch 11 | 12 | message ">>> Starting PR" 13 | 14 | [[ ! -x "$(command -v gh)" ]] && echo "gh not found, you need to install github CLI" && exit 1 15 | 16 | gh auth status 17 | 18 | # 1. Make sure branch is set to develop 19 | [[ $(git rev-parse --abbrev-ref HEAD) != "develop" ]] && echo "ERROR: Checkout to develop" && exit 1 20 | 21 | # 2. Make sure branch is clean 22 | [[ $(git status --porcelain) ]] && echo "ERROR: The branch is not clean, commit your changes before creating the release" && exit 1 23 | 24 | message ">>> Pulling develop" 25 | git pull origin develop 26 | 27 | BRANCH_NAME="merge/$LATEST_TAG" 28 | 29 | git checkout -b $BRANCH_NAME develop 30 | git pull origin main 31 | git push origin "$BRANCH_NAME" 32 | 33 | gh pr create --base develop --head "$BRANCH_NAME" --title "Merge into develop $LATEST_TAG" --fill 34 | 35 | -------------------------------------------------------------------------------- /scripts/create_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # exit on first error (used for return 3 | 4 | message() { 5 | echo -e "\n######################################################################" 6 | echo "# $1" 7 | echo "######################################################################" 8 | } 9 | 10 | getReleaseVersion() { 11 | # 1. Create array based on LATEST_TAG 12 | LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") # gets tags across all branches, not just the current branch 13 | TAG_LIST=($(echo "$LATEST_TAG" | tr '.' ' ')) 14 | 15 | # 2. Exit if invalid version 16 | [[ "${#TAG_LIST[@]}" -ne 2 ]] && echo "$LATEST_TAG is not a valid version" && exit 1 17 | 18 | # 3. Calculate release version 19 | V_MINOR=$(( TAG_LIST[0] + 1 )) 20 | V_PATCH=0 21 | RELEASE_VERSION=${V_MINOR}.${V_PATCH} 22 | } 23 | 24 | message ">>> Starting release" 25 | 26 | [[ ! -x "$(command -v gh)" ]] && echo "gh not found, you need to install github CLI" && exit 1 27 | 28 | gh auth status 29 | 30 | # 1. Make sure branch is set to develop 31 | [[ $(git rev-parse --abbrev-ref HEAD) != "develop" ]] && echo "ERROR: Checkout to develop" && exit 1 32 | 33 | # 2. Make sure branch is clean 34 | [[ $(git status --porcelain) ]] && echo "ERROR: The branch is not clean, commit your changes before creating the release" && exit 1 35 | 36 | message ">>> Pulling develop" 37 | git pull origin develop ## 38 | message ">>> Pulling tags" 39 | git fetch --prune --prune-tags origin 40 | 41 | getReleaseVersion 42 | 43 | message ">>> Release: $RELEASE_VERSION" 44 | 45 | # 5. Start release 46 | read -r -p "Last release version was '$LATEST_TAG', do you want to create '$RELEASE_VERSION' [Y/n]: " RESPONSE 47 | if [[ $RESPONSE =~ ^([yY][eE][sS]|[yY])$ ]]; then 48 | 49 | BRANCH_NAME="release/$RELEASE_VERSION" 50 | message ">>>>> Creating branch '$BRANCH_NAME' from develop..." 51 | 52 | git checkout -b "$BRANCH_NAME" develop 53 | git push origin "$BRANCH_NAME" 54 | gh pr create --base main --head "$BRANCH_NAME" --title "Release - $RELEASE_VERSION" --fill 55 | 56 | else 57 | 58 | message "Action cancelled exiting..." 59 | exit 1 60 | 61 | fi 62 | 63 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/ApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import java.util.TimeZone; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 9 | import org.springframework.boot.web.servlet.ServletComponentScan; 10 | import org.springframework.cache.annotation.EnableCaching; 11 | import org.springframework.scheduling.annotation.EnableAsync; 12 | 13 | @Slf4j 14 | @EnableAsync 15 | @EnableCaching 16 | @ConfigurationPropertiesScan 17 | @ServletComponentScan 18 | @SpringBootApplication 19 | public class ApiApplication { 20 | 21 | // static { 22 | // BlockHound.install(); 23 | // } 24 | 25 | public static void main(final String[] args) { 26 | 27 | final Runtime r = Runtime.getRuntime(); 28 | 29 | log.info("[APP] Active processors: {}", r.availableProcessors()); 30 | log.info("[APP] Total memory: {}", r.totalMemory()); 31 | log.info("[APP] Free memory: {}", r.freeMemory()); 32 | log.info("[APP] Max memory: {}", r.maxMemory()); 33 | 34 | SpringApplication.run(ApiApplication.class, args); 35 | } 36 | 37 | @PostConstruct 38 | void started() { 39 | TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo")); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/clients/http/DefaultRestTemplate.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.clients.http; 2 | 3 | import org.springframework.boot.web.client.RestTemplateBuilder; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | @Configuration(proxyBeanMethods = false) 9 | public class DefaultRestTemplate { 10 | 11 | @Bean 12 | public RestTemplate restTemplate(final RestTemplateBuilder builder) { 13 | return builder.build(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/clients/http/WebhookSiteHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.clients.http; 2 | 3 | import static com.mycompany.microservice.api.clients.http.WebhookSiteUrlEnum.POST; 4 | import static com.mycompany.microservice.api.utils.WebClientUtils.getErrorMessage; 5 | 6 | import com.mycompany.microservice.api.utils.WebClientUtils; 7 | import io.github.resilience4j.circuitbreaker.CircuitBreaker; 8 | import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; 9 | import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.SlidingWindowType; 10 | import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; 11 | import java.time.Duration; 12 | import lombok.Getter; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.reactive.function.client.WebClient; 17 | import reactor.core.publisher.Mono; 18 | 19 | @Slf4j 20 | @Getter 21 | @Component 22 | public class WebhookSiteHttpClient { 23 | 24 | private static final String NAME = "webhookSite"; 25 | 26 | private final WebClient webClient; 27 | private final CircuitBreaker defaultCircuitBreaker; 28 | 29 | public WebhookSiteHttpClient( 30 | @Value("${http.clients.webhook-site.base-url}") final String baseUrl, 31 | @Value("${http.clients.default-timeout}") final Integer timeOutInMs, 32 | // Builder Bean is needed for Spring boot to autoconfigure tracing in HttpClient 33 | final WebClient.Builder builder) { 34 | this.webClient = WebClientUtils.createWebClient(builder, baseUrl, timeOutInMs, NAME); 35 | 36 | https: // resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker 37 | this.defaultCircuitBreaker = 38 | CircuitBreaker.of( 39 | NAME, 40 | CircuitBreakerConfig.custom() 41 | .slidingWindowSize(10) 42 | .slidingWindowType(SlidingWindowType.COUNT_BASED) 43 | // api is offline 44 | .failureRateThreshold(70.0f) 45 | // api is slow 46 | .slowCallDurationThreshold(Duration.ofSeconds(2)) 47 | .slowCallRateThreshold(70.0f) 48 | // wait for 10s 49 | .waitDurationInOpenState(Duration.ofSeconds(10)) 50 | // verify threshold again 51 | .permittedNumberOfCallsInHalfOpenState(10) 52 | .build()); 53 | } 54 | 55 | public Mono post(final Object request) { 56 | log.info("HTTP[{}] request {}", NAME, request); 57 | return this.getWebClient() 58 | .post() 59 | .uri(POST.getUrl()) 60 | .bodyValue(request) 61 | .exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class)) 62 | // Handle network exception (Timeout, SslClosedEngine, PrematureClose etc.) 63 | .onErrorResume(this::defaultErrorHandler); 64 | } 65 | 66 | public Mono postWithCircuitBreaker(final Object request) { 67 | log.info("HTTP[{}] requestCb {}", NAME, request); 68 | return this.getWebClient() 69 | .post() 70 | .uri(POST.getUrl()) 71 | .bodyValue(request) 72 | .retrieve() 73 | .bodyToMono(String.class) 74 | // .exchangeToMono(this::defaultResponseHandler) 75 | .transformDeferred(CircuitBreakerOperator.of(this.defaultCircuitBreaker)); 76 | } 77 | 78 | // private Mono defaultResponseHandler(final ClientResponse response) { 79 | // final HttpStatusCode status = response.statusCode(); 80 | // 81 | // return response 82 | // .bodyToMono(String.class) 83 | // .defaultIfEmpty(StringUtils.EMPTY) 84 | // .map( 85 | // body -> { 86 | // if (status.isError()) { 87 | // throw new RuntimeException( 88 | // format("HTTP[%s] errorResponse '%s' '%s'", NAME, status.value(), body)); 89 | // } 90 | // return body; 91 | // }); 92 | // } 93 | 94 | private Mono defaultErrorHandler(final Throwable ex) { 95 | log.warn("HTTP[{}}] errorInternal '{}'", NAME, getErrorMessage(ex), ex); 96 | return Mono.empty(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/clients/http/WebhookSiteUrlEnum.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.clients.http; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 9 | public enum WebhookSiteUrlEnum { 10 | POST("/"); 11 | private final String url; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/clients/slack/BaseSlackClient.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.clients.slack; 2 | 3 | import static java.lang.String.format; 4 | import static org.apache.commons.lang3.StringUtils.isNotBlank; 5 | 6 | import com.slack.api.Slack; 7 | import com.slack.api.webhook.WebhookResponse; 8 | import java.time.OffsetDateTime; 9 | import java.time.ZoneId; 10 | import java.time.format.DateTimeFormatter; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.apache.commons.text.StringEscapeUtils; 14 | import org.springframework.scheduling.annotation.Async; 15 | 16 | @Slf4j 17 | public abstract class BaseSlackClient { 18 | 19 | public abstract String getEnv(); 20 | 21 | public abstract String getUrl(); 22 | 23 | public abstract String getChannel(); 24 | 25 | @Async 26 | public void notify(final String message) { 27 | try { 28 | 29 | final WebhookResponse response = 30 | Slack.getInstance().send(this.getUrl(), this.buildBodyMessage(message)); 31 | 32 | if (response.getCode() != 200) { 33 | log.warn("[SLACK][ERROR] status[{}] body[{}]", response.getCode(), response.getBody()); 34 | 35 | } else { 36 | log.info("[SLACK] error message send with success"); 37 | } 38 | 39 | } catch (final Exception ex) { 40 | log.error("[SLACK][ERROR] An error has occurred when sending error message", ex); 41 | } 42 | } 43 | 44 | private String buildBodyMessage(final String message) { 45 | final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); 46 | final String timestamp = 47 | formatter.format(OffsetDateTime.now().atZoneSameInstant(ZoneId.of("America/Sao_Paulo"))); 48 | 49 | return format( 50 | """ 51 | { 52 | "channel": "#%s", 53 | "text": "*error*", 54 | "blocks": [ 55 | { 56 | "type": "section", 57 | "fields": [ 58 | { 59 | "type": "mrkdwn", 60 | "text": "*Team:*\\n API" 61 | }, 62 | { 63 | "type": "mrkdwn", 64 | "text": "*When:*\\n%s" 65 | }, 66 | { 67 | "type": "mrkdwn", 68 | "text": "* Env:*\\n%s" 69 | }, 70 | { 71 | "type": "mrkdwn", 72 | "text": "*Reason:*\\n%s." 73 | } 74 | ] 75 | }, 76 | %s 77 | ] 78 | } 79 | """, 80 | this.getChannel(), 81 | timestamp, 82 | this.getEnv(), 83 | StringEscapeUtils.escapeJson(message), 84 | this.buildTraceAndLogButtons()); 85 | } 86 | 87 | private String buildTraceAndLogButtons() { 88 | // Link to your logs system filtered by traceId 89 | final String logUrl = "http://example.com"; 90 | // Link to your trace system filtered by traceId 91 | final String traceUrl = "http://example.com"; 92 | 93 | return isNotBlank(logUrl) 94 | ? format( 95 | """ 96 | { 97 | "type": "actions", 98 | "elements": [ 99 | { 100 | "type": "button", 101 | "url": "%s", 102 | "style": "primary", 103 | "text": { 104 | "type": "plain_text", 105 | "text": "Logs", 106 | "emoji": true 107 | }, 108 | "value": "btn-logs", 109 | "action_id": "logUrl" 110 | }, 111 | { 112 | "type": "button", 113 | "url": "%s", 114 | "text": { 115 | "type": "plain_text", 116 | "text": "Traces", 117 | "emoji": true 118 | }, 119 | "value": "btn-trace", 120 | "action_id": "traceUrl" 121 | } 122 | ] 123 | } 124 | """, 125 | logUrl, traceUrl) 126 | : StringUtils.EMPTY; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/clients/slack/SlackAlertClient.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.clients.slack; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Slf4j 9 | @Getter 10 | @Component 11 | public class SlackAlertClient extends BaseSlackClient { 12 | 13 | private final String url; 14 | private final String channel; 15 | private final String env; 16 | 17 | public SlackAlertClient( 18 | @Value("${slack.env}") final String env, 19 | @Value("${slack.channels.api-alert.url}") final String url, 20 | @Value("${slack.channels.api-alert.name}") final String channel) { 21 | this.env = env; 22 | this.url = url; 23 | this.channel = channel; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/constants/AppCompanySlug.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.constants; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class AppCompanySlug { 7 | public static final String INTERNAL = "internal-company"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/constants/AppConstants.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.constants; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public final class AppConstants { 7 | public static final String API_DEFAULT_ERROR_MESSAGE = 8 | "Something went wrong. Please try again later or enter in contact with our service."; 9 | public static final String API_DEFAULT_REQUEST_FAILED_MESSAGE = "Request failed."; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/constants/AppHeaders.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.constants; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class AppHeaders { 7 | public static final String RESPONSE_TIME_HEADER = "X-Response-Time"; 8 | public static final String API_KEY_HEADER = "Api-Key"; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/constants/AppUrls.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.constants; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class AppUrls { 7 | 8 | public static final String PLATFORM = "/platform"; 9 | public static final String PLATFORM_API = PLATFORM + "/api"; 10 | public static final String PLATFORM_MOBILE = PLATFORM + "/mobile"; 11 | public static final String PLATFORM_WEB = PLATFORM + "/web"; 12 | 13 | public static final String MANAGEMENT = "/management"; 14 | public static final String INTERNAL = "/internal"; 15 | 16 | public static final String BACK_OFFICE = "/back-office"; 17 | 18 | public static final String PUBLIC = "/public"; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/constants/JWTClaims.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.constants; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class JWTClaims { 7 | public static final String CLAIM_COMPANY_SLUG = "company_slug"; 8 | public static final String CLAIM_EMAIL = "email"; 9 | public static final String CLAIM_REALM_ACCESS = "realm_access"; 10 | public static final String CLAIM_ROLES = "roles"; 11 | public static final String CLAIM_ISSUER = "iss"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/backoffice/BackOfficeController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.backoffice; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @Slf4j 13 | @RestController 14 | @RequestMapping(BackOfficeController.BASE_URL) 15 | @RequiredArgsConstructor 16 | public class BackOfficeController { 17 | public static final String BASE_URL = AppUrls.BACK_OFFICE; 18 | 19 | @GetMapping("/hello-world") 20 | @ResponseStatus(HttpStatus.OK) 21 | public String helloWorld() { 22 | return "Hello world"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/internal/CacheInternalApiController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.internal; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import com.mycompany.microservice.api.services.LocalCacheManagerService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.DeleteMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @Slf4j 14 | @RestController 15 | @RequestMapping(CacheInternalApiController.BASE_URL) 16 | @RequiredArgsConstructor 17 | public class CacheInternalApiController { 18 | 19 | public static final String BASE_URL = AppUrls.INTERNAL + "/caches"; 20 | 21 | private final LocalCacheManagerService localCacheManagerService; 22 | 23 | @DeleteMapping 24 | public ResponseEntity clearCaches() { 25 | log.info("[request] clearing all local caches"); 26 | this.localCacheManagerService.evictAll(); 27 | return ResponseEntity.ok().build(); 28 | } 29 | 30 | @DeleteMapping("/{cache-name}") 31 | public ResponseEntity clearCaches(@PathVariable("cache-name") final String cacheName) { 32 | log.info("[request] clearing local cache '{}'", cacheName); 33 | this.localCacheManagerService.evictByName(cacheName); 34 | return ResponseEntity.ok().build(); 35 | } 36 | 37 | @DeleteMapping("/kubernetes") 38 | public ResponseEntity evictAllCacheFromKubernetesPods() { 39 | log.info("[request] evicting all cache from kubernetes pods"); 40 | this.localCacheManagerService.evictCacheInAllKubernetesInstances(); 41 | return ResponseEntity.ok().build(); 42 | } 43 | 44 | @DeleteMapping("/kubernetes/{cache-name}") 45 | public ResponseEntity evictAllCacheFromKubernetesPods( 46 | @PathVariable("cache-name") final String cacheName) { 47 | log.info("[request] evicting cache {} from kubernetes pods", cacheName); 48 | this.localCacheManagerService.evictCacheInAllKubernetesInstances(cacheName); 49 | return ResponseEntity.ok().build(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/internal/actuator/WebMvcPreStopHookEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.internal.actuator; 2 | 3 | import static org.springframework.http.HttpStatus.OK; 4 | 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | 13 | /** 14 | * WebMvcPreStopHookEndpoint: 15 | * 16 | *

This API is used to create a preStop hook for kubernetes (kubelet) to await a certain 17 | * delayInMillis before sending the SIGTERM signal. It allows 0 downtime deployment. 18 | */ 19 | @Slf4j 20 | @Component 21 | @ControllerEndpoint(id = "preStopHook") 22 | class WebMvcPreStopHookEndpoint { 23 | 24 | @ResponseStatus(OK) 25 | @GetMapping("/{delayInMillis}") 26 | public ResponseEntity preStopHook(@PathVariable("delayInMillis") final long delayInMillis) 27 | throws InterruptedException { 28 | log.info("[preStopHook] received signal to sleep for {}ms", delayInMillis); 29 | Thread.sleep(delayInMillis); 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/management/ApikeyManagementController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.management; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import com.mycompany.microservice.api.controllers.management.base.BaseManagementController; 5 | import com.mycompany.microservice.api.entities.ApiKey; 6 | import com.mycompany.microservice.api.mappers.ApiKeyMapper; 7 | import com.mycompany.microservice.api.requests.management.CreateApiKeyManagementRequest; 8 | import com.mycompany.microservice.api.requests.management.UpdateApiKeyManagementRequest; 9 | import com.mycompany.microservice.api.responses.management.ApikeyManagementResponse; 10 | import com.mycompany.microservice.api.services.ApiKeyService; 11 | import lombok.Getter; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.web.bind.annotation.DeleteMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.ResponseStatus; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | @Slf4j 22 | @RestController 23 | @RequestMapping(ApikeyManagementController.BASE_URL) 24 | @RequiredArgsConstructor 25 | public class ApikeyManagementController 26 | extends BaseManagementController< 27 | ApiKey, 28 | CreateApiKeyManagementRequest, 29 | UpdateApiKeyManagementRequest, 30 | ApikeyManagementResponse> { 31 | public static final String BASE_URL = AppUrls.MANAGEMENT + "/api-keys"; 32 | 33 | @Getter private final ApiKeyService service; 34 | @Getter private final ApiKeyMapper mapper; 35 | 36 | @ResponseStatus(HttpStatus.NO_CONTENT) 37 | @DeleteMapping("/{id}") 38 | @Override 39 | public void delete(@PathVariable("id") final Long id) { 40 | log.info("[request] inactive {} '{}'", ApiKey.TABLE_NAME, id); 41 | this.service.inactivate(id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/management/CompanyManagementController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.management; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import com.mycompany.microservice.api.controllers.management.base.BaseManagementController; 5 | import com.mycompany.microservice.api.entities.Company; 6 | import com.mycompany.microservice.api.mappers.CompanyMapper; 7 | import com.mycompany.microservice.api.requests.management.CreateCompanyManagementRequest; 8 | import com.mycompany.microservice.api.requests.management.UpdateCompanyManagementRequest; 9 | import com.mycompany.microservice.api.responses.management.CompanyManagementResponse; 10 | import com.mycompany.microservice.api.services.CompanyService; 11 | import lombok.Getter; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | @Slf4j 17 | @RestController 18 | @RequestMapping(CompanyManagementController.BASE_URL) 19 | @RequiredArgsConstructor 20 | public class CompanyManagementController 21 | extends BaseManagementController< 22 | Company, 23 | CreateCompanyManagementRequest, 24 | UpdateCompanyManagementRequest, 25 | CompanyManagementResponse> { 26 | 27 | public static final String BASE_URL = AppUrls.MANAGEMENT + "/companies"; 28 | 29 | @Getter private final CompanyService service; 30 | @Getter private final CompanyMapper mapper; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/management/base/BaseManagementController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.management.base; 2 | 3 | import com.mycompany.microservice.api.entities.base.BaseEntity; 4 | import com.mycompany.microservice.api.mappers.base.ManagementBaseMapper; 5 | import com.mycompany.microservice.api.responses.shared.ApiListPaginationSuccess; 6 | import com.mycompany.microservice.api.services.base.BaseService; 7 | import jakarta.persistence.Table; 8 | import jakarta.validation.Valid; 9 | import java.lang.reflect.ParameterizedType; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.Pageable; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | /** 17 | * The type Base management controller. 18 | * 19 | * @param the type parameter Entity 20 | * @param the type parameter CreateRequest 21 | * @param the type parameter UpdateRequest 22 | * @param the type parameter Response 23 | */ 24 | @Slf4j 25 | public abstract class BaseManagementController { 26 | 27 | public abstract ManagementBaseMapper getMapper(); 28 | 29 | public abstract BaseService getService(); 30 | 31 | @ResponseStatus(HttpStatus.OK) 32 | @GetMapping("/{id}") 33 | public R findById(@PathVariable("id") final Long id) { 34 | log.debug("[request] retrieve {} with id {}", this.getName(), id); 35 | final E entity = this.getService().findById(id); 36 | return this.getMapper().toManagementResponse(entity); 37 | } 38 | 39 | @ResponseStatus(HttpStatus.OK) 40 | @GetMapping 41 | public ApiListPaginationSuccess findAll(final Pageable pageable) { 42 | log.debug("[request] retrieve all {}", this.getName()); 43 | final Page entities = this.getService().findAll(pageable); 44 | final Page response = entities.map(this.getMapper()::toManagementResponse); 45 | return ApiListPaginationSuccess.of(response); 46 | } 47 | 48 | @ResponseStatus(HttpStatus.CREATED) 49 | @PostMapping 50 | public R create(@Valid @RequestBody final C request) { 51 | log.info("[request] create {}", request); 52 | final E entity = this.getService().create(this.getMapper().toEntity(request)); 53 | return this.getMapper().toManagementResponse(entity); 54 | } 55 | 56 | @ResponseStatus(HttpStatus.OK) 57 | @PutMapping("/{id}") 58 | public R update(@PathVariable("id") final Long id, @Valid @RequestBody final U request) { 59 | log.info("[request] update '{}' {}", id, request); 60 | 61 | final E original = this.getService().findById(id); 62 | final E merged = this.getMapper().update(request, original); 63 | final E entity = this.getService().update(merged); 64 | 65 | return this.getMapper().toManagementResponse(entity); 66 | } 67 | 68 | @ResponseStatus(HttpStatus.OK) 69 | @PatchMapping("/{id}") 70 | public R patch(@PathVariable("id") final Long id, @RequestBody final U request) { 71 | log.info("[request] patch '{}' {}", id, request); 72 | 73 | final E original = this.getService().findById(id); 74 | final E merged = this.getMapper().patch(request, original); 75 | final E entity = this.getService().update(merged); 76 | 77 | return this.getMapper().toManagementResponse(entity); 78 | } 79 | 80 | @ResponseStatus(HttpStatus.NO_CONTENT) 81 | @DeleteMapping("/{id}") 82 | public void delete(@PathVariable("id") final Long id) { 83 | log.info("[request] delete {} with id {}", this.getName(), id); 84 | this.getService().delete(id); 85 | } 86 | 87 | private String getName() { 88 | final Class entityModelClass = 89 | (Class) 90 | ((ParameterizedType) this.getClass().getGenericSuperclass()) 91 | .getActualTypeArguments()[0]; 92 | final Table annotation = entityModelClass.getAnnotation(Table.class); 93 | return annotation.name(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/platform/api/PlatformApiController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.platform.api; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @Slf4j 13 | @RestController 14 | @RequestMapping(PlatformApiController.BASE_URL) 15 | @RequiredArgsConstructor 16 | public class PlatformApiController { 17 | public static final String BASE_URL = AppUrls.PLATFORM_API; 18 | 19 | @GetMapping("/hello-world") 20 | @ResponseStatus(HttpStatus.OK) 21 | public String helloWorld() { 22 | return "Hello world"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/platform/mobile/PlatformMobileController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.platform.mobile; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @Slf4j 13 | @RestController 14 | @RequestMapping(PlatformMobileController.BASE_URL) 15 | @RequiredArgsConstructor 16 | public class PlatformMobileController { 17 | public static final String BASE_URL = AppUrls.PLATFORM_MOBILE; 18 | 19 | @GetMapping("/hello-world") 20 | @ResponseStatus(HttpStatus.OK) 21 | public String helloWorld() { 22 | return "Hello world"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/platform/web/PlatformWebController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.platform.web; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @Slf4j 13 | @RestController 14 | @RequestMapping(PlatformWebController.BASE_URL) 15 | @RequiredArgsConstructor 16 | public class PlatformWebController { 17 | public static final String BASE_URL = AppUrls.PLATFORM_WEB; 18 | 19 | @GetMapping("/hello-world") 20 | @ResponseStatus(HttpStatus.OK) 21 | public String helloWorld() { 22 | return "Hello world"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/controllers/pubic/PublicController.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.pubic; 2 | 3 | import com.mycompany.microservice.api.constants.AppUrls; 4 | import com.mycompany.microservice.api.rabbitmq.publishers.EventPublisher; 5 | import com.mycompany.microservice.api.services.WebhookSiteService; 6 | import java.util.Map; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.ResponseStatus; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import reactor.core.publisher.Mono; 16 | 17 | @Slf4j 18 | @RestController 19 | @RequestMapping(PublicController.BASE_URL) 20 | @RequiredArgsConstructor 21 | public class PublicController { 22 | public static final String BASE_URL = AppUrls.PUBLIC; 23 | 24 | private final EventPublisher eventPublisher; 25 | private final WebhookSiteService webhookSiteService; 26 | 27 | @Value("${rabbitmq.publishers.webhook.exchange}") 28 | private String exchange; 29 | 30 | @Value("${rabbitmq.publishers.webhook.routingkey}") 31 | private String routingKey; 32 | 33 | @GetMapping("/hello-world") 34 | @ResponseStatus(HttpStatus.OK) 35 | public String helloWorld() { 36 | return "Hello world"; 37 | } 38 | 39 | @GetMapping("/publish") 40 | @ResponseStatus(HttpStatus.OK) 41 | public String publish() { 42 | this.eventPublisher.publish(this.exchange, this.routingKey, Map.of("test", "test")); 43 | return "published"; 44 | } 45 | 46 | @GetMapping("/call-external-api") 47 | @ResponseStatus(HttpStatus.OK) 48 | public Mono callExternalAPI() { 49 | return this.webhookSiteService.post(Map.of()); 50 | } 51 | 52 | @GetMapping("/call-external-api-with-cb") 53 | @ResponseStatus(HttpStatus.OK) 54 | public Mono callExternalAPIWithCircuitBreaker() { 55 | return this.webhookSiteService.postWithCircuitBreaker(Map.of()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/entities/ApiKey.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.entities; 2 | 3 | import static com.mycompany.microservice.api.entities.ApiKey.TABLE_NAME; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.mycompany.microservice.api.entities.base.BaseEntity; 7 | import io.hypersistence.utils.hibernate.id.BatchSequenceGenerator; 8 | import jakarta.persistence.Column; 9 | import jakarta.persistence.Entity; 10 | import jakarta.persistence.EntityListeners; 11 | import jakarta.persistence.GeneratedValue; 12 | import jakarta.persistence.GenerationType; 13 | import jakarta.persistence.Id; 14 | import jakarta.persistence.JoinColumn; 15 | import jakarta.persistence.Table; 16 | import java.io.Serial; 17 | import lombok.AllArgsConstructor; 18 | import lombok.Getter; 19 | import lombok.NoArgsConstructor; 20 | import lombok.Setter; 21 | import lombok.experimental.SuperBuilder; 22 | import org.hibernate.annotations.GenericGenerator; 23 | import org.hibernate.annotations.Parameter; 24 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 25 | 26 | @Entity 27 | @EntityListeners(AuditingEntityListener.class) 28 | @Getter 29 | @Setter 30 | @NoArgsConstructor 31 | @AllArgsConstructor 32 | @SuperBuilder 33 | @Table(name = TABLE_NAME, schema = "public") 34 | public class ApiKey extends BaseEntity { 35 | public static final String TABLE_NAME = "api_key"; 36 | 37 | @Serial private static final long serialVersionUID = -3552577854495026179L; 38 | 39 | @Id 40 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = TABLE_NAME) 41 | @GenericGenerator( 42 | name = TABLE_NAME, 43 | type = BatchSequenceGenerator.class, 44 | parameters = { 45 | @Parameter(name = "sequence", value = TABLE_NAME + "_id_seq"), 46 | @Parameter(name = "fetch_size", value = "1") 47 | }) 48 | private Long id; 49 | 50 | @JoinColumn(nullable = false) 51 | private Long companyId; 52 | 53 | @Column(nullable = false) 54 | private String name; 55 | 56 | @JsonIgnore 57 | @Column(unique = true, nullable = false) 58 | private String key; 59 | 60 | @Column(nullable = false) 61 | private Boolean isActive; 62 | 63 | @Override 64 | public Long getId() { 65 | return this.id; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "Apikey{" 71 | + "id=" 72 | + this.id 73 | + ", companyId=" 74 | + this.companyId 75 | + ", name='" 76 | + this.name 77 | + "', isActive=" 78 | + this.isActive 79 | + "', createdBy=" 80 | + this.getCreatedBy() 81 | + ", updatedBy=" 82 | + this.getUpdatedBy() 83 | + "', createdAt=" 84 | + this.getCreatedAt() 85 | + ", updatedAt=" 86 | + this.getUpdatedAt() 87 | + '}'; 88 | } 89 | 90 | @Override 91 | public boolean equals(final Object o) { 92 | if (this == o) { 93 | return true; 94 | } 95 | if (!(o instanceof final ApiKey other)) { 96 | return false; 97 | } 98 | return this.getId() != null && this.getId().equals(other.getId()); 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | return this.getClass().hashCode(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/entities/base/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.entities.base; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.MappedSuperclass; 5 | import java.io.Serial; 6 | import java.io.Serializable; 7 | import java.time.LocalDateTime; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.Setter; 12 | import lombok.experimental.SuperBuilder; 13 | import org.springframework.data.annotation.CreatedBy; 14 | import org.springframework.data.annotation.CreatedDate; 15 | import org.springframework.data.annotation.LastModifiedBy; 16 | import org.springframework.data.annotation.LastModifiedDate; 17 | 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @SuperBuilder 23 | @MappedSuperclass 24 | public abstract class BaseEntity implements Serializable { 25 | 26 | @Serial private static final long serialVersionUID = 7677353645504602647L; 27 | 28 | @CreatedBy @Column private String createdBy; 29 | @LastModifiedBy @Column private String updatedBy; 30 | 31 | @CreatedDate 32 | @Column(nullable = false, updatable = false) 33 | private LocalDateTime createdAt; 34 | 35 | @LastModifiedDate 36 | @Column(nullable = false) 37 | private LocalDateTime updatedAt; 38 | 39 | public abstract Long getId(); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/enums/UserRolesEnum.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.enums; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 9 | public enum UserRolesEnum { 10 | PLATFORM_USER("platform_user"), 11 | PLATFORM_ADMIN("platform_admin"), 12 | PLATFORM_API_USER("platform_api_user"), // Set from company.is_platform 13 | 14 | BACK_OFFICE_USER("back_office_user"), 15 | BACK_OFFICE_ADMIN("back_office_admin"), 16 | 17 | MANAGEMENT_USER("management_user"), 18 | MANAGEMENT_ADMIN("management_admin"), 19 | 20 | INTERNAL_API_USER("internal_api_user"); // Set from company.is_internal 21 | 22 | private final String name; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/exceptions/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.exceptions; 2 | 3 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 4 | 5 | import java.io.Serial; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | @ResponseStatus(code = BAD_REQUEST) 9 | public class BadRequestException extends RootException { 10 | 11 | @Serial private static final long serialVersionUID = 1L; 12 | 13 | public BadRequestException(final String message) { 14 | super(BAD_REQUEST, message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/exceptions/InternalServerErrorException.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.exceptions; 2 | 3 | import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; 4 | 5 | import com.mycompany.microservice.api.constants.AppConstants; 6 | import java.io.Serial; 7 | import org.springframework.web.bind.annotation.ResponseStatus; 8 | 9 | @ResponseStatus(code = INTERNAL_SERVER_ERROR) 10 | public class InternalServerErrorException extends RootException { 11 | 12 | @Serial private static final long serialVersionUID = 694110374288090930L; 13 | 14 | public InternalServerErrorException() { 15 | super(INTERNAL_SERVER_ERROR, AppConstants.API_DEFAULT_ERROR_MESSAGE); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/exceptions/NotAllowedException.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.exceptions; 2 | 3 | import static org.springframework.http.HttpStatus.FORBIDDEN; 4 | 5 | import java.io.Serial; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | @ResponseStatus(code = FORBIDDEN) 9 | public class NotAllowedException extends RootException { 10 | 11 | @Serial private static final long serialVersionUID = 1L; 12 | 13 | public NotAllowedException() { 14 | super(FORBIDDEN); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/exceptions/NotAuthorizedException.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.exceptions; 2 | 3 | import static org.springframework.http.HttpStatus.UNAUTHORIZED; 4 | 5 | import java.io.Serial; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | @ResponseStatus(code = UNAUTHORIZED) 9 | public class NotAuthorizedException extends RootException { 10 | 11 | @Serial private static final long serialVersionUID = -711441617476620028L; 12 | 13 | public NotAuthorizedException() { 14 | super(UNAUTHORIZED); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/exceptions/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.exceptions; 2 | 3 | import static java.lang.String.format; 4 | import static org.springframework.http.HttpStatus.NOT_FOUND; 5 | 6 | import java.io.Serial; 7 | import org.springframework.web.bind.annotation.ResponseStatus; 8 | 9 | @ResponseStatus(code = NOT_FOUND) 10 | public class ResourceNotFoundException extends RootException { 11 | 12 | @Serial private static final long serialVersionUID = 26377136569699646L; 13 | 14 | public ResourceNotFoundException() { 15 | super(NOT_FOUND, "entity not found, please provide a valid id"); 16 | } 17 | 18 | public ResourceNotFoundException(final Long id) { 19 | super(NOT_FOUND, format("entity with id '%s' not found", id)); 20 | } 21 | 22 | public ResourceNotFoundException(final String message) { 23 | super(NOT_FOUND, message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/exceptions/RootException.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.exceptions; 2 | 3 | import com.mycompany.microservice.api.responses.shared.ApiErrorDetails; 4 | import java.io.Serial; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import lombok.Getter; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.lang.NonNull; 10 | 11 | @Getter 12 | public class RootException extends RuntimeException { 13 | 14 | @Serial private static final long serialVersionUID = 6378336966214073013L; 15 | 16 | private final HttpStatus httpStatus; 17 | private final List errors = new ArrayList<>(); 18 | 19 | public RootException(@NonNull final HttpStatus httpStatus) { 20 | super(); 21 | this.httpStatus = httpStatus; 22 | } 23 | 24 | public RootException(@NonNull final HttpStatus httpStatus, final String message) { 25 | super(message); 26 | this.httpStatus = httpStatus; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/facades/AuthFacade.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.facades; 2 | 3 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_COMPANY_SLUG; 4 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_EMAIL; 5 | 6 | import com.mycompany.microservice.api.exceptions.InternalServerErrorException; 7 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthentication; 8 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthentication.ApiKeyDetails; 9 | import java.util.Optional; 10 | import lombok.experimental.UtilityClass; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.oauth2.jwt.Jwt; 16 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 17 | 18 | @Slf4j 19 | @UtilityClass 20 | public class AuthFacade { 21 | 22 | public static String getCompanySlug() { 23 | return getCompanySlugOptional().orElse(StringUtils.EMPTY); 24 | } 25 | 26 | public static String getUserEmail() { 27 | return getUserEmailOptional().orElse(StringUtils.EMPTY); 28 | } 29 | 30 | public static Optional getCompanySlugOptional() { 31 | try { 32 | 33 | final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 34 | 35 | if (isJWT(authentication)) { 36 | return getCompanySlugFromJwt((Jwt) authentication.getPrincipal()); 37 | } else if (isApiKey(authentication)) { 38 | return getCompanySlugFromApikey(authentication); 39 | } 40 | 41 | return Optional.empty(); 42 | 43 | } catch (final Exception ex) { 44 | log.error("error getting company_slug from AuthFacade", ex); 45 | throw new InternalServerErrorException(); 46 | } 47 | } 48 | 49 | public static Optional getUserEmailOptional() { 50 | try { 51 | 52 | final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 53 | 54 | if (isJWT(authentication)) { 55 | final Jwt jwt = (Jwt) authentication.getPrincipal(); 56 | return Optional.ofNullable(jwt.getClaimAsString(CLAIM_EMAIL)); 57 | 58 | } else if (isApiKey(authentication)) { 59 | final ApiKeyAuthentication apiKeyAuthentication = (ApiKeyAuthentication) authentication; 60 | return Optional.ofNullable(apiKeyAuthentication.getApiKeyDetails().getEmail()); 61 | } 62 | 63 | return Optional.empty(); 64 | 65 | } catch (final Exception ex) { 66 | log.error("error getting user_email from AuthFacade", ex); 67 | throw new InternalServerErrorException(); 68 | } 69 | } 70 | 71 | private boolean isJWT(final Authentication authentication) { 72 | return (authentication instanceof Jwt || authentication instanceof JwtAuthenticationToken); 73 | } 74 | 75 | private boolean isApiKey(final Authentication authentication) { 76 | return authentication instanceof ApiKeyAuthentication; 77 | } 78 | 79 | private Optional getCompanySlugFromJwt(final Jwt jwt) { 80 | final Optional companySlugOptional = 81 | Optional.ofNullable(jwt.getClaimAsString(CLAIM_COMPANY_SLUG)); 82 | 83 | if (companySlugOptional.isEmpty()) { 84 | log.warn("user '{}' does not have a company_slug", jwt.getClaimAsString(CLAIM_EMAIL)); 85 | } 86 | 87 | return companySlugOptional; 88 | } 89 | 90 | private Optional getCompanySlugFromApikey(final Authentication authentication) { 91 | 92 | final ApiKeyAuthentication apiKeyAuthentication = (ApiKeyAuthentication) authentication; 93 | final ApiKeyDetails apiKeyDetails = apiKeyAuthentication.getApiKeyDetails(); 94 | 95 | if (StringUtils.isBlank(apiKeyDetails.getCompanySlug())) { 96 | log.warn("api-key '{}' does not have a company_slug", apiKeyDetails.getId()); 97 | } 98 | 99 | return Optional.ofNullable(apiKeyDetails.getCompanySlug()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/advices/ResponseHeaderAdvice.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.advices; 2 | 3 | import com.mycompany.microservice.api.constants.AppHeaders; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.core.MethodParameter; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.converter.HttpMessageConverter; 9 | import org.springframework.http.server.ServerHttpRequest; 10 | import org.springframework.http.server.ServerHttpResponse; 11 | import org.springframework.http.server.ServletServerHttpRequest; 12 | import org.springframework.lang.NonNull; 13 | import org.springframework.web.bind.annotation.ControllerAdvice; 14 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; 15 | 16 | /* 17 | * GeneralControllerAdvice 18 | * 19 | * This response advice is used to inject the api response time 20 | * within response header 21 | * 22 | */ 23 | @Slf4j 24 | @ControllerAdvice 25 | @RequiredArgsConstructor 26 | public class ResponseHeaderAdvice implements ResponseBodyAdvice { 27 | 28 | private static final String TIME = "StopWatch"; 29 | 30 | private static void addResponseTimeHeader( 31 | final ServerHttpResponse response, final ServletServerHttpRequest servletServerRequest) { 32 | 33 | final Long startTime = (Long) servletServerRequest.getServletRequest().getAttribute(TIME); 34 | if (startTime != null) { 35 | response.getHeaders().add(AppHeaders.RESPONSE_TIME_HEADER, fromTimeToString(startTime)); 36 | } 37 | } 38 | 39 | private static String fromTimeToString(final Long startTime) { 40 | final long elapsed = System.nanoTime() - startTime; 41 | final long millis = elapsed / 1_000_000; 42 | return millis > 0 ? millis + " ms" : elapsed + " ns"; 43 | } 44 | 45 | @Override 46 | public boolean supports( 47 | @NonNull final MethodParameter returnType, 48 | @NonNull final Class> converterType) { 49 | return true; 50 | } 51 | 52 | @Override 53 | public Object beforeBodyWrite( 54 | final Object body, 55 | @NonNull final MethodParameter methodParameter, 56 | @NonNull final MediaType mediaType, 57 | @NonNull final Class> aClass, 58 | @NonNull final ServerHttpRequest request, 59 | @NonNull final ServerHttpResponse response) { 60 | 61 | final ServletServerHttpRequest servletServerRequest = (ServletServerHttpRequest) request; 62 | ResponseHeaderAdvice.addResponseTimeHeader(response, servletServerRequest); 63 | 64 | return body; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/auditors/AuditorConfig.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.auditors; 2 | 3 | import com.mycompany.microservice.api.facades.AuthFacade; 4 | import java.util.Optional; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.domain.AuditorAware; 8 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContext; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | 14 | @Configuration(proxyBeanMethods = false) 15 | @EnableJpaAuditing(auditorAwareRef = "auditorProvider") 16 | public class AuditorConfig { 17 | 18 | @Bean 19 | public AuditorAware auditorProvider() { 20 | return new AuditorAwareImpl(); 21 | } 22 | 23 | public static class AuditorAwareImpl implements AuditorAware { 24 | 25 | @Override 26 | public @NonNull Optional getCurrentAuditor() { 27 | 28 | return Optional.ofNullable(SecurityContextHolder.getContext()) 29 | .map(SecurityContext::getAuthentication) 30 | .filter(Authentication::isAuthenticated) 31 | .flatMap(authentication -> AuthFacade.getUserEmailOptional()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/auth/converters/KeycloakJwtConverter.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.auth.converters; 2 | 3 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_EMAIL; 4 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_REALM_ACCESS; 5 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_ROLES; 6 | import static java.lang.String.format; 7 | 8 | import java.util.Collection; 9 | import java.util.Collections; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.springframework.core.convert.converter.Converter; 16 | import org.springframework.lang.NonNull; 17 | import org.springframework.security.authentication.AbstractAuthenticationToken; 18 | import org.springframework.security.core.GrantedAuthority; 19 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 20 | import org.springframework.security.oauth2.jwt.Jwt; 21 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 22 | 23 | @Slf4j 24 | @RequiredArgsConstructor 25 | public class KeycloakJwtConverter implements Converter { 26 | 27 | @Override 28 | public AbstractAuthenticationToken convert(@NonNull final Jwt jwt) { 29 | final Collection authorities = this.extractRealmAccessRoles(jwt); 30 | return new JwtAuthenticationToken(jwt, authorities, this.extractEmail(jwt)); 31 | } 32 | 33 | private Collection extractRealmAccessRoles(final Jwt jwt) { 34 | final Map> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS); 35 | 36 | if (realmAccess == null) { 37 | log.warn( 38 | format("realm_access is null for jwt %s verify realm configuration.", jwt.getClaims())); 39 | return Collections.emptyList(); 40 | } 41 | 42 | final Collection realmAccessRoles = realmAccess.get(CLAIM_ROLES); 43 | 44 | if (realmAccessRoles == null) { 45 | log.warn( 46 | format( 47 | "realm_access.roles is null for jwt %s verify realm configuration.", 48 | jwt.getClaims())); 49 | return Collections.emptyList(); 50 | } 51 | 52 | return realmAccessRoles.stream() 53 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) 54 | .collect(Collectors.toSet()); 55 | } 56 | 57 | private String extractEmail(final Jwt jwt) { 58 | final String email = jwt.getClaimAsString(CLAIM_EMAIL); 59 | return StringUtils.isNotBlank(email) ? email : StringUtils.EMPTY; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/auth/jwt/ClaimValidator.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.auth.jwt; 2 | 3 | import com.mycompany.microservice.api.entities.Company; 4 | import com.mycompany.microservice.api.services.CompanyService; 5 | import java.util.Optional; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.security.oauth2.core.OAuth2TokenValidator; 11 | import org.springframework.security.oauth2.jwt.Jwt; 12 | import org.springframework.security.oauth2.jwt.JwtClaimValidator; 13 | 14 | @Slf4j 15 | @Configuration(proxyBeanMethods = false) 16 | @RequiredArgsConstructor 17 | public class ClaimValidator { 18 | 19 | private final CompanyService companyService; 20 | 21 | /* 22 | * Prevent user without company to access the API 23 | * */ 24 | @Bean 25 | OAuth2TokenValidator companySlugValidator() { 26 | return new JwtClaimValidator( 27 | "company_slug", 28 | slug -> { 29 | final Optional companyOptional = this.companyService.findBySlugOptional(slug); 30 | if (companyOptional.isEmpty()) { 31 | log.warn("[companySlugValidator] company with slug {} not found", slug); 32 | } 33 | return companyOptional.isPresent(); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/auth/providers/ApiKeyAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.auth.providers; 2 | 3 | import java.io.Serial; 4 | import java.util.Collection; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.security.authentication.AbstractAuthenticationToken; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.Transient; 12 | import org.springframework.security.core.authority.AuthorityUtils; 13 | 14 | @Getter 15 | @Transient 16 | public class ApiKeyAuthentication extends AbstractAuthenticationToken { 17 | 18 | @Serial private static final long serialVersionUID = -1137277407288808164L; 19 | 20 | private String apiKey; 21 | private transient ApiKeyDetails apiKeyDetails; 22 | 23 | public ApiKeyAuthentication( 24 | final String apiKey, 25 | final boolean authenticated, 26 | final ApiKeyDetails apiKeyDetails, 27 | final Collection authorities) { 28 | super(authorities); 29 | this.apiKey = apiKey; 30 | this.apiKeyDetails = apiKeyDetails; 31 | this.setAuthenticated(authenticated); 32 | } 33 | 34 | public ApiKeyAuthentication(final String apiKey) { 35 | super(AuthorityUtils.NO_AUTHORITIES); 36 | this.apiKey = apiKey; 37 | this.setAuthenticated(false); 38 | } 39 | 40 | public ApiKeyAuthentication() { 41 | super(AuthorityUtils.NO_AUTHORITIES); 42 | this.setAuthenticated(false); 43 | } 44 | 45 | @Override 46 | public Object getCredentials() { 47 | return null; 48 | } 49 | 50 | @Override 51 | public Object getPrincipal() { 52 | return this.apiKey; 53 | } 54 | 55 | @Getter 56 | @NoArgsConstructor 57 | @AllArgsConstructor 58 | @Builder 59 | public static class ApiKeyDetails { 60 | private Long id; 61 | private String email; 62 | private String companySlug; 63 | private boolean isManagement; 64 | private boolean isInternal; 65 | private boolean isPlatform; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/auth/providers/ApiKeyAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.auth.providers; 2 | 3 | import com.mycompany.microservice.api.constants.AppHeaders; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import java.util.Optional; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.springframework.security.authentication.AuthenticationManager; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 16 | 17 | @Slf4j 18 | public class ApiKeyAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 19 | 20 | public ApiKeyAuthenticationFilter( 21 | final String defaultFilterProcessesUrl, final AuthenticationManager authenticationManager) { 22 | super(defaultFilterProcessesUrl); 23 | this.setAuthenticationManager(authenticationManager); 24 | } 25 | 26 | @Override 27 | public Authentication attemptAuthentication( 28 | final HttpServletRequest request, final HttpServletResponse response) { 29 | 30 | final String apiKeyHeader = request.getHeader(AppHeaders.API_KEY_HEADER); 31 | 32 | final Optional apiKeyOptional = 33 | StringUtils.isNotBlank(apiKeyHeader) ? Optional.of(apiKeyHeader) : Optional.empty(); 34 | 35 | log.debug("found header value '{}'", apiKeyOptional); 36 | 37 | final ApiKeyAuthentication apiKey = 38 | apiKeyOptional.map(ApiKeyAuthentication::new).orElse(new ApiKeyAuthentication()); 39 | 40 | return this.getAuthenticationManager().authenticate(apiKey); 41 | } 42 | 43 | @Override 44 | protected void successfulAuthentication( 45 | final HttpServletRequest request, 46 | final HttpServletResponse response, 47 | final FilterChain chain, 48 | final Authentication authResult) 49 | throws IOException, ServletException { 50 | SecurityContextHolder.getContext().setAuthentication(authResult); 51 | chain.doFilter(request, response); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/auth/providers/ApiKeyAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.auth.providers; 2 | 3 | import com.mycompany.microservice.api.entities.ApiKey; 4 | import com.mycompany.microservice.api.entities.Company; 5 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthentication.ApiKeyDetails; 6 | import com.mycompany.microservice.api.services.ApiKeyService; 7 | import com.mycompany.microservice.api.services.CompanyService; 8 | import java.util.Optional; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.security.authentication.AuthenticationProvider; 13 | import org.springframework.security.authentication.BadCredentialsException; 14 | import org.springframework.security.authentication.InsufficientAuthenticationException; 15 | import org.springframework.security.core.Authentication; 16 | import org.springframework.security.core.AuthenticationException; 17 | 18 | @Slf4j 19 | public class ApiKeyAuthenticationProvider implements AuthenticationProvider { 20 | 21 | private static final String LOG_NAME = "ApiKeyAuthProvider"; 22 | 23 | @Autowired private ApiKeyService apiKeyService; 24 | @Autowired private CompanyService companyService; 25 | 26 | @Override 27 | public Authentication authenticate(final Authentication authentication) 28 | throws AuthenticationException { 29 | 30 | final String apiKeyInRequest = (String) authentication.getPrincipal(); 31 | 32 | if (StringUtils.isBlank(apiKeyInRequest)) { 33 | log.info("[{}] api-key is not defined on request, returning 401", LOG_NAME); 34 | throw new InsufficientAuthenticationException("api-key is not defined on request"); 35 | } else { 36 | log.debug("[{}] start searching for api-key '{}'", LOG_NAME, apiKeyInRequest); 37 | final Optional apiKeyOptional = this.apiKeyService.findByKeyOptional(apiKeyInRequest); 38 | 39 | if (apiKeyOptional.isPresent()) { 40 | final ApiKey apiKey = apiKeyOptional.get(); 41 | final Company company = this.companyService.findById(apiKey.getCompanyId()); 42 | log.debug( 43 | "[{}] api-key '{}' found with authorities '{}'", 44 | LOG_NAME, 45 | apiKeyInRequest, 46 | company.getGrantedAuthoritiesFromCompanyType()); 47 | 48 | final ApiKeyDetails apiKeyDetails = 49 | ApiKeyDetails.builder() 50 | .id(apiKey.getId()) 51 | .companySlug(company.getSlug()) 52 | .email(company.getEmail()) 53 | .isInternal(Boolean.TRUE.equals(company.getIsInternal())) 54 | .isPlatform(Boolean.TRUE.equals(company.getIsPlatform())) 55 | .build(); 56 | 57 | return new ApiKeyAuthentication( 58 | apiKey.getKey(), true, apiKeyDetails, company.getGrantedAuthoritiesFromCompanyType()); 59 | } 60 | 61 | log.info("[{}] api-key '{}' not found, returning 401", LOG_NAME, apiKeyInRequest); 62 | throw new BadCredentialsException("invalid api-key"); 63 | } 64 | } 65 | 66 | @Override 67 | public boolean supports(final Class authentication) { 68 | return ApiKeyAuthentication.class.isAssignableFrom(authentication); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/filters/AddCredsToMDCFilter.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.filters; 2 | 3 | import com.mycompany.microservice.api.facades.AuthFacade; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import org.slf4j.MDC; 10 | import org.springframework.lang.NonNull; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | 14 | /* 15 | * This filter is used to inject user and company value 16 | * within MDC context. 17 | */ 18 | @Component 19 | public class AddCredsToMDCFilter extends OncePerRequestFilter { 20 | 21 | private static final String USER_MDC_KEY = "user"; 22 | private static final String COMPANY_MDC_KEY = "company"; 23 | 24 | @Override 25 | protected void doFilterInternal( 26 | final @NonNull HttpServletRequest request, 27 | final @NonNull HttpServletResponse response, 28 | final FilterChain filterChain) 29 | throws ServletException, IOException { 30 | 31 | MDC.put(USER_MDC_KEY, AuthFacade.getUserEmail()); 32 | MDC.put(COMPANY_MDC_KEY, AuthFacade.getCompanySlug()); 33 | 34 | try { 35 | filterChain.doFilter(request, response); 36 | } finally { 37 | MDC.remove(USER_MDC_KEY); 38 | MDC.remove(COMPANY_MDC_KEY); 39 | } 40 | } 41 | 42 | @Override 43 | protected boolean shouldNotFilterAsyncDispatch() { 44 | return false; 45 | } 46 | 47 | @Override 48 | protected boolean shouldNotFilterErrorDispatch() { 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/filters/AddNginxReqIdToMDCFilter.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.filters; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import java.io.IOException; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.slf4j.MDC; 10 | import org.springframework.lang.NonNull; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | 14 | /* 15 | * This filter is used to inject nginx trace value 16 | * within MDC context. 17 | */ 18 | @Component 19 | public class AddNginxReqIdToMDCFilter extends OncePerRequestFilter { 20 | 21 | private static final String NGINX_REQUEST_ID_HEADER = "X-Request-ID"; 22 | 23 | @Override 24 | protected void doFilterInternal( 25 | final @NonNull HttpServletRequest request, 26 | final @NonNull HttpServletResponse response, 27 | final FilterChain filterChain) 28 | throws ServletException, IOException { 29 | 30 | final String nginxRequestId = request.getHeader(NGINX_REQUEST_ID_HEADER); 31 | 32 | MDC.put( 33 | NGINX_REQUEST_ID_HEADER, 34 | StringUtils.isNotBlank(nginxRequestId) ? nginxRequestId : StringUtils.EMPTY); 35 | 36 | try { 37 | filterChain.doFilter(request, response); 38 | } finally { 39 | MDC.remove(NGINX_REQUEST_ID_HEADER); 40 | } 41 | } 42 | 43 | @Override 44 | protected boolean shouldNotFilterAsyncDispatch() { 45 | return false; 46 | } 47 | 48 | @Override 49 | protected boolean shouldNotFilterErrorDispatch() { 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/filters/RateLimitFilter.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.filters; 2 | 3 | import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS; 4 | 5 | import com.mycompany.microservice.api.infra.ratelimit.DefaultRateLimit; 6 | import com.mycompany.microservice.api.infra.ratelimit.base.BaseRateLimit; 7 | import io.github.bucket4j.Bucket; 8 | import io.github.bucket4j.ConsumptionProbe; 9 | import jakarta.servlet.FilterChain; 10 | import jakarta.servlet.ServletException; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | import java.util.Map; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import lombok.RequiredArgsConstructor; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.lang.NonNull; 20 | import org.springframework.stereotype.Component; 21 | import org.springframework.web.filter.OncePerRequestFilter; 22 | 23 | @Slf4j 24 | @Component 25 | @RequiredArgsConstructor 26 | public class RateLimitFilter extends OncePerRequestFilter { 27 | 28 | public static final String HEADER_RATE_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; 29 | public static final String HEADER_RATE_LIMIT_RETRY_AFTER_SECONDS = 30 | "X-Rate-Limit-Retry-After-Milliseconds"; 31 | 32 | private final DefaultRateLimit defaultRateLimit; 33 | private final Map cache = new ConcurrentHashMap<>(); 34 | 35 | @Override 36 | protected void doFilterInternal( 37 | @NonNull final HttpServletRequest request, 38 | @NonNull final HttpServletResponse response, 39 | @NonNull final FilterChain filterChain) 40 | throws ServletException, IOException { 41 | 42 | final Bucket bucket = this.resolveBucket(request); 43 | final ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1); 44 | 45 | if (probe.isConsumed()) { 46 | // Comment if you want to hide remaining request. 47 | response.addHeader(HEADER_RATE_LIMIT_REMAINING, String.valueOf(probe.getRemainingTokens())); 48 | filterChain.doFilter(request, response); 49 | } else { 50 | 51 | final long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000; 52 | 53 | response.reset(); 54 | // Comment if you want to hide remaining time before refill. 55 | response.addHeader(HEADER_RATE_LIMIT_RETRY_AFTER_SECONDS, String.valueOf(waitForRefill)); 56 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 57 | response.setStatus(TOO_MANY_REQUESTS.value()); 58 | } 59 | } 60 | 61 | private Bucket resolveBucket(final HttpServletRequest request) { 62 | final BaseRateLimit rateLimit = this.getRateLimitFor(request.getRequestURI()); 63 | return this.cache.computeIfAbsent( 64 | // Rate limit bucket on remote address = IP address 65 | request.getRemoteAddr(), s -> Bucket.builder().addLimit(rateLimit.getLimit()).build()); 66 | } 67 | 68 | private BaseRateLimit getRateLimitFor(final String requestedUri) { 69 | // Use a switch case if you want to rate limit multiple URL. 70 | return this.defaultRateLimit; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/interceptors/InterceptorConfig.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.interceptors; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @Configuration(proxyBeanMethods = false) 10 | @RequiredArgsConstructor 11 | public class InterceptorConfig implements WebMvcConfigurer { 12 | 13 | @Value("${miscellaneous.max-response-time-to-log-in-ms}") 14 | private final int maxResponseTimeToLogInMs; 15 | 16 | @Override 17 | public void addInterceptors(final InterceptorRegistry registry) { 18 | registry.addInterceptor(new TimeExecutionInterceptor()).addPathPatterns("/**"); 19 | registry 20 | .addInterceptor(new LogSlowResponseTimeInterceptor(this.maxResponseTimeToLogInMs)) 21 | .addPathPatterns("/**"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/interceptors/LogSlowResponseTimeInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.interceptors; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | import org.springframework.web.servlet.ModelAndView; 9 | 10 | @Slf4j 11 | public class LogSlowResponseTimeInterceptor implements HandlerInterceptor { 12 | 13 | private static final String EXEC_TIME = "execTime"; 14 | private final int maxResponseTimeToLogInMs; 15 | 16 | public LogSlowResponseTimeInterceptor(final int maxResponseTimeToLogInMs) { 17 | this.maxResponseTimeToLogInMs = maxResponseTimeToLogInMs; 18 | } 19 | 20 | @Override 21 | public boolean preHandle( 22 | final HttpServletRequest request, 23 | final @NonNull HttpServletResponse response, 24 | final @NonNull Object handler) { 25 | request.setAttribute(EXEC_TIME, System.nanoTime()); 26 | return true; 27 | } 28 | 29 | @Override 30 | public void postHandle( 31 | final HttpServletRequest request, 32 | final @NonNull HttpServletResponse response, 33 | final @NonNull Object handler, 34 | final ModelAndView modelAndView) { 35 | final Long startTime = (Long) request.getAttribute(EXEC_TIME); 36 | if (startTime != null) { 37 | final long elapsedInNanoS = System.nanoTime() - startTime; 38 | final long responseTimeInMs = elapsedInNanoS / 1_000_000; 39 | if (responseTimeInMs > this.maxResponseTimeToLogInMs) { 40 | log.warn( 41 | "[SLOW_REQUEST] {}ms {} '{}'", 42 | responseTimeInMs, 43 | request.getMethod(), 44 | request.getRequestURI()); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/interceptors/TimeExecutionInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.interceptors; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.springframework.lang.NonNull; 6 | import org.springframework.web.servlet.HandlerInterceptor; 7 | 8 | public class TimeExecutionInterceptor implements HandlerInterceptor { 9 | 10 | private static final String TIME = "StopWatch"; 11 | 12 | @Override 13 | public boolean preHandle( 14 | final HttpServletRequest request, 15 | @NonNull final HttpServletResponse response, 16 | @NonNull final Object handler) { 17 | final long nano = System.nanoTime(); 18 | 19 | request.setAttribute(TIME, nano); 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/profiling/PyroscopeConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.profiling; 2 | 3 | import io.pyroscope.http.Format; 4 | import io.pyroscope.javaagent.EventType; 5 | import io.pyroscope.javaagent.PyroscopeAgent; 6 | import io.pyroscope.javaagent.config.Config; 7 | import java.util.Map; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | @ConditionalOnProperty(prefix = "profiling", name = "pyroscope.enabled", havingValue = "true") 14 | public class PyroscopeConfiguration { 15 | 16 | public PyroscopeConfiguration( 17 | @Value("${profiling.pyroscope.server}") final String pyroscopeServer) { 18 | PyroscopeAgent.start( 19 | new Config.Builder() 20 | .setApplicationName("cloud-diplomats-api-java") 21 | .setLabels(Map.of("project", "cloud-diplomats", "type", "api")) 22 | .setProfilingEvent(EventType.ITIMER) 23 | .setProfilingAlloc("512k") 24 | // .setAllocLive(true) 25 | .setFormat(Format.JFR) 26 | .setServerAddress(pyroscopeServer) 27 | .build()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/ratelimit/DefaultRateLimit.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.ratelimit; 2 | 3 | import com.mycompany.microservice.api.infra.ratelimit.base.BaseRateLimit; 4 | import io.github.bucket4j.Bandwidth; 5 | import io.github.bucket4j.Refill; 6 | import java.time.Duration; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | public class DefaultRateLimit extends BaseRateLimit { 14 | 15 | @Value("${rate-limit.default.name}") 16 | private String name; 17 | 18 | @Value("${rate-limit.default.max-requests}") 19 | private int maxRequests; 20 | 21 | @Value("${rate-limit.default.refill-in-seconds}") 22 | private int refillInSeconds; 23 | 24 | @Override 25 | public String getName() { 26 | return this.name; 27 | } 28 | 29 | @Override 30 | public Bandwidth getLimit() { 31 | return Bandwidth.classic( 32 | this.maxRequests, 33 | Refill.intervally(this.maxRequests, Duration.ofSeconds(this.refillInSeconds))); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/ratelimit/base/BaseRateLimit.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.ratelimit.base; 2 | 3 | import io.github.bucket4j.Bandwidth; 4 | 5 | public abstract class BaseRateLimit { 6 | public abstract String getName(); 7 | 8 | public abstract Bandwidth getLimit(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/infra/security/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.infra.security; 2 | 3 | import static com.mycompany.microservice.api.enums.UserRolesEnum.*; 4 | 5 | import com.mycompany.microservice.api.constants.AppUrls; 6 | import com.mycompany.microservice.api.infra.auth.converters.KeycloakJwtConverter; 7 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthenticationFilter; 8 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthenticationProvider; 9 | import java.util.Collections; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.security.authentication.AuthenticationManager; 14 | import org.springframework.security.authentication.AuthenticationProvider; 15 | import org.springframework.security.authentication.ProviderManager; 16 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 17 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 18 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 19 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 20 | import org.springframework.security.config.http.SessionCreationPolicy; 21 | import org.springframework.security.web.SecurityFilterChain; 22 | import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; 23 | 24 | @Slf4j 25 | @Configuration 26 | @EnableMethodSecurity 27 | @EnableWebSecurity 28 | public class SecurityConfiguration { 29 | 30 | @Bean 31 | public AuthenticationProvider companyApiKeyAuthenticationProvider() { 32 | return new ApiKeyAuthenticationProvider(); 33 | } 34 | 35 | @Bean 36 | public AuthenticationManager authenticationManager() { 37 | return new ProviderManager( 38 | Collections.singletonList(this.companyApiKeyAuthenticationProvider())); 39 | } 40 | 41 | @Bean 42 | public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { 43 | http.addFilterBefore( 44 | new ApiKeyAuthenticationFilter(AppUrls.INTERNAL + "/**", this.authenticationManager()), 45 | AnonymousAuthenticationFilter.class) 46 | .addFilterBefore( 47 | new ApiKeyAuthenticationFilter( 48 | AppUrls.PLATFORM_API + "/**", this.authenticationManager()), 49 | AnonymousAuthenticationFilter.class) 50 | .authorizeHttpRequests( 51 | authorize -> 52 | authorize 53 | // 54 | .requestMatchers(AppUrls.PLATFORM_WEB + "/**", AppUrls.PLATFORM_MOBILE + "/**") 55 | .hasAnyRole(PLATFORM_USER.getName(), PLATFORM_ADMIN.getName()) 56 | .requestMatchers(AppUrls.PLATFORM_API + "/**") 57 | .hasAnyRole(PLATFORM_API_USER.getName()) 58 | // 59 | .requestMatchers(AppUrls.BACK_OFFICE + "/**") 60 | .hasAnyRole(BACK_OFFICE_USER.getName(), BACK_OFFICE_ADMIN.getName()) 61 | // 62 | .requestMatchers(AppUrls.MANAGEMENT + "/**") 63 | .hasAnyRole(MANAGEMENT_USER.getName(), MANAGEMENT_ADMIN.getName()) 64 | // 65 | .requestMatchers(AppUrls.INTERNAL + "/**") 66 | .hasAnyRole(INTERNAL_API_USER.getName()) 67 | // 68 | .requestMatchers(AppUrls.PUBLIC + "/**") 69 | .permitAll() 70 | // 71 | .requestMatchers("/actuator/**") 72 | .permitAll() 73 | // 74 | .anyRequest() 75 | .denyAll()) 76 | // Necessary if we want to be able to call POST/PUT/DELETE 77 | .csrf(AbstractHttpConfigurer::disable) 78 | // To prevent any misconfiguration we disable explicitly all authentication scheme 79 | .sessionManagement( 80 | session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 81 | .httpBasic(AbstractHttpConfigurer::disable) 82 | .formLogin(AbstractHttpConfigurer::disable) 83 | .logout(AbstractHttpConfigurer::disable) 84 | .oauth2ResourceServer( 85 | oauth2 -> 86 | oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtConverter()))); 87 | 88 | return http.build(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/listeners/EntityTransactionLogListener.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.listeners; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.scheduling.annotation.Async; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.transaction.event.TransactionPhase; 10 | import org.springframework.transaction.event.TransactionalEventListener; 11 | 12 | /* 13 | * EntityTransactionLogListener: 14 | * 15 | * Log transaction event for all entities after they were committed/rollback 16 | * ex: [created] company [16] 17 | * 18 | */ 19 | @Slf4j 20 | @Component 21 | public class EntityTransactionLogListener { 22 | 23 | @Async 24 | @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 25 | public void onCommitEvent(final EntityTransactionLogEvent event) { 26 | log.info("[{}] {} {}", event.operation().getName(), event.entityName(), event.entitiesToLog()); 27 | } 28 | 29 | @Async 30 | @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) 31 | public void onRollbackEvent(final EntityTransactionLogEvent event) { 32 | log.info( 33 | "[{}] {} {} rollback.", 34 | event.operation().getName(), 35 | event.entityName(), 36 | event.entitiesToLog()); 37 | } 38 | 39 | public record EntityTransactionLogEvent( 40 | EntityTransactionLogEnum operation, String entityName, String entitiesToLog) { 41 | 42 | @Getter 43 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 44 | public enum EntityTransactionLogEnum { 45 | CREATE("created"), 46 | UPDATE("updated"), 47 | DELETE("deleted"), 48 | UNKNOWN("unknown"); 49 | 50 | private final String name; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/mappers/ApiKeyMapper.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.mappers; 2 | 3 | import com.mycompany.microservice.api.entities.ApiKey; 4 | import com.mycompany.microservice.api.mappers.base.ManagementBaseMapper; 5 | import com.mycompany.microservice.api.requests.management.CreateApiKeyManagementRequest; 6 | import com.mycompany.microservice.api.requests.management.UpdateApiKeyManagementRequest; 7 | import com.mycompany.microservice.api.responses.management.ApikeyManagementResponse; 8 | import org.mapstruct.Mapper; 9 | 10 | @Mapper(componentModel = "spring") 11 | public interface ApiKeyMapper 12 | extends ManagementBaseMapper< 13 | ApiKey, 14 | CreateApiKeyManagementRequest, 15 | UpdateApiKeyManagementRequest, 16 | ApikeyManagementResponse> { 17 | 18 | @Override 19 | ApiKey toEntity(CreateApiKeyManagementRequest request); 20 | 21 | @Override 22 | ApikeyManagementResponse toManagementResponse(ApiKey entity); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/mappers/CompanyMapper.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.mappers; 2 | 3 | import com.mycompany.microservice.api.entities.Company; 4 | import com.mycompany.microservice.api.mappers.base.ManagementBaseMapper; 5 | import com.mycompany.microservice.api.requests.management.CreateCompanyManagementRequest; 6 | import com.mycompany.microservice.api.requests.management.UpdateCompanyManagementRequest; 7 | import com.mycompany.microservice.api.responses.management.CompanyManagementResponse; 8 | import org.mapstruct.Mapper; 9 | 10 | @Mapper(componentModel = "spring") 11 | public interface CompanyMapper 12 | extends ManagementBaseMapper< 13 | Company, 14 | CreateCompanyManagementRequest, 15 | UpdateCompanyManagementRequest, 16 | CompanyManagementResponse> {} 17 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/mappers/annotations/ToEntity.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.mappers.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | import org.mapstruct.Mapping; 6 | 7 | @Retention(RetentionPolicy.CLASS) 8 | @Mapping(target = "id", ignore = true) 9 | @Mapping(target = "createdBy", ignore = true) 10 | @Mapping(target = "updatedBy", ignore = true) 11 | @Mapping(target = "createdAt", ignore = true) 12 | @Mapping(target = "updatedAt", ignore = true) 13 | public @interface ToEntity {} 14 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/mappers/base/ManagementBaseMapper.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.mappers.base; 2 | 3 | import com.mycompany.microservice.api.mappers.annotations.ToEntity; 4 | import java.util.Collection; 5 | import org.mapstruct.BeanMapping; 6 | import org.mapstruct.MappingTarget; 7 | import org.mapstruct.NullValuePropertyMappingStrategy; 8 | 9 | /** 10 | * The interface Base mapper management. 11 | * 12 | * @param the type parameter Entity 13 | * @param the type parameter CreateRequest 14 | * @param the type parameter UpdateRequest 15 | * @param the type parameter Response 16 | */ 17 | public interface ManagementBaseMapper { 18 | 19 | @ToEntity 20 | E toEntity(C request); 21 | 22 | @ToEntity 23 | E update(U request, @MappingTarget E entity); 24 | 25 | @ToEntity 26 | @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) 27 | E patch(U request, @MappingTarget E entity); 28 | 29 | R toManagementResponse(E entity); 30 | 31 | Collection toManagementResponse(Collection entity); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/rabbitmq/configs/RabbitApplicationStartupListener.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.rabbitmq.configs; 2 | 3 | import static com.mycompany.microservice.api.rabbitmq.listeners.EventListener.RABBIT_ASYNC_EVENT_LISTENER_ID; 4 | 5 | import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.context.event.ApplicationReadyEvent; 8 | import org.springframework.context.ApplicationListener; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class RabbitApplicationStartupListener 14 | implements ApplicationListener { 15 | 16 | @Autowired RabbitListenerEndpointRegistry registry; 17 | 18 | /** 19 | * This event is executed as late as conceivably possible to indicate that the application is 20 | * ready to service requests. 21 | */ 22 | @Override 23 | public void onApplicationEvent(final @NonNull ApplicationReadyEvent event) { 24 | this.registry.getListenerContainer(RABBIT_ASYNC_EVENT_LISTENER_ID).start(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/rabbitmq/configs/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.rabbitmq.configs; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.amqp.core.AcknowledgeMode; 5 | import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; 6 | import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; 7 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 8 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 9 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.retry.support.RetryTemplate; 14 | 15 | @Slf4j 16 | @Configuration(proxyBeanMethods = false) 17 | public class RabbitConfig { 18 | 19 | public static final String RABBIT_ASYNC_EVENT_LISTENER_FACTORY = "AsyncEventListener"; 20 | public static final String RABBIT_EVENT_PUBLISHER = "EventPublisher"; 21 | 22 | @Value("${rabbitmq.host}") 23 | private String host; 24 | 25 | @Value("${rabbitmq.port}") 26 | private int port; 27 | 28 | @Value("${rabbitmq.username}") 29 | private String username; 30 | 31 | @Value("${rabbitmq.password}") 32 | private String password; 33 | 34 | @Value("${rabbitmq.listeners.event.prefetch-count}") 35 | private Integer prefetchCount; 36 | 37 | private ConnectionFactory connectionFactory(final String connectionName) { 38 | final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); 39 | connectionFactory.setConnectionNameStrategy(conn -> connectionName); 40 | 41 | connectionFactory.setHost(this.host); 42 | connectionFactory.setPort(this.port); 43 | connectionFactory.setUsername(this.username); 44 | connectionFactory.setPassword(this.password); 45 | 46 | return connectionFactory; 47 | } 48 | 49 | @Bean(name = RABBIT_ASYNC_EVENT_LISTENER_FACTORY) 50 | public DirectRabbitListenerContainerFactory eventListenerFactory() { 51 | final DirectRabbitListenerContainerFactory factory = new DirectRabbitListenerContainerFactory(); 52 | factory.setConnectionFactory(this.connectionFactory("api-event-listener")); 53 | factory.setMessageConverter(new Jackson2JsonMessageConverter()); 54 | factory.setObservationEnabled(true); 55 | factory.setAutoStartup(false); // started at ApplicationReadyEvent 56 | 57 | // https://docs.spring.io/spring-amqp/docs/current/reference/html/#async-listeners 58 | factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); 59 | factory.setDefaultRequeueRejected(false); 60 | factory.setPrefetchCount(this.prefetchCount); 61 | return factory; 62 | } 63 | 64 | @Bean(name = RABBIT_EVENT_PUBLISHER) 65 | public RabbitTemplate rabbitTemplate() { 66 | final RabbitTemplate factory = 67 | new RabbitTemplate(this.connectionFactory("api-event-publisher")); 68 | factory.setMessageConverter(new Jackson2JsonMessageConverter()); 69 | factory.setObservationEnabled(true); 70 | factory.setRetryTemplate(RetryTemplate.defaultInstance()); 71 | 72 | return factory; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/rabbitmq/listeners/EventListener.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.rabbitmq.listeners; 2 | 3 | import static com.mycompany.microservice.api.rabbitmq.configs.RabbitConfig.RABBIT_ASYNC_EVENT_LISTENER_FACTORY; 4 | 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.messaging.Message; 10 | import org.springframework.stereotype.Component; 11 | import reactor.core.publisher.Mono; 12 | 13 | @Slf4j 14 | @Component 15 | @RequiredArgsConstructor 16 | public class EventListener { 17 | 18 | public static final String RABBIT_ASYNC_EVENT_LISTENER_ID = "EventListener"; 19 | 20 | @Value("${rabbitmq.listeners.event.queue}") 21 | private String queueName; 22 | 23 | @RabbitListener( 24 | id = RABBIT_ASYNC_EVENT_LISTENER_ID, 25 | containerFactory = RABBIT_ASYNC_EVENT_LISTENER_FACTORY, 26 | queues = "${rabbitmq.listeners.event.queue}") 27 | public Mono process(final Message message) { 28 | log.info("[RABBITMQ][SUB][{}] {}", this.queueName, message); 29 | return Mono.empty(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/rabbitmq/publishers/EventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.rabbitmq.publishers; 2 | 3 | import static com.mycompany.microservice.api.rabbitmq.configs.RabbitConfig.RABBIT_EVENT_PUBLISHER; 4 | import static java.lang.String.format; 5 | 6 | import com.mycompany.microservice.api.utils.JsonUtils; 7 | import java.nio.charset.StandardCharsets; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.amqp.core.AmqpTemplate; 11 | import org.springframework.amqp.core.Message; 12 | import org.springframework.amqp.core.MessageBuilder; 13 | import org.springframework.amqp.core.MessageProperties; 14 | import org.springframework.amqp.core.MessagePropertiesBuilder; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.lang.NonNull; 17 | import org.springframework.stereotype.Component; 18 | 19 | @Slf4j 20 | @Component 21 | @RequiredArgsConstructor 22 | public class EventPublisher { 23 | 24 | @Qualifier(RABBIT_EVENT_PUBLISHER) 25 | private final AmqpTemplate amqpTemplate; 26 | 27 | public void publish( 28 | @NonNull final String exchange, 29 | @NonNull final String routingKey, 30 | @NonNull final Object payload) { 31 | 32 | try { 33 | 34 | final String msg = JsonUtils.serializeToCamelCase(payload); 35 | 36 | final MessageProperties props = 37 | MessagePropertiesBuilder.newInstance() 38 | .setContentType(MessageProperties.CONTENT_TYPE_JSON) 39 | .setContentEncoding(StandardCharsets.UTF_8.toString()) 40 | .build(); 41 | 42 | log.info( 43 | "[RABBITMQ][PUB][{}] headers {} payload {} ", routingKey, props.getHeaders(), payload); 44 | 45 | final Message message = MessageBuilder.withBody(msg.getBytes()).andProperties(props).build(); 46 | this.amqpTemplate.send(exchange, routingKey, message); 47 | 48 | } catch (final Exception ex) { 49 | log.error( 50 | format( 51 | "[RABBITMQ][PUB][%s] error publishing message with payload %s", routingKey, payload), 52 | ex); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/repositories/ApikeyRepository.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.repositories; 2 | 3 | import com.mycompany.microservice.api.entities.ApiKey; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.cache.annotation.CacheEvict; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.cache.annotation.Caching; 9 | import org.springframework.data.jpa.repository.JpaRepository; 10 | import org.springframework.lang.NonNull; 11 | 12 | public interface ApikeyRepository extends JpaRepository { 13 | 14 | String CACHE_NAME = "apiKey"; 15 | 16 | ApiKey findFirstByCompanyIdAndIsActive(Long companyId, boolean isActive); 17 | 18 | @Cacheable(value = CACHE_NAME, key = "{'findByKeyAndIsActive', #key}") 19 | Optional findByKeyAndIsActive(String key, boolean isActive); 20 | 21 | @Caching(evict = {@CacheEvict(value = CACHE_NAME, key = "{'findByKeyAndIsActive', #entity.key}")}) 22 | @Override 23 | @NonNull S save(@NonNull S entity); 24 | 25 | /* 26 | * This cache implementation is only valid if the table is not 27 | * frequently updated since it will clear the cache at every update operation 28 | * If you want to be more performant you can use something like https://github.com/ms100/cache-as-multi 29 | * */ 30 | @NonNull 31 | @CacheEvict(cacheNames = CACHE_NAME, allEntries = true) 32 | @Override 33 | List saveAll(@NonNull Iterable entities); 34 | 35 | @Caching(evict = {@CacheEvict(value = CACHE_NAME, key = "{'findByKeyAndIsActive', #entity.key}")}) 36 | @Override 37 | void delete(@NonNull ApiKey entity); 38 | 39 | /* 40 | * This cache implementation is only valid if the table is not 41 | * frequently updated since it will clear the cache at every delete operation 42 | * If you want to be more performant you can use something like https://github.com/ms100/cache-as-multi 43 | * */ 44 | @CacheEvict(cacheNames = CACHE_NAME, allEntries = true) 45 | @Override 46 | void deleteAll(@NonNull Iterable entities); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/repositories/CompanyRepository.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.repositories; 2 | 3 | import com.mycompany.microservice.api.entities.Company; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.cache.annotation.CacheEvict; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.cache.annotation.Caching; 9 | import org.springframework.data.jpa.repository.JpaRepository; 10 | import org.springframework.lang.NonNull; 11 | 12 | public interface CompanyRepository extends JpaRepository { 13 | 14 | String CACHE_NAME = "company"; 15 | 16 | @NonNull 17 | @Cacheable(value = CACHE_NAME, key = "{'byId', #id}") 18 | @Override 19 | Optional findById(@NonNull Long id); 20 | 21 | @Cacheable(value = CACHE_NAME, key = "{'bySlug', #slug}") 22 | Optional findBySlug(String slug); 23 | 24 | @Caching( 25 | evict = { 26 | @CacheEvict(value = CACHE_NAME, key = "{'byId', #entity.id}"), 27 | @CacheEvict(value = CACHE_NAME, key = "{'bySlug', #entity.slug}"), 28 | }) 29 | @Override 30 | @NonNull S save(@NonNull S entity); 31 | 32 | /* 33 | * This cache implementation is only valid if the table is not 34 | * frequently updated since it will clear the cache at every update operation 35 | * If you want to be more performant you can use something like https://github.com/ms100/cache-as-multi 36 | * */ 37 | @NonNull 38 | @CacheEvict(cacheNames = CACHE_NAME, allEntries = true) 39 | @Override 40 | List saveAll(@NonNull Iterable entities); 41 | 42 | @Caching( 43 | evict = { 44 | @CacheEvict(value = CACHE_NAME, key = "{'byId', #entity.id}"), 45 | @CacheEvict(value = CACHE_NAME, key = "{'bySlug', #entity.slug}"), 46 | }) 47 | @Override 48 | void delete(@NonNull Company entity); 49 | 50 | /* 51 | * This cache implementation is only valid if the table is not 52 | * frequently updated since it will clear the cache at every delete operation 53 | * If you want to be more performant you can use something like https://github.com/ms100/cache-as-multi 54 | * */ 55 | @CacheEvict(cacheNames = CACHE_NAME, allEntries = true) 56 | @Override 57 | void deleteAll(@NonNull Iterable entities); 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/requests/management/CreateApiKeyManagementRequest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.requests.management; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotNull; 5 | 6 | public record CreateApiKeyManagementRequest(@NotNull Long companyId, @NotBlank String name) {} 7 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/requests/management/CreateCompanyManagementRequest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.requests.management; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import java.math.BigDecimal; 5 | 6 | public record CreateCompanyManagementRequest( 7 | @NotBlank String slug, 8 | @NotBlank String name, 9 | String officialName, 10 | String federalTaxId, 11 | String stateTaxId, 12 | String phone, 13 | String email, 14 | String addressStreet, 15 | String addressStreetNumber, 16 | String addressComplement, 17 | String addressCityDistrict, 18 | String addressPostCode, 19 | String addressCity, 20 | String addressStateCode, 21 | String addressCountry, 22 | BigDecimal addressLatitude, 23 | BigDecimal addressLongitude, 24 | Boolean isManagement, 25 | Boolean isInternal, 26 | Boolean isPlatform) {} 27 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/requests/management/UpdateApiKeyManagementRequest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.requests.management; 2 | 3 | public record UpdateApiKeyManagementRequest(String name) {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/requests/management/UpdateCompanyManagementRequest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.requests.management; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public record UpdateCompanyManagementRequest( 6 | String slug, 7 | String name, 8 | String officialName, 9 | String federalTaxId, 10 | String stateTaxId, 11 | String phone, 12 | String email, 13 | String addressStreet, 14 | String addressStreetNumber, 15 | String addressComplement, 16 | String addressCityDistrict, 17 | String addressPostCode, 18 | String addressCity, 19 | String addressStateCode, 20 | String addressCountry, 21 | BigDecimal addressLatitude, 22 | BigDecimal addressLongitude, 23 | Boolean isManagement, 24 | Boolean isInternal, 25 | Boolean isPlatform) {} 26 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/responses/management/ApikeyManagementResponse.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.responses.management; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public record ApikeyManagementResponse( 6 | Long id, 7 | Long companyId, 8 | String name, 9 | String key, 10 | Boolean isActive, 11 | String createdBy, 12 | String updatedBy, 13 | LocalDateTime createdAt, 14 | LocalDateTime updatedAt) {} 15 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/responses/management/CompanyManagementResponse.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.responses.management; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | 6 | public record CompanyManagementResponse( 7 | Long id, 8 | String slug, 9 | String name, 10 | String officialName, 11 | String federalTaxId, 12 | String stateTaxId, 13 | String phone, 14 | String email, 15 | String addressStreet, 16 | String addressStreetNumber, 17 | String addressComplement, 18 | String addressCityDistrict, 19 | String addressPostCode, 20 | String addressCity, 21 | String addressStateCode, 22 | String addressCountry, 23 | BigDecimal addressLatitude, 24 | BigDecimal addressLongitude, 25 | Boolean isManagement, 26 | Boolean isInternal, 27 | Boolean isPlatform, 28 | String createdBy, 29 | String updatedBy, 30 | LocalDateTime createdAt, 31 | LocalDateTime updatedAt) {} 32 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/responses/shared/ApiErrorDetails.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.responses.shared; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 5 | import java.io.Serializable; 6 | import lombok.Builder; 7 | 8 | @Builder 9 | public record ApiErrorDetails(@JsonInclude(Include.NON_NULL) String pointer, String reason) 10 | implements Serializable {} 11 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/responses/shared/ApiListPaginationSimple.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.responses.shared; 2 | 3 | import java.util.Collection; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.data.domain.Page; 6 | 7 | public record ApiListPaginationSimple(PaginationMeta meta, Collection data) { 8 | 9 | public ApiListPaginationSimple(final Page page) { 10 | this(new PaginationMeta<>(page), page.getContent()); 11 | } 12 | 13 | public static ApiListPaginationSimple of(final Page page) { 14 | return new ApiListPaginationSimple<>(page); 15 | } 16 | 17 | public record PaginationMeta(Integer currentPage, Integer pageSize, String sortedBy) { 18 | 19 | public PaginationMeta(final Page page) { 20 | this( 21 | page.getNumber(), 22 | page.getSize(), 23 | page.getSort().isSorted() ? page.getSort().toString() : StringUtils.EMPTY); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/responses/shared/ApiListPaginationSuccess.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.responses.shared; 2 | 3 | import com.mycompany.microservice.api.utils.UrlUtils; 4 | import java.util.Collection; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 8 | 9 | public record ApiListPaginationSuccess( 10 | PaginationMeta meta, Collection data, PaginationLink links) { 11 | 12 | public ApiListPaginationSuccess(final Page page) { 13 | this(new PaginationMeta<>(page), page.getContent(), new PaginationLink<>(page)); 14 | } 15 | 16 | public static ApiListPaginationSuccess of(final Page page) { 17 | return new ApiListPaginationSuccess<>(page); 18 | } 19 | 20 | public record PaginationMeta( 21 | Integer currentPage, Integer pageSize, Integer totalPages, Long totalItems, String sortedBy) { 22 | 23 | public PaginationMeta(final Page page) { 24 | this( 25 | page.getNumber(), 26 | page.getSize(), 27 | page.getTotalPages(), 28 | page.getTotalElements(), 29 | page.getSort().isSorted() ? page.getSort().toString() : ""); 30 | } 31 | } 32 | 33 | public record PaginationLink( 34 | String self, String first, String last, String next, String previous) { 35 | 36 | public static final String PAGE_PARAMS = "page"; 37 | public static final String SIZE_PARAMS = "size"; 38 | public static final String SORT_PARAMS = "sort"; 39 | 40 | public PaginationLink(final Page page) { 41 | this( 42 | createBuilder() 43 | .queryParam(PAGE_PARAMS, page.getNumber()) 44 | .queryParam(SIZE_PARAMS, page.getSize()) 45 | .query(sortString(page.getSort())) 46 | .build() 47 | .toUriString(), 48 | createBuilder() 49 | .queryParam(PAGE_PARAMS, 0) 50 | .queryParam(SIZE_PARAMS, page.getSize()) 51 | .query(sortString(page.getSort())) 52 | .build() 53 | .toUriString(), 54 | createBuilder() 55 | .queryParam(PAGE_PARAMS, page.getTotalPages() - 1) 56 | .queryParam(SIZE_PARAMS, page.getSize()) 57 | .query(sortString(page.getSort())) 58 | .build() 59 | .toUriString(), 60 | !page.isFirst() 61 | ? createBuilder() 62 | .queryParam(PAGE_PARAMS, page.getNumber() - 1) 63 | .queryParam(SIZE_PARAMS, page.getSize()) 64 | .query(sortString(page.getSort())) 65 | .build() 66 | .toUriString() 67 | : "", 68 | page.hasNext() 69 | ? createBuilder() 70 | .queryParam(PAGE_PARAMS, page.getNumber() + 1) 71 | .queryParam(SIZE_PARAMS, page.getSize()) 72 | .query(sortString(page.getSort())) 73 | .build() 74 | .toUriString() 75 | : ""); 76 | } 77 | 78 | private static ServletUriComponentsBuilder createBuilder() { 79 | return ServletUriComponentsBuilder.fromCurrentRequestUri(); 80 | } 81 | 82 | private static String sortString(final Sort sort) { 83 | final StringBuilder ans = new StringBuilder(); 84 | 85 | for (final Sort.Order s : sort) { 86 | ans.append("&" + SORT_PARAMS + "="); 87 | ans.append(UrlUtils.encodeURLComponent(s.getProperty())); 88 | ans.append(","); 89 | ans.append(UrlUtils.encodeURLComponent(s.getDirection().name().toLowerCase())); 90 | } 91 | 92 | return ans.toString(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/responses/shared/ApiListSuccess.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.responses.shared; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | 7 | public record ApiListSuccess(List data) { 8 | 9 | public static > ApiListSuccess of(final S data) { 10 | return new ApiListSuccess<>(new ArrayList<>(data)); 11 | } 12 | 13 | public static ApiListSuccess of(final List data) { 14 | return new ApiListSuccess<>(data); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/services/ApiKeyService.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.services; 2 | 3 | import static com.mycompany.microservice.api.utils.CryptoUtils.randomKey; 4 | import static java.lang.String.format; 5 | 6 | import com.mycompany.microservice.api.entities.ApiKey; 7 | import com.mycompany.microservice.api.exceptions.ResourceNotFoundException; 8 | import com.mycompany.microservice.api.repositories.ApikeyRepository; 9 | import com.mycompany.microservice.api.services.base.BaseService; 10 | import java.util.Optional; 11 | import lombok.Getter; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | @Slf4j 18 | @Transactional(readOnly = true) 19 | @Service 20 | @RequiredArgsConstructor 21 | public class ApiKeyService extends BaseService { 22 | @Getter private final ApikeyRepository repository; 23 | 24 | @Override 25 | protected void activitiesBeforeCreateEntity(final ApiKey entity) { 26 | entity.setIsActive(true); 27 | entity.setKey(randomKey(18)); 28 | } 29 | 30 | public Optional findByKeyOptional(final String key) { 31 | log.debug("[retrieving] apiKey"); 32 | return this.repository.findByKeyAndIsActive(key, true); 33 | } 34 | 35 | public ApiKey findFirstByCompanyIdAndIsActive(final Long companyId) { 36 | log.debug("[retrieving] apiKey with companyId {}", companyId); 37 | return this.repository.findFirstByCompanyIdAndIsActive(companyId, true); 38 | } 39 | 40 | @Transactional 41 | public void inactivate(final Long id) { 42 | log.info("[inactivating] apiKey with id '{}'", id); 43 | 44 | final Optional apiKeyOptional = this.getRepository().findById(id); 45 | if (apiKeyOptional.isEmpty()) { 46 | throw new ResourceNotFoundException(format("apiKey '%s' not found", id)); 47 | } 48 | 49 | final ApiKey entity = apiKeyOptional.get(); 50 | entity.setIsActive(false); 51 | this.update(entity); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/services/CompanyService.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.services; 2 | 3 | import com.mycompany.microservice.api.entities.Company; 4 | import com.mycompany.microservice.api.exceptions.ResourceNotFoundException; 5 | import com.mycompany.microservice.api.repositories.CompanyRepository; 6 | import com.mycompany.microservice.api.services.base.BaseService; 7 | import java.util.Optional; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | @Slf4j 16 | @Transactional(readOnly = true) 17 | @Service 18 | @RequiredArgsConstructor 19 | public class CompanyService extends BaseService { 20 | @Getter private final CompanyRepository repository; 21 | 22 | public Optional findBySlugOptional(final String slug) { 23 | log.debug("[retrieving] company with slug '{}'", slug); 24 | if (StringUtils.isBlank(slug)) { 25 | return Optional.empty(); 26 | } 27 | return this.repository.findBySlug(slug); 28 | } 29 | 30 | public Company findBySlug(final String slug) { 31 | return this.findBySlugOptional(slug) 32 | .orElseThrow( 33 | () -> 34 | new ResourceNotFoundException( 35 | String.format("company with slug '%s' not found", slug))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/services/LocalCacheManagerService.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.services; 2 | 3 | import static com.mycompany.microservice.api.constants.AppHeaders.API_KEY_HEADER; 4 | import static java.lang.String.format; 5 | 6 | import com.mycompany.microservice.api.constants.AppCompanySlug; 7 | import com.mycompany.microservice.api.controllers.internal.CacheInternalApiController; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.cache.Cache; 15 | import org.springframework.cache.CacheManager; 16 | import org.springframework.cloud.client.ServiceInstance; 17 | import org.springframework.cloud.client.discovery.DiscoveryClient; 18 | import org.springframework.http.HttpEntity; 19 | import org.springframework.http.HttpHeaders; 20 | import org.springframework.http.HttpMethod; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.scheduling.annotation.Async; 23 | import org.springframework.stereotype.Service; 24 | import org.springframework.web.client.RestTemplate; 25 | 26 | /** 27 | * LocalCacheManagerService: 28 | * 29 | *

Only use this service if the cache provider is concurrentHashMap. It provides utility function 30 | * to help clearing local caches. 31 | */ 32 | @Slf4j 33 | @Service 34 | @RequiredArgsConstructor 35 | public class LocalCacheManagerService { 36 | 37 | private final ApiKeyService apiKeyService; 38 | private final CompanyService companyService; 39 | private final DiscoveryClient discoveryClient; 40 | private final RestTemplate restTemplate; 41 | private final CacheManager cacheManager; 42 | 43 | @Value("${kubernetes.service-name}") 44 | private String kubernetesServiceName; 45 | 46 | public void evictByName(final String cacheName) { 47 | final Cache cache = this.cacheManager.getCache(cacheName); 48 | if (cache != null) { 49 | cache.clear(); 50 | log.info("[cache-eviction] evicted local cache '{}'", cacheName); 51 | } 52 | } 53 | 54 | public void evictAll() { 55 | this.cacheManager.getCacheNames().forEach(name -> this.cacheManager.getCache(name).clear()); 56 | log.info("[cache-eviction] evicted all local caches"); 57 | } 58 | 59 | public void evictCacheInAllKubernetesInstances() { 60 | this.evictCacheInAllKubernetesInstances(StringUtils.EMPTY); 61 | } 62 | 63 | @Async 64 | public void evictCacheInAllKubernetesInstances(final String cacheName) { 65 | 66 | final List instances = 67 | this.discoveryClient.getInstances(this.kubernetesServiceName); 68 | 69 | instances.forEach( 70 | instance -> { 71 | final String url = 72 | format( 73 | "%s%s%s", 74 | instance.getUri(), 75 | CacheInternalApiController.BASE_URL, 76 | StringUtils.isBlank(cacheName) ? StringUtils.EMPTY : "/" + cacheName); 77 | log.info("[cache-eviction] sending request to evict cache for '{}'", url); 78 | this.restTemplate.exchange(url, HttpMethod.POST, this.buildRequest(), Void.class); 79 | }); 80 | } 81 | 82 | private HttpEntity buildRequest() { 83 | final HttpHeaders headers = new HttpHeaders(); 84 | headers.setContentType(MediaType.APPLICATION_JSON); 85 | headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); 86 | headers.add(API_KEY_HEADER, this.getInternalApikey()); 87 | return new HttpEntity<>(null, headers); 88 | } 89 | 90 | private String getInternalApikey() { 91 | final Long companyId = this.companyService.findBySlug(AppCompanySlug.INTERNAL).getId(); 92 | return this.apiKeyService.findFirstByCompanyIdAndIsActive(companyId).getKey(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/services/WebhookSiteService.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.services; 2 | 3 | import com.mycompany.microservice.api.clients.http.WebhookSiteHttpClient; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Component; 7 | import reactor.core.publisher.Mono; 8 | 9 | @Slf4j 10 | @Component 11 | @RequiredArgsConstructor 12 | public class WebhookSiteService { 13 | 14 | private final WebhookSiteHttpClient client; 15 | 16 | public Mono post(final Object request) { 17 | // Deserialization (if needed) is done at service level to keep code DRY. 18 | return this.client.post(request).map(response -> response); 19 | } 20 | 21 | public Mono postWithCircuitBreaker(final Object request) { 22 | return this.client.postWithCircuitBreaker(request).map(response -> response); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/utils/CryptoUtils.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import java.security.SecureRandom; 4 | import lombok.experimental.UtilityClass; 5 | 6 | @UtilityClass 7 | public class CryptoUtils { 8 | 9 | public static String randomKey(final int length) { 10 | final byte[] apiKey = new byte[length]; 11 | final SecureRandom secureRandom = new SecureRandom(); 12 | secureRandom.nextBytes(apiKey); 13 | 14 | final StringBuilder sb = new StringBuilder(); 15 | for (final byte b : apiKey) { 16 | sb.append(String.format("%02x", b)); 17 | } 18 | 19 | return sb.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; 4 | import com.fasterxml.jackson.annotation.PropertyAccessor; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 8 | import com.fasterxml.jackson.databind.SerializationFeature; 9 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 10 | import lombok.experimental.UtilityClass; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | @Slf4j 15 | @UtilityClass 16 | public class JsonUtils { 17 | 18 | private static final String SERIALIZATION_ERROR_MESSAGE = 19 | "Something went wrong during serialization/deserialization"; 20 | 21 | public static T deserializeFromCamelCase(final String content, final Class valueType) { 22 | return getMapperSerialize(content, valueType, false); 23 | } 24 | 25 | public static T deserializeFromSnakeCase(final String content, final Class valueType) { 26 | return getMapperSerialize(content, valueType, true); 27 | } 28 | 29 | public static String serializeToCamelCase(final Object content) { 30 | try { 31 | return new ObjectMapper() 32 | .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 33 | // Note: Force jackson to only serialize field and not getters. 34 | .setVisibility(PropertyAccessor.ALL, Visibility.NONE) 35 | .setVisibility(PropertyAccessor.FIELD, Visibility.ANY) 36 | .setPropertyNamingStrategy(new PropertyNamingStrategies.LowerCamelCaseStrategy()) 37 | .registerModule(new JavaTimeModule()) 38 | .writeValueAsString(content); 39 | } catch (final Exception ex) { 40 | log.error(SERIALIZATION_ERROR_MESSAGE, ex); 41 | throw new IllegalArgumentException(ex); 42 | } 43 | } 44 | 45 | public static String serializeToSnakeCase(final Object content) { 46 | try { 47 | return new ObjectMapper() 48 | .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 49 | // Note: Force jackson to only serialize field and not getters. 50 | .setVisibility(PropertyAccessor.ALL, Visibility.NONE) 51 | .setVisibility(PropertyAccessor.FIELD, Visibility.ANY) 52 | .setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy()) 53 | .registerModule(new JavaTimeModule()) 54 | .writeValueAsString(content); 55 | } catch (final Exception ex) { 56 | log.error(SERIALIZATION_ERROR_MESSAGE, ex); 57 | throw new IllegalArgumentException(ex); 58 | } 59 | } 60 | 61 | private static T getMapperSerialize( 62 | final String content, final Class valueType, final boolean fromSnakeCase) { 63 | try { 64 | 65 | if (StringUtils.isBlank(content)) { 66 | return valueType.getDeclaredConstructor().newInstance(); 67 | } 68 | 69 | return new ObjectMapper() 70 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 71 | .setPropertyNamingStrategy( 72 | fromSnakeCase 73 | ? new PropertyNamingStrategies.SnakeCaseStrategy() 74 | : new PropertyNamingStrategies.LowerCamelCaseStrategy()) 75 | .registerModule(new JavaTimeModule()) 76 | .readValue(content, valueType); 77 | } catch (final Exception ex) { 78 | log.warn(SERIALIZATION_ERROR_MESSAGE, ex); 79 | throw new IllegalArgumentException(ex); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/utils/LogUtils.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import com.mycompany.microservice.api.entities.base.BaseEntity; 4 | import java.util.List; 5 | import lombok.experimental.UtilityClass; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.springframework.lang.NonNull; 8 | 9 | @UtilityClass 10 | public class LogUtils { 11 | 12 | public static final String NULL = "null"; 13 | 14 | public static String logId(final BaseEntity entity) { 15 | return entity != null && entity.getId() != null ? entity.getId().toString() : StringUtils.EMPTY; 16 | } 17 | 18 | public static String logIds(@NonNull final List entities) { 19 | if (entities == null || entities.isEmpty()) { 20 | return List.of().toString(); 21 | } 22 | return entities.stream() 23 | .map(e -> e.getId() != null ? e.getId().toString() : NULL) 24 | .toList() 25 | .toString(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/utils/UrlUtils.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import com.mycompany.microservice.api.entities.base.BaseEntity; 4 | import java.net.URI; 5 | import java.net.URLEncoder; 6 | import java.nio.charset.StandardCharsets; 7 | import lombok.experimental.UtilityClass; 8 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 9 | 10 | @UtilityClass 11 | public class UrlUtils { 12 | public static String encodeURLComponent(final String component) { 13 | return URLEncoder.encode(component, StandardCharsets.UTF_8); 14 | } 15 | 16 | public static URI buildUriFromEntity(final BaseEntity entity) { 17 | return ServletUriComponentsBuilder.fromCurrentRequest() 18 | .path("/{id}") 19 | .buildAndExpand(entity.getId()) 20 | .toUri(); 21 | } 22 | 23 | public static URI buildUriFromEntityWithPath(final BaseEntity entity, final String path) { 24 | return ServletUriComponentsBuilder.fromCurrentRequest() 25 | .path(path) 26 | .buildAndExpand(entity.getId()) 27 | .toUri(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/microservice/api/utils/WebClientUtils.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import io.netty.channel.ChannelOption; 4 | import io.netty.handler.ssl.SslClosedEngineException; 5 | import io.netty.handler.timeout.ReadTimeoutHandler; 6 | import io.netty.handler.timeout.WriteTimeoutHandler; 7 | import java.time.Duration; 8 | import java.util.concurrent.TimeUnit; 9 | import lombok.experimental.UtilityClass; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.apache.commons.lang3.exception.ExceptionUtils; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.http.HttpStatusCode; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; 17 | import org.springframework.web.reactive.function.client.ExchangeFilterFunction; 18 | import org.springframework.web.reactive.function.client.WebClient; 19 | import org.springframework.web.reactive.function.client.WebClient.Builder; 20 | import reactor.netty.http.client.HttpClient; 21 | import reactor.netty.http.client.PrematureCloseException; 22 | import reactor.util.retry.Retry; 23 | 24 | @Slf4j 25 | @UtilityClass 26 | public class WebClientUtils { 27 | 28 | public static WebClient createWebClient( 29 | final Builder builder, final String baseUrl, final int timeOutInMs, final String name) { 30 | 31 | final Builder webClientBuilder = 32 | builder 33 | .clientConnector( 34 | new ReactorClientHttpConnector(createHttpClientWithProvider(timeOutInMs))) 35 | .baseUrl(baseUrl) 36 | .defaultHeader( 37 | HttpHeaders.CONTENT_TYPE, 38 | MediaType.APPLICATION_JSON_VALUE, 39 | HttpHeaders.ACCEPT, 40 | MediaType.APPLICATION_JSON_VALUE) 41 | .filter(retryOnNetworkInstability()) 42 | .filter(logResponse(name)); 43 | 44 | return webClientBuilder.build(); 45 | } 46 | 47 | public static HttpClient createHttpClientWithProvider(final int timeOutInMs) { 48 | return HttpClient.create() 49 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeOutInMs) 50 | .responseTimeout(Duration.ofMillis(timeOutInMs)) 51 | .doOnConnected( 52 | conn -> 53 | conn.addHandlerLast(new ReadTimeoutHandler(timeOutInMs, TimeUnit.MILLISECONDS)) 54 | .addHandlerLast(new WriteTimeoutHandler(timeOutInMs, TimeUnit.MILLISECONDS))); 55 | } 56 | 57 | public static String getErrorMessage(final Throwable ex) { 58 | final Throwable rootCause = ExceptionUtils.getRootCause(ex); 59 | return StringUtils.isBlank(rootCause.getMessage()) 60 | ? rootCause.getClass().getSimpleName() 61 | : rootCause.getMessage(); 62 | } 63 | 64 | /* 65 | * Error handler to improve network reliability. 66 | * */ 67 | private ExchangeFilterFunction retryOnNetworkInstability() { 68 | return (request, next) -> 69 | next.exchange(request) 70 | .retryWhen( 71 | Retry.backoff(3, Duration.ofMillis(100)) 72 | .filter( 73 | ex -> { 74 | if (ExceptionUtils.getRootCause(ex) instanceof PrematureCloseException) { 75 | log.info("HTTP[RETRY] PrematureCloseException detected retrying"); 76 | return true; 77 | } else if (ExceptionUtils.getRootCause(ex) 78 | instanceof SslClosedEngineException) { 79 | log.info("HTTP[RETRY] SslClosedEngineException detected retrying"); 80 | return true; 81 | } 82 | return false; 83 | })); 84 | } 85 | 86 | public static ExchangeFilterFunction logResponse(final String webClientName) { 87 | return ExchangeFilterFunction.ofResponseProcessor( 88 | response -> { 89 | final HttpStatusCode status = response.statusCode(); 90 | return response 91 | .bodyToMono(String.class) 92 | // Force mono execution on empty response or will throw IllegalStateException 93 | .defaultIfEmpty(StringUtils.EMPTY) 94 | .map( 95 | body -> { 96 | if (status.is2xxSuccessful()) { 97 | log.info("HTTP[{}] response {} '{}'", webClientName, status.value(), body); 98 | } else { 99 | log.warn( 100 | "HTTP[{}] errorResponse '{}' '{}'", webClientName, status.value(), body); 101 | } 102 | return response; 103 | }); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/logback-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "org.apache.tomcat.util.modeler.modules.MbeansDescriptorsIntrospectionSource", 4 | "allDeclaredConstructors": true 5 | }, 6 | { 7 | "name": "org.flywaydb.core.internal.logging.slf4j.Slf4jLogCreator", 8 | "allDeclaredConstructors": true 9 | }, 10 | { 11 | "name": "ch.qos.logback.classic.AsyncAppender", 12 | "allDeclaredConstructors": true, 13 | "allPublicConstructors": true, 14 | "allDeclaredMethods": true, 15 | "allPublicMethods": true, 16 | "allDeclaredFields": true, 17 | "allPublicFields": true, 18 | "allDeclaredClasses": true, 19 | "allPublicClasses": true 20 | }, 21 | { 22 | "name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder", 23 | "allDeclaredConstructors": true, 24 | "allPublicConstructors": true, 25 | "allDeclaredMethods": true, 26 | "allPublicMethods": true, 27 | "allDeclaredFields": true, 28 | "allPublicFields": true, 29 | "allDeclaredClasses": true, 30 | "allPublicClasses": true 31 | }, 32 | { 33 | "name": "ch.qos.logback.classic.pattern.DateConverter", 34 | "allDeclaredConstructors": true, 35 | "allPublicConstructors": true, 36 | "allDeclaredMethods": true, 37 | "allPublicMethods": true, 38 | "allDeclaredFields": true, 39 | "allPublicFields": true, 40 | "allDeclaredClasses": true, 41 | "allPublicClasses": true 42 | }, 43 | { 44 | "name": "ch.qos.logback.classic.pattern.LevelConverter", 45 | "allDeclaredConstructors": true, 46 | "allPublicConstructors": true, 47 | "allDeclaredMethods": true, 48 | "allPublicMethods": true, 49 | "allDeclaredFields": true, 50 | "allPublicFields": true, 51 | "allDeclaredClasses": true, 52 | "allPublicClasses": true 53 | }, 54 | { 55 | "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter", 56 | "allDeclaredConstructors": true, 57 | "allPublicConstructors": true, 58 | "allDeclaredMethods": true, 59 | "allPublicMethods": true, 60 | "allDeclaredFields": true, 61 | "allPublicFields": true, 62 | "allDeclaredClasses": true, 63 | "allPublicClasses": true 64 | }, 65 | { 66 | "name": "ch.qos.logback.classic.pattern.LoggerConverter", 67 | "allDeclaredConstructors": true, 68 | "allPublicConstructors": true, 69 | "allDeclaredMethods": true, 70 | "allPublicMethods": true, 71 | "allDeclaredFields": true, 72 | "allPublicFields": true, 73 | "allDeclaredClasses": true, 74 | "allPublicClasses": true 75 | }, 76 | { 77 | "name": "ch.qos.logback.classic.pattern.MessageConverter", 78 | "allDeclaredConstructors": true, 79 | "allPublicConstructors": true, 80 | "allDeclaredMethods": true, 81 | "allPublicMethods": true, 82 | "allDeclaredFields": true, 83 | "allPublicFields": true, 84 | "allDeclaredClasses": true, 85 | "allPublicClasses": true 86 | }, 87 | { 88 | "name": "ch.qos.logback.classic.pattern.ThreadConverter", 89 | "allDeclaredConstructors": true, 90 | "allPublicConstructors": true, 91 | "allDeclaredMethods": true, 92 | "allPublicMethods": true, 93 | "allDeclaredFields": true, 94 | "allPublicFields": true, 95 | "allDeclaredClasses": true, 96 | "allPublicClasses": true 97 | }, 98 | { 99 | "name": "ch.qos.logback.core.ConsoleAppender", 100 | "allDeclaredConstructors": true, 101 | "allPublicConstructors": true, 102 | "allDeclaredMethods": true, 103 | "allPublicMethods": true, 104 | "allDeclaredFields": true, 105 | "allPublicFields": true, 106 | "allDeclaredClasses": true, 107 | "allPublicClasses": true 108 | }, 109 | { 110 | "name": "ch.qos.logback.core.FileAppender", 111 | "allDeclaredConstructors": true, 112 | "allPublicConstructors": true, 113 | "allDeclaredMethods": true, 114 | "allPublicMethods": true, 115 | "allDeclaredFields": true, 116 | "allPublicFields": true, 117 | "allDeclaredClasses": true, 118 | "allPublicClasses": true 119 | } 120 | ] -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | slack: 2 | env: api-dev 3 | channels: 4 | api-alert: 5 | url: http://your-slack-bot-url.com/example 6 | name: api-alert 7 | 8 | http: 9 | clients: 10 | webhook-site: 11 | base-url: https://webhook.site/f6ec3af8-08d7-427d-b839-3d88f84fa9c0 12 | 13 | rabbitmq: 14 | host: localhost 15 | port: 5672 16 | username: user 17 | password: password 18 | listeners: 19 | event: 20 | queue: webhook 21 | publishers: 22 | webhook: 23 | exchange: outbound 24 | routingkey: to_outbound_webhook 25 | 26 | profiling: 27 | pyroscope: 28 | enabled: false 29 | server: http://pyroscope-local.cloud-diplomats.com 30 | 31 | management: 32 | endpoint: 33 | health: 34 | show-details: always 35 | 36 | # for testing purpose you can use https://github.com/CtrlSpice/otel-desktop-viewer 37 | 38 | spring: 39 | datasource: 40 | url: jdbc:postgresql://localhost:5432/api 41 | username: user 42 | password: password 43 | jpa: 44 | properties: 45 | hibernate: 46 | show_sql: false 47 | format_sql: false 48 | 49 | security: 50 | oauth2: 51 | resourceserver: 52 | jwt: 53 | issuer-uri: http://localhost:8000/realms/api 54 | jwk-set-uri: http://localhost:8000/realms/api/protocol/openid-connect/certs 55 | 56 | devtools: 57 | restart: 58 | enabled: false 59 | 60 | logging: 61 | pattern.correlation: "[${spring.application.name:},%X{trace_id:-},%X{span_id:-},%X{trace_flags:-}]" 62 | level: 63 | web: info 64 | # org.springframework.cache: TRACE 65 | # hibernate.SQL: DEBUG 66 | # hibernate.engine.jdbc.batch.internal.BatchingBatch: DEBUG 67 | # reactor.netty.http.client.HttpClient: DEBUG -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | miscellaneous: 2 | max-response-time-to-log-in-ms: ${MAX_RESPONSE_TIME_TO_LOG_IN_MS:1000} 3 | 4 | http: 5 | clients: 6 | default-timeout: ${HTTP_CLIENTS_DEFAULT_TIMEOUT:5000} 7 | webhook-site: 8 | base-url: ${HTTP_CLIENTS_MY_EXTERNAL_API_BASE_URL} 9 | 10 | rate-limit: 11 | default: 12 | name: ${DEFAULT_RATE_LIMIT_NAME:DEFAULT} 13 | max-requests: ${DEFAULT_MAX_REQUESTS:50} 14 | refill-in-seconds: ${DEFAULT_REFILL_IN_SECONDS:1} 15 | 16 | slack: 17 | env: ${SLACK_ENV} 18 | channels: 19 | api-alert: 20 | url: ${SLACK_API_ALERT_URL} 21 | name: ${SLACK_API_ALERT_CHANNEL} 22 | 23 | rabbitmq: 24 | host: ${RABBITMQ_HOST} 25 | port: ${RABBITMQ_PORT} 26 | username: ${RABBITMQ_USERNAME} 27 | password: ${RABBITMQ_PASSWORD} 28 | listeners: 29 | event: 30 | queue: ${RABBITMQ_LISTENERS_EVENT_QUEUE} 31 | prefetch-count: ${RABBITMQ_LISTENERS_EVENT_PREFETCH_COUNT:10} 32 | publishers: 33 | webhook: 34 | exchange: ${RABBITMQ_PUBLISHERS_WEBHOOK_EXCHANGE} 35 | routingkey: ${RABBITMQ_PUBLISHERS_WEBHOOK_ROUTING_KEY} 36 | 37 | kubernetes: 38 | service-name: ${KUBERNETES_SERVICE_NAME:api} 39 | 40 | management: 41 | server: 42 | port: 8081 43 | info: 44 | java: 45 | enabled: true 46 | endpoint: 47 | health: 48 | probes: 49 | enabled: true 50 | add-additional-paths: true 51 | # group: 52 | # readiness: 53 | # include: rabbit 54 | 55 | endpoints: 56 | web: 57 | exposure: 58 | include: info, health, metrics, sbom, preStopHook # prometheus 59 | 60 | spring: 61 | application: 62 | name: api 63 | main: 64 | keep-alive: true # ensures JVM is kept alive, even if all threads are virtual threads https://docs.spring.io/spring-boot/docs/3.2.0-RC2/reference/htmlsingle/#features.spring-application.virtual-threads 65 | web: 66 | resources: 67 | add-mappings: false # disable static content. 68 | mvc: 69 | log-resolved-exception: false # remove tomcat log exception since it is already treated in GlobalExceptionHandler 70 | reactor: 71 | context-propagation: auto # automatically propagates trace and span in reactive pipelines. 72 | threads: 73 | virtual: 74 | enabled: true 75 | security: 76 | oauth2: 77 | resourceserver: 78 | jwt: 79 | # can not be used for now as claim name does not allow nested values. 80 | # authorities-claim-name: 81 | # authority-prefix: ROLE_ 82 | # principal-claim-name: email 83 | issuer-uri: ${SECURITY_OAUTH_ISSUER_URI} 84 | jwk-set-uri: ${SECURITY_OAUTH_JWK_SET_URI} 85 | data: 86 | web: 87 | pageable: 88 | max-page-size: 20 # default 2000 89 | 90 | flyway: 91 | locations: classpath:/db/migration/postgresql 92 | 93 | datasource: 94 | hikari: 95 | maximum-pool-size: ${DB_MAX_POOL_SIZE:20} 96 | data-source-properties: 97 | ApplicationName: ${spring.application.name} # allows to see what applications are connected to the server and what resources they are using through views like pg_stat_activity 98 | reWriteBatchedInserts: true 99 | stringtype: unspecified 100 | url: jdbc:postgresql://${DB_HOST}/${DB_NAME} 101 | username: ${DB_USER} 102 | password: ${DB_PASSWORD} 103 | 104 | jpa: 105 | open-in-view: false # disables lazy loading in web views, important for performance. 106 | hibernate: 107 | ddl-auto: validate 108 | properties: 109 | hibernate: 110 | generate_statistics: false 111 | jdbc: 112 | # important: When using GenerationType.IDENTITY Hibernate disable batching, you need to use BatchSequenceGenerator 113 | batch_size: ${JDBC_BATCH_SIZE:10} 114 | time_zone: ${JDBC_TIMEZONE:America/Sao_Paulo} 115 | 116 | # jackson: 117 | # property-naming-strategy: LOWER_CAMEL_CASE 118 | # visibility.field: any 119 | # visibility.getter: none 120 | # visibility.setter: none 121 | # visibility.is-getter: none 122 | 123 | serialization: 124 | write-dates-as-timestamps: false 125 | fail-on-empty-beans: false 126 | 127 | lifecycle: 128 | timeout-per-shutdown-phase: 20s 129 | 130 | server: 131 | shutdown: graceful 132 | 133 | --- 134 | spring: 135 | config: 136 | activate: 137 | on-profile: replication 138 | flyway: 139 | locations: classpath:/db/migration/replication,classpath:/db/migration/postgresql -------------------------------------------------------------------------------- /src/main/resources/db/migration/postgresql/V1.1.1__Create_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.company ( 2 | id bigserial PRIMARY KEY, 3 | -- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- can not be used cf. https://github.com/vladmihalcea/hypersistence-utils/issues/1 4 | slug varchar(255) NOT NULL UNIQUE, 5 | name varchar(255) NOT NULL UNIQUE, 6 | official_name varchar(255) UNIQUE, 7 | state_tax_id varchar(255), 8 | federal_tax_id varchar(255) UNIQUE, -- CNPJ 9 | phone varchar(255), 10 | email varchar(255), 11 | 12 | address_street varchar(255), 13 | address_street_number varchar(255), 14 | address_complement varchar(255), 15 | address_city_district varchar(255), 16 | address_post_code varchar(255), 17 | address_city varchar(255), 18 | address_state_code varchar(255), 19 | address_country varchar(255), 20 | address_latitude numeric, 21 | address_longitude numeric, 22 | 23 | is_platform boolean DEFAULT false, 24 | is_back_office boolean DEFAULT false, 25 | is_internal boolean DEFAULT false, 26 | is_management boolean DEFAULT false, 27 | 28 | created_by varchar(255), 29 | updated_by varchar(255), 30 | 31 | created_at timestamp NOT NULL DEFAULT current_timestamp, 32 | updated_at timestamp NOT NULL DEFAULT current_timestamp 33 | ); 34 | 35 | CREATE TABLE public.api_key 36 | ( 37 | id bigserial PRIMARY KEY, 38 | -- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- can not be used cf. https://github.com/vladmihalcea/hypersistence-utils/issues/1 39 | company_id bigint NOT NULL REFERENCES company (id), 40 | name varchar(255) NOT NULL, 41 | key varchar(255) NOT NULL UNIQUE, 42 | is_active boolean NOT NULL DEFAULT FALSE, 43 | 44 | created_by varchar(255), 45 | updated_by varchar(255), 46 | 47 | created_at timestamp NOT NULL DEFAULT current_timestamp, 48 | updated_at timestamp NOT NULL DEFAULT current_timestamp 49 | ); 50 | CREATE INDEX api_key_key_is_active_idx ON api_key (key, is_active); 51 | 52 | INSERT INTO public.company (slug, name, email, created_at, updated_at) 53 | VALUES ('management', 'management-company', 'management-company@gmail.com', NOW(), NOW()); 54 | 55 | INSERT INTO public.company (slug, name, email, created_at, updated_at) 56 | VALUES ('back-office', 'back-office-company', 'back-office-company@gmail.com', NOW(), NOW()); 57 | 58 | INSERT INTO public.company (slug, name, email, is_internal, created_at, updated_at) 59 | VALUES ('internal', 'internal-company', 'internal-company@gmail.com', true, NOW(), NOW()); 60 | INSERT INTO public.api_key (company_id, name, key, is_active, created_at, updated_at) 61 | VALUES (3, 'apikey-internal', 'internal-apikey', true, NOW(), NOW()); 62 | 63 | INSERT INTO public.company (slug, name, email, is_platform, created_at, updated_at) 64 | VALUES ('platform', 'platform-company', 'platform-company@gmail.com', true, NOW(), NOW()); 65 | INSERT INTO public.api_key (company_id, name, key, is_active, created_at, updated_at) 66 | VALUES (4, 'apikey-platform', 'platform-apikey', true, NOW(), NOW()); 67 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/replication/V2.1.1__Create_replication.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Only used to simplify replication test with debezium 3 | ALTER ROLE "user" WITH REPLICATION; 4 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "user"; 5 | 6 | CREATE PUBLICATION dbz_publication FOR ALL TABLES; 7 | 8 | CREATE TABLE debezium_signal (id VARCHAR(42) PRIMARY KEY, type VARCHAR(32) NOT NULL, data VARCHAR(2048) NULL); 9 | CREATE TABLE debezium_heartbeat (message VARCHAR(32) NOT NULL, heartbeat timestamp primary key NOT NULL); 10 | 11 | CREATE PUBLICATION my_publication FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate'); 12 | SELECT PG_CREATE_LOGICAL_REPLICATION_SLOT ('my_replication_slot', 'pgoutput'); 13 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/replication/V2.1.1__Create_replication.sql.conf: -------------------------------------------------------------------------------- 1 | 2 | # This is needed to prevent replication slot creation error. 3 | 4 | executeInTransaction=false -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | America/Sao_Paulo 11 | 12 | 13 | 14 | { 15 | "level": "%level", 16 | "company": "%mdc{company}", 17 | "user": "%mdc{user}", 18 | "message": "%message", 19 | "traceId": "%mdc{trace_id}", 20 | "spanId": "%mdc{span_id}", 21 | "traceFlags": "%mdc{trace_flags}", 22 | 23 | "requestId": "%mdc{X-Request-ID}", 24 | "logger": "%logger", 25 | "thread": "%thread" 26 | } 27 | 28 | 29 | 30 | stackTrace 31 | 33 | 4 34 | 35 | 30 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/BaseIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api; 2 | 3 | import java.nio.file.Paths; 4 | import org.apache.commons.lang3.RandomStringUtils; 5 | import org.junit.jupiter.api.TestInstance; 6 | import org.junit.jupiter.api.TestInstance.Lifecycle; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 11 | import org.springframework.test.context.ActiveProfiles; 12 | import org.springframework.test.context.DynamicPropertyRegistry; 13 | import org.springframework.test.context.DynamicPropertySource; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | import org.testcontainers.containers.PostgreSQLContainer; 16 | import org.testcontainers.containers.RabbitMQContainer; 17 | import org.testcontainers.junit.jupiter.Container; 18 | import org.testcontainers.lifecycle.Startables; 19 | import org.testcontainers.utility.MountableFile; 20 | 21 | @ActiveProfiles("test") 22 | @AutoConfigureMockMvc 23 | @TestInstance(Lifecycle.PER_CLASS) 24 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 25 | public abstract class BaseIntegrationTest { 26 | 27 | @Container @ServiceConnection 28 | public static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); 29 | 30 | @Container public static RabbitMQContainer rabbit = new RabbitMQContainer("rabbitmq:3.12.9"); 31 | 32 | static { 33 | setRabbitConfig(rabbit); 34 | Startables.deepStart(postgres, rabbit).join(); 35 | } 36 | 37 | @Autowired public MockMvc mockMvc; 38 | 39 | @DynamicPropertySource 40 | static void applicationProperties(final DynamicPropertyRegistry registry) { 41 | registry.add("rabbitmq.host", rabbit::getHost); 42 | registry.add("rabbitmq.port", rabbit::getAmqpPort); 43 | // defined in resources/testcontainers/rabbitmq-definition.json 44 | registry.add("rabbitmq.username", () -> "user"); 45 | registry.add("rabbitmq.password", () -> "password"); 46 | } 47 | 48 | public static String random(final Integer... args) { 49 | return RandomStringUtils.randomAlphabetic(args.length == 0 ? 10 : args[0]); 50 | } 51 | 52 | public static String randomNumeric(final Integer... args) { 53 | return RandomStringUtils.randomNumeric(args.length == 0 ? 10 : args[0]); 54 | } 55 | 56 | private static String getResourcesDir() { 57 | return Paths.get("src", "test", "resources").toFile().getAbsolutePath(); 58 | } 59 | 60 | private static String getRabbitDefinition() { 61 | return getResourcesDir() + "/testcontainers/rabbitmq-definition.json"; 62 | } 63 | 64 | private static String getRabbitConfig() { 65 | return getResourcesDir() + "/testcontainers/rabbitmq.conf"; 66 | } 67 | 68 | private static void setRabbitConfig(final RabbitMQContainer rabbit) { 69 | rabbit.withCopyFileToContainer( 70 | MountableFile.forHostPath(getRabbitDefinition()), "/etc/rabbitmq/definitions.json"); 71 | rabbit.withCopyFileToContainer( 72 | MountableFile.forHostPath(getRabbitConfig()), "/etc/rabbitmq/rabbitmq.conf"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/controllers/backoffice/AuthorizationBackOfficeControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.backoffice; 2 | 3 | import static com.mycompany.microservice.api.enums.UserRolesEnum.BACK_OFFICE_USER; 4 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 7 | 8 | import com.mycompany.microservice.api.BaseIntegrationTest; 9 | import com.mycompany.microservice.api.enums.UserRolesEnum; 10 | import com.mycompany.microservice.api.testutils.builders.JwtBuilder; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.EnumSource; 15 | 16 | class AuthorizationBackOfficeControllerIT extends BaseIntegrationTest { 17 | 18 | private final String URL = BackOfficeController.BASE_URL + "/hello-world"; 19 | 20 | @BeforeAll 21 | void init() {} 22 | 23 | @Test 24 | void return_401_IfNoJwtPassed() throws Exception { 25 | this.mockMvc 26 | .perform(get(this.URL).with(authentication(null))) 27 | .andExpect(status().isUnauthorized()); 28 | } 29 | 30 | @Test 31 | void return_401_IfInvalidRole() throws Exception { 32 | this.mockMvc 33 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), random())))) 34 | .andExpect(status().isForbidden()); 35 | } 36 | 37 | @ParameterizedTest 38 | @EnumSource( 39 | value = UserRolesEnum.class, 40 | names = {"BACK_OFFICE_USER", "BACK_OFFICE_ADMIN"}, 41 | mode = EnumSource.Mode.EXCLUDE) 42 | void return_401_IfNotAValidRole(final UserRolesEnum role) throws Exception { 43 | this.mockMvc 44 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), role)))) 45 | .andExpect(status().isForbidden()); 46 | } 47 | 48 | @Test 49 | void return_200() throws Exception { 50 | this.mockMvc 51 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), BACK_OFFICE_USER)))) 52 | .andExpect(status().isOk()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/controllers/internal/AuthorizationInternalControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.internal; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 5 | 6 | import com.mycompany.microservice.api.BaseIntegrationTest; 7 | import com.mycompany.microservice.api.constants.AppHeaders; 8 | import com.mycompany.microservice.api.services.ApiKeyService; 9 | import com.mycompany.microservice.api.services.CompanyService; 10 | import com.mycompany.microservice.api.testutils.builders.ApiKeyBuilder; 11 | import com.mycompany.microservice.api.testutils.builders.CompanyBuilder; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.Test; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | class AuthorizationInternalControllerIT extends BaseIntegrationTest { 17 | 18 | private static final String URL = CacheInternalApiController.BASE_URL; 19 | 20 | @Autowired private ApiKeyService apiKeyService; 21 | @Autowired private CompanyService companyService; 22 | 23 | @BeforeAll 24 | void init() {} 25 | 26 | @Test 27 | void return_401_IfApikeyIsNotFound() throws Exception { 28 | this.mockMvc 29 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, random())) 30 | .andExpect(status().isUnauthorized()); 31 | } 32 | 33 | @Test 34 | void return_401_IfApikeyCompanyHasWrongRole() throws Exception { 35 | final var company = this.companyService.create(CompanyBuilder.company()); 36 | final var apiKey = this.apiKeyService.create(ApiKeyBuilder.apiKey(company)); 37 | this.mockMvc 38 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, apiKey.getKey())) 39 | .andExpect(status().isForbidden()); 40 | 41 | final var management = this.companyService.create(CompanyBuilder.management()); 42 | final var apiKeyManagement = this.apiKeyService.create(ApiKeyBuilder.apiKey(management)); 43 | this.mockMvc 44 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, apiKeyManagement.getKey())) 45 | .andExpect(status().isForbidden()); 46 | } 47 | 48 | @Test 49 | void return_401_IfApikeyIsDisabled() throws Exception { 50 | final var internal = this.companyService.create(CompanyBuilder.internal()); 51 | final var apiKey = this.apiKeyService.create(ApiKeyBuilder.apiKey(internal)); 52 | this.apiKeyService.delete(apiKey.getId()); 53 | this.mockMvc 54 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, apiKey.getKey())) 55 | .andExpect(status().isUnauthorized()); 56 | } 57 | 58 | @Test 59 | void return_200() throws Exception { 60 | final var internal = this.companyService.create(CompanyBuilder.internal()); 61 | final var apiKey = this.apiKeyService.create(ApiKeyBuilder.apiKey(internal)); 62 | this.mockMvc 63 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, apiKey.getKey())) 64 | .andExpect(status().isOk()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/controllers/management/AuthorizationManagementControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.management; 2 | 3 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import com.mycompany.microservice.api.BaseIntegrationTest; 8 | import com.mycompany.microservice.api.enums.UserRolesEnum; 9 | import com.mycompany.microservice.api.services.ApiKeyService; 10 | import com.mycompany.microservice.api.services.CompanyService; 11 | import com.mycompany.microservice.api.testutils.builders.JwtBuilder; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.EnumSource; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | 18 | class AuthorizationManagementControllerIT extends BaseIntegrationTest { 19 | 20 | private final String URL = CompanyManagementController.BASE_URL; 21 | 22 | @Autowired private ApiKeyService apiKeyService; 23 | @Autowired private CompanyService companyService; 24 | 25 | @BeforeAll 26 | void init() {} 27 | 28 | @Test 29 | void return_401_IfNoJwtPassed() throws Exception { 30 | this.mockMvc 31 | .perform(get(this.URL).with(authentication(null))) 32 | .andExpect(status().isUnauthorized()); 33 | } 34 | 35 | @Test 36 | void return_401_IfInvalidRole() throws Exception { 37 | this.mockMvc 38 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), random())))) 39 | .andExpect(status().isForbidden()); 40 | } 41 | 42 | @ParameterizedTest 43 | @EnumSource( 44 | value = UserRolesEnum.class, 45 | names = {"MANAGEMENT_USER", "MANAGEMENT_ADMIN"}, 46 | mode = EnumSource.Mode.EXCLUDE) 47 | void return_401_IfNotAValidRole(final UserRolesEnum role) throws Exception { 48 | this.mockMvc 49 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), role)))) 50 | .andExpect(status().isForbidden()); 51 | } 52 | 53 | @Test 54 | void return_200() throws Exception { 55 | this.mockMvc 56 | .perform( 57 | get(this.URL) 58 | .with(authentication(JwtBuilder.jwt(random(), UserRolesEnum.MANAGEMENT_USER)))) 59 | .andExpect(status().isOk()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/controllers/platform/api/AuthorizationPlatformApiControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.platform.api; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import com.mycompany.microservice.api.BaseIntegrationTest; 8 | import com.mycompany.microservice.api.constants.AppHeaders; 9 | import com.mycompany.microservice.api.services.ApiKeyService; 10 | import com.mycompany.microservice.api.services.CompanyService; 11 | import com.mycompany.microservice.api.testutils.builders.ApiKeyBuilder; 12 | import com.mycompany.microservice.api.testutils.builders.CompanyBuilder; 13 | import org.junit.jupiter.api.BeforeAll; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | 17 | class AuthorizationPlatformApiControllerIT extends BaseIntegrationTest { 18 | 19 | private static final String URL = PlatformApiController.BASE_URL + "/hello-world"; 20 | 21 | @Autowired private ApiKeyService apiKeyService; 22 | @Autowired private CompanyService companyService; 23 | 24 | @BeforeAll 25 | void init() {} 26 | 27 | @Test 28 | void return_401_IfApikeyIsNotFound() throws Exception { 29 | this.mockMvc 30 | .perform(get(URL).header(AppHeaders.API_KEY_HEADER, random())) 31 | .andExpect(status().isUnauthorized()); 32 | } 33 | 34 | @Test 35 | void return_401_IfApikeyCompanyHasWrongRole() throws Exception { 36 | final var company = this.companyService.create(CompanyBuilder.company()); 37 | final var apiKey = this.apiKeyService.create(ApiKeyBuilder.apiKey(company)); 38 | this.mockMvc 39 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, apiKey.getKey())) 40 | .andExpect(status().isForbidden()); 41 | 42 | final var management = this.companyService.create(CompanyBuilder.management()); 43 | final var apiKeyManagement = this.apiKeyService.create(ApiKeyBuilder.apiKey(management)); 44 | this.mockMvc 45 | .perform(delete(URL).header(AppHeaders.API_KEY_HEADER, apiKeyManagement.getKey())) 46 | .andExpect(status().isForbidden()); 47 | } 48 | 49 | @Test 50 | void return_401_IfApikeyIsDisabled() throws Exception { 51 | final var platform = this.companyService.create(CompanyBuilder.platform()); 52 | final var apiKey = this.apiKeyService.create(ApiKeyBuilder.apiKey(platform)); 53 | this.apiKeyService.delete(apiKey.getId()); 54 | this.mockMvc 55 | .perform(get(URL).header(AppHeaders.API_KEY_HEADER, apiKey.getKey())) 56 | .andExpect(status().isUnauthorized()); 57 | } 58 | 59 | @Test 60 | void return_200() throws Exception { 61 | final var platform = this.companyService.create(CompanyBuilder.platform()); 62 | final var apiKey = this.apiKeyService.create(ApiKeyBuilder.apiKey(platform)); 63 | this.mockMvc 64 | .perform(get(URL).header(AppHeaders.API_KEY_HEADER, apiKey.getKey())) 65 | .andExpect(status().isOk()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/controllers/platform/mobile/AuthorizationPlatformMobileControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.platform.mobile; 2 | 3 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import com.mycompany.microservice.api.BaseIntegrationTest; 8 | import com.mycompany.microservice.api.enums.UserRolesEnum; 9 | import com.mycompany.microservice.api.testutils.builders.JwtBuilder; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.EnumSource; 14 | 15 | class AuthorizationPlatformMobileControllerIT extends BaseIntegrationTest { 16 | 17 | private final String URL = PlatformMobileController.BASE_URL + "/hello-world"; 18 | 19 | @BeforeAll 20 | void init() {} 21 | 22 | @Test 23 | void return_401_IfNoJwtPassed() throws Exception { 24 | this.mockMvc 25 | .perform(get(this.URL).with(authentication(null))) 26 | .andExpect(status().isUnauthorized()); 27 | } 28 | 29 | @Test 30 | void return_401_IfInvalidRole() throws Exception { 31 | this.mockMvc 32 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), random())))) 33 | .andExpect(status().isForbidden()); 34 | } 35 | 36 | @ParameterizedTest 37 | @EnumSource( 38 | value = UserRolesEnum.class, 39 | names = {"PLATFORM_USER", "PLATFORM_ADMIN"}, 40 | mode = EnumSource.Mode.EXCLUDE) 41 | void return_401_IfNotAValidRole(final UserRolesEnum role) throws Exception { 42 | this.mockMvc 43 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), role)))) 44 | .andExpect(status().isForbidden()); 45 | } 46 | 47 | @Test 48 | void return_200() throws Exception { 49 | this.mockMvc 50 | .perform( 51 | get(this.URL) 52 | .with(authentication(JwtBuilder.jwt(random(), UserRolesEnum.PLATFORM_USER)))) 53 | .andExpect(status().isOk()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/controllers/platform/web/AuthorizationPlatformWebControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.controllers.platform.web; 2 | 3 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import com.mycompany.microservice.api.BaseIntegrationTest; 8 | import com.mycompany.microservice.api.enums.UserRolesEnum; 9 | import com.mycompany.microservice.api.testutils.builders.JwtBuilder; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.EnumSource; 14 | 15 | class AuthorizationPlatformWebControllerIT extends BaseIntegrationTest { 16 | 17 | private final String URL = PlatformWebController.BASE_URL + "/hello-world"; 18 | 19 | @BeforeAll 20 | void init() {} 21 | 22 | @Test 23 | void return_401_IfNoJwtPassed() throws Exception { 24 | this.mockMvc 25 | .perform(get(this.URL).with(authentication(null))) 26 | .andExpect(status().isUnauthorized()); 27 | } 28 | 29 | @Test 30 | void return_401_IfInvalidRole() throws Exception { 31 | this.mockMvc 32 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), random())))) 33 | .andExpect(status().isForbidden()); 34 | } 35 | 36 | @ParameterizedTest 37 | @EnumSource( 38 | value = UserRolesEnum.class, 39 | names = {"PLATFORM_USER", "PLATFORM_ADMIN"}, 40 | mode = EnumSource.Mode.EXCLUDE) 41 | void return_401_IfNotAValidRole(final UserRolesEnum role) throws Exception { 42 | this.mockMvc 43 | .perform(get(this.URL).with(authentication(JwtBuilder.jwt(random(), role)))) 44 | .andExpect(status().isForbidden()); 45 | } 46 | 47 | @Test 48 | void return_200() throws Exception { 49 | this.mockMvc 50 | .perform( 51 | get(this.URL) 52 | .with(authentication(JwtBuilder.jwt(random(), UserRolesEnum.PLATFORM_USER)))) 53 | .andExpect(status().isOk()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/facades/AuthFacadeTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.facades; 2 | 3 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_EMAIL; 4 | 5 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthentication; 6 | import com.mycompany.microservice.api.infra.auth.providers.ApiKeyAuthentication.ApiKeyDetails; 7 | import java.util.Collections; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.core.authority.AuthorityUtils; 14 | import org.springframework.security.core.context.SecurityContext; 15 | import org.springframework.security.core.context.SecurityContextHolder; 16 | import org.springframework.security.oauth2.jwt.Jwt; 17 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 18 | 19 | public class AuthFacadeTest { 20 | 21 | public static String API_KEY = "my-apikey-test"; 22 | public static String EMAIL = "test@gmail.com"; 23 | public static String COMPANY_SLUG = "my-company-test"; 24 | 25 | @Test 26 | void verifyGetCompanySlugIsEmptyOnEmptyAuthentication() { 27 | final var securityContext = Mockito.mock(SecurityContext.class); 28 | SecurityContextHolder.setContext(securityContext); 29 | 30 | Assertions.assertEquals(StringUtils.EMPTY, AuthFacade.getCompanySlug()); 31 | } 32 | 33 | @Test 34 | void verifyGetCompanySlugIsEmptyOnInvalidAuthentication() { 35 | final var securityContext = Mockito.mock(SecurityContext.class); 36 | final var authentication = Mockito.mock(Authentication.class); 37 | 38 | Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); 39 | SecurityContextHolder.setContext(securityContext); 40 | 41 | Assertions.assertTrue(AuthFacade.getCompanySlug().isEmpty()); 42 | } 43 | 44 | @Test 45 | void verifyGetCompanySlugOnJwtAuthentication() { 46 | 47 | final var jwt = 48 | Jwt.withTokenValue("token") 49 | .header("alg", "none") 50 | .claim("scope", "read") 51 | .claim("company_slug", COMPANY_SLUG) 52 | .build(); 53 | 54 | final var securityContext = Mockito.mock(SecurityContext.class); 55 | final var authentication = new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES); 56 | 57 | Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); 58 | SecurityContextHolder.setContext(securityContext); 59 | 60 | Assertions.assertEquals(AuthFacade.getCompanySlug(), COMPANY_SLUG); 61 | } 62 | 63 | @Test 64 | void verifyGetUserEmailOnJwtAuthentication() { 65 | 66 | final var jwt = 67 | Jwt.withTokenValue("token") 68 | .header("alg", "none") 69 | .claim("scope", "read") 70 | .claim(CLAIM_EMAIL, EMAIL) 71 | .build(); 72 | 73 | final var securityContext = Mockito.mock(SecurityContext.class); 74 | final var authentication = new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES); 75 | 76 | Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); 77 | SecurityContextHolder.setContext(securityContext); 78 | 79 | Assertions.assertEquals(AuthFacade.getUserEmail(), EMAIL); 80 | } 81 | 82 | @Test 83 | void verifyGetCompanySlugOnApiAuthentication() { 84 | 85 | final var authentication = 86 | new ApiKeyAuthentication( 87 | API_KEY, 88 | true, 89 | ApiKeyDetails.builder().id(1L).companySlug(COMPANY_SLUG).build(), 90 | Collections.emptyList()); 91 | 92 | final var securityContext = Mockito.mock(SecurityContext.class); 93 | Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); 94 | SecurityContextHolder.setContext(securityContext); 95 | 96 | Assertions.assertEquals(AuthFacade.getCompanySlug(), COMPANY_SLUG); 97 | } 98 | 99 | @Test 100 | void verifyGetUserEmailOnApiAuthentication() { 101 | 102 | final var authentication = 103 | new ApiKeyAuthentication( 104 | API_KEY, 105 | true, 106 | ApiKeyDetails.builder().id(1L).email(EMAIL).build(), 107 | Collections.emptyList()); 108 | 109 | final var securityContext = Mockito.mock(SecurityContext.class); 110 | Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); 111 | SecurityContextHolder.setContext(securityContext); 112 | 113 | Assertions.assertEquals(AuthFacade.getUserEmail(), EMAIL); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/junit/ParallelizableTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.junit; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.junit.jupiter.api.TestInstance; 8 | import org.junit.jupiter.api.parallel.Execution; 9 | import org.junit.jupiter.api.parallel.ExecutionMode; 10 | 11 | /** 12 | * Marks the annotated test class as eligible for parallel execution. Unlike the {@link Execution}, 13 | * it can be used only on the test class level. Additionally, it explicitly enables "separate 14 | * instance per method" semantics to improve the isolation between the test cases. 15 | */ 16 | @Execution(ExecutionMode.CONCURRENT) 17 | @TestInstance(TestInstance.Lifecycle.PER_METHOD) 18 | @Retention(RetentionPolicy.RUNTIME) 19 | @Target(ElementType.TYPE) 20 | public @interface ParallelizableTest {} 21 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/testutils/builders/ApiKeyBuilder.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.testutils.builders; 2 | 3 | import static com.mycompany.microservice.api.BaseIntegrationTest.random; 4 | 5 | import com.mycompany.microservice.api.entities.ApiKey; 6 | import com.mycompany.microservice.api.entities.Company; 7 | import lombok.experimental.UtilityClass; 8 | 9 | @UtilityClass 10 | public class ApiKeyBuilder { 11 | public static ApiKey apiKey(final Company company) { 12 | return ApiKey.builder().name(random()).companyId(company.getId()).build(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/testutils/builders/CompanyBuilder.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.testutils.builders; 2 | 3 | import static com.mycompany.microservice.api.BaseIntegrationTest.random; 4 | 5 | import com.mycompany.microservice.api.entities.Company; 6 | import lombok.experimental.UtilityClass; 7 | 8 | @UtilityClass 9 | public class CompanyBuilder { 10 | 11 | public static Company company() { 12 | return Company.builder().name(random()).slug(random()).build(); 13 | } 14 | 15 | public static Company platform() { 16 | return Company.builder().name(random()).slug(random()).isPlatform(true).build(); 17 | } 18 | 19 | public static Company management() { 20 | return Company.builder().name(random()).slug(random()).isManagement(true).build(); 21 | } 22 | 23 | public static Company internal() { 24 | return Company.builder().name(random()).slug(random()).isInternal(true).build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/testutils/builders/JwtBuilder.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.testutils.builders; 2 | 3 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_REALM_ACCESS; 4 | import static com.mycompany.microservice.api.constants.JWTClaims.CLAIM_ROLES; 5 | 6 | import com.mycompany.microservice.api.enums.UserRolesEnum; 7 | import java.util.List; 8 | import java.util.Map; 9 | import lombok.experimental.UtilityClass; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.security.core.authority.AuthorityUtils; 12 | import org.springframework.security.oauth2.jwt.Jwt; 13 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 14 | 15 | @UtilityClass 16 | public class JwtBuilder { 17 | 18 | public static JwtAuthenticationToken jwt(final String companySlug, final UserRolesEnum role) { 19 | return jwt(companySlug, role.getName()); 20 | } 21 | 22 | public static JwtAuthenticationToken jwt(final String companySlug, final String role) { 23 | 24 | final var authority = AuthorityUtils.createAuthorityList("ROLE_" + role); 25 | final var roleClaim = 26 | Map.of( 27 | CLAIM_REALM_ACCESS, 28 | Map.of(CLAIM_ROLES, List.of(StringUtils.isNotBlank(role) ? role : StringUtils.EMPTY))); 29 | 30 | final var jwt = 31 | Jwt.withTokenValue("token") 32 | .header("alg", "none") 33 | .claim("scope", "read") 34 | .claim("company_slug", companySlug) 35 | .claim("realms", roleClaim) 36 | .build(); 37 | 38 | return new JwtAuthenticationToken(jwt, authority); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/testutils/configs/TestContainersConfig.java: -------------------------------------------------------------------------------- 1 | // package com.mycompany.microservice.api.testutils.configs; 2 | // 3 | // import org.springframework.boot.test.context.TestConfiguration; 4 | // import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 5 | // import org.springframework.context.annotation.Bean; 6 | // import org.testcontainers.containers.PostgreSQLContainer; 7 | // 8 | // @TestConfiguration(proxyBeanMethods = false) 9 | // public class TestContainersConfig { 10 | // 11 | // @Bean 12 | // @ServiceConnection 13 | // public PostgreSQLContainer postgresContainer() { 14 | // return new PostgreSQLContainer<>("bitnami/postgresql:15.4.0-debian-11-r21").withReuse(true); 15 | // } 16 | // } 17 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/utils/CryptoUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class CryptoUtilsTest { 7 | 8 | @Test 9 | void verifyRandomKey() { 10 | final int length = 10; 11 | Assertions.assertEquals(CryptoUtils.randomKey(length).length(), length * 2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/utils/JsonUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import com.mycompany.microservice.api.entities.Company; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class JsonUtilsTest { 9 | 10 | @Test 11 | void verifyDeserializeFromCamelCase() { 12 | final Company company = 13 | JsonUtils.deserializeFromCamelCase("{\"officialName\": \"test\"}", Company.class); 14 | Assertions.assertTrue(StringUtils.isNotEmpty(company.getOfficialName())); 15 | } 16 | 17 | @Test 18 | void verifyDeserializeFromSnakeCase() { 19 | final Company company = 20 | JsonUtils.deserializeFromSnakeCase("{\"official_name\": \"test\"}", Company.class); 21 | Assertions.assertTrue(StringUtils.isNotEmpty(company.getOfficialName())); 22 | } 23 | 24 | @Test 25 | void verifySerializeToCamelCase() { 26 | final String json = JsonUtils.serializeToCamelCase(new Company()); 27 | Assertions.assertTrue(json.contains("officialName")); 28 | } 29 | 30 | @Test 31 | void verifySerializeToSnakeCase() { 32 | final String json = JsonUtils.serializeToSnakeCase(new Company()); 33 | Assertions.assertTrue(json.contains("official_name")); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/mycompany/microservice/api/utils/LogUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.microservice.api.utils; 2 | 3 | import static org.apache.commons.lang3.StringUtils.EMPTY; 4 | 5 | import com.mycompany.microservice.api.entities.Company; 6 | import java.util.List; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class LogUtilsTest { 11 | 12 | @Test 13 | void verifyLogIdNullSafe() { 14 | Assertions.assertEquals(EMPTY, LogUtils.logId(null)); 15 | Assertions.assertEquals(EMPTY, LogUtils.logId(new Company())); 16 | } 17 | 18 | @Test 19 | void verifyLogId() { 20 | final var company = new Company(1L); 21 | Assertions.assertEquals(LogUtils.logId(company), company.getId().toString()); 22 | } 23 | 24 | @Test 25 | void verifyLogIdsNullSafe() { 26 | Assertions.assertEquals(LogUtils.logIds(null), List.of().toString()); 27 | Assertions.assertEquals(LogUtils.logIds(List.of()), List.of().toString()); 28 | } 29 | 30 | @Test 31 | void verifyLogIds() { 32 | final var company = new Company(1L); 33 | Assertions.assertEquals(LogUtils.logIds(List.of(company)), List.of(company.getId()).toString()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | http: 2 | clients: 3 | webhook-site: 4 | base-url: http://test.com 5 | 6 | slack: 7 | env: api-test 8 | channels: 9 | api-alert: 10 | url: http://test.com 11 | name: api-alert 12 | 13 | rabbitmq: 14 | listeners: 15 | event: 16 | queue: webhook 17 | publishers: 18 | webhook: 19 | exchange: outbound 20 | routingkey: to_outbound_webhook 21 | 22 | 23 | spring: 24 | security: 25 | oauth2: 26 | resourceserver: 27 | jwt: 28 | issuer-uri: http://test:8000/realms/api 29 | jwk-set-uri: http://test:8000/realms/api/protocol/openid-connect/certs 30 | 31 | -------------------------------------------------------------------------------- /src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | # enable parallel execution 2 | junit.jupiter.execution.parallel.enabled = true 3 | 4 | # preserve old (sequential) behavior by default 5 | junit.jupiter.execution.parallel.mode.default = same_thread 6 | junit.jupiter.execution.parallel.mode.classes.default = same_thread -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/testcontainers/rabbitmq-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "rabbit_version": "3.12.4", 3 | "rabbitmq_version": "3.12.4", 4 | "product_name": "RabbitMQ", 5 | "product_version": "3.12.4", 6 | "users": [ 7 | { 8 | "name": "user", 9 | "password_hash": "uRpW/Oo8IwVY9z/V2i48TfsRXlrkrDM8yi+gdte4m49Snm+A", 10 | "hashing_algorithm": "rabbit_password_hashing_sha256", 11 | "tags": [ 12 | "administrator" 13 | ], 14 | "limits": {} 15 | } 16 | ], 17 | "vhosts": [ 18 | { 19 | "name": "/" 20 | } 21 | ], 22 | "permissions": [ 23 | { 24 | "user": "user", 25 | "vhost": "/", 26 | "configure": ".*", 27 | "write": ".*", 28 | "read": ".*" 29 | } 30 | ], 31 | "topic_permissions": [], 32 | "parameters": [], 33 | "global_parameters": [ 34 | { 35 | "name": "internal_cluster_id", 36 | "value": "rabbitmq-cluster-id-7RK4AQm26a2tDE2NFh2zdw" 37 | } 38 | ], 39 | "policies": [], 40 | "queues": [ 41 | { 42 | "name": "event", 43 | "vhost": "/", 44 | "durable": true, 45 | "auto_delete": false, 46 | "arguments": { 47 | "x-max-length": 100000, 48 | "x-overflow": "reject-publish" 49 | } 50 | }, 51 | { 52 | "name": "webhook", 53 | "vhost": "/", 54 | "durable": true, 55 | "auto_delete": false, 56 | "arguments": { 57 | "x-max-length": 100000, 58 | "x-overflow": "reject-publish" 59 | } 60 | } 61 | ], 62 | "exchanges": [ 63 | { 64 | "name": "outbound", 65 | "vhost": "/", 66 | "type": "direct", 67 | "durable": true, 68 | "auto_delete": false, 69 | "internal": false, 70 | "arguments": { 71 | "x-delayed-type": "direct" 72 | } 73 | }, 74 | { 75 | "name": "inbound", 76 | "vhost": "/", 77 | "type": "direct", 78 | "durable": true, 79 | "auto_delete": false, 80 | "internal": false, 81 | "arguments": {} 82 | } 83 | ], 84 | "bindings": [ 85 | { 86 | "source": "inbound", 87 | "vhost": "/", 88 | "destination": "event", 89 | "destination_type": "queue", 90 | "routing_key": "to_inbound_event", 91 | "arguments": {} 92 | }, 93 | { 94 | "source": "outbound", 95 | "vhost": "/", 96 | "destination": "webhook", 97 | "destination_type": "queue", 98 | "routing_key": "to_outbound_webhook", 99 | "arguments": {} 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /src/test/resources/testcontainers/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | load_definitions = /etc/rabbitmq/definitions.json --------------------------------------------------------------------------------