diff --git a/.docker/.dockerignore b/.docker/.dockerignore
new file mode 100644
index 0000000..0159db9
--- /dev/null
+++ b/.docker/.dockerignore
@@ -0,0 +1,13 @@
+*.bat
+*.dockerignore
+*.editorconfig
+*.gitattributes
+*.gitignore
+*.iml
+*.md
+*.yml
+.git/
+.github/
+.idea/
+.vscode/
+target/
diff --git a/.docker/docker-compose.yaml b/.docker/docker-compose.yaml
new file mode 100644
index 0000000..c2ec3bc
--- /dev/null
+++ b/.docker/docker-compose.yaml
@@ -0,0 +1,143 @@
+# docker compose up --detach --build --force-recreate --remove-orphans
+
+name: java
+services:
+ application:
+ image: application
+ container_name: application
+ depends_on:
+ - elk-elasticsearch
+ - elk-kibana
+ - kafka
+ - localstack
+ - mongo
+ - postgres
+ build:
+ context: ..
+ dockerfile: .docker/dockerfile
+ ports:
+ - "8090:8080"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/database
+ SPRING_DATASOURCE_USERNAME: admin
+ SPRING_DATASOURCE_PASSWORD: password
+ SPRING_DATA_MONGODB_URI: mongodb://admin:password@mongo:27017/database?authSource=admin
+ SPRING_KAFKA: kafka:9094
+ AWS_ENDPOINT: http://localstack:4566
+ AWS_REGION: us-east-1
+ AWS_ACCESS_KEY_ID: test
+ AWS_SECRET_ACCESS_KEY: test
+ SPRING_CLOUD_AWS_S3_BUCKET: bucket
+ SPRING_CLOUD_AWS_SQS_QUEUE: queue
+ elk-elasticsearch:
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.15.2
+ container_name: elk-elasticsearch
+ ports:
+ - "9200:9200"
+ - "9300:9300"
+ volumes:
+ - elk-elasticsearch:/usr/share/elasticsearch/data
+ environment:
+ discovery.type: single-node
+ xpack.security.enabled: false
+ xpack.security.enrollment.enabled: false
+ ES_JAVA_OPTS: -Xms512m -Xmx512m
+ elk-kibana:
+ image: docker.elastic.co/kibana/kibana:8.15.2
+ container_name: elk-kibana
+ depends_on:
+ - elk-elasticsearch
+ ports:
+ - "5601:5601"
+ environment:
+ ELASTICSEARCH_URL: http://elk-elasticsearch:9200
+ ELASTICSEARCH_HOSTS: http://elk-elasticsearch:9200
+ kafka:
+ image: bitnami/kafka
+ container_name: kafka
+ ports:
+ - "9092:9092"
+ - "9094:9094"
+ environment:
+ KAFKA_KRAFT_CLUSTER_ID: 0
+ KAFKA_CFG_NODE_ID: 0
+ KAFKA_CFG_PROCESS_ROLES: controller,broker
+ KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
+ KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
+ KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
+ KAFKA_CFG_LISTENERS: CONTROLLER://:9093,PLAINTEXT://:9094,EXTERNAL://:9092
+ KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9094,EXTERNAL://localhost:9092
+ KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
+ kafka-admin:
+ image: obsidiandynamics/kafdrop
+ container_name: kafka-admin
+ depends_on:
+ - kafka
+ ports:
+ - "9000:9000"
+ environment:
+ KAFKA_BROKERCONNECT: kafka:9094
+ localstack:
+ image: localstack/localstack
+ container_name: localstack
+ ports:
+ - "4566:4566"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ./localstack.sh:/etc/localstack/init/ready.d/localstack.sh
+ environment:
+ - SERVICES=sqs,sqs-query,s3
+ mongo:
+ image: mongo
+ container_name: mongo
+ ports:
+ - "27017:27017"
+ volumes:
+ - mongo:/data/db
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: admin
+ MONGO_INITDB_ROOT_PASSWORD: password
+ mongo-admin:
+ image: mongo-express
+ container_name: mongo-admin
+ depends_on:
+ - mongo
+ ports:
+ - "27018:8081"
+ environment:
+ ME_CONFIG_MONGODB_URL: mongodb://admin:password@mongo:27017
+ ME_CONFIG_MONGODB_ADMINUSERNAME: admin
+ ME_CONFIG_MONGODB_ADMINPASSWORD: password
+ ME_CONFIG_BASICAUTH: false
+ postgres:
+ image: postgres
+ container_name: postgres
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres:/var/lib/postgresql/data
+ environment:
+ POSTGRES_DB: database
+ POSTGRES_USER: admin
+ POSTGRES_PASSWORD: password
+ postgres-admin:
+ image: dpage/pgadmin4
+ container_name: postgres-admin
+ depends_on:
+ - postgres
+ ports:
+ - "5433:80"
+ volumes:
+ - ./servers.json:/pgadmin4/servers.json
+ - postgres-admin:/var/lib/pgadmin
+ environment:
+ PGADMIN_DEFAULT_EMAIL: admin@admin.com
+ PGADMIN_DEFAULT_PASSWORD: password
+ PGADMIN_CONFIG_SERVER_MODE: "False"
+ PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
+volumes:
+ elk-elasticsearch:
+ mongo:
+ postgres:
+ postgres-admin:
diff --git a/.docker/dockerfile b/.docker/dockerfile
new file mode 100644
index 0000000..04b72ca
--- /dev/null
+++ b/.docker/dockerfile
@@ -0,0 +1,13 @@
+FROM eclipse-temurin:23-jdk-alpine AS build
+RUN apk add --no-cache maven
+WORKDIR /source
+COPY source/pom.xml .
+RUN mvn dependency:go-offline
+COPY source .
+RUN mvn clean package -DskipTests
+
+FROM eclipse-temurin:23-jre-alpine
+WORKDIR /app
+COPY --from=build /source/target/*.jar app.jar
+EXPOSE 8090
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/.docker/localstack.sh b/.docker/localstack.sh
new file mode 100644
index 0000000..45717fe
--- /dev/null
+++ b/.docker/localstack.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+localstack status services
+awslocal sqs create-queue --queue-name queue
+awslocal s3 mb s3://bucket
diff --git a/.docker/servers.json b/.docker/servers.json
new file mode 100644
index 0000000..ed9fecc
--- /dev/null
+++ b/.docker/servers.json
@@ -0,0 +1,15 @@
+{
+ "Servers": {
+ "Database": {
+ "Group": "Servers",
+ "Name": "Docker",
+ "Host": "postgres",
+ "Port": 5432,
+ "MaintenanceDB": "postgres",
+ "Username": "admin",
+ "Password": "password",
+ "SSLMode": "prefer",
+ "Favorite": true
+ }
+ }
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..54a09f4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 500
+tab_width = 4
+trim_trailing_whitespace = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1c7c280
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto
+*.java diff=java
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..a62efac
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,26 @@
+name: build
+on:
+ push:
+ branches: [main]
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Java Setup
+ uses: actions/setup-java@v4
+ with:
+ java-version: 23
+ distribution: temurin
+ cache: maven
+
+ - name: Java Publish
+ run: mvn -B clean package --file source/pom.xml
+
+ - name: Artifact Upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: app
+ path: source/target/*.jar
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f919350
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.bat
+*.iml
+.idea
+.vscode
+target
\ No newline at end of file
diff --git a/license.md b/license.md
new file mode 100644
index 0000000..1f95d26
--- /dev/null
+++ b/license.md
@@ -0,0 +1,5 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..b0f6d31
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,69 @@
+# JAVA
+
+![](https://github.com/rafaelfgx/Java/actions/workflows/build.yaml/badge.svg)
+
+API using Java, Spring Boot, Docker, Testcontainers, PostgreSQL, MongoDB, Kafka, LocalStack, SQS, S3, JWT, Swagger.
+
+## TECHNOLOGIES
+
+* [Java](https://dev.java)
+* [Spring Boot](https://spring.io/projects/spring-boot)
+* [Docker](https://www.docker.com/get-started)
+* [Testcontainers](https://testcontainers.com)
+* [PostgreSQL](https://www.postgresql.org/)
+* [MongoDB](https://www.mongodb.com/docs/manual)
+* [Kafka](https://kafka.apache.org)
+* [LocalStack](https://localstack.cloud)
+* [AWS SQS](https://aws.amazon.com/sqs)
+* [AWS S3](https://aws.amazon.com/s3)
+* [JWT](https://jwt.io)
+* [Swagger](https://swagger.io)
+
+## RUN
+
+
+IntelliJ IDEA
+
+#### Prerequisites
+
+* [Docker](https://www.docker.com/get-started)
+* [Java JDK](https://www.oracle.com/java/technologies/downloads)
+* [IntelliJ IDEA](https://www.jetbrains.com/idea/download)
+
+#### Steps
+
+1. Execute **docker compose up --detach --build --force-recreate --remove-orphans** in **docker** directory.
+2. Open **source** directory in **IntelliJ IDEA**.
+3. Select **Application.java** class.
+4. Click **Run** or **Debug**.
+5. Open .
+
+
+
+
+Docker
+
+#### Prerequisites
+
+* [Docker](https://www.docker.com/get-started)
+
+#### Steps
+
+1. Execute **docker compose up --detach --build --force-recreate --remove-orphans** in **docker** directory.
+2. Open .
+
+
+
+## EXAMPLES
+
+* **AWS:** Amazon Web Services [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/aws) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/aws)
+* **Auth:** Authentication and Authorization [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/auth) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/auth)
+* **Category:** Cache [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/category) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/category)
+* **Game:** Mocks [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/game) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/game)
+* **Group:** Groups [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/group) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/group)
+* **Invoice:** PostgreSQL [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/invoice) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/invoice)
+* **Location:** Flat Object to Nested Object [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/location) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/location)
+* **Notification:** Kafka [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/notification) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/notification)
+* **Payment:** Strategy Pattern [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/payment) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/payment)
+* **Product:** MongoDB [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/product) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/product)
+* **User:** Business Rules [Main](https://github.com/rafaelfgx/Java/tree/main/source/src/main/java/com/company/architecture/user) | [Tests](https://github.com/rafaelfgx/Java/tree/main/source/src/test/java/com/company/architecture/user)
diff --git a/source/.run/Application.run.xml b/source/.run/Application.run.xml
new file mode 100644
index 0000000..2e0cb46
--- /dev/null
+++ b/source/.run/Application.run.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/.run/Tests.run.xml b/source/.run/Tests.run.xml
new file mode 100644
index 0000000..bf0684e
--- /dev/null
+++ b/source/.run/Tests.run.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/source/lombok.config b/source/lombok.config
new file mode 100644
index 0000000..7a21e88
--- /dev/null
+++ b/source/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/source/pom.xml b/source/pom.xml
new file mode 100644
index 0000000..58d42e1
--- /dev/null
+++ b/source/pom.xml
@@ -0,0 +1,133 @@
+
+
+ 4.0.0
+ com.company
+ architecture
+ architecture
+ 1.0.0
+
+ 23
+ full
+ UTF-8
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.6.0
+
+
+ org.projectlombok
+ lombok
+ provided
+ 1.18.34
+
+
+ org.postgresql
+ postgresql
+ 42.7.4
+
+
+ io.awspring.cloud
+ spring-cloud-aws-starter-s3
+ 3.2.1
+
+
+ io.awspring.cloud
+ spring-cloud-aws-starter-sqs
+ 3.2.1
+
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
+ com.auth0
+ jwks-rsa
+ 0.22.1
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+ 1.20.3
+
+
+ org.testcontainers
+ postgresql
+ test
+ 1.20.3
+
+
+ org.testcontainers
+ mongodb
+ test
+ 1.20.3
+
+
+ org.testcontainers
+ localstack
+ test
+ 1.20.3
+
+
+ org.testcontainers
+ kafka
+ test
+ 1.20.3
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/source/src/main/java/com/company/architecture/Application.java b/source/src/main/java/com/company/architecture/Application.java
new file mode 100644
index 0000000..ac7f2f7
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/Application.java
@@ -0,0 +1,15 @@
+package com.company.architecture;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@EnableCaching
+@EnableScheduling
+@SpringBootApplication
+public class Application {
+ public static void main(final String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/auth/AuthConfiguration.java b/source/src/main/java/com/company/architecture/auth/AuthConfiguration.java
new file mode 100644
index 0000000..7d83b2a
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/AuthConfiguration.java
@@ -0,0 +1,43 @@
+package com.company.architecture.auth;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@RequiredArgsConstructor
+public class AuthConfiguration {
+ private final AuthFilter authFilter;
+
+ @Bean
+ AuthenticationManager authenticationManager(final AuthenticationConfiguration authenticationConfiguration) throws Exception {
+ return authenticationConfiguration.getAuthenticationManager();
+ }
+
+ @Bean
+ PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ SecurityFilterChain securityFilterChain(final HttpSecurity httpSecurity) throws Exception {
+ return httpSecurity
+ .csrf(AbstractHttpConfigurer::disable)
+ .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
+ .authorizeHttpRequests(registry -> registry
+ .requestMatchers("/", "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**", "/auth").permitAll()
+ .requestMatchers(HttpMethod.DELETE).hasAuthority(Authority.ADMINISTRATOR.name())
+ .anyRequest().authenticated()
+ )
+ .build();
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/auth/AuthController.java b/source/src/main/java/com/company/architecture/auth/AuthController.java
new file mode 100644
index 0000000..d6fd4b0
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/AuthController.java
@@ -0,0 +1,25 @@
+package com.company.architecture.auth;
+
+import com.company.architecture.shared.swagger.PostApiResponses;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "Auth")
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+ private final AuthService authService;
+
+ @Operation(summary = "Auth")
+ @PostApiResponses
+ @PostMapping
+ @ResponseStatus(HttpStatus.OK)
+ public String auth(@RequestBody @Valid final AuthDto dto) {
+ return authService.auth(dto);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/auth/AuthDto.java b/source/src/main/java/com/company/architecture/auth/AuthDto.java
new file mode 100644
index 0000000..10b9453
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/AuthDto.java
@@ -0,0 +1,6 @@
+package com.company.architecture.auth;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record AuthDto(@NotBlank String username, @NotBlank String password) {
+}
diff --git a/source/src/main/java/com/company/architecture/auth/AuthFilter.java b/source/src/main/java/com/company/architecture/auth/AuthFilter.java
new file mode 100644
index 0000000..2a13a41
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/AuthFilter.java
@@ -0,0 +1,33 @@
+package com.company.architecture.auth;
+
+import jakarta.annotation.Nonnull;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class AuthFilter extends OncePerRequestFilter {
+ private final JwtService jwtService;
+
+ @Override
+ protected void doFilterInternal(final HttpServletRequest request, final @Nonnull HttpServletResponse response, final @Nonnull FilterChain filterChain) throws ServletException, IOException {
+ final var jwt = StringUtils.removeStart(StringUtils.defaultString(request.getHeader(HttpHeaders.AUTHORIZATION)), "Bearer").trim();
+
+ if (jwtService.verify(jwt)) {
+ SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.authenticated(jwtService.getSubject(jwt), null, jwtService.getAuthorities(jwt)));
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/auth/AuthService.java b/source/src/main/java/com/company/architecture/auth/AuthService.java
new file mode 100644
index 0000000..abd3c61
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/AuthService.java
@@ -0,0 +1,24 @@
+package com.company.architecture.auth;
+
+import com.company.architecture.shared.exception.ApplicationException;
+import com.company.architecture.user.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+ private final PasswordEncoder passwordEncoder;
+ private final JwtService jwtService;
+ private final UserRepository userRepository;
+
+ public String auth(final AuthDto dto) {
+ return userRepository
+ .findByUsername(dto.username())
+ .filter(user -> passwordEncoder.matches(dto.password(), user.getPassword()))
+ .map(jwtService::create)
+ .orElseThrow(() -> new ApplicationException(HttpStatus.UNAUTHORIZED, "auth.unauthorized"));
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/auth/Authority.java b/source/src/main/java/com/company/architecture/auth/Authority.java
new file mode 100644
index 0000000..1dc34b5
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/Authority.java
@@ -0,0 +1,6 @@
+package com.company.architecture.auth;
+
+public enum Authority {
+ DEFAULT,
+ ADMINISTRATOR
+}
diff --git a/source/src/main/java/com/company/architecture/auth/JwtService.java b/source/src/main/java/com/company/architecture/auth/JwtService.java
new file mode 100644
index 0000000..7857245
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/auth/JwtService.java
@@ -0,0 +1,61 @@
+package com.company.architecture.auth;
+
+import com.auth0.jwk.JwkProviderBuilder;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.company.architecture.user.User;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.stereotype.Service;
+
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.List;
+
+@Service
+public class JwtService {
+ private final Algorithm algorithm;
+
+ public JwtService() throws NoSuchAlgorithmException {
+ final var keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
+ algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate());
+ }
+
+ public String create(final User user) {
+ final var authorities = user.getAuthorities().stream().map(Enum::name).toArray(String[]::new);
+ return JWT.create().withSubject(user.getId().toString()).withArrayClaim("authorities", authorities).sign(algorithm);
+ }
+
+ public boolean verify(final String jwt) {
+ try {
+ algorithm.verify(JWT.decode(jwt));
+ return true;
+ } catch (Exception exception) {
+ return false;
+ }
+ }
+
+ public boolean verifyInProvider(final String jwt) {
+ try {
+ final var decoded = JWT.decode(jwt);
+ final var provider = new JwkProviderBuilder("DOMAIN").build();
+ final var jwk = provider.get(decoded.getKeyId());
+ final var key = (RSAKey) jwk.getPublicKey();
+ Algorithm.RSA256(key).verify(decoded);
+ return true;
+ } catch (Exception exception) {
+ return false;
+ }
+ }
+
+ public String getSubject(final String jwt) {
+ return JWT.decode(jwt).getSubject();
+ }
+
+ public List getAuthorities(final String jwt) {
+ return AuthorityUtils.createAuthorityList(JWT.decode(jwt).getClaim("authorities").asList(String.class));
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/aws/AwsConfiguration.java b/source/src/main/java/com/company/architecture/aws/AwsConfiguration.java
new file mode 100644
index 0000000..9c1a24a
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/aws/AwsConfiguration.java
@@ -0,0 +1,36 @@
+package com.company.architecture.aws;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.sqs.SqsAsyncClient;
+
+import java.net.URI;
+import java.util.Objects;
+
+@RequiredArgsConstructor
+@Configuration
+public class AwsConfiguration {
+ private final Environment environment;
+
+ @Bean
+ public SqsAsyncClient sqsAsyncClient() {
+ final var client = SqsAsyncClient.builder().endpointOverride(endpoint()).build();
+ client.createQueue(builder -> builder.queueName(environment.getProperty("spring.cloud.aws.sqs.queue")));
+ return client;
+ }
+
+ @Bean
+ public S3Client s3Client() {
+ final var client = S3Client.builder().endpointOverride(endpoint()).serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()).build();
+ client.createBucket(builder -> builder.bucket(environment.getProperty("spring.cloud.aws.s3.bucket")));
+ return client;
+ }
+
+ private URI endpoint() {
+ return URI.create(Objects.requireNonNullElse(environment.getProperty("AWS_ENDPOINT"), environment.getProperty("aws.endpoint")));
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/aws/AwsController.java b/source/src/main/java/com/company/architecture/aws/AwsController.java
new file mode 100644
index 0000000..d0bfffc
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/aws/AwsController.java
@@ -0,0 +1,46 @@
+package com.company.architecture.aws;
+
+import com.company.architecture.shared.swagger.GetApiResponses;
+import com.company.architecture.shared.swagger.PostApiResponses;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+@Tag(name = "AWS")
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/aws")
+public class AwsController {
+ private final AwsService awsService;
+
+ @Operation(summary = "Send")
+ @PostApiResponses
+ @PostMapping("queues/send")
+ public void send(@RequestBody final String message) {
+ awsService.send(message);
+ }
+
+ @Operation(summary = "Upload")
+ @PostApiResponses
+ @PostMapping(value = "files/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
+ public String upload(@RequestParam MultipartFile file) throws IOException {
+ return awsService.upload(file).getFilename();
+ }
+
+ @Operation(summary = "Download")
+ @GetApiResponses
+ @GetMapping("files/download/{key}")
+ public ResponseEntity get(@PathVariable final String key) {
+ final var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+ return ResponseEntity.ok().headers(headers).body(awsService.download(key));
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/aws/AwsS3Service.java b/source/src/main/java/com/company/architecture/aws/AwsS3Service.java
new file mode 100644
index 0000000..0bbc7a4
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/aws/AwsS3Service.java
@@ -0,0 +1,39 @@
+package com.company.architecture.aws;
+
+import io.awspring.cloud.s3.ObjectMetadata;
+import io.awspring.cloud.s3.S3Resource;
+import io.awspring.cloud.s3.S3Template;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+@Service
+@RequiredArgsConstructor
+public class AwsS3Service {
+ private final S3Template s3Template;
+
+ public S3Resource store(final String bucket, final String key, final Object object) {
+ return s3Template.store(bucket, key, object);
+ }
+
+ public T read(final String bucket, final String key, final Class clazz) {
+ return s3Template.read(bucket, key, clazz);
+ }
+
+ public S3Resource upload(final String bucket, final MultipartFile file) throws IOException {
+ return upload(bucket, file.getOriginalFilename(), file.getBytes());
+ }
+
+ public S3Resource upload(final String bucket, final String key, final byte[] bytes) throws IOException {
+ return s3Template.upload(bucket, key, new ByteArrayInputStream(bytes), ObjectMetadata.builder().contentType(Files.probeContentType(Paths.get(key))).build());
+ }
+
+ public S3Resource download(final String bucket, final String key) {
+ return s3Template.download(bucket, key);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/aws/AwsService.java b/source/src/main/java/com/company/architecture/aws/AwsService.java
new file mode 100644
index 0000000..8f01e61
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/aws/AwsService.java
@@ -0,0 +1,42 @@
+package com.company.architecture.aws;
+
+import io.awspring.cloud.s3.S3Resource;
+import io.awspring.cloud.sqs.annotation.SqsListener;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AwsService {
+ private final AwsSqsService awsSqsService;
+ private final AwsS3Service awsS3Service;
+
+ @Value("${spring.cloud.aws.sqs.queue}")
+ private String queue;
+
+ @Value("${spring.cloud.aws.s3.bucket}")
+ private String bucket;
+
+ @SqsListener("${spring.cloud.aws.sqs.queue}")
+ public void listen(final Object object) {
+ log.info("[AwsSqsService.listen]: {}", object);
+ }
+
+ public void send(final Object object) {
+ awsSqsService.send(queue, object);
+ }
+
+ public S3Resource upload(final MultipartFile file) throws IOException {
+ return awsS3Service.upload(bucket, file.getOriginalFilename(), file.getBytes());
+ }
+
+ public S3Resource download(final String key) {
+ return awsS3Service.download(bucket, key);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/aws/AwsSqsService.java b/source/src/main/java/com/company/architecture/aws/AwsSqsService.java
new file mode 100644
index 0000000..1de1147
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/aws/AwsSqsService.java
@@ -0,0 +1,17 @@
+package com.company.architecture.aws;
+
+import io.awspring.cloud.sqs.operations.SqsTemplate;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AwsSqsService {
+ private final SqsTemplate sqsTemplate;
+
+ public void send(final String queue, final Object object) {
+ sqsTemplate.send(queue, object);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/category/Category.java b/source/src/main/java/com/company/architecture/category/Category.java
new file mode 100644
index 0000000..a35caba
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/category/Category.java
@@ -0,0 +1,6 @@
+package com.company.architecture.category;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record Category(@NotBlank String name) {
+}
diff --git a/source/src/main/java/com/company/architecture/category/CategoryCacheService.java b/source/src/main/java/com/company/architecture/category/CategoryCacheService.java
new file mode 100644
index 0000000..d029403
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/category/CategoryCacheService.java
@@ -0,0 +1,31 @@
+package com.company.architecture.category;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CategoryCacheService {
+ private static final String KEY = "Category";
+ private final CategoryRepository categoryRepository;
+
+ @Cacheable(KEY)
+ public List list() {
+ log.info("Cache -> Loading: {}", KEY);
+ return categoryRepository.list();
+ }
+
+ @CacheEvict(allEntries = true, cacheNames = {KEY})
+ @Scheduled(fixedRateString = "1", timeUnit = TimeUnit.HOURS)
+ public void cacheEvict() {
+ log.info("Cache -> Cleaning: {}", KEY);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/category/CategoryRepository.java b/source/src/main/java/com/company/architecture/category/CategoryRepository.java
new file mode 100644
index 0000000..756826d
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/category/CategoryRepository.java
@@ -0,0 +1,12 @@
+package com.company.architecture.category;
+
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public class CategoryRepository {
+ public List list() {
+ return List.of(new Category("Category"));
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/category/CategoryService.java b/source/src/main/java/com/company/architecture/category/CategoryService.java
new file mode 100644
index 0000000..43f4961
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/category/CategoryService.java
@@ -0,0 +1,16 @@
+package com.company.architecture.category;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class CategoryService {
+ private final CategoryCacheService categoryCacheService;
+
+ public List list() {
+ return categoryCacheService.list();
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/game/Game.java b/source/src/main/java/com/company/architecture/game/Game.java
new file mode 100644
index 0000000..1f6a708
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/game/Game.java
@@ -0,0 +1,6 @@
+package com.company.architecture.game;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record Game(@NotBlank String title) {
+}
diff --git a/source/src/main/java/com/company/architecture/game/GameRepository.java b/source/src/main/java/com/company/architecture/game/GameRepository.java
new file mode 100644
index 0000000..959b65f
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/game/GameRepository.java
@@ -0,0 +1,14 @@
+package com.company.architecture.game;
+
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public class GameRepository {
+ private static final List games = List.of(new Game("Game A"), new Game("Game B"));
+
+ public List list(final Game game) {
+ return games.stream().filter(item -> item.title().contains(game.title())).toList();
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/game/GameService.java b/source/src/main/java/com/company/architecture/game/GameService.java
new file mode 100644
index 0000000..16d7880
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/game/GameService.java
@@ -0,0 +1,16 @@
+package com.company.architecture.game;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class GameService {
+ private final GameRepository gameRepository;
+
+ public List list(final Game game) {
+ return gameRepository.list(game);
+ }
+}
diff --git a/source/src/main/java/com/company/architecture/group/GroupService.java b/source/src/main/java/com/company/architecture/group/GroupService.java
new file mode 100644
index 0000000..ab7409a
--- /dev/null
+++ b/source/src/main/java/com/company/architecture/group/GroupService.java
@@ -0,0 +1,36 @@
+package com.company.architecture.group;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.BeanWrapperImpl;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class GroupService {
+ public Map