From 9db246f9df2358037389c988d5dd8a2861111667 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Filip=20Paw=C5=82owicz?=
<117346592+pawlowiczf@users.noreply.github.com>
Date: Wed, 20 Aug 2025 19:56:38 +0200
Subject: [PATCH 01/17] add support for downloading gRPC stubs from remote
Nexus repository (#1)
---
pom.xml | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/pom.xml b/pom.xml
index f0dcaaf..a441778 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,12 @@
spring-cloud-starter-contract-verifier
test
+
+
+ com.ecmsp
+ protos
+ 1.0.0-SNAPSHOT
+
@@ -121,4 +127,16 @@
+
+
+ nexus-ecmsp-releases
+ nexus-ecmsp-releases
+ https://nexus.ecmsp.pl/repository/maven-releases/
+
+
+ nexus-ecmsp-snapshots
+ nexus-ecmsp-snapshots
+ https://nexus.ecmsp.pl/repository/maven-snapshots/
+
+
From c3e2c27d35e4a143981165729e283144612c745f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Filip=20Paw=C5=82owicz?=
<117346592+pawlowiczf@users.noreply.github.com>
Date: Mon, 25 Aug 2025 18:08:18 +0200
Subject: [PATCH 02/17] Create workflows to validate, build and push service to
remote GCP registry (#2)
* create workflows to validate, build and push service to remote GCP registry
* create workflows to validate, build and push service to remote GCP registry
* create workflows to validate, build and push service to remote GCP registry
---
.env | 24 ++++++
.../build-java-push-gcp-registry.yaml | 68 +++++++++++++++++
.../workflows/build-test-java-project.yaml | 45 +++++++++++
Dockerfile | 11 +++
docker/app-env.txt | 2 +
docker/database-env.txt | 2 +
docker/database-password.txt | 1 +
docker/docker-compose.yml | 75 +++++++++++++++++++
docker/init.sql | 0
pom.xml | 33 +++++---
.../resources/application-compose.properties | 11 +++
src/main/resources/application-dev.properties | 13 ++++
.../resources/application-prod.properties | 13 ++++
13 files changed, 287 insertions(+), 11 deletions(-)
create mode 100644 .env
create mode 100644 .github/workflows/build-java-push-gcp-registry.yaml
create mode 100644 .github/workflows/build-test-java-project.yaml
create mode 100644 Dockerfile
create mode 100644 docker/app-env.txt
create mode 100644 docker/database-env.txt
create mode 100644 docker/database-password.txt
create mode 100644 docker/docker-compose.yml
create mode 100644 docker/init.sql
create mode 100644 src/main/resources/application-compose.properties
create mode 100644 src/main/resources/application-dev.properties
create mode 100644 src/main/resources/application-prod.properties
diff --git a/.env b/.env
new file mode 100644
index 0000000..4200663
--- /dev/null
+++ b/.env
@@ -0,0 +1,24 @@
+CART_SERVICE_PORT=8100
+CART_SERVICE_DB_PORT=9100
+
+PAYMENT_SERVICE_PORT=8200
+PAYMENT_SERVICE_DB_PORT=9200
+
+ORDER_SERVICE_PORT=8300
+ORDER_SERVICE_DB_PORT=9300
+
+PRODUCT_SERVICE_PORT=8400
+PRODUCT_SERVICE_DB_PORT=9400
+
+#######################################
+
+# intentionally left blank (it is automatically set to 'compose' profile)
+PAYMENT_SERVICE_SPRING_PROFILES_ACTIVE=
+ORDER_SERVICE_SPRING_PROFILES_ACTIVE=
+PRODUCT_SERVICE_SPRING_PROFILES_ACTIVE=
+CART_SERVICE_SPRING_PROFILES_ACTIVE=
+
+#######################################
+
+COMPOSE_PROFILES=payment-service,order-service,cart-service,product-service,kafka
+
diff --git a/.github/workflows/build-java-push-gcp-registry.yaml b/.github/workflows/build-java-push-gcp-registry.yaml
new file mode 100644
index 0000000..2447053
--- /dev/null
+++ b/.github/workflows/build-java-push-gcp-registry.yaml
@@ -0,0 +1,68 @@
+name: Build and deploy Docker images to GCP Artifact Registry
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+env:
+ GCP_REGISTRY: ${{ secrets.ECMSP_GCP_ARTIFACT_REGISTRY }}
+ IMAGE_TAG: ${{ github.sha }}
+ IMAGE_NAME: ${{ github.event.repository.name }}
+
+jobs:
+ build_deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ -
+ name: Check out code
+ uses: actions/checkout@v3
+
+ -
+ name: Set up JDK 21
+ uses: actions/setup-java@v3
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ -
+ name: Update Maven snapshots
+ run: mvn -U dependency:resolve
+
+ -
+ name: Authenticate to Google Cloud
+ uses: google-github-actions/auth@v1
+ with:
+ credentials_json: ${{ secrets.ECMSP_REGISTRY_PUSHER_GCP_SA_KEY }}
+
+ -
+ name: Setup gcloud CLI
+ uses: google-github-actions/setup-gcloud@v2
+ with:
+ version: "latest"
+ project_id: ecmsp
+
+ -
+ name: Install gcloud auth
+ run: gcloud components install gke-gcloud-auth-plugin
+
+ -
+ name: Configure Docker for Artifact Registry
+ run: gcloud auth configure-docker europe-west1-docker.pkg.dev
+
+ -
+ name: Build, tag, and push backend Docker image to GCP Artifact Registry
+ run: |
+ docker build \
+ -t $GCP_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
+ -t $GCP_REGISTRY/$IMAGE_NAME:latest \
+ -f ./Dockerfile .
+
+ docker push $GCP_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
+ docker push $GCP_REGISTRY/$IMAGE_NAME:latest
+
+
+
diff --git a/.github/workflows/build-test-java-project.yaml b/.github/workflows/build-test-java-project.yaml
new file mode 100644
index 0000000..406cfdf
--- /dev/null
+++ b/.github/workflows/build-test-java-project.yaml
@@ -0,0 +1,45 @@
+name: Build and test Java project
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ build_test:
+ runs-on: ubuntu-latest
+ steps:
+ -
+ name: Checkout repository
+ uses: actions/checkout@v2
+
+ -
+ name: Set up JDK 21
+ uses: actions/setup-java@v3
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ -
+ name: Run tests with Maven
+ run: mvn clean test -U
+
+# -
+# name: Build Docker image for Trivy
+# if: github.event_name == 'push' && (github.event.pull_request.base.ref == 'test' || github.event.pull_request.base.ref == 'development')
+# run: |
+# docker build \
+# -t service:latest \
+# -f ./Dockerfile .
+#
+# -
+# name: Run Trivy vulnerability scanner
+# if: github.event_name == 'push' && (github.event.pull_request.base.ref == 'test' || github.event.pull_request.base.ref == 'development')
+# uses: aquasecurity/trivy-action@0.28.0
+# with:
+# image-ref: "service:latest"
+# format: table
+# exit-code: 1
+# ignore-unfixed: true
+# vuln-type: os,library
+# severity: CRITICAL,HIGH
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..580b49f
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM maven:3.9-eclipse-temurin-21 AS builder
+WORKDIR /app
+COPY pom.xml .
+COPY src ./src
+RUN mvn clean package
+
+FROM eclipse-temurin:21-jdk AS runtime
+WORKDIR /app
+
+COPY --from=builder /app/target/*.jar ./app.jar
+CMD ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/docker/app-env.txt b/docker/app-env.txt
new file mode 100644
index 0000000..d1158d6
--- /dev/null
+++ b/docker/app-env.txt
@@ -0,0 +1,2 @@
+SPRING_DATASOURCE_USERNAME=admin
+SPRING_DATASOURCE_PASSWORD=admin
\ No newline at end of file
diff --git a/docker/database-env.txt b/docker/database-env.txt
new file mode 100644
index 0000000..801e32f
--- /dev/null
+++ b/docker/database-env.txt
@@ -0,0 +1,2 @@
+POSTGRES_DB=user-service-db
+POSTGRES_USER=admin
\ No newline at end of file
diff --git a/docker/database-password.txt b/docker/database-password.txt
new file mode 100644
index 0000000..f77b004
--- /dev/null
+++ b/docker/database-password.txt
@@ -0,0 +1 @@
+admin
\ No newline at end of file
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000..4b9cd64
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,75 @@
+name: ecmsp-dev
+
+services:
+ user-service-db-dev:
+ image: postgres:15-alpine
+ container_name: user-service-db-dev
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
+ interval: 10s
+ retries: 5
+ start_period: 30s
+ timeout: 10s
+
+ ports:
+ - "5432:5432"
+ volumes:
+ - user-service-db-dev:/var/lib/postgresql/data
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+ secrets:
+ - user-service-db-password
+ environment:
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=user-service-db
+ - POSTGRES_PASSWORD_FILE=/run/secrets/user-service-db-password
+ env_file:
+ -
+ path: ./database-env.txt
+ required: false
+
+ kafka:
+ image: apache/kafka:3.7.0
+ container_name: broker
+ environment:
+ KAFKA_NODE_ID: 1
+ KAFKA_PROCESS_ROLES: broker,controller
+ KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:9092,EXTERNAL://localhost:9094
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
+ KAFKA_CONTROLLER_QUORUM_VOTERS: 1@broker:9093
+ KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ ports:
+ - "9094:9094"
+ healthcheck:
+ test:
+ [
+ "CMD-SHELL",
+ "./opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 > /dev/null 2>&1",
+ ]
+ interval: 10s
+ timeout: 10s
+ retries: 5
+
+ kafka-ui:
+ image: provectuslabs/kafka-ui:latest
+ container_name: kafka-ui
+ ports:
+ - "8088:8080"
+ environment:
+ KAFKA_CLUSTERS_0_NAME: local
+ KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: broker:9092
+ KAFKA_CLUSTERS_0_READONLY: "false"
+ # KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry:8081
+ depends_on:
+ kafka:
+ condition: service_healthy
+
+volumes:
+ user-service-db-dev:
+
+secrets:
+ user-service-db-password:
+ file: ./database-password.txt
\ No newline at end of file
diff --git a/docker/init.sql b/docker/init.sql
new file mode 100644
index 0000000..e69de29
diff --git a/pom.xml b/pom.xml
index a441778..a971294 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,8 +66,22 @@
test
- org.springframework.cloud
- spring-cloud-starter-contract-verifier
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.kafka
+ spring-kafka
+ 3.3.4
+
+
+ io.hypersistence
+ hypersistence-utils-hibernate-63
+ 3.10.1
+
+
+ com.h2database
+ h2
test
@@ -76,6 +90,7 @@
protos
1.0.0-SNAPSHOT
+
@@ -103,15 +118,6 @@
-
- org.springframework.cloud
- spring-cloud-contract-maven-plugin
- 4.3.0
- true
-
- JUNIT5
-
-
org.springframework.boot
spring-boot-maven-plugin
@@ -128,6 +134,11 @@
+
+ central
+ Maven Central
+ https://repo.maven.apache.org/maven2
+
nexus-ecmsp-releases
nexus-ecmsp-releases
diff --git a/src/main/resources/application-compose.properties b/src/main/resources/application-compose.properties
new file mode 100644
index 0000000..818d5dd
--- /dev/null
+++ b/src/main/resources/application-compose.properties
@@ -0,0 +1,11 @@
+spring.datasource.url=jdbc:postgresql://user-service-db:5432/user-service-db
+
+spring.kafka.bootstrap-servers=kafka:9092
+
+spring.kafka.consumer.group-id=user-service
+spring.kafka.consumer.auto-offset-reset=earliest
+spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
\ No newline at end of file
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
new file mode 100644
index 0000000..849915f
--- /dev/null
+++ b/src/main/resources/application-dev.properties
@@ -0,0 +1,13 @@
+spring.datasource.url=jdbc:postgresql://localhost:5432/user-service-db
+spring.datasource.username=admin
+spring.datasource.password=admin
+
+spring.kafka.bootstrap-servers=localhost:9094
+
+spring.kafka.consumer.group-id=user-service
+spring.kafka.consumer.auto-offset-reset=earliest
+spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
\ No newline at end of file
diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties
new file mode 100644
index 0000000..3c82733
--- /dev/null
+++ b/src/main/resources/application-prod.properties
@@ -0,0 +1,13 @@
+spring.datasource.url=
+spring.datasource.username=
+spring.datasource.password=
+
+spring.kafka.bootstrap-servers=
+
+spring.kafka.consumer.group-id=user-service
+spring.kafka.consumer.auto-offset-reset=earliest
+spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
\ No newline at end of file
From 2245b4abcaadcf7c18d49db5e0d7fe36074f3d6d Mon Sep 17 00:00:00 2001
From: Ariel
Date: Mon, 22 Sep 2025 09:36:07 +0200
Subject: [PATCH 03/17] Add jwt token generation
---
pom.xml | 22 ++++++++++
readme.md | 11 +++++
.../userservice/UserServiceApplication.java | 12 +++++-
.../userservice/api/auth/AuthController.java | 30 +++++++++++++
.../userservice/api/auth/AuthRequest.java | 4 ++
.../userservice/api/auth/AuthResponseDto.java | 4 ++
.../application/config/ApplicationConfig.java | 16 +++++++
.../adapter/generator/JwtTokenGenerator.java | 39 +++++++++++++++++
.../generator/JwtTokenGeneratorConfig.java | 42 +++++++++++++++++++
.../userservice/auth/config/AuthConfig.java | 15 +++++++
.../userservice/auth/domain/AuthFacade.java | 28 +++++++++++++
.../auth/domain/AuthenticationResult.java | 6 +++
.../ecmsp/userservice/auth/domain/Token.java | 8 ++++
.../auth/domain/TokenGenerator.java | 7 ++++
.../inmemory/InMemoryUserRepository.java | 38 +++++++++++++++++
.../InMemoryUserRepositoryConfig.java | 15 +++++++
.../userservice/user/config/UserConfig.java | 14 +++++++
.../user/domain/PasswordHasher.java | 11 +++++
.../ecmsp/userservice/user/domain/User.java | 5 +++
.../userservice/user/domain/UserFacade.java | 31 ++++++++++++++
.../ecmsp/userservice/user/domain/UserId.java | 11 +++++
.../user/domain/UserRepository.java | 9 ++++
.../userservice/user/domain/UserToCreate.java | 4 ++
src/main/resources/application-local.yml | 3 ++
src/main/resources/application.properties | 1 -
src/main/resources/application.yml | 7 ++++
src/main/resources/local/secrets/INSTALL | 4 ++
27 files changed, 394 insertions(+), 3 deletions(-)
create mode 100644 readme.md
create mode 100644 src/main/java/com/ecmsp/userservice/api/auth/AuthController.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java
create mode 100644 src/main/java/com/ecmsp/userservice/application/config/ApplicationConfig.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorConfig.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/config/AuthConfig.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/domain/AuthFacade.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/domain/AuthenticationResult.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/domain/Token.java
create mode 100644 src/main/java/com/ecmsp/userservice/auth/domain/TokenGenerator.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepositoryConfig.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/config/UserConfig.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/PasswordHasher.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/User.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/UserId.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/UserToCreate.java
create mode 100644 src/main/resources/application-local.yml
delete mode 100644 src/main/resources/application.properties
create mode 100644 src/main/resources/application.yml
create mode 100644 src/main/resources/local/secrets/INSTALL
diff --git a/pom.xml b/pom.xml
index f0dcaaf..7c14782 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,28 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.mindrot
+ jbcrypt
+ 0.4
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
org.springframework.boot
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..4377312
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,11 @@
+You can test the login endpoint using the following command. Just copy & paste it into HTTP request or any other REST client.
+Added user is hardcoded in inMemoryRepository.
+```
+POST http://localhost:8080/auth/authenticate
+Content-Type: application/json
+
+{
+ "login": "testuser",
+ "password": "testpassword"
+}
+```
\ No newline at end of file
diff --git a/src/main/java/com/ecmsp/userservice/UserServiceApplication.java b/src/main/java/com/ecmsp/userservice/UserServiceApplication.java
index 88b1482..87c9272 100644
--- a/src/main/java/com/ecmsp/userservice/UserServiceApplication.java
+++ b/src/main/java/com/ecmsp/userservice/UserServiceApplication.java
@@ -2,9 +2,17 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
-@SpringBootApplication
-public class UserServiceApplication {
+@SpringBootApplication(
+ exclude = {
+ DataSourceAutoConfiguration.class,
+ HibernateJpaAutoConfiguration.class,
+ }
+)
+public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
diff --git a/src/main/java/com/ecmsp/userservice/api/auth/AuthController.java b/src/main/java/com/ecmsp/userservice/api/auth/AuthController.java
new file mode 100644
index 0000000..ecb7cab
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/auth/AuthController.java
@@ -0,0 +1,30 @@
+package com.ecmsp.userservice.api.auth;
+
+import com.ecmsp.userservice.auth.domain.AuthFacade;
+import com.ecmsp.userservice.auth.domain.AuthenticationResult;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+
+ private final AuthFacade authFacade;
+
+ public AuthController(AuthFacade authFacade) {
+ this.authFacade = authFacade;
+ }
+
+
+ @PostMapping("/authenticate")
+ public ResponseEntity authenticate(@RequestBody AuthRequest request) {
+ AuthenticationResult result = authFacade.authenticate(request.login(), request.password());
+ return switch (result) {
+ case AuthenticationResult.Success success -> ResponseEntity.ok(new AuthResponseDto(success.token().value()));
+ case AuthenticationResult.Failure ignored -> ResponseEntity.badRequest().build();
+ };
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java b/src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java
new file mode 100644
index 0000000..764d7e8
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java
@@ -0,0 +1,4 @@
+package com.ecmsp.userservice.api.auth;
+
+public record AuthRequest(String login, String password) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java b/src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java
new file mode 100644
index 0000000..55e4a44
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java
@@ -0,0 +1,4 @@
+package com.ecmsp.userservice.api.auth;
+
+public record AuthResponseDto(String token) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/application/config/ApplicationConfig.java b/src/main/java/com/ecmsp/userservice/application/config/ApplicationConfig.java
new file mode 100644
index 0000000..c5c361c
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/application/config/ApplicationConfig.java
@@ -0,0 +1,16 @@
+package com.ecmsp.userservice.application.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Clock;
+
+@Configuration
+class ApplicationConfig {
+ @Bean
+ public Clock clock() {
+ return Clock.systemUTC();
+ }
+
+
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java b/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java
new file mode 100644
index 0000000..f416a9d
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java
@@ -0,0 +1,39 @@
+package com.ecmsp.userservice.auth.adapter.generator;
+
+import com.ecmsp.userservice.auth.domain.Token;
+import com.ecmsp.userservice.auth.domain.TokenGenerator;
+import com.ecmsp.userservice.user.domain.User;
+import io.jsonwebtoken.Jwts;
+
+import java.security.PrivateKey;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+
+class JwtTokenGenerator implements TokenGenerator {
+ private final PrivateKey privateKey;
+ private final Clock clock;
+
+
+ public JwtTokenGenerator(PrivateKey privateKey, Clock clock) {
+ this.privateKey = privateKey;
+ this.clock = clock;
+ }
+
+ @Override
+ public Token generate(User user) {
+ Instant now = clock.instant();
+ Instant expiration = now.plus(1, ChronoUnit.HOURS);
+
+ String jwt = Jwts.builder()
+ .subject(user.id().value().toString())
+ .claim("login", user.login())
+ .issuedAt(Date.from(now))
+ .expiration(Date.from(expiration))
+ .signWith(privateKey)
+ .compact();
+
+ return new Token(jwt);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorConfig.java b/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorConfig.java
new file mode 100644
index 0000000..8f4b313
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorConfig.java
@@ -0,0 +1,42 @@
+package com.ecmsp.userservice.auth.adapter.generator;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.time.Clock;
+import java.util.Base64;
+
+@Configuration
+@ConditionalOnProperty(
+ name = "auth.token-generator.type",
+ havingValue = "jwt"
+)
+class JwtTokenGeneratorConfig {
+
+ @Bean
+ public JwtTokenGenerator jwtTokenGenerator(Clock clock, @Value("${auth.token-generator.secret-key-file}") File secretKeyFile)
+ throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+ String keyString = Files.readString(secretKeyFile.toPath()).trim();
+
+ // Remove PEM headers and footers if present
+ keyString = keyString.replaceAll("-----BEGIN PRIVATE KEY-----", "")
+ .replaceAll("-----END PRIVATE KEY-----", "")
+ .replaceAll("\\s", "");
+
+ byte[] keyBytes = Base64.getDecoder().decode(keyString);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
+
+ return new JwtTokenGenerator(privateKey, clock);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/config/AuthConfig.java b/src/main/java/com/ecmsp/userservice/auth/config/AuthConfig.java
new file mode 100644
index 0000000..6fb5807
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/config/AuthConfig.java
@@ -0,0 +1,15 @@
+package com.ecmsp.userservice.auth.config;
+
+import com.ecmsp.userservice.auth.domain.AuthFacade;
+import com.ecmsp.userservice.auth.domain.TokenGenerator;
+import com.ecmsp.userservice.user.domain.UserFacade;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class AuthConfig {
+ @Bean
+ public AuthFacade authFacade(UserFacade userFacade, TokenGenerator generator){
+ return new AuthFacade(userFacade, generator);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/domain/AuthFacade.java b/src/main/java/com/ecmsp/userservice/auth/domain/AuthFacade.java
new file mode 100644
index 0000000..70b6e60
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/domain/AuthFacade.java
@@ -0,0 +1,28 @@
+package com.ecmsp.userservice.auth.domain;
+
+import com.ecmsp.userservice.user.domain.UserFacade;
+import org.mindrot.jbcrypt.BCrypt;
+
+public class AuthFacade {
+ private final UserFacade userFacade;
+ private final TokenGenerator tokenGenerator;
+
+ public AuthFacade(UserFacade userFacade, TokenGenerator tokenGenerator) {
+ this.userFacade = userFacade;
+ this.tokenGenerator = tokenGenerator;
+ }
+
+ public AuthenticationResult authenticate(String login, String password) {
+ return userFacade.findUserByLogin(login)
+ .map(user -> {
+ boolean passwordMatches = BCrypt.checkpw(password, user.passwordHash());
+ if (passwordMatches) {
+ return new AuthenticationResult.Success(tokenGenerator.generate(user));
+ }
+ return new AuthenticationResult.Failure("Invalid credentials");
+ })
+ .orElse(new AuthenticationResult.Failure("User not found"));
+ }
+
+
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/domain/AuthenticationResult.java b/src/main/java/com/ecmsp/userservice/auth/domain/AuthenticationResult.java
new file mode 100644
index 0000000..e291d81
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/domain/AuthenticationResult.java
@@ -0,0 +1,6 @@
+package com.ecmsp.userservice.auth.domain;
+
+public sealed interface AuthenticationResult {
+ record Success(Token token) implements AuthenticationResult {}
+ record Failure(String reason) implements AuthenticationResult {}
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/domain/Token.java b/src/main/java/com/ecmsp/userservice/auth/domain/Token.java
new file mode 100644
index 0000000..455c0e8
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/domain/Token.java
@@ -0,0 +1,8 @@
+package com.ecmsp.userservice.auth.domain;
+
+public record Token(String value) {
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/domain/TokenGenerator.java b/src/main/java/com/ecmsp/userservice/auth/domain/TokenGenerator.java
new file mode 100644
index 0000000..20e4866
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/auth/domain/TokenGenerator.java
@@ -0,0 +1,7 @@
+package com.ecmsp.userservice.auth.domain;
+
+import com.ecmsp.userservice.user.domain.User;
+
+public interface TokenGenerator {
+ Token generate(User user);
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
new file mode 100644
index 0000000..0b36a41
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
@@ -0,0 +1,38 @@
+package com.ecmsp.userservice.user.adapter.repository.inmemory;
+
+import com.ecmsp.userservice.user.domain.User;
+import com.ecmsp.userservice.user.domain.UserId;
+import com.ecmsp.userservice.user.domain.UserRepository;
+import org.mindrot.jbcrypt.BCrypt;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+class InMemoryUserRepository implements UserRepository {
+ private final Map users = Map.of(
+ new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")),
+ new User(new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")), "testuser", BCrypt.hashpw("testpassword", BCrypt.gensalt()))
+ );
+
+ @Override
+ public User save(User user) {
+ users.put(user.id(), user);
+ return user;
+ }
+
+ @Override
+ public Optional findById(UserId userId) {
+ User user = users.get(userId);
+ return Optional.ofNullable(user);
+ }
+
+ @Override
+ public Optional findByLogin(String login) {
+ return users.values().stream()
+ .filter(user -> user.login().equals(login))
+ .findFirst();
+ }
+
+
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepositoryConfig.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepositoryConfig.java
new file mode 100644
index 0000000..30bfcc4
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepositoryConfig.java
@@ -0,0 +1,15 @@
+package com.ecmsp.userservice.user.adapter.repository.inmemory;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConditionalOnProperty(value = "user.repository.type", havingValue = "in-memory")
+class InMemoryUserRepositoryConfig {
+
+ @Bean
+ InMemoryUserRepository inMemoryUserRepository() {
+ return new InMemoryUserRepository();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java b/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java
new file mode 100644
index 0000000..4e0d976
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java
@@ -0,0 +1,14 @@
+package com.ecmsp.userservice.user.config;
+
+import com.ecmsp.userservice.user.domain.UserFacade;
+import com.ecmsp.userservice.user.domain.UserRepository;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class UserConfig {
+ @Bean
+ public UserFacade userFacade(UserRepository userRepository){
+ return new UserFacade(userRepository);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/PasswordHasher.java b/src/main/java/com/ecmsp/userservice/user/domain/PasswordHasher.java
new file mode 100644
index 0000000..afc68ff
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/PasswordHasher.java
@@ -0,0 +1,11 @@
+package com.ecmsp.userservice.user.domain;
+
+import org.mindrot.jbcrypt.BCrypt;
+
+class PasswordHasher {
+ String hash(String password) {
+ return BCrypt.hashpw(password, BCrypt.gensalt());
+ }
+
+
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/User.java b/src/main/java/com/ecmsp/userservice/user/domain/User.java
new file mode 100644
index 0000000..1843e72
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/User.java
@@ -0,0 +1,5 @@
+package com.ecmsp.userservice.user.domain;
+
+public record User(UserId id, String login, String passwordHash) {
+
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
new file mode 100644
index 0000000..1b1d229
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
@@ -0,0 +1,31 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.Optional;
+import java.util.UUID;
+
+public class UserFacade {
+ private final UserRepository userRepository;
+ private final PasswordHasher passwordHasher = new PasswordHasher();
+
+ public UserFacade(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ public User createUser(UserToCreate userToCreate){
+ UserId userId = new UserId(UUID.randomUUID());
+ String hashedPassword = passwordHasher.hash(userToCreate.password());
+ User user = new User(userId, userToCreate.login(), hashedPassword);
+ return userRepository.save(user);
+ }
+
+ public Optional findUserById(UserId userId){
+ return userRepository.findById(userId);
+ }
+
+
+ public Optional findUserByLogin(String login) {
+ return userRepository.findByLogin(login);
+ }
+
+
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserId.java b/src/main/java/com/ecmsp/userservice/user/domain/UserId.java
new file mode 100644
index 0000000..2ffc6bf
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserId.java
@@ -0,0 +1,11 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.UUID;
+
+public record UserId(UUID value) {
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java b/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
new file mode 100644
index 0000000..06ed51d
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
@@ -0,0 +1,9 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.Optional;
+
+public interface UserRepository {
+ User save(User user);
+ Optional findById(UserId userId);
+ Optional findByLogin(String login);
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserToCreate.java b/src/main/java/com/ecmsp/userservice/user/domain/UserToCreate.java
new file mode 100644
index 0000000..96f08ca
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserToCreate.java
@@ -0,0 +1,4 @@
+package com.ecmsp.userservice.user.domain;
+
+public record UserToCreate(String login, String password) {
+}
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
new file mode 100644
index 0000000..e08da08
--- /dev/null
+++ b/src/main/resources/application-local.yml
@@ -0,0 +1,3 @@
+user:
+ repository:
+ type: in-memory
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index f9c237e..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=User service
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..0305910
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,7 @@
+spring:
+ application:
+ name: User service
+auth:
+ token-generator:
+ type: jwt
+ secret-key-file: classpath:local/secrets/local.private.key
\ No newline at end of file
diff --git a/src/main/resources/local/secrets/INSTALL b/src/main/resources/local/secrets/INSTALL
new file mode 100644
index 0000000..49a1168
--- /dev/null
+++ b/src/main/resources/local/secrets/INSTALL
@@ -0,0 +1,4 @@
+Generate private/public RSA4096 key pair:
+
+openssl genrsa > local.private.key
+openssl rsa -in local.private.key -pubout > local.public.key
\ No newline at end of file
From 03532ff56e77b8bf023b7903e3d0100fbfe4c337 Mon Sep 17 00:00:00 2001
From: Ariel
Date: Tue, 23 Sep 2025 12:10:14 +0200
Subject: [PATCH 04/17] Add posgres db setup
---
pom.xml | 22 +-
.../repository/db/DbUserRepository.java | 38 ++++
.../db/DbUserRepositoryConfiguration.java | 26 +++
.../adapter/repository/db/UserEntity.java | 64 ++++++
.../repository/db/UserEntityMapper.java | 23 ++
.../repository/db/UserEntityRepository.java | 14 ++
.../inmemory/InMemoryUserRepository.java | 3 +-
.../userservice/user/domain/UserFacade.java | 4 +-
.../user/domain/UserRepository.java | 2 +-
src/main/resources/application-local.yml | 1 +
src/main/resources/application.yml | 13 +-
.../repository/db/DbUserRepositoryTest.java | 202 ++++++++++++++++++
12 files changed, 400 insertions(+), 12 deletions(-)
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryConfiguration.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java
create mode 100644 src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
diff --git a/pom.xml b/pom.xml
index 7c14782..70782e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,11 +39,13 @@
org.springframework.boot
spring-boot-starter-validation
-
- org.springframework.boot
- spring-boot-starter-web
-
-
+
+ com.h2database
+ h2
+ test
+
+
+
org.mindrot
jbcrypt
0.4
@@ -90,9 +92,17 @@
org.springframework.cloud
spring-cloud-starter-contract-verifier
+
+
+ com.h2database
+ h2
test
-
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
new file mode 100644
index 0000000..20ea784
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
@@ -0,0 +1,38 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import com.ecmsp.userservice.user.domain.User;
+import com.ecmsp.userservice.user.domain.UserId;
+import com.ecmsp.userservice.user.domain.UserRepository;
+
+import java.util.Optional;
+
+class DbUserRepository implements UserRepository {
+ private final UserEntityMapper userMapper = new UserEntityMapper();
+ private final UserEntityRepository userEntityRepository;
+
+ DbUserRepository(UserEntityRepository userEntityRepository) {
+ this.userEntityRepository = userEntityRepository;
+ }
+
+
+ @Override
+ public void save(User user) {
+ if(userEntityRepository.existsById(user.id().value())) {
+ throw new RuntimeException("User already exists: " + user.id());
+ }
+ userEntityRepository.save(userMapper.toUserEntity(user));
+ }
+
+ //TODO: should all these finds be transactional?
+ @Override
+ public Optional findById(UserId userId) {
+ return userEntityRepository.findById(userId.value())
+ .map(userMapper::toUser);
+ }
+
+ @Override
+ public Optional findByLogin(String login) {
+ return userEntityRepository.findByLogin(login)
+ .map(userMapper::toUser);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryConfiguration.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryConfiguration.java
new file mode 100644
index 0000000..71f0bfe
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryConfiguration.java
@@ -0,0 +1,26 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import com.ecmsp.userservice.user.domain.UserRepository;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@Configuration
+@ConditionalOnProperty(
+ prefix = "user.repository",
+ name = "type",
+ havingValue = "db")
+@Import({DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
+@EnableJpaRepositories(basePackages = "com.ecmsp.userservice.user.adapter.repository.db")
+class DbUserRepositoryConfiguration {
+ @Bean
+ UserRepository dbUserRepository(UserEntityRepository userEntityRepository) {
+ return new DbUserRepository(
+ /* orderEntityRepository = */ userEntityRepository
+ );
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java
new file mode 100644
index 0000000..63b51bb
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java
@@ -0,0 +1,64 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "users")
+class UserEntity {
+
+ @Id
+ @Column(name = "user_id")
+ private UUID userId;
+
+ //TODO change it to username or email or keep both
+ @Column(name = "login")
+ private String login;
+
+ //it's already hashed
+ @Column(name = "password")
+ private String password;
+
+ public UUID getUserId() {
+ return userId;
+ }
+
+ public void setUserId(UUID userId) {
+ this.userId = userId;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public void setLogin(String login) {
+ this.login = login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ @Override
+ public String toString() {
+ return "UserEntity{" +
+ "userId=" + userId +
+ ", login='" + login + '\'' +
+ ", password='" + password + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
new file mode 100644
index 0000000..be71e13
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
@@ -0,0 +1,23 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import com.ecmsp.userservice.user.domain.User;
+import com.ecmsp.userservice.user.domain.UserId;
+
+class UserEntityMapper {
+
+ public User toUser(UserEntity entity) {
+ return new User(
+ new UserId(entity.getUserId()),
+ entity.getLogin(),
+ entity.getPassword()
+ );
+ }
+
+ public UserEntity toUserEntity(User user) {
+ return UserEntity.builder()
+ .userId(user.id().value())
+ .login(user.login())
+ .password(user.passwordHash())
+ .build();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java
new file mode 100644
index 0000000..06498dc
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java
@@ -0,0 +1,14 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+interface UserEntityRepository extends JpaRepository {
+ @Query("SELECT u FROM UserEntity u WHERE u.login = :login")
+ Optional findByLogin(String login);
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
index 0b36a41..2ad6875 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
@@ -16,9 +16,8 @@ class InMemoryUserRepository implements UserRepository {
);
@Override
- public User save(User user) {
+ public void save(User user) {
users.put(user.id(), user);
- return user;
}
@Override
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
index 1b1d229..d588f46 100644
--- a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
@@ -11,11 +11,11 @@ public UserFacade(UserRepository userRepository) {
this.userRepository = userRepository;
}
- public User createUser(UserToCreate userToCreate){
+ public void createUser(UserToCreate userToCreate){
UserId userId = new UserId(UUID.randomUUID());
String hashedPassword = passwordHasher.hash(userToCreate.password());
User user = new User(userId, userToCreate.login(), hashedPassword);
- return userRepository.save(user);
+ userRepository.save(user);
}
public Optional findUserById(UserId userId){
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java b/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
index 06ed51d..86a90b6 100644
--- a/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
@@ -3,7 +3,7 @@
import java.util.Optional;
public interface UserRepository {
- User save(User user);
+ void save(User user);
Optional findById(UserId userId);
Optional findByLogin(String login);
}
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
index e08da08..0efe0d1 100644
--- a/src/main/resources/application-local.yml
+++ b/src/main/resources/application-local.yml
@@ -1,3 +1,4 @@
+
user:
repository:
type: in-memory
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 0305910..d70dae6 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,7 +1,18 @@
spring:
application:
name: User service
+ jpa:
+ hibernate.ddl-auto: create # TODO: set to none once flyway integration is finished
+ properties.hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
+ show-sql: true
+ datasource:
+ url: jdbc:postgresql://localhost:5432/user-service-db
+ username: admin
+ password: admin
auth:
token-generator:
type: jwt
- secret-key-file: classpath:local/secrets/local.private.key
\ No newline at end of file
+ secret-key-file: classpath:local/secrets/local.private.key
+user:
+ repository:
+ type: db
\ No newline at end of file
diff --git a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
new file mode 100644
index 0000000..b6327c8
--- /dev/null
+++ b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
@@ -0,0 +1,202 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import com.ecmsp.userservice.user.domain.*;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DataJpaTest
+class DbUserRepositoryTest {
+
+ private static final UserId USER_1_ID = new UserId(UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
+ private static final UserId USER_2_ID = new UserId(UUID.fromString("9e349a18-1203-4224-829c-dc15700c68a5"));
+
+ private static final User USER_1 = new User(
+ /* id = */ USER_1_ID,
+ /* login = */ "user1",
+ /* passwordHash = */ "hashedPassword1"
+ );
+
+ private static final User USER_2 = new User(
+ /* id = */ USER_2_ID,
+ /* login = */ "user2",
+ /* passwordHash = */ "hashedPassword2"
+ );
+
+ @Autowired
+ private UserEntityRepository userEntityRepository;
+
+ @Autowired
+ private TestEntityManager testEntityManager;
+
+ @Test
+ void should_save_new_user() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // when:
+ repository.save(USER_1);
+
+ // then:
+ User savedUser = repository.findById(USER_1_ID).orElseThrow();
+ assertThat(savedUser).isEqualTo(USER_1);
+
+ // and:
+ UserEntity userEntity = testEntityManager.find(UserEntity.class, USER_1_ID.value());
+ assertThat(userEntity).isNotNull();
+ assertThat(userEntity.getUserId()).isEqualTo(USER_1_ID.value());
+ assertThat(userEntity.getLogin()).isEqualTo("user1");
+ assertThat(userEntity.getPassword()).isEqualTo("hashedPassword1");
+ }
+
+ @Test
+ void should_throw_exception_when_save_user_with_existing_id() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // and:
+ testEntityManager.persistAndFlush(
+ UserEntity.builder()
+ .userId(USER_1_ID.value())
+ .login("user1")
+ .password("hashedPassword1")
+ .build()
+ );
+
+ // when:
+ var error = assertThatThrownBy(() -> {
+ repository.save(USER_1); // trying to save user with the same ID
+ });
+
+ // then:
+ error.isInstanceOf(RuntimeException.class);
+ error.hasMessageContaining("User already exists: %s".formatted(USER_1_ID));
+ }
+
+ @Test
+ void should_find_user_by_id() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // and:
+ testEntityManager.persistAndFlush(
+ UserEntity.builder()
+ .userId(USER_1_ID.value())
+ .login("user1")
+ .password("hashedPassword1")
+ .build()
+ );
+
+ // when:
+ Optional user = repository.findById(USER_1_ID);
+
+ // then:
+ assertThat(user).isPresent();
+ assertThat(user.get()).isEqualTo(USER_1);
+ }
+
+ @Test
+ void should_return_empty_optional_when_user_with_given_id_not_exist() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // and:
+ testEntityManager.persistAndFlush(
+ UserEntity.builder()
+ .userId(USER_1_ID.value())
+ .login("user1")
+ .password("hashedPassword1")
+ .build()
+ );
+
+ // when:
+ Optional user = repository.findById(USER_2_ID); // USER_2_ID does not exist
+
+ // then:
+ assertThat(user).isEmpty();
+ }
+
+ @Test
+ void should_find_user_by_login() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // and:
+ testEntityManager.persistAndFlush(
+ UserEntity.builder()
+ .userId(USER_1_ID.value())
+ .login("user1")
+ .password("hashedPassword1")
+ .build()
+ );
+
+ // when:
+ Optional user = repository.findByLogin("user1");
+
+ // then:
+ assertThat(user).isPresent();
+ assertThat(user.get()).isEqualTo(USER_1);
+ }
+
+ @Test
+ void should_return_empty_optional_when_user_with_given_login_not_exist() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // and:
+ testEntityManager.persistAndFlush(
+ UserEntity.builder()
+ .userId(USER_1_ID.value())
+ .login("user1")
+ .password("hashedPassword1")
+ .build()
+ );
+
+ // when:
+ Optional user = repository.findByLogin("nonexistentuser"); // login does not exist
+
+ // then:
+ assertThat(user).isEmpty();
+ }
+
+ @Test
+ void should_find_correct_user_when_multiple_users_exist() {
+ // given:
+ UserRepository repository = new DbUserRepository(userEntityRepository);
+
+ // and:
+ testEntityManager.persist(
+ UserEntity.builder()
+ .userId(USER_1_ID.value())
+ .login("user1")
+ .password("hashedPassword1")
+ .build()
+ );
+ testEntityManager.persist(
+ UserEntity.builder()
+ .userId(USER_2_ID.value())
+ .login("user2")
+ .password("hashedPassword2")
+ .build()
+ );
+ testEntityManager.flush();
+
+ // when:
+ Optional user1 = repository.findByLogin("user1");
+ Optional user2 = repository.findByLogin("user2");
+
+ // then:
+ assertThat(user1).isPresent();
+ assertThat(user1.get()).isEqualTo(USER_1);
+ assertThat(user2).isPresent();
+ assertThat(user2.get()).isEqualTo(USER_2);
+ }
+
+}
\ No newline at end of file
From 4bbe47daa9af7ccea1c0837b28a1fc2b086fd7a6 Mon Sep 17 00:00:00 2001
From: pawlowiczf
Date: Wed, 24 Sep 2025 19:36:25 +0200
Subject: [PATCH 05/17] add, refactor docker, docker compose files
---
.env | 9 +-
docker-compose.yaml | 308 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 316 insertions(+), 1 deletion(-)
create mode 100644 docker-compose.yaml
diff --git a/.env b/.env
index 4200663..3309f31 100644
--- a/.env
+++ b/.env
@@ -10,6 +10,11 @@ ORDER_SERVICE_DB_PORT=9300
PRODUCT_SERVICE_PORT=8400
PRODUCT_SERVICE_DB_PORT=9400
+USER_SERVICE_PORT=8500
+USER_SERVICE_DB_PORT=9500
+
+GATEWAY_SERVICE_PORT=8600
+
#######################################
# intentionally left blank (it is automatically set to 'compose' profile)
@@ -17,8 +22,10 @@ PAYMENT_SERVICE_SPRING_PROFILES_ACTIVE=
ORDER_SERVICE_SPRING_PROFILES_ACTIVE=
PRODUCT_SERVICE_SPRING_PROFILES_ACTIVE=
CART_SERVICE_SPRING_PROFILES_ACTIVE=
+USER_SERVICE_SPRING_PROFILES_ACTIVE=
+GATEWAY_SERVICE_SPRING_PROFILES_ACTIVE=
#######################################
-COMPOSE_PROFILES=payment-service,order-service,cart-service,product-service,kafka
+COMPOSE_PROFILES=payment-service,order-service,cart-service,product-service,gateway-service,kafka
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..2f83061
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,308 @@
+name: ecmsp-full
+
+services:
+ gateway-service:
+ build:
+ context: ../gateway-service/
+ dockerfile: Dockerfile
+ container_name: gateway-service
+ ports:
+ - "${GATEWAY_SERVICE_PORT:?error}:8080"
+ env_file:
+ -
+ path: ../gateway-service/docker/app-env.txt
+ required: false
+ environment:
+ - SPRING_PROFILES_ACTIVE=${GATEWAY_SERVICE_SPRING_PROFILES_ACTIVE:-compose}
+ profiles: [gateway-service]
+
+ user-service:
+ build:
+ context: ../user-service/
+ dockerfile: Dockerfile
+ container_name: user-service
+ ports:
+ - "${USER_SERVICE_PORT:?error}:8080"
+ env_file:
+ -
+ path: ../user-service/docker/app-env.txt
+ required: false
+ environment:
+ - SPRING_PROFILES_ACTIVE=${USER_SERVICE_SPRING_PROFILES_ACTIVE:-compose}
+ profiles: [user-service]
+ user-service-db:
+ image: postgres:15-alpine
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
+ interval: 10s
+ retries: 5
+ start_period: 30s
+ timeout: 10s
+ ports:
+ - "${USER_SERVICE_DB_PORT:?error}:5432"
+ volumes:
+ - ../user-service/docker/init.sql:/docker-entrypoint-initdb.d/init.sql
+ - user-service-db:/var/lib/postgresql/data
+ secrets:
+ - user-service-db-password
+ environment:
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=user-service-db
+ - POSTGRES_PASSWORD_FILE=/run/secrets/user-service-db
+ env_file:
+ -
+ path: ../user-service-db/docker/database-env.txt
+ required: false
+ profiles: [user-service]
+
+ product-service:
+ build:
+ context: ../product-service
+ dockerfile: Dockerfile
+ container_name: product-service
+ ports:
+ - "${PRODUCT_SERVICE_PORT:?error}:8080"
+ env_file:
+ -
+ path: ../product-service/docker/app-env.txt
+ required: false
+ environment:
+ - SPRING_PROFILES_ACTIVE=${PRODUCT_SERVICE_SPRING_PROFILES_ACTIVE:-compose}
+ depends_on:
+ product-service-db:
+ restart: true
+ condition: service_healthy
+ kafka:
+ condition: service_healthy
+ profiles: [product-service]
+ #
+ product-service-db:
+ image: postgres:15-alpine
+ container_name: product-service-db
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
+ interval: 10s
+ retries: 5
+ start_period: 30s
+ timeout: 10s
+ ports:
+ - "${PRODUCT_SERVICE_DB_PORT:?error}:5432"
+ volumes:
+ - ../product-service/docker/init.sql:/docker-entrypoint-initdb.d/init.sql
+ - product-service-db:/var/lib/postgresql/data
+ secrets:
+ - product-service-db-password
+ environment:
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=product-service-db
+ - POSTGRES_PASSWORD_FILE=/run/secrets/product-service-db-password
+ env_file:
+ -
+ path: ../product-service/docker/database-env.txt
+ required: false
+ profiles: [product-service]
+ #
+
+ order-service:
+ build:
+ context: ../order-service
+ dockerfile: Dockerfile
+ container_name: order-service
+ ports:
+ - "${ORDER_SERVICE_PORT:?error}:8080"
+ env_file:
+ -
+ path: ../order-service/docker/app-env.txt
+ required: false
+ environment:
+ - SPRING_PROFILES_ACTIVE=${ORDER_SERVICE_SPRING_PROFILES_ACTIVE:-compose}
+ depends_on:
+ order-service-db:
+ restart: true
+ condition: service_healthy
+ kafka:
+ condition: service_healthy
+ profiles: [order-service]
+ #
+ order-service-db:
+ image: postgres:15-alpine
+ container_name: order-service-db
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
+ interval: 10s
+ retries: 5
+ start_period: 30s
+ timeout: 10s
+ ports:
+ - "${ORDER_SERVICE_DB_PORT:?error}:5432"
+ volumes:
+ - ../order-service/docker/init.sql:/docker-entrypoint-initdb.d/init.sql
+ - order-service-db:/var/lib/postgresql/data
+ secrets:
+ - order-service-db-password
+ environment:
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=order-service-db
+ - POSTGRES_PASSWORD_FILE=/run/secrets/order-service-db-password
+ env_file:
+ -
+ path: ../order-service/docker/database-env.txt
+ required: false
+ profiles: [order-service]
+ #
+
+ payment-service:
+ build:
+ context: ../payment-service
+ dockerfile: Dockerfile
+ container_name: payment-service
+ ports:
+ - "${PAYMENT_SERVICE_PORT:?error}:8080"
+ env_file:
+ -
+ path: ../payment-service/docker/app-env.txt
+ required: false
+ environment:
+ - SPRING_PROFILES_ACTIVE=${PAYMENT_SERVICE_SPRING_PROFILES_ACTIVE:-compose}
+ depends_on:
+ payment-service-db:
+ restart: true
+ condition: service_healthy
+ kafka:
+ condition: service_healthy
+ profiles: [payment-service]
+ #
+ payment-service-db:
+ image: postgres:15-alpine
+ container_name: payment-service-db
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
+ interval: 10s
+ retries: 5
+ start_period: 30s
+ timeout: 10s
+ ports:
+ - "${PAYMENT_SERVICE_DB_PORT:?error}:5432"
+ volumes:
+ - ../payment-service/docker/init.sql:/docker-entrypoint-initdb.d/init.sql
+ - payment-service-db:/var/lib/postgresql/data
+ secrets:
+ - payment-service-db-password
+ environment:
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=payment-service-db
+ - POSTGRES_PASSWORD_FILE=/run/secrets/payment-service-db-password
+ env_file:
+ -
+ path: ../payment-service/docker/database-env.txt
+ required: false
+ profiles: [payment-service]
+ #
+
+ cart-service:
+ build:
+ context: ../cart-service
+ dockerfile: Dockerfile
+ container_name: cart-service
+ ports:
+ - "${CART_SERVICE_PORT:?error}:8080"
+ env_file:
+ -
+ path: ../cart-service/docker/app-env.txt
+ required: false
+ environment:
+ - SPRING_PROFILES_ACTIVE=${CART_SERVICE_SPRING_PROFILES_ACTIVE:-compose}
+ depends_on:
+ cart-service-db:
+ restart: true
+ condition: service_healthy
+ kafka:
+ condition: service_healthy
+ profiles: [cart-service]
+ #
+ cart-service-db:
+ image: postgres:15-alpine
+ container_name: cart-service-db
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
+ interval: 10s
+ retries: 5
+ start_period: 30s
+ timeout: 10s
+ ports:
+ - "${CART_SERVICE_DB_PORT:?error}:5432"
+ volumes:
+ - ../cart-service/docker/init.sql:/docker-entrypoint-initdb.d/init.sql
+ - cart-service-db:/var/lib/postgresql/data
+ secrets:
+ - cart-service-db-password
+ environment:
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=cart-service-db
+ - POSTGRES_PASSWORD_FILE=/run/secrets/cart-service-db-password
+ env_file:
+ -
+ path: ../cart-service/docker/database-env.txt
+ required: false
+ profiles: [cart-service]
+ #
+
+ kafka:
+ image: apache/kafka:3.7.0
+ container_name: broker
+ environment:
+ KAFKA_NODE_ID: 1
+ KAFKA_PROCESS_ROLES: broker,controller
+ KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:9092,EXTERNAL://localhost:9094
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
+ KAFKA_CONTROLLER_QUORUM_VOTERS: 1@broker:9093
+ KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ ports:
+ - "9094:9094"
+ healthcheck:
+ test:
+ [
+ "CMD-SHELL",
+ "./opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 > /dev/null 2>&1",
+ ]
+ interval: 10s
+ timeout: 10s
+ retries: 5
+ profiles: [kafka]
+
+ kafka-ui:
+ image: provectuslabs/kafka-ui:latest
+ container_name: kafka-ui
+ ports:
+ - "8088:8080"
+ environment:
+ KAFKA_CLUSTERS_0_NAME: local
+ KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: broker:9092
+ KAFKA_CLUSTERS_0_READONLY: "false"
+ # KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry:8081
+ depends_on:
+ kafka:
+ condition: service_healthy
+ profiles: [kafka]
+
+secrets:
+ product-service-db-password:
+ file: ../product-service/docker/database-password.txt
+ order-service-db-password:
+ file: ../order-service/docker/database-password.txt
+ payment-service-db-password:
+ file: ../payment-service/docker/database-password.txt
+ cart-service-db-password:
+ file: ../cart-service/docker/database-password.txt
+ user-service-db-password:
+ file: ../user-service/docker/database-password.txt
+volumes:
+ product-service-db:
+ order-service-db:
+ payment-service-db:
+ cart-service-db:
+ user-service-db:
From d8cb49f488e8ce6dc02e7ec471a16e815b038f81 Mon Sep 17 00:00:00 2001
From: Ariel
Date: Wed, 24 Sep 2025 00:41:13 +0200
Subject: [PATCH 06/17] Add flyway for migrations
---
pom.xml | 64 ++++++++++++++++---
.../userservice/UserServiceApplication.java | 1 -
.../userservice/api/rest/UserController.java | 29 +++++++++
.../api/rest/UserCreateRequest.java | 4 ++
.../api/{ => rest}/auth/AuthController.java | 2 +-
.../api/{ => rest}/auth/AuthRequest.java | 2 +-
.../api/{ => rest}/auth/AuthResponseDto.java | 2 +-
.../config/LocalPostgreSQLConfiguration.java | 35 ++++++++++
.../repository/db/DbUserRepository.java | 1 -
src/main/resources/application-local.yml | 7 +-
src/main/resources/application.yml | 12 ++--
.../migration/V1__Create_initial_schema.sql | 10 +++
.../db/migration/V2__Insert_example_data.sql | 9 +++
13 files changed, 159 insertions(+), 19 deletions(-)
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/UserController.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/UserCreateRequest.java
rename src/main/java/com/ecmsp/userservice/api/{ => rest}/auth/AuthController.java (96%)
rename src/main/java/com/ecmsp/userservice/api/{ => rest}/auth/AuthRequest.java (57%)
rename src/main/java/com/ecmsp/userservice/api/{ => rest}/auth/AuthResponseDto.java (52%)
create mode 100644 src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java
create mode 100644 src/main/resources/db/migration/V1__Create_initial_schema.sql
create mode 100644 src/main/resources/db/migration/V2__Insert_example_data.sql
diff --git a/pom.xml b/pom.xml
index 70782e1..c0163f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,10 @@
jbcrypt
0.4
+
+ org.springframework.boot
+ spring-boot-starter-web
+
io.jsonwebtoken
jjwt-api
@@ -102,6 +106,34 @@
org.springframework.boot
spring-boot-starter-data-jpa
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ 3.5.3
+
+
+ org.testcontainers
+ testcontainers
+ 1.21.3
+
+
+
+ org.testcontainers
+ postgresql
+ 1.21.3
+
+
+ org.flywaydb
+ flyway-core
+ 11.11.2
+
+
+
+ org.flywaydb
+ flyway-database-postgresql
+ 11.11.2
+ runtime
+
@@ -129,15 +161,6 @@
-
- org.springframework.cloud
- spring-cloud-contract-maven-plugin
- 4.3.0
- true
-
- JUNIT5
-
-
org.springframework.boot
spring-boot-maven-plugin
@@ -150,6 +173,29 @@
+
+ org.flywaydb
+ flyway-maven-plugin
+ 11.11.2
+
+ jdbc:postgresql://localhost:5432/order-service-db
+ admin
+ admin
+
+ public
+
+
+ classpath:db/migration
+
+
+
+
+ org.postgresql
+ postgresql
+ 42.7.4
+
+
+
diff --git a/src/main/java/com/ecmsp/userservice/UserServiceApplication.java b/src/main/java/com/ecmsp/userservice/UserServiceApplication.java
index 87c9272..759bfbb 100644
--- a/src/main/java/com/ecmsp/userservice/UserServiceApplication.java
+++ b/src/main/java/com/ecmsp/userservice/UserServiceApplication.java
@@ -3,7 +3,6 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
-import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
@SpringBootApplication(
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/UserController.java b/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
new file mode 100644
index 0000000..fa6a16e
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
@@ -0,0 +1,29 @@
+package com.ecmsp.userservice.api.rest;
+
+
+import com.ecmsp.userservice.user.domain.UserFacade;
+import com.ecmsp.userservice.user.domain.UserToCreate;
+import org.springframework.cloud.contract.spec.internal.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/users")
+public class UserController {
+
+ private final UserFacade userFacade;
+
+ public UserController(UserFacade userFacade) {
+ this.userFacade = userFacade;
+ }
+
+ @PostMapping
+ public ResponseEntity createUser(@RequestBody UserCreateRequest request) {
+ UserToCreate user = new UserToCreate(request.login(), request.password());
+ userFacade.createUser(user);
+ return ResponseEntity.status(HttpStatus.CREATED).build();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/UserCreateRequest.java b/src/main/java/com/ecmsp/userservice/api/rest/UserCreateRequest.java
new file mode 100644
index 0000000..b097445
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/UserCreateRequest.java
@@ -0,0 +1,4 @@
+package com.ecmsp.userservice.api.rest;
+
+public record UserCreateRequest(String login, String password) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/auth/AuthController.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
similarity index 96%
rename from src/main/java/com/ecmsp/userservice/api/auth/AuthController.java
rename to src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
index ecb7cab..497aea5 100644
--- a/src/main/java/com/ecmsp/userservice/api/auth/AuthController.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
@@ -1,4 +1,4 @@
-package com.ecmsp.userservice.api.auth;
+package com.ecmsp.userservice.api.rest.auth;
import com.ecmsp.userservice.auth.domain.AuthFacade;
import com.ecmsp.userservice.auth.domain.AuthenticationResult;
diff --git a/src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthRequest.java
similarity index 57%
rename from src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java
rename to src/main/java/com/ecmsp/userservice/api/rest/auth/AuthRequest.java
index 764d7e8..e6072a4 100644
--- a/src/main/java/com/ecmsp/userservice/api/auth/AuthRequest.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthRequest.java
@@ -1,4 +1,4 @@
-package com.ecmsp.userservice.api.auth;
+package com.ecmsp.userservice.api.rest.auth;
public record AuthRequest(String login, String password) {
}
diff --git a/src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponseDto.java
similarity index 52%
rename from src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java
rename to src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponseDto.java
index 55e4a44..1fef208 100644
--- a/src/main/java/com/ecmsp/userservice/api/auth/AuthResponseDto.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponseDto.java
@@ -1,4 +1,4 @@
-package com.ecmsp.userservice.api.auth;
+package com.ecmsp.userservice.api.rest.auth;
public record AuthResponseDto(String token) {
}
diff --git a/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java b/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java
new file mode 100644
index 0000000..46385f8
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java
@@ -0,0 +1,35 @@
+package com.ecmsp.userservice.application.config;
+
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.PortBinding;
+import com.github.dockerjava.api.model.Ports;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+@Configuration
+@Profile("local")
+public class LocalPostgreSQLConfiguration {
+
+ @Bean
+ @ServiceConnection
+ PostgreSQLContainer> postgresContainer() {
+ return new PostgreSQLContainer<>("postgres:15")
+ .withDatabaseName("user-service-db")
+ .withCreateContainerCmdModifier(cmd ->
+ cmd
+ .withName("postgres-user-db")
+ .getHostConfig().withPortBindings(
+ new PortBinding(
+ /* hostPort = */ Ports.Binding.bindPort(5432),
+ /* containerPort = */ new ExposedPort(5432)
+ )
+ )
+ )
+ .withUsername("admin")
+ .withPassword("admin");
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
index 20ea784..e129363 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
@@ -23,7 +23,6 @@ public void save(User user) {
userEntityRepository.save(userMapper.toUserEntity(user));
}
- //TODO: should all these finds be transactional?
@Override
public Optional findById(UserId userId) {
return userEntityRepository.findById(userId.value())
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
index 0efe0d1..528b1fe 100644
--- a/src/main/resources/application-local.yml
+++ b/src/main/resources/application-local.yml
@@ -1,4 +1,9 @@
+spring:
+ datasource:
+ url: jdbc:postgresql://localhost:5432/user-service-db
+ username: admin
+ password: admin
user:
repository:
- type: in-memory
\ No newline at end of file
+ type: db
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index d70dae6..0d2bfef 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,13 +2,17 @@ spring:
application:
name: User service
jpa:
- hibernate.ddl-auto: create # TODO: set to none once flyway integration is finished
+ hibernate.ddl-auto: none # TODO: set to none once flyway integration is finished
properties.hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
show-sql: true
datasource:
- url: jdbc:postgresql://localhost:5432/user-service-db
- username: admin
- password: admin
+ url: ${DB_URL}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
+ flyway:
+ enabled: true
+ locations: classpath:db/migration
+ baseline-on-migrate: true
auth:
token-generator:
type: jwt
diff --git a/src/main/resources/db/migration/V1__Create_initial_schema.sql b/src/main/resources/db/migration/V1__Create_initial_schema.sql
new file mode 100644
index 0000000..f9f8f27
--- /dev/null
+++ b/src/main/resources/db/migration/V1__Create_initial_schema.sql
@@ -0,0 +1,10 @@
+
+CREATE TABLE users
+(
+ user_id UUID PRIMARY KEY,
+ login VARCHAR(100) NOT NULL,
+ password VARCHAR(100) NOT NULL
+);
+
+
+CREATE INDEX idx_users_login ON users (login);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V2__Insert_example_data.sql b/src/main/resources/db/migration/V2__Insert_example_data.sql
new file mode 100644
index 0000000..1238ba1
--- /dev/null
+++ b/src/main/resources/db/migration/V2__Insert_example_data.sql
@@ -0,0 +1,9 @@
+-- Insert example users for testing and development
+
+INSERT INTO users (user_id, login, password) VALUES
+ ('550e8400-e29b-41d4-a716-446655440000', 'john_doe', '$2a$10$N9qo8uLOickgx2ZMRZoMye7VPKRdQqYBBgOFGhqxfDGqCxo8Ckzq2'), -- password: password123
+ ('550e8400-e29b-41d4-a716-446655440001', 'jane_smith', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.'), -- password: secret456
+ ('550e8400-e29b-41d4-a716-446655440002', 'admin_user', '$2a$10$DOwJoMGp.NiaVisaQeK2eOhZYkyiwQQw2O.CzpG9k4CrcSKTF9q8e'); -- password: admin789
+
+-- Note: Passwords are hashed using BCrypt with salt rounds = 10
+-- These are example hashes for demonstration purposes
\ No newline at end of file
From 94f079d5140c49726ca766cb66f0ed96237ad3ea Mon Sep 17 00:00:00 2001
From: Ariel
Date: Wed, 24 Sep 2025 21:33:24 +0200
Subject: [PATCH 07/17] Update readme
---
pom.xml | 2 --
readme.md | 27 +++++++++++++++++--
.../userservice/api/rest/UserController.java | 2 +-
.../UserServiceApplicationTests.java | 16 +++++------
4 files changed, 34 insertions(+), 13 deletions(-)
diff --git a/pom.xml b/pom.xml
index c057928..658e504 100644
--- a/pom.xml
+++ b/pom.xml
@@ -94,8 +94,6 @@
test
- org.springframework.cloud
- spring-cloud-starter-contract-verifier
org.springframework.boot
spring-boot-starter-data-jpa
diff --git a/readme.md b/readme.md
index 4377312..0742d0a 100644
--- a/readme.md
+++ b/readme.md
@@ -1,5 +1,28 @@
-You can test the login endpoint using the following command. Just copy & paste it into HTTP request or any other REST client.
-Added user is hardcoded in inMemoryRepository.
+# User Service
+
+## Installation
+
+**Generate RSA keys:**
+ ```bash
+ cd src/main/resources/local/secrets/
+ openssl genrsa > local.private.key
+ openssl rsa -in local.private.key -pubout > local.public.key
+ ```
+
+## User Controller API
+
+### Create User
+```
+POST http://localhost:8080/api/users
+Content-Type: application/json
+
+{
+ "login": "newuser",
+ "password": "password123"
+}
+```
+
+### Authenticate User
```
POST http://localhost:8080/auth/authenticate
Content-Type: application/json
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/UserController.java b/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
index fa6a16e..b6219b9 100644
--- a/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
@@ -3,7 +3,7 @@
import com.ecmsp.userservice.user.domain.UserFacade;
import com.ecmsp.userservice.user.domain.UserToCreate;
-import org.springframework.cloud.contract.spec.internal.HttpStatus;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
diff --git a/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java b/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java
index e9f320f..00f2140 100644
--- a/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java
+++ b/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java
@@ -3,11 +3,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
-@SpringBootTest
-class UserServiceApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
+//@SpringBootTest
+//class UserServiceApplicationTests {
+//
+// @Test
+// void contextLoads() {
+// }
+//
+//}
From ecb5bc0fd63ac4a6463d29191d2e74b9a618f5ac Mon Sep 17 00:00:00 2001
From: Ariel
Date: Sun, 28 Sep 2025 18:26:24 +0200
Subject: [PATCH 08/17] Add Jwt token generation test
---
readme.md | 4 +-
.../api/rest/auth/AuthController.java | 4 +-
src/main/resources/application.yml | 5 +-
.../db/migration/V2__Insert_example_data.sql | 3 +-
.../generator/JwtTokenGeneratorTest.java | 186 ++++++++++++++++++
5 files changed, 197 insertions(+), 5 deletions(-)
create mode 100644 src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
diff --git a/readme.md b/readme.md
index 0742d0a..c3fc35e 100644
--- a/readme.md
+++ b/readme.md
@@ -17,8 +17,8 @@ POST http://localhost:8080/api/users
Content-Type: application/json
{
- "login": "newuser",
- "password": "password123"
+ "login": "testuser",
+ "password": "testpassword"
}
```
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
index 497aea5..a9d8402 100644
--- a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
@@ -9,7 +9,7 @@
import org.springframework.web.bind.annotation.RestController;
@RestController
-@RequestMapping("/auth")
+@RequestMapping("/api/auth")
public class AuthController {
private final AuthFacade authFacade;
@@ -19,6 +19,7 @@ public AuthController(AuthFacade authFacade) {
}
+ //TODO: reconsider rename to login
@PostMapping("/authenticate")
public ResponseEntity authenticate(@RequestBody AuthRequest request) {
AuthenticationResult result = authFacade.authenticate(request.login(), request.password());
@@ -27,4 +28,5 @@ public ResponseEntity authenticate(@RequestBody AuthRequest req
case AuthenticationResult.Failure ignored -> ResponseEntity.badRequest().build();
};
}
+
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 0d2bfef..f983943 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -19,4 +19,7 @@ auth:
secret-key-file: classpath:local/secrets/local.private.key
user:
repository:
- type: db
\ No newline at end of file
+ type: db
+#for testing
+server:
+ port: 8500
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V2__Insert_example_data.sql b/src/main/resources/db/migration/V2__Insert_example_data.sql
index 1238ba1..90db716 100644
--- a/src/main/resources/db/migration/V2__Insert_example_data.sql
+++ b/src/main/resources/db/migration/V2__Insert_example_data.sql
@@ -3,7 +3,8 @@
INSERT INTO users (user_id, login, password) VALUES
('550e8400-e29b-41d4-a716-446655440000', 'john_doe', '$2a$10$N9qo8uLOickgx2ZMRZoMye7VPKRdQqYBBgOFGhqxfDGqCxo8Ckzq2'), -- password: password123
('550e8400-e29b-41d4-a716-446655440001', 'jane_smith', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.'), -- password: secret456
- ('550e8400-e29b-41d4-a716-446655440002', 'admin_user', '$2a$10$DOwJoMGp.NiaVisaQeK2eOhZYkyiwQQw2O.CzpG9k4CrcSKTF9q8e'); -- password: admin789
+ ('550e8400-e29b-41d4-a716-446655440002', 'admin_user', '$2a$10$DOwJoMGp.NiaVisaQeK2eOhZYkyiwQQw2O.CzpG9k4CrcSKTF9q8e'), -- password: admin789
+ ('123e4567-e89b-12d3-a456-426614174001', 'andy', '$2a$12$Oox0qAgn0eVRfyNEy1CGIeSRJ9lmMVl7kWaZ5/UPKkWz0c28ZAzDq'); -- password: password123
-- Note: Passwords are hashed using BCrypt with salt rounds = 10
-- These are example hashes for demonstration purposes
\ No newline at end of file
diff --git a/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java b/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
new file mode 100644
index 0000000..5fa3bd9
--- /dev/null
+++ b/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
@@ -0,0 +1,186 @@
+package com.ecmsp.userservice.auth.adapter.generator;
+
+import com.ecmsp.userservice.auth.domain.Token;
+import com.ecmsp.userservice.user.domain.User;
+import com.ecmsp.userservice.user.domain.UserId;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class JwtTokenGeneratorTest {
+
+ private JwtTokenGenerator jwtTokenGenerator;
+ private PrivateKey privateKey;
+ private PublicKey publicKey;
+ private Clock fixedClock;
+ private final Instant fixedInstant = Instant.now();
+
+ @BeforeEach
+ void setUp() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ privateKey = keyPair.getPrivate();
+ publicKey = keyPair.getPublic();
+ fixedClock = Clock.fixed(fixedInstant, ZoneOffset.UTC);
+
+ jwtTokenGenerator = new JwtTokenGenerator(privateKey, fixedClock);
+ }
+ @Test
+ void shouldGenerateValidJwtToken() {
+ UUID userId = UUID.fromString("123e4567-e89b-12d3-a456-426614174001");
+ String login = "andy";
+ User user = new User(new UserId(userId), login, "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+ System.out.println("Generated Token: " + token.value());
+
+ assertNotNull(token);
+ assertNotNull(token.value());
+ assertFalse(token.value().isEmpty());
+ }
+
+ @Test
+ void shouldIncludeCorrectSubjectInToken() {
+ UUID userId = UUID.randomUUID();
+ String login = "testuser";
+ User user = new User(new UserId(userId), login, "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ Claims claims = parseTokenClaims(token.value());
+ assertEquals(userId.toString(), claims.getSubject());
+ }
+
+ @Test
+ void shouldIncludeLoginClaimInToken() {
+ UUID userId = UUID.randomUUID();
+ String login = "testuser";
+ User user = new User(new UserId(userId), login, "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ Claims claims = parseTokenClaims(token.value());
+ assertEquals(login, claims.get("login"));
+ }
+
+ @Test
+ void shouldSetCorrectIssuedAtTime() {
+ UUID userId = UUID.randomUUID();
+ User user = new User(new UserId(userId), "testuser", "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ Claims claims = parseTokenClaims(token.value());
+ long expectedTime = fixedInstant.getEpochSecond();
+ long actualTime = claims.getIssuedAt().toInstant().getEpochSecond();
+ assertEquals(expectedTime, actualTime);
+ }
+
+ @Test
+ void shouldSetExpirationTimeOneHourFromIssuedAt() {
+ UUID userId = UUID.randomUUID();
+ User user = new User(new UserId(userId), "testuser", "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ Claims claims = parseTokenClaims(token.value());
+ long expectedExpirationTime = fixedInstant.plusSeconds(3600).getEpochSecond(); // 1 hour
+ long actualExpirationTime = claims.getExpiration().toInstant().getEpochSecond();
+ assertEquals(expectedExpirationTime, actualExpirationTime);
+ }
+
+ @Test
+ void shouldSignTokenWithProvidedPrivateKey() {
+ UUID userId = UUID.randomUUID();
+ User user = new User(new UserId(userId), "testuser", "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ assertDoesNotThrow(() -> {
+ Jwts.parser()
+ .verifyWith(publicKey)
+ .build()
+ .parseSignedClaims(token.value());
+ });
+ }
+
+ @Test
+ void shouldThrowExceptionWhenVerifyingWithWrongKey() throws Exception {
+ UUID userId = UUID.randomUUID();
+ User user = new User(new UserId(userId), "testuser", "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair wrongKeyPair = keyPairGenerator.generateKeyPair();
+ PublicKey wrongPublicKey = wrongKeyPair.getPublic();
+
+ assertThrows(Exception.class, () -> Jwts.parser()
+ .verifyWith(wrongPublicKey)
+ .build()
+ .parseSignedClaims(token.value()));
+ }
+
+ @Test
+ void shouldGenerateDifferentTokensForDifferentUsers() {
+ User user1 = new User(new UserId(UUID.randomUUID()), "user1", "password1");
+ User user2 = new User(new UserId(UUID.randomUUID()), "user2", "password2");
+
+ Token token1 = jwtTokenGenerator.generate(user1);
+ Token token2 = jwtTokenGenerator.generate(user2);
+
+ assertNotEquals(token1.value(), token2.value());
+ }
+
+ @Test
+ void shouldGenerateDifferentTokensAtDifferentTimes() {
+ UUID userId = UUID.randomUUID();
+ User user = new User(new UserId(userId), "testuser", "hashedPassword");
+
+ Token token1 = jwtTokenGenerator.generate(user);
+
+ Clock laterClock = Clock.fixed(fixedInstant.plusSeconds(60), ZoneOffset.UTC);
+ JwtTokenGenerator laterGenerator = new JwtTokenGenerator(privateKey, laterClock);
+ Token token2 = laterGenerator.generate(user);
+
+ assertNotEquals(token1.value(), token2.value());
+ }
+
+ @Test
+ void shouldGenerateCompactJwtFormat() {
+ UUID userId = UUID.randomUUID();
+ User user = new User(new UserId(userId), "testuser", "hashedPassword");
+
+ Token token = jwtTokenGenerator.generate(user);
+
+ String[] parts = token.value().split("\\.");
+ assertEquals(3, parts.length, "JWT should have 3 parts separated by dots");
+
+ for (String part : parts) {
+ assertFalse(part.isEmpty(), "Each JWT part should not be empty");
+ }
+ }
+
+ private Claims parseTokenClaims(String token) {
+ return Jwts.parser()
+ .verifyWith(publicKey)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+}
\ No newline at end of file
From 526b57793d2cae5b571fc3d9c2a6a1c22fe7bac2 Mon Sep 17 00:00:00 2001
From: Ariel
Date: Sun, 28 Sep 2025 22:04:23 +0200
Subject: [PATCH 09/17] Update readme
---
readme.md | 4 ++--
.../com/ecmsp/userservice/api/rest/auth/AuthController.java | 2 --
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/readme.md b/readme.md
index c3fc35e..143dc55 100644
--- a/readme.md
+++ b/readme.md
@@ -13,7 +13,7 @@
### Create User
```
-POST http://localhost:8080/api/users
+POST http://localhost:8500/api/users
Content-Type: application/json
{
@@ -24,7 +24,7 @@ Content-Type: application/json
### Authenticate User
```
-POST http://localhost:8080/auth/authenticate
+POST http://localhost:8500/api/auth/authenticate
Content-Type: application/json
{
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
index a9d8402..0eb217d 100644
--- a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
@@ -18,8 +18,6 @@ public AuthController(AuthFacade authFacade) {
this.authFacade = authFacade;
}
-
- //TODO: reconsider rename to login
@PostMapping("/authenticate")
public ResponseEntity authenticate(@RequestBody AuthRequest request) {
AuthenticationResult result = authFacade.authenticate(request.login(), request.password());
From 72f5114587b905fe049a546f3badde4cb33d753b Mon Sep 17 00:00:00 2001
From: Ariel
Date: Sun, 28 Sep 2025 22:13:18 +0200
Subject: [PATCH 10/17] Remove debug printing
---
.../auth/adapter/generator/JwtTokenGeneratorTest.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java b/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
index 5fa3bd9..5060bb5 100644
--- a/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
+++ b/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
@@ -46,7 +46,6 @@ void shouldGenerateValidJwtToken() {
User user = new User(new UserId(userId), login, "hashedPassword");
Token token = jwtTokenGenerator.generate(user);
- System.out.println("Generated Token: " + token.value());
assertNotNull(token);
assertNotNull(token.value());
From 5de765ded6e5aa9351fe4d073b27e85465e31432 Mon Sep 17 00:00:00 2001
From: Ariel
Date: Mon, 29 Sep 2025 10:05:42 +0200
Subject: [PATCH 11/17] update db port
---
pom.xml | 2 +-
.../application/config/LocalPostgreSQLConfiguration.java | 2 +-
src/main/resources/application-dev.properties | 2 +-
src/main/resources/application-local.yml | 2 +-
src/main/resources/application.yml | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/pom.xml b/pom.xml
index 658e504..e1e540a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -196,7 +196,7 @@
flyway-maven-plugin
11.11.2
- jdbc:postgresql://localhost:5432/order-service-db
+ jdbc:postgresql://localhost:9500/user-service-db
admin
admin
diff --git a/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java b/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java
index 46385f8..fc87579 100644
--- a/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java
+++ b/src/main/java/com/ecmsp/userservice/application/config/LocalPostgreSQLConfiguration.java
@@ -23,7 +23,7 @@ PostgreSQLContainer> postgresContainer() {
.withName("postgres-user-db")
.getHostConfig().withPortBindings(
new PortBinding(
- /* hostPort = */ Ports.Binding.bindPort(5432),
+ /* hostPort = */ Ports.Binding.bindPort(9500),
/* containerPort = */ new ExposedPort(5432)
)
)
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index 849915f..ef24ef5 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -1,4 +1,4 @@
-spring.datasource.url=jdbc:postgresql://localhost:5432/user-service-db
+spring.datasource.url=jdbc:postgresql://localhost:9500/user-service-db
spring.datasource.username=admin
spring.datasource.password=admin
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
index 528b1fe..b3c8937 100644
--- a/src/main/resources/application-local.yml
+++ b/src/main/resources/application-local.yml
@@ -1,6 +1,6 @@
spring:
datasource:
- url: jdbc:postgresql://localhost:5432/user-service-db
+ url: jdbc:postgresql://localhost:9500/user-service-db
username: admin
password: admin
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index f983943..7aad341 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -20,6 +20,6 @@ auth:
user:
repository:
type: db
-#for testing
+
server:
port: 8500
\ No newline at end of file
From 2abe476d6d88922ce44ce35081cd70e9d102beda Mon Sep 17 00:00:00 2001
From: Ariel
Date: Thu, 2 Oct 2025 16:30:39 +0200
Subject: [PATCH 12/17] fix db tests
---
.gitignore | 2 +-
pom.xml | 110 +++++-------------
.../api/rest/auth/AuthController.java | 4 +-
...AuthResponseDto.java => AuthResponse.java} | 2 +-
src/main/resources/application.yml | 2 +-
.../UserServiceApplicationTests.java | 3 +-
.../repository/db/DbUserRepositoryTest.java | 4 +
src/test/resources/application-test.yml | 25 ++++
8 files changed, 64 insertions(+), 88 deletions(-)
rename src/main/java/com/ecmsp/userservice/api/rest/auth/{AuthResponseDto.java => AuthResponse.java} (51%)
create mode 100644 src/test/resources/application-test.yml
diff --git a/.gitignore b/.gitignore
index 667aaef..2bcbfd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,7 @@ target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
-
+**/src/main/resources/local/secrets
### STS ###
.apt_generated
.classpath
diff --git a/pom.xml b/pom.xml
index e1e540a..d80a03e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,6 @@
21
- 2025.0.0
@@ -39,13 +38,7 @@
org.springframework.boot
spring-boot-starter-validation
-
- com.h2database
- h2
- test
-
-
-
+
org.mindrot
jbcrypt
0.4
@@ -97,54 +90,42 @@
org.springframework.boot
spring-boot-starter-data-jpa
+
+ com.h2database
+ h2
+ test
+
org.springframework.kafka
spring-kafka
3.3.4
- io.hypersistence
- hypersistence-utils-hibernate-63
- 3.10.1
+ org.springframework.boot
+ spring-boot-testcontainers
+ 3.5.3
- com.h2database
- h2
- test
+ org.testcontainers
+ testcontainers
+ 1.21.3
+
+
+ org.testcontainers
+ postgresql
+ 1.21.3
+
+
+ org.flywaydb
+ flyway-core
+ 11.11.2
+
+
+ org.flywaydb
+ flyway-database-postgresql
+ 11.11.2
+ runtime
-
-
- org.springframework.boot
- spring-boot-starter-data-jpa
-
-
- org.springframework.boot
- spring-boot-testcontainers
- 3.5.3
-
-
- org.testcontainers
- testcontainers
- 1.21.3
-
-
-
- org.testcontainers
- postgresql
- 1.21.3
-
-
- org.flywaydb
- flyway-core
- 11.11.2
-
-
-
- org.flywaydb
- flyway-database-postgresql
- 11.11.2
- runtime
-
com.ecmsp
protos
@@ -153,18 +134,6 @@
-
-
-
- org.springframework.cloud
- spring-cloud-dependencies
- ${spring-cloud.version}
- pom
- import
-
-
-
-
@@ -191,29 +160,6 @@
-
- org.flywaydb
- flyway-maven-plugin
- 11.11.2
-
- jdbc:postgresql://localhost:9500/user-service-db
- admin
- admin
-
- public
-
-
- classpath:db/migration
-
-
-
-
- org.postgresql
- postgresql
- 42.7.4
-
-
-
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
index 0eb217d..b2248cf 100644
--- a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthController.java
@@ -19,10 +19,10 @@ public AuthController(AuthFacade authFacade) {
}
@PostMapping("/authenticate")
- public ResponseEntity authenticate(@RequestBody AuthRequest request) {
+ public ResponseEntity authenticate(@RequestBody AuthRequest request) {
AuthenticationResult result = authFacade.authenticate(request.login(), request.password());
return switch (result) {
- case AuthenticationResult.Success success -> ResponseEntity.ok(new AuthResponseDto(success.token().value()));
+ case AuthenticationResult.Success success -> ResponseEntity.ok(new AuthResponse(success.token().value()));
case AuthenticationResult.Failure ignored -> ResponseEntity.badRequest().build();
};
}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponseDto.java b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponse.java
similarity index 51%
rename from src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponseDto.java
rename to src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponse.java
index 1fef208..1f6f34e 100644
--- a/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponseDto.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/auth/AuthResponse.java
@@ -1,4 +1,4 @@
package com.ecmsp.userservice.api.rest.auth;
-public record AuthResponseDto(String token) {
+public record AuthResponse(String token) {
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 7aad341..206ce4c 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,7 +2,7 @@ spring:
application:
name: User service
jpa:
- hibernate.ddl-auto: none # TODO: set to none once flyway integration is finished
+ hibernate.ddl-auto: validate
properties.hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
show-sql: true
datasource:
diff --git a/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java b/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java
index 00f2140..6a5d977 100644
--- a/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java
+++ b/src/test/java/com/ecmsp/userservice/UserServiceApplicationTests.java
@@ -2,10 +2,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+//@ActiveProfiles("test")
//@SpringBootTest
//class UserServiceApplicationTests {
-//
// @Test
// void contextLoads() {
// }
diff --git a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
index b6327c8..aef5fee 100644
--- a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
+++ b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
@@ -3,8 +3,10 @@
import com.ecmsp.userservice.user.domain.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.test.context.ActiveProfiles;
import java.util.Optional;
import java.util.UUID;
@@ -13,6 +15,8 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DataJpaTest
+@ActiveProfiles("test")
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
class DbUserRepositoryTest {
private static final UserId USER_1_ID = new UserId(UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
new file mode 100644
index 0000000..15116a7
--- /dev/null
+++ b/src/test/resources/application-test.yml
@@ -0,0 +1,25 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
+ driver-class-name: org.h2.Driver
+ jpa:
+ hibernate:
+ ddl-auto: validate
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.H2Dialect
+ show-sql: true
+ flyway:
+ enabled: true
+ locations: classpath:db/migration
+ baseline-on-migrate: true
+
+user:
+ repository:
+ type: db
+
+auth:
+ token-generator:
+ type: jwt
+ secret-key-file: classpath:local/secrets/local.private.key
+
From ca4954fbc1b4edc736224267670fda7e30156513 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wojciech=20Or=C5=82owski?=
Date: Thu, 2 Oct 2025 20:48:54 +0200
Subject: [PATCH 13/17] add roles and permissions
---
.../userservice/api/rest/UserController.java | 46 +++++++++-
.../api/rest/role/AddPermissionRequest.java | 6 ++
.../api/rest/role/AssignRoleRequest.java | 6 ++
.../api/rest/role/RoleController.java | 91 +++++++++++++++++++
.../api/rest/role/RoleCreateRequest.java | 8 ++
.../api/rest/role/RoleResponse.java | 9 ++
.../adapter/generator/JwtTokenGenerator.java | 10 ++
.../userservice/role/config/RoleConfig.java | 14 +++
.../repository/db/DbRoleRepository.java | 52 +++++++++++
.../repository/db/PermissionEntity.java | 26 ++++++
.../adapter/repository/db/RoleEntity.java | 57 ++++++++++++
.../repository/db/RoleEntityMapper.java | 37 ++++++++
.../repository/db/RoleEntityRepository.java | 10 ++
.../adapter/repository/db/UserEntity.java | 24 ++++-
.../repository/db/UserEntityMapper.java | 22 ++++-
.../userservice/user/config/UserConfig.java | 5 +-
.../userservice/user/domain/Permission.java | 22 +++++
.../ecmsp/userservice/user/domain/Role.java | 41 +++++++++
.../userservice/user/domain/RoleFacade.java | 50 ++++++++++
.../ecmsp/userservice/user/domain/RoleId.java | 6 ++
.../user/domain/RoleRepository.java | 12 +++
.../userservice/user/domain/RoleToCreate.java | 6 ++
.../ecmsp/userservice/user/domain/User.java | 4 +-
.../userservice/user/domain/UserFacade.java | 35 ++++++-
.../V3__Create_roles_and_permissions.sql | 44 +++++++++
25 files changed, 628 insertions(+), 15 deletions(-)
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/role/AddPermissionRequest.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/role/AssignRoleRequest.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/role/RoleController.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/role/RoleCreateRequest.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/rest/role/RoleResponse.java
create mode 100644 src/main/java/com/ecmsp/userservice/role/config/RoleConfig.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbRoleRepository.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/PermissionEntity.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntity.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityMapper.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityRepository.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/Permission.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/Role.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/RoleFacade.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/RoleId.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/RoleRepository.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/RoleToCreate.java
create mode 100644 src/main/resources/db/migration/V3__Create_roles_and_permissions.sql
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/UserController.java b/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
index b6219b9..10297d3 100644
--- a/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
+++ b/src/main/java/com/ecmsp/userservice/api/rest/UserController.java
@@ -1,14 +1,21 @@
package com.ecmsp.userservice.api.rest;
+import com.ecmsp.userservice.api.rest.role.AssignRoleRequest;
+import com.ecmsp.userservice.api.rest.role.RoleResponse;
+import com.ecmsp.userservice.user.domain.Role;
+import com.ecmsp.userservice.user.domain.RoleId;
import com.ecmsp.userservice.user.domain.UserFacade;
+import com.ecmsp.userservice.user.domain.UserId;
import com.ecmsp.userservice.user.domain.UserToCreate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/users")
@@ -26,4 +33,35 @@ public ResponseEntity createUser(@RequestBody UserCreateRequest request) {
userFacade.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
+
+ @PostMapping("/{userId}/roles")
+ public ResponseEntity assignRoleToUser(
+ @PathVariable UUID userId,
+ @RequestBody AssignRoleRequest request) {
+ userFacade.assignRoleToUser(new UserId(userId), new RoleId(request.roleId()));
+ return ResponseEntity.ok().build();
+ }
+
+ @DeleteMapping("/{userId}/roles/{roleId}")
+ public ResponseEntity removeRoleFromUser(
+ @PathVariable UUID userId,
+ @PathVariable UUID roleId) {
+ userFacade.removeRoleFromUser(new UserId(userId), new RoleId(roleId));
+ return ResponseEntity.ok().build();
+ }
+
+ @GetMapping("/{userId}/roles")
+ public ResponseEntity> getUserRoles(@PathVariable UUID userId) {
+ Set roles = userFacade.getUserRoles(new UserId(userId));
+
+ List roleResponses = roles.stream()
+ .map(role -> new RoleResponse(
+ role.id().value(),
+ role.name(),
+ role.permissions()
+ ))
+ .collect(Collectors.toList());
+
+ return ResponseEntity.ok(roleResponses);
+ }
}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/role/AddPermissionRequest.java b/src/main/java/com/ecmsp/userservice/api/rest/role/AddPermissionRequest.java
new file mode 100644
index 0000000..c5e86ba
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/role/AddPermissionRequest.java
@@ -0,0 +1,6 @@
+package com.ecmsp.userservice.api.rest.role;
+
+import com.ecmsp.userservice.user.domain.Permission;
+
+public record AddPermissionRequest(Permission permission) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/role/AssignRoleRequest.java b/src/main/java/com/ecmsp/userservice/api/rest/role/AssignRoleRequest.java
new file mode 100644
index 0000000..066b5da
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/role/AssignRoleRequest.java
@@ -0,0 +1,6 @@
+package com.ecmsp.userservice.api.rest.role;
+
+import java.util.UUID;
+
+public record AssignRoleRequest(UUID roleId) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/role/RoleController.java b/src/main/java/com/ecmsp/userservice/api/rest/role/RoleController.java
new file mode 100644
index 0000000..a179c24
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/role/RoleController.java
@@ -0,0 +1,91 @@
+package com.ecmsp.userservice.api.rest.role;
+
+import com.ecmsp.userservice.user.domain.Role;
+import com.ecmsp.userservice.user.domain.RoleFacade;
+import com.ecmsp.userservice.user.domain.RoleId;
+import com.ecmsp.userservice.user.domain.RoleToCreate;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api/roles")
+public class RoleController {
+
+ private final RoleFacade roleFacade;
+
+ public RoleController(RoleFacade roleFacade) {
+ this.roleFacade = roleFacade;
+ }
+
+ @PostMapping
+ public ResponseEntity createRole(@RequestBody RoleCreateRequest request) {
+ RoleToCreate roleToCreate = new RoleToCreate(request.name(), request.permissions());
+ RoleId roleId = roleFacade.createRole(roleToCreate);
+
+ Role createdRole = roleFacade.findRoleById(roleId)
+ .orElseThrow(() -> new IllegalStateException("Role was not created"));
+
+ RoleResponse response = new RoleResponse(
+ createdRole.id().value(),
+ createdRole.name(),
+ createdRole.permissions()
+ );
+
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ }
+
+ @GetMapping("/{roleId}")
+ public ResponseEntity getRoleById(@PathVariable UUID roleId) {
+ return roleFacade.findRoleById(new RoleId(roleId))
+ .map(role -> new RoleResponse(
+ role.id().value(),
+ role.name(),
+ role.permissions()
+ ))
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @GetMapping
+ public ResponseEntity> getAllRoles() {
+ List roles = roleFacade.getAllRoles().stream()
+ .map(role -> new RoleResponse(
+ role.id().value(),
+ role.name(),
+ role.permissions()
+ ))
+ .collect(Collectors.toList());
+
+ return ResponseEntity.ok(roles);
+ }
+
+ @PostMapping("/{roleId}/permissions")
+ public ResponseEntity addPermissionToRole(
+ @PathVariable UUID roleId,
+ @RequestBody AddPermissionRequest request) {
+ roleFacade.addPermissionToRole(new RoleId(roleId), request.permission());
+ return ResponseEntity.ok().build();
+ }
+
+ @DeleteMapping("/{roleId}/permissions/{permission}")
+ public ResponseEntity removePermissionFromRole(
+ @PathVariable UUID roleId,
+ @PathVariable String permission) {
+ roleFacade.removePermissionFromRole(
+ new RoleId(roleId),
+ com.ecmsp.userservice.user.domain.Permission.valueOf(permission)
+ );
+ return ResponseEntity.ok().build();
+ }
+
+ @DeleteMapping("/{roleId}")
+ public ResponseEntity deleteRole(@PathVariable UUID roleId) {
+ roleFacade.deleteRole(new RoleId(roleId));
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/role/RoleCreateRequest.java b/src/main/java/com/ecmsp/userservice/api/rest/role/RoleCreateRequest.java
new file mode 100644
index 0000000..8f90b94
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/role/RoleCreateRequest.java
@@ -0,0 +1,8 @@
+package com.ecmsp.userservice.api.rest.role;
+
+import com.ecmsp.userservice.user.domain.Permission;
+
+import java.util.Set;
+
+public record RoleCreateRequest(String name, Set permissions) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/rest/role/RoleResponse.java b/src/main/java/com/ecmsp/userservice/api/rest/role/RoleResponse.java
new file mode 100644
index 0000000..4fda4de
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/rest/role/RoleResponse.java
@@ -0,0 +1,9 @@
+package com.ecmsp.userservice.api.rest.role;
+
+import com.ecmsp.userservice.user.domain.Permission;
+
+import java.util.Set;
+import java.util.UUID;
+
+public record RoleResponse(UUID id, String name, Set permissions) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java b/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java
index f416a9d..296c780 100644
--- a/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java
+++ b/src/main/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGenerator.java
@@ -2,6 +2,7 @@
import com.ecmsp.userservice.auth.domain.Token;
import com.ecmsp.userservice.auth.domain.TokenGenerator;
+import com.ecmsp.userservice.user.domain.Permission;
import com.ecmsp.userservice.user.domain.User;
import io.jsonwebtoken.Jwts;
@@ -10,6 +11,8 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
+import java.util.Set;
+import java.util.stream.Collectors;
class JwtTokenGenerator implements TokenGenerator {
private final PrivateKey privateKey;
@@ -26,9 +29,16 @@ public Token generate(User user) {
Instant now = clock.instant();
Instant expiration = now.plus(1, ChronoUnit.HOURS);
+ // Collect all permissions from all user roles
+ Set permissions = user.roles().stream()
+ .flatMap(role -> role.permissions().stream())
+ .map(Permission::name)
+ .collect(Collectors.toSet());
+
String jwt = Jwts.builder()
.subject(user.id().value().toString())
.claim("login", user.login())
+ .claim("permissions", permissions)
.issuedAt(Date.from(now))
.expiration(Date.from(expiration))
.signWith(privateKey)
diff --git a/src/main/java/com/ecmsp/userservice/role/config/RoleConfig.java b/src/main/java/com/ecmsp/userservice/role/config/RoleConfig.java
new file mode 100644
index 0000000..51d3538
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/role/config/RoleConfig.java
@@ -0,0 +1,14 @@
+package com.ecmsp.userservice.role.config;
+
+import com.ecmsp.userservice.user.domain.RoleFacade;
+import com.ecmsp.userservice.user.domain.RoleRepository;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class RoleConfig {
+ @Bean
+ public RoleFacade roleFacade(RoleRepository roleRepository) {
+ return new RoleFacade(roleRepository);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbRoleRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbRoleRepository.java
new file mode 100644
index 0000000..161852f
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbRoleRepository.java
@@ -0,0 +1,52 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import com.ecmsp.userservice.user.domain.Role;
+import com.ecmsp.userservice.user.domain.RoleId;
+import com.ecmsp.userservice.user.domain.RoleRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Repository
+class DbRoleRepository implements RoleRepository {
+
+ private final RoleEntityRepository roleEntityRepository;
+ private final RoleEntityMapper roleEntityMapper;
+
+ public DbRoleRepository(RoleEntityRepository roleEntityRepository) {
+ this.roleEntityRepository = roleEntityRepository;
+ this.roleEntityMapper = new RoleEntityMapper();
+ }
+
+ @Override
+ public void save(Role role) {
+ RoleEntity entity = roleEntityMapper.toRoleEntity(role);
+ roleEntityRepository.save(entity);
+ }
+
+ @Override
+ public Optional findById(RoleId roleId) {
+ return roleEntityRepository.findById(roleId.value())
+ .map(roleEntityMapper::toRole);
+ }
+
+ @Override
+ public Optional findByName(String name) {
+ return roleEntityRepository.findByRoleName(name)
+ .map(roleEntityMapper::toRole);
+ }
+
+ @Override
+ public List findAll() {
+ return roleEntityRepository.findAll().stream()
+ .map(roleEntityMapper::toRole)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void delete(RoleId roleId) {
+ roleEntityRepository.deleteById(roleId.value());
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/PermissionEntity.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/PermissionEntity.java
new file mode 100644
index 0000000..43fd845
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/PermissionEntity.java
@@ -0,0 +1,26 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.NoArgsConstructor;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "permissions")
+class PermissionEntity {
+
+ @Id
+ @Column(name = "permission_name")
+ private String permissionName;
+
+ public String getPermissionName() {
+ return permissionName;
+ }
+
+ public void setPermissionName(String permissionName) {
+ this.permissionName = permissionName;
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntity.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntity.java
new file mode 100644
index 0000000..6cf28ae
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntity.java
@@ -0,0 +1,57 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.NoArgsConstructor;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "roles")
+class RoleEntity {
+
+ @Id
+ @Column(name = "role_id")
+ private UUID roleId;
+
+ @Column(name = "role_name", nullable = false, unique = true)
+ private String roleName;
+
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(
+ name = "role_permissions",
+ joinColumns = @JoinColumn(name = "role_id"),
+ inverseJoinColumns = @JoinColumn(name = "permission_name")
+ )
+ private Set permissions = new HashSet<>();
+
+ public UUID getRoleId() {
+ return roleId;
+ }
+
+ public void setRoleId(UUID roleId) {
+ this.roleId = roleId;
+ }
+
+ public String getRoleName() {
+ return roleName;
+ }
+
+ public void setRoleName(String roleName) {
+ this.roleName = roleName;
+ }
+
+ public Set getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(Set permissions) {
+ this.permissions = permissions;
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityMapper.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityMapper.java
new file mode 100644
index 0000000..1338f8e
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityMapper.java
@@ -0,0 +1,37 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import com.ecmsp.userservice.user.domain.Permission;
+import com.ecmsp.userservice.user.domain.Role;
+import com.ecmsp.userservice.user.domain.RoleId;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+class RoleEntityMapper {
+
+ public Role toRole(RoleEntity entity) {
+ Set permissions = entity.getPermissions().stream()
+ .map(permissionEntity -> Permission.valueOf(permissionEntity.getPermissionName()))
+ .collect(Collectors.toSet());
+
+ return new Role(
+ new RoleId(entity.getRoleId()),
+ entity.getRoleName(),
+ permissions
+ );
+ }
+
+ public RoleEntity toRoleEntity(Role role) {
+ Set permissionEntities = role.permissions().stream()
+ .map(permission -> PermissionEntity.builder()
+ .permissionName(permission.name())
+ .build())
+ .collect(Collectors.toSet());
+
+ return RoleEntity.builder()
+ .roleId(role.id().value())
+ .roleName(role.name())
+ .permissions(permissionEntities)
+ .build();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityRepository.java
new file mode 100644
index 0000000..1e8d548
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/RoleEntityRepository.java
@@ -0,0 +1,10 @@
+package com.ecmsp.userservice.user.adapter.repository.db;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+interface RoleEntityRepository extends JpaRepository {
+ Optional findByRoleName(String roleName);
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java
index 63b51bb..9ebed41 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntity.java
@@ -1,13 +1,12 @@
package com.ecmsp.userservice.user.adapter.repository.db;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
+import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
+import java.util.HashSet;
+import java.util.Set;
import java.util.UUID;
@Builder
@@ -29,6 +28,14 @@ class UserEntity {
@Column(name = "password")
private String password;
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(
+ name = "user_roles",
+ joinColumns = @JoinColumn(name = "user_id"),
+ inverseJoinColumns = @JoinColumn(name = "role_id")
+ )
+ private Set roles = new HashSet<>();
+
public UUID getUserId() {
return userId;
}
@@ -53,12 +60,21 @@ public void setPassword(String password) {
this.password = password;
}
+ public Set getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Set roles) {
+ this.roles = roles;
+ }
+
@Override
public String toString() {
return "UserEntity{" +
"userId=" + userId +
", login='" + login + '\'' +
", password='" + password + '\'' +
+ ", roles=" + roles +
'}';
}
}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
index be71e13..f16b750 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
@@ -1,23 +1,43 @@
package com.ecmsp.userservice.user.adapter.repository.db;
+import com.ecmsp.userservice.user.domain.Role;
import com.ecmsp.userservice.user.domain.User;
import com.ecmsp.userservice.user.domain.UserId;
+import java.util.Set;
+import java.util.stream.Collectors;
+
class UserEntityMapper {
+ private final RoleEntityMapper roleEntityMapper;
+
+ public UserEntityMapper() {
+ this.roleEntityMapper = new RoleEntityMapper();
+ }
+
public User toUser(UserEntity entity) {
+ Set roles = entity.getRoles().stream()
+ .map(roleEntityMapper::toRole)
+ .collect(Collectors.toSet());
+
return new User(
new UserId(entity.getUserId()),
entity.getLogin(),
- entity.getPassword()
+ entity.getPassword(),
+ roles
);
}
public UserEntity toUserEntity(User user) {
+ Set roleEntities = user.roles().stream()
+ .map(roleEntityMapper::toRoleEntity)
+ .collect(Collectors.toSet());
+
return UserEntity.builder()
.userId(user.id().value())
.login(user.login())
.password(user.passwordHash())
+ .roles(roleEntities)
.build();
}
}
diff --git a/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java b/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java
index 4e0d976..0090dd9 100644
--- a/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java
+++ b/src/main/java/com/ecmsp/userservice/user/config/UserConfig.java
@@ -1,5 +1,6 @@
package com.ecmsp.userservice.user.config;
+import com.ecmsp.userservice.user.domain.RoleRepository;
import com.ecmsp.userservice.user.domain.UserFacade;
import com.ecmsp.userservice.user.domain.UserRepository;
import org.springframework.context.annotation.Bean;
@@ -8,7 +9,7 @@
@Configuration
class UserConfig {
@Bean
- public UserFacade userFacade(UserRepository userRepository){
- return new UserFacade(userRepository);
+ public UserFacade userFacade(UserRepository userRepository, RoleRepository roleRepository){
+ return new UserFacade(userRepository, roleRepository);
}
}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/Permission.java b/src/main/java/com/ecmsp/userservice/user/domain/Permission.java
new file mode 100644
index 0000000..3ca403e
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/Permission.java
@@ -0,0 +1,22 @@
+package com.ecmsp.userservice.user.domain;
+
+public enum Permission {
+ // Product permissions
+ WRITE_PRODUCTS,
+ DELETE_PRODUCTS,
+
+ // Order permissions
+ READ_ORDERS,
+ WRITE_ORDERS,
+ CANCEL_ORDERS,
+
+ // User management permissions
+ MANAGE_USERS,
+
+ // Role management permissions
+ MANAGE_ROLES,
+
+ // Payment permissions
+ PROCESS_PAYMENTS,
+ REFUND_PAYMENTS
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/Role.java b/src/main/java/com/ecmsp/userservice/user/domain/Role.java
new file mode 100644
index 0000000..9fbd364
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/Role.java
@@ -0,0 +1,41 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Role {
+ private final RoleId id;
+ private final String name;
+ private final Set permissions;
+
+ public Role(RoleId id, String name, Set permissions) {
+ this.id = id;
+ this.name = name;
+ this.permissions = new HashSet<>(permissions);
+ }
+
+ public RoleId id() {
+ return id;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public Set permissions() {
+ return Collections.unmodifiableSet(permissions);
+ }
+
+ public void addPermission(Permission permission) {
+ permissions.add(permission);
+ }
+
+ public void removePermission(Permission permission) {
+ permissions.remove(permission);
+ }
+
+ public boolean hasPermission(Permission permission) {
+ return permissions.contains(permission);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/RoleFacade.java b/src/main/java/com/ecmsp/userservice/user/domain/RoleFacade.java
new file mode 100644
index 0000000..3562ae9
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/RoleFacade.java
@@ -0,0 +1,50 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class RoleFacade {
+ private final RoleRepository roleRepository;
+
+ public RoleFacade(RoleRepository roleRepository) {
+ this.roleRepository = roleRepository;
+ }
+
+ public RoleId createRole(RoleToCreate roleToCreate) {
+ RoleId roleId = new RoleId(UUID.randomUUID());
+ Role role = new Role(roleId, roleToCreate.name(), roleToCreate.permissions());
+ roleRepository.save(role);
+ return roleId;
+ }
+
+ public void addPermissionToRole(RoleId roleId, Permission permission) {
+ Role role = roleRepository.findById(roleId)
+ .orElseThrow(() -> new IllegalArgumentException("Role not found: " + roleId));
+ role.addPermission(permission);
+ roleRepository.save(role);
+ }
+
+ public void removePermissionFromRole(RoleId roleId, Permission permission) {
+ Role role = roleRepository.findById(roleId)
+ .orElseThrow(() -> new IllegalArgumentException("Role not found: " + roleId));
+ role.removePermission(permission);
+ roleRepository.save(role);
+ }
+
+ public Optional findRoleById(RoleId roleId) {
+ return roleRepository.findById(roleId);
+ }
+
+ public Optional findRoleByName(String name) {
+ return roleRepository.findByName(name);
+ }
+
+ public List getAllRoles() {
+ return roleRepository.findAll();
+ }
+
+ public void deleteRole(RoleId roleId) {
+ roleRepository.delete(roleId);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/RoleId.java b/src/main/java/com/ecmsp/userservice/user/domain/RoleId.java
new file mode 100644
index 0000000..8ca483a
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/RoleId.java
@@ -0,0 +1,6 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.UUID;
+
+public record RoleId(UUID value) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/RoleRepository.java b/src/main/java/com/ecmsp/userservice/user/domain/RoleRepository.java
new file mode 100644
index 0000000..7ac24eb
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/RoleRepository.java
@@ -0,0 +1,12 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface RoleRepository {
+ void save(Role role);
+ Optional findById(RoleId roleId);
+ Optional findByName(String name);
+ List findAll();
+ void delete(RoleId roleId);
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/RoleToCreate.java b/src/main/java/com/ecmsp/userservice/user/domain/RoleToCreate.java
new file mode 100644
index 0000000..af8ec84
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/RoleToCreate.java
@@ -0,0 +1,6 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.Set;
+
+public record RoleToCreate(String name, Set permissions) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/User.java b/src/main/java/com/ecmsp/userservice/user/domain/User.java
index 1843e72..97c3eb4 100644
--- a/src/main/java/com/ecmsp/userservice/user/domain/User.java
+++ b/src/main/java/com/ecmsp/userservice/user/domain/User.java
@@ -1,5 +1,7 @@
package com.ecmsp.userservice.user.domain;
-public record User(UserId id, String login, String passwordHash) {
+import java.util.Set;
+
+public record User(UserId id, String login, String passwordHash, Set roles) {
}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
index d588f46..8deaa9f 100644
--- a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
@@ -1,20 +1,24 @@
package com.ecmsp.userservice.user.domain;
+import java.util.HashSet;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
public class UserFacade {
private final UserRepository userRepository;
+ private final RoleRepository roleRepository;
private final PasswordHasher passwordHasher = new PasswordHasher();
- public UserFacade(UserRepository userRepository) {
+ public UserFacade(UserRepository userRepository, RoleRepository roleRepository) {
this.userRepository = userRepository;
+ this.roleRepository = roleRepository;
}
public void createUser(UserToCreate userToCreate){
UserId userId = new UserId(UUID.randomUUID());
String hashedPassword = passwordHasher.hash(userToCreate.password());
- User user = new User(userId, userToCreate.login(), hashedPassword);
+ User user = new User(userId, userToCreate.login(), hashedPassword, new HashSet<>());
userRepository.save(user);
}
@@ -22,10 +26,35 @@ public Optional findUserById(UserId userId){
return userRepository.findById(userId);
}
-
public Optional findUserByLogin(String login) {
return userRepository.findByLogin(login);
}
+ public void assignRoleToUser(UserId userId, RoleId roleId) {
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
+ Role role = roleRepository.findById(roleId)
+ .orElseThrow(() -> new IllegalArgumentException("Role not found: " + roleId));
+
+ Set updatedRoles = new HashSet<>(user.roles());
+ updatedRoles.add(role);
+ User updatedUser = new User(user.id(), user.login(), user.passwordHash(), updatedRoles);
+ userRepository.save(updatedUser);
+ }
+
+ public void removeRoleFromUser(UserId userId, RoleId roleId) {
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
+ Set updatedRoles = new HashSet<>(user.roles());
+ updatedRoles.removeIf(role -> role.id().equals(roleId));
+ User updatedUser = new User(user.id(), user.login(), user.passwordHash(), updatedRoles);
+ userRepository.save(updatedUser);
+ }
+
+ public Set getUserRoles(UserId userId) {
+ return userRepository.findById(userId)
+ .map(User::roles)
+ .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
+ }
}
diff --git a/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql b/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql
new file mode 100644
index 0000000..2a463b9
--- /dev/null
+++ b/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql
@@ -0,0 +1,44 @@
+-- Create permissions table
+CREATE TABLE permissions (
+ permission_name VARCHAR(100) PRIMARY KEY
+);
+
+-- Create roles table
+CREATE TABLE roles (
+ role_id UUID PRIMARY KEY,
+ role_name VARCHAR(100) NOT NULL UNIQUE
+);
+
+-- Create role_permissions join table
+CREATE TABLE role_permissions (
+ role_id UUID REFERENCES roles(role_id) ON DELETE CASCADE,
+ permission_name VARCHAR(100) REFERENCES permissions(permission_name) ON DELETE CASCADE,
+ PRIMARY KEY (role_id, permission_name)
+);
+
+-- Create user_roles join table
+CREATE TABLE user_roles (
+ user_id UUID REFERENCES users(user_id) ON DELETE CASCADE,
+ role_id UUID REFERENCES roles(role_id) ON DELETE CASCADE,
+ PRIMARY KEY (user_id, role_id)
+);
+
+-- Insert predefined permissions
+INSERT INTO permissions (permission_name) VALUES
+ ('READ_PRODUCTS'),
+ ('WRITE_PRODUCTS'),
+ ('DELETE_PRODUCTS'),
+ ('READ_ORDERS'),
+ ('WRITE_ORDERS'),
+ ('CANCEL_ORDERS'),
+ ('READ_CARTS'),
+ ('WRITE_CARTS'),
+ ('MANAGE_USERS'),
+ ('MANAGE_ROLES'),
+ ('PROCESS_PAYMENTS'),
+ ('REFUND_PAYMENTS');
+
+-- Create indexes for performance
+CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
+CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
+CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);
From f9def4fcba4b0c82722623e5ad527bade7f48b2f Mon Sep 17 00:00:00 2001
From: Ariel
Date: Sun, 5 Oct 2025 10:05:30 +0200
Subject: [PATCH 14/17] fix tests
---
.../inmemory/InMemoryUserRepository.java | 3 ++-
.../generator/JwtTokenGeneratorTest.java | 23 ++++++++++---------
.../repository/db/DbUserRepositoryTest.java | 7 ++++--
3 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
index 2ad6875..cb42193 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
@@ -7,12 +7,13 @@
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
class InMemoryUserRepository implements UserRepository {
private final Map users = Map.of(
new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")),
- new User(new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")), "testuser", BCrypt.hashpw("testpassword", BCrypt.gensalt()))
+ new User(new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")), "testuser", BCrypt.hashpw("testpassword", BCrypt.gensalt()), Set.of())
);
@Override
diff --git a/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java b/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
index 5060bb5..a3cc0da 100644
--- a/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
+++ b/src/test/java/com/ecmsp/userservice/auth/adapter/generator/JwtTokenGeneratorTest.java
@@ -15,6 +15,7 @@
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
+import java.util.Set;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@@ -43,7 +44,7 @@ void setUp() throws Exception {
void shouldGenerateValidJwtToken() {
UUID userId = UUID.fromString("123e4567-e89b-12d3-a456-426614174001");
String login = "andy";
- User user = new User(new UserId(userId), login, "hashedPassword");
+ User user = new User(new UserId(userId), login, "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -56,7 +57,7 @@ void shouldGenerateValidJwtToken() {
void shouldIncludeCorrectSubjectInToken() {
UUID userId = UUID.randomUUID();
String login = "testuser";
- User user = new User(new UserId(userId), login, "hashedPassword");
+ User user = new User(new UserId(userId), login, "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -68,7 +69,7 @@ void shouldIncludeCorrectSubjectInToken() {
void shouldIncludeLoginClaimInToken() {
UUID userId = UUID.randomUUID();
String login = "testuser";
- User user = new User(new UserId(userId), login, "hashedPassword");
+ User user = new User(new UserId(userId), login, "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -79,7 +80,7 @@ void shouldIncludeLoginClaimInToken() {
@Test
void shouldSetCorrectIssuedAtTime() {
UUID userId = UUID.randomUUID();
- User user = new User(new UserId(userId), "testuser", "hashedPassword");
+ User user = new User(new UserId(userId), "testuser", "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -92,7 +93,7 @@ void shouldSetCorrectIssuedAtTime() {
@Test
void shouldSetExpirationTimeOneHourFromIssuedAt() {
UUID userId = UUID.randomUUID();
- User user = new User(new UserId(userId), "testuser", "hashedPassword");
+ User user = new User(new UserId(userId), "testuser", "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -105,7 +106,7 @@ void shouldSetExpirationTimeOneHourFromIssuedAt() {
@Test
void shouldSignTokenWithProvidedPrivateKey() {
UUID userId = UUID.randomUUID();
- User user = new User(new UserId(userId), "testuser", "hashedPassword");
+ User user = new User(new UserId(userId), "testuser", "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -120,7 +121,7 @@ void shouldSignTokenWithProvidedPrivateKey() {
@Test
void shouldThrowExceptionWhenVerifyingWithWrongKey() throws Exception {
UUID userId = UUID.randomUUID();
- User user = new User(new UserId(userId), "testuser", "hashedPassword");
+ User user = new User(new UserId(userId), "testuser", "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
@@ -137,8 +138,8 @@ void shouldThrowExceptionWhenVerifyingWithWrongKey() throws Exception {
@Test
void shouldGenerateDifferentTokensForDifferentUsers() {
- User user1 = new User(new UserId(UUID.randomUUID()), "user1", "password1");
- User user2 = new User(new UserId(UUID.randomUUID()), "user2", "password2");
+ User user1 = new User(new UserId(UUID.randomUUID()), "user1", "password1", Set.of());
+ User user2 = new User(new UserId(UUID.randomUUID()), "user2", "password2", Set.of());
Token token1 = jwtTokenGenerator.generate(user1);
Token token2 = jwtTokenGenerator.generate(user2);
@@ -149,7 +150,7 @@ void shouldGenerateDifferentTokensForDifferentUsers() {
@Test
void shouldGenerateDifferentTokensAtDifferentTimes() {
UUID userId = UUID.randomUUID();
- User user = new User(new UserId(userId), "testuser", "hashedPassword");
+ User user = new User(new UserId(userId), "testuser", "hashedPassword", Set.of());
Token token1 = jwtTokenGenerator.generate(user);
@@ -163,7 +164,7 @@ void shouldGenerateDifferentTokensAtDifferentTimes() {
@Test
void shouldGenerateCompactJwtFormat() {
UUID userId = UUID.randomUUID();
- User user = new User(new UserId(userId), "testuser", "hashedPassword");
+ User user = new User(new UserId(userId), "testuser", "hashedPassword", Set.of());
Token token = jwtTokenGenerator.generate(user);
diff --git a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
index b6327c8..3febdca 100644
--- a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
+++ b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
@@ -7,6 +7,7 @@
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@@ -21,13 +22,15 @@ class DbUserRepositoryTest {
private static final User USER_1 = new User(
/* id = */ USER_1_ID,
/* login = */ "user1",
- /* passwordHash = */ "hashedPassword1"
+ /* passwordHash = */ "hashedPassword1",
+ /* roles = */ Set.of()
);
private static final User USER_2 = new User(
/* id = */ USER_2_ID,
/* login = */ "user2",
- /* passwordHash = */ "hashedPassword2"
+ /* passwordHash = */ "hashedPassword2",
+ Set.of()
);
@Autowired
From 363f6e0fd7e49d34e0ef06d3b60c23b3d0002b2e Mon Sep 17 00:00:00 2001
From: Ariel
Date: Thu, 16 Oct 2025 21:54:10 +0200
Subject: [PATCH 15/17] add example data
---
...ert_roles_and_permissions_example_data.sql | 67 +++++++++++++++++++
1 file changed, 67 insertions(+)
create mode 100644 src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql
diff --git a/src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql b/src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql
new file mode 100644
index 0000000..bd07d79
--- /dev/null
+++ b/src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql
@@ -0,0 +1,67 @@
+-- Insert example roles
+
+INSERT INTO roles (role_id, role_name) VALUES
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'ADMIN'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'USER'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'MANAGER'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'CUSTOMER_SUPPORT');
+
+-- Assign permissions to ADMIN role (full access)
+INSERT INTO role_permissions (role_id, permission_name) VALUES
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'READ_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'WRITE_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'DELETE_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'READ_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'WRITE_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'CANCEL_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'READ_CARTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'WRITE_CARTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'MANAGE_USERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'MANAGE_ROLES'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'PROCESS_PAYMENTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'REFUND_PAYMENTS');
+
+-- Assign permissions to USER role (basic access)
+INSERT INTO role_permissions (role_id, permission_name) VALUES
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'READ_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'READ_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'WRITE_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'READ_CARTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'WRITE_CARTS');
+
+-- Assign permissions to MANAGER role (moderate access)
+INSERT INTO role_permissions (role_id, permission_name) VALUES
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'READ_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'WRITE_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'READ_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'WRITE_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'CANCEL_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'READ_CARTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'PROCESS_PAYMENTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'REFUND_PAYMENTS');
+
+-- Assign permissions to CUSTOMER_SUPPORT role (support access)
+INSERT INTO role_permissions (role_id, permission_name) VALUES
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'READ_PRODUCTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'READ_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'CANCEL_ORDERS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'READ_CARTS'),
+ ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'REFUND_PAYMENTS');
+
+-- Assign roles to existing users
+-- admin_user gets ADMIN role
+INSERT INTO user_roles (user_id, role_id) VALUES
+ ('550e8400-e29b-41d4-a716-446655440002', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11');
+
+-- john_doe gets USER role
+INSERT INTO user_roles (user_id, role_id) VALUES
+ ('550e8400-e29b-41d4-a716-446655440000', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12');
+
+-- jane_smith gets MANAGER role
+INSERT INTO user_roles (user_id, role_id) VALUES
+ ('550e8400-e29b-41d4-a716-446655440001', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13');
+
+-- andy gets USER and CUSTOMER_SUPPORT roles (example of multiple roles)
+INSERT INTO user_roles (user_id, role_id) VALUES
+ ('123e4567-e89b-12d3-a456-426614174001', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12'),
+ ('123e4567-e89b-12d3-a456-426614174001', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14');
\ No newline at end of file
From 2d9107a4e6ab658c40b1931e518eeb063dd5c500 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wojciech=20Or=C5=82owski?=
Date: Wed, 22 Oct 2025 19:55:29 +0200
Subject: [PATCH 16/17] add readme.md for macos users
---
readme.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/readme.md b/readme.md
index 143dc55..9d3c4ec 100644
--- a/readme.md
+++ b/readme.md
@@ -8,7 +8,11 @@
openssl genrsa > local.private.key
openssl rsa -in local.private.key -pubout > local.public.key
```
-
+**For macOS (maybe windows) users run 1 more command:**
+ ```bash
+ openssl pkcs8 -topk8 -inform PEM -outform PEM -in local.private.key -out local.private.pkcs8.key -nocrypt
+ ```
+then change `secret-key-file` in application.yml to ```secret-key-file: classpath:local/secrets/local.private.pkcs8.key```
## User Controller API
### Create User
From 069aa85094605fab33f8fe4e0c2fba572195b3e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wojciech=20Or=C5=82owski?=
Date: Sat, 8 Nov 2025 13:39:36 +0100
Subject: [PATCH 17/17] add roles and permissions management to user service
---
mvnw | 0
pom.xml | 17 +-
.../userservice/api/grpc/UserGrpcMapper.java | 62 +++++
.../userservice/api/grpc/UserGrpcService.java | 256 ++++++++++++++++++
.../api/grpc/context/UserContextData.java | 5 +
.../grpc/context/UserContextGrpcHolder.java | 12 +
.../context/UserContextGrpcInterceptor.java | 25 ++
.../repository/db/DbUserRepository.java | 29 ++
.../repository/db/UserEntityMapper.java | 13 +
.../repository/db/UserEntityRepository.java | 9 +
.../inmemory/InMemoryUserRepository.java | 46 +++-
.../userservice/user/domain/UserFacade.java | 23 +-
.../user/domain/UserRepository.java | 5 +
.../userservice/user/domain/UserView.java | 6 +
src/main/resources/application.yml | 11 +-
.../V3__Create_roles_and_permissions.sql | 3 -
...ert_roles_and_permissions_example_data.sql | 67 -----
.../repository/db/DbUserRepositoryTest.java | 5 +
18 files changed, 510 insertions(+), 84 deletions(-)
mode change 100644 => 100755 mvnw
create mode 100644 src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcMapper.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcHolder.java
create mode 100644 src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java
create mode 100644 src/main/java/com/ecmsp/userservice/user/domain/UserView.java
delete mode 100644 src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql
diff --git a/mvnw b/mvnw
old mode 100644
new mode 100755
diff --git a/pom.xml b/pom.xml
index e1e540a..40c7dcf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,7 @@
21
2025.0.0
+ 1.63.0
@@ -44,7 +45,11 @@
h2
test
-
+
+ net.devh
+ grpc-spring-boot-starter
+ 3.1.0.RELEASE
+
org.mindrot
jbcrypt
@@ -148,9 +153,8 @@
com.ecmsp
protos
- 1.0.0-SNAPSHOT
+ 1.0.0-20251105.185705-41
-
@@ -162,6 +166,13 @@
pom
import
+
+ io.grpc
+ grpc-bom
+ ${grpc.version}
+ pom
+ import
+
diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcMapper.java b/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcMapper.java
new file mode 100644
index 0000000..202d055
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcMapper.java
@@ -0,0 +1,62 @@
+package com.ecmsp.userservice.api.grpc;
+
+import com.ecmsp.user.v1.RoleId;
+import com.ecmsp.user.v1.UserId;
+import com.ecmsp.userservice.user.domain.Permission;
+import com.ecmsp.userservice.user.domain.Role;
+import com.ecmsp.userservice.user.domain.UserView;
+import org.springframework.stereotype.Component;
+
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Component
+public class UserGrpcMapper {
+
+ public com.ecmsp.user.v1.User toProtoUser(UserView userView) {
+ return com.ecmsp.user.v1.User.newBuilder()
+ .setId(toProtoUserId(userView.id()))
+ .setLogin(userView.login())
+ .addAllRoles(userView.roles().stream()
+ .map(this::toProtoRole)
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ public UserId toProtoUserId(com.ecmsp.userservice.user.domain.UserId domainUserId) {
+ return UserId.newBuilder()
+ .setValue(domainUserId.value().toString())
+ .build();
+ }
+
+ public com.ecmsp.user.v1.Role toProtoRole(Role domainRole) {
+ return com.ecmsp.user.v1.Role.newBuilder()
+ .setId(toProtoRoleId(domainRole.id()))
+ .setName(domainRole.name())
+ .addAllPermissions(domainRole.permissions().stream()
+ .map(this::toProtoPermission)
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ public RoleId toProtoRoleId(com.ecmsp.userservice.user.domain.RoleId domainRoleId) {
+ return RoleId.newBuilder()
+ .setValue(domainRoleId.value().toString())
+ .build();
+ }
+
+ public String toProtoPermission(Permission permission) {
+ return permission.name();
+ }
+
+ public com.ecmsp.userservice.user.domain.UserId toDomainUserId(UserId protoUserId) {
+ return new com.ecmsp.userservice.user.domain.UserId(UUID.fromString(protoUserId.getValue()));
+ }
+
+ public com.ecmsp.userservice.user.domain.UserToCreate toDomainUserToCreate(com.ecmsp.user.v1.UserToCreate protoUserToCreate) {
+ return new com.ecmsp.userservice.user.domain.UserToCreate(
+ protoUserToCreate.getLogin(),
+ protoUserToCreate.getPassword()
+ );
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java b/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java
new file mode 100644
index 0000000..a0dc821
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java
@@ -0,0 +1,256 @@
+package com.ecmsp.userservice.api.grpc;
+
+import com.ecmsp.user.v1.*;
+import com.ecmsp.userservice.user.domain.User;
+import com.ecmsp.userservice.user.domain.UserFacade;
+import com.ecmsp.userservice.user.domain.UserId;
+import com.ecmsp.userservice.user.domain.UserView;
+import io.grpc.Status;
+import io.grpc.stub.StreamObserver;
+import net.devh.boot.grpc.server.service.GrpcService;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@GrpcService
+public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase {
+ private final UserFacade userFacade;
+ private final UserGrpcMapper mapper;
+
+ public UserGrpcService(UserFacade userFacade, UserGrpcMapper mapper) {
+ this.userFacade = userFacade;
+ this.mapper = mapper;
+ }
+
+ @Override
+ public void getUser(GetUserRequest request, StreamObserver responseObserver) {
+ try {
+ com.ecmsp.user.v1.UserId protoUserId = com.ecmsp.user.v1.UserId.newBuilder()
+ .setValue(request.getUserId())
+ .build();
+ UserId userId = mapper.toDomainUserId(protoUserId);
+ Optional userOptional = userFacade.findUserById(userId);
+
+ if (userOptional.isPresent()) {
+ User user = userOptional.get();
+ UserView userView = new UserView(user.id(), user.login(), user.roles());
+ com.ecmsp.user.v1.User protoUser = mapper.toProtoUser(userView);
+
+ GetUserResponse response = GetUserResponse.newBuilder()
+ .setUser(protoUser)
+ .build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(Status.NOT_FOUND
+ .withDescription("User not found with id: " + request.getUserId())
+ .asRuntimeException());
+ }
+ } catch (IllegalArgumentException e) {
+ responseObserver.onError(Status.INVALID_ARGUMENT
+ .withDescription(e.getMessage())
+ .asRuntimeException());
+ } catch (Exception e) {
+ responseObserver.onError(Status.INTERNAL
+ .withDescription("Internal error: " + e.getMessage())
+ .asRuntimeException());
+ }
+ }
+
+ @Override
+ public void createUser(CreateUserRequest request, StreamObserver responseObserver) {
+ try {
+ com.ecmsp.user.v1.UserToCreate protoUserToCreate = request.getUserToCreate();
+
+ // Validate input
+ if (protoUserToCreate.getLogin() == null || protoUserToCreate.getLogin().isBlank()) {
+ responseObserver.onError(Status.INVALID_ARGUMENT
+ .withDescription("Login cannot be empty")
+ .asRuntimeException());
+ return;
+ }
+
+ if (protoUserToCreate.getPassword() == null || protoUserToCreate.getPassword().isBlank()) {
+ responseObserver.onError(Status.INVALID_ARGUMENT
+ .withDescription("Password cannot be empty")
+ .asRuntimeException());
+ return;
+ }
+
+ com.ecmsp.userservice.user.domain.UserToCreate domainUserToCreate = mapper.toDomainUserToCreate(protoUserToCreate);
+ User createdUser = userFacade.createUser(domainUserToCreate);
+
+ UserView userView = new UserView(createdUser.id(), createdUser.login(), createdUser.roles());
+ com.ecmsp.user.v1.User protoUser = mapper.toProtoUser(userView);
+
+ CreateUserResponse response = CreateUserResponse.newBuilder()
+ .setUser(protoUser)
+ .build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ } catch (RuntimeException e) {
+ if (e.getMessage() != null && e.getMessage().contains("already exists")) {
+ responseObserver.onError(Status.ALREADY_EXISTS
+ .withDescription("User with this login already exists")
+ .asRuntimeException());
+ } else {
+ responseObserver.onError(Status.INTERNAL
+ .withDescription("Internal error: " + e.getMessage())
+ .asRuntimeException());
+ }
+ } catch (Exception e) {
+ responseObserver.onError(Status.INTERNAL
+ .withDescription("Internal error: " + e.getMessage())
+ .asRuntimeException());
+ }
+ }
+
+ @Override
+ public void updateUser(UpdateUserRequest request, StreamObserver responseObserver) {
+ try {
+ UserId userId = mapper.toDomainUserId(request.getUser().getId());
+ String newLogin = request.getUser().getLogin();
+
+ if (newLogin == null || newLogin.isBlank()) {
+ responseObserver.onError(Status.INVALID_ARGUMENT
+ .withDescription("Login cannot be empty")
+ .asRuntimeException());
+ return;
+ }
+
+ userFacade.updateUserLogin(userId, newLogin);
+
+ // Fetch updated user
+ Optional updatedUserOptional = userFacade.findUserById(userId);
+ if (updatedUserOptional.isPresent()) {
+ User updatedUser = updatedUserOptional.get();
+ UserView userView = new UserView(updatedUser.id(), updatedUser.login(), updatedUser.roles());
+ com.ecmsp.user.v1.User protoUser = mapper.toProtoUser(userView);
+
+ UpdateUserResponse response = UpdateUserResponse.newBuilder()
+ .setUser(protoUser)
+ .build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ } else {
+ responseObserver.onError(Status.NOT_FOUND
+ .withDescription("User not found after update")
+ .asRuntimeException());
+ }
+ } catch (IllegalArgumentException e) {
+ responseObserver.onError(Status.NOT_FOUND
+ .withDescription(e.getMessage())
+ .asRuntimeException());
+ } catch (Exception e) {
+ responseObserver.onError(Status.INTERNAL
+ .withDescription("Internal error: " + e.getMessage())
+ .asRuntimeException());
+ }
+ }
+
+ @Override
+ public void deleteUser(DeleteUserRequest request, StreamObserver responseObserver) {
+ try {
+ com.ecmsp.user.v1.UserId protoUserId = com.ecmsp.user.v1.UserId.newBuilder()
+ .setValue(request.getUserId())
+ .build();
+ UserId userId = mapper.toDomainUserId(protoUserId);
+ userFacade.deleteUser(userId);
+
+ DeleteUserResponse response = DeleteUserResponse.newBuilder().build();
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ } catch (IllegalArgumentException e) {
+ responseObserver.onError(Status.NOT_FOUND
+ .withDescription(e.getMessage())
+ .asRuntimeException());
+ } catch (Exception e) {
+ responseObserver.onError(Status.INTERNAL
+ .withDescription("Internal error: " + e.getMessage())
+ .asRuntimeException());
+ }
+ }
+
+ @Override
+ public void listUsers(ListUsersRequest request, StreamObserver responseObserver) {
+ try {
+ String filterLogin = request.getFilterLogin();
+ List users = userFacade.listUsers(filterLogin);
+
+ List protoUsers = users.stream()
+ .map(mapper::toProtoUser)
+ .collect(Collectors.toList());
+
+ ListUsersResponse response = ListUsersResponse.newBuilder()
+ .addAllUsers(protoUsers)
+ .build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ } catch (Exception e) {
+ responseObserver.onError(Status.INTERNAL
+ .withDescription("Internal error: " + e.getMessage())
+ .asRuntimeException());
+ }
+ }
+
+ @Override
+ public void createRole(CreateRoleRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+
+ @Override
+ public void updateRole(UpdateRoleRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+
+ @Override
+ public void deleteRole(DeleteRoleRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+
+ @Override
+ public void listRoles(ListRolesRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+
+ @Override
+ public void assignRoleToUsers(AssignRoleToUsersRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+
+ @Override
+ public void removeRoleFromUsers(RemoveRoleFromUsersRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+
+ @Override
+ public void listAllPermissions(ListAllPermissionsRequest request, StreamObserver responseObserver) {
+ // TODO: Implement role management later
+ responseObserver.onError(Status.UNIMPLEMENTED
+ .withDescription("Role management not yet implemented")
+ .asRuntimeException());
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java
new file mode 100644
index 0000000..9ad440a
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java
@@ -0,0 +1,5 @@
+package com.ecmsp.userservice.api.grpc.context;
+
+public record UserContextData(String userId,
+ String login) {
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcHolder.java b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcHolder.java
new file mode 100644
index 0000000..5819e47
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcHolder.java
@@ -0,0 +1,12 @@
+package com.ecmsp.userservice.api.grpc.context;
+
+import io.grpc.Context;
+
+public class UserContextGrpcHolder {
+
+ public static final Context.Key USER_CONTEXT_KEY = Context.key("user-context");
+
+ public static UserContextData getUserContext() {
+ return USER_CONTEXT_KEY.get();
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java
new file mode 100644
index 0000000..d37de61
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java
@@ -0,0 +1,25 @@
+package com.ecmsp.userservice.api.grpc.context;
+
+import io.grpc.*;
+import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;
+
+@GrpcGlobalServerInterceptor
+public class UserContextGrpcInterceptor implements ServerInterceptor {
+
+ @Override
+ public ServerCall.Listener interceptCall(
+ ServerCall call,
+ Metadata headers,
+ ServerCallHandler next) {
+
+ UserContextData userContextData = new UserContextData(
+ headers.get(Metadata.Key.of("X-User-ID", Metadata.ASCII_STRING_MARSHALLER)),
+ headers.get(Metadata.Key.of("X-Login", Metadata.ASCII_STRING_MARSHALLER))
+ );
+
+ Context context = Context.current()
+ .withValue(UserContextGrpcHolder.USER_CONTEXT_KEY, userContextData);
+
+ return Contexts.interceptCall(context, call, headers, next);
+ }
+}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
index e129363..ddc439b 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepository.java
@@ -3,8 +3,12 @@
import com.ecmsp.userservice.user.domain.User;
import com.ecmsp.userservice.user.domain.UserId;
import com.ecmsp.userservice.user.domain.UserRepository;
+import com.ecmsp.userservice.user.domain.UserView;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
class DbUserRepository implements UserRepository {
private final UserEntityMapper userMapper = new UserEntityMapper();
@@ -34,4 +38,29 @@ public Optional findByLogin(String login) {
return userEntityRepository.findByLogin(login)
.map(userMapper::toUser);
}
+
+ @Override
+ public void deleteById(UserId userId) {
+ userEntityRepository.deleteById(userId.value());
+ }
+
+ @Override
+ public List findAll() {
+ return userEntityRepository.findAll().stream()
+ .map(userMapper::toUserView)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List findByLoginContaining(String loginFilter) {
+ return userEntityRepository.findByLoginContaining(loginFilter).stream()
+ .map(userMapper::toUserView)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ @Transactional
+ public void updateLogin(UserId userId, String newLogin) {
+ userEntityRepository.updateLogin(userId.value(), newLogin);
+ }
}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
index f16b750..c16ce74 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityMapper.java
@@ -3,6 +3,7 @@
import com.ecmsp.userservice.user.domain.Role;
import com.ecmsp.userservice.user.domain.User;
import com.ecmsp.userservice.user.domain.UserId;
+import com.ecmsp.userservice.user.domain.UserView;
import java.util.Set;
import java.util.stream.Collectors;
@@ -28,6 +29,18 @@ public User toUser(UserEntity entity) {
);
}
+ public UserView toUserView(UserEntity entity) {
+ Set roles = entity.getRoles().stream()
+ .map(roleEntityMapper::toRole)
+ .collect(Collectors.toSet());
+
+ return new UserView(
+ new UserId(entity.getUserId()),
+ entity.getLogin(),
+ roles
+ );
+ }
+
public UserEntity toUserEntity(User user) {
Set roleEntities = user.roles().stream()
.map(roleEntityMapper::toRoleEntity)
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java
index 06498dc..1f0bead 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/db/UserEntityRepository.java
@@ -1,9 +1,11 @@
package com.ecmsp.userservice.user.adapter.repository.db;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
+import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -11,4 +13,11 @@
interface UserEntityRepository extends JpaRepository {
@Query("SELECT u FROM UserEntity u WHERE u.login = :login")
Optional findByLogin(String login);
+
+ @Query("SELECT u FROM UserEntity u WHERE LOWER(u.login) LIKE LOWER(CONCAT('%', :loginFilter, '%'))")
+ List findByLoginContaining(String loginFilter);
+
+ @Modifying
+ @Query("UPDATE UserEntity u SET u.login = :newLogin WHERE u.userId = :userId")
+ void updateLogin(UUID userId, String newLogin);
}
diff --git a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
index cb42193..bb8e340 100644
--- a/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/adapter/repository/inmemory/InMemoryUserRepository.java
@@ -3,18 +3,21 @@
import com.ecmsp.userservice.user.domain.User;
import com.ecmsp.userservice.user.domain.UserId;
import com.ecmsp.userservice.user.domain.UserRepository;
+import com.ecmsp.userservice.user.domain.UserView;
import org.mindrot.jbcrypt.BCrypt;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
+import java.util.*;
+import java.util.stream.Collectors;
class InMemoryUserRepository implements UserRepository {
- private final Map users = Map.of(
- new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")),
- new User(new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94")), "testuser", BCrypt.hashpw("testpassword", BCrypt.gensalt()), Set.of())
- );
+ private final Map users = new HashMap<>();
+
+ public InMemoryUserRepository() {
+ // Initialize with test user
+ UserId testUserId = new UserId(UUID.fromString("3d1af02a-bba2-4df1-b551-e944bb60bc94"));
+ User testUser = new User(testUserId, "testuser", BCrypt.hashpw("testpassword", BCrypt.gensalt()), Set.of());
+ users.put(testUserId, testUser);
+ }
@Override
public void save(User user) {
@@ -34,5 +37,32 @@ public Optional findByLogin(String login) {
.findFirst();
}
+ @Override
+ public void deleteById(UserId userId) {
+ users.remove(userId);
+ }
+
+ @Override
+ public List findAll() {
+ return users.values().stream()
+ .map(user -> new UserView(user.id(), user.login(), user.roles()))
+ .collect(Collectors.toList());
+ }
+ @Override
+ public List findByLoginContaining(String loginFilter) {
+ return users.values().stream()
+ .filter(user -> user.login().toLowerCase().contains(loginFilter.toLowerCase()))
+ .map(user -> new UserView(user.id(), user.login(), user.roles()))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void updateLogin(UserId userId, String newLogin) {
+ User user = users.get(userId);
+ if (user != null) {
+ User updatedUser = new User(user.id(), newLogin, user.passwordHash(), user.roles());
+ users.put(userId, updatedUser);
+ }
+ }
}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
index 8deaa9f..bd52c57 100644
--- a/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserFacade.java
@@ -1,6 +1,7 @@
package com.ecmsp.userservice.user.domain;
import java.util.HashSet;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -15,11 +16,12 @@ public UserFacade(UserRepository userRepository, RoleRepository roleRepository)
this.roleRepository = roleRepository;
}
- public void createUser(UserToCreate userToCreate){
+ public User createUser(UserToCreate userToCreate){
UserId userId = new UserId(UUID.randomUUID());
String hashedPassword = passwordHasher.hash(userToCreate.password());
User user = new User(userId, userToCreate.login(), hashedPassword, new HashSet<>());
userRepository.save(user);
+ return user;
}
public Optional findUserById(UserId userId){
@@ -30,6 +32,25 @@ public Optional findUserByLogin(String login) {
return userRepository.findByLogin(login);
}
+ public void updateUserLogin(UserId userId, String newLogin) {
+ userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
+ userRepository.updateLogin(userId, newLogin);
+ }
+
+ public void deleteUser(UserId userId) {
+ userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
+ userRepository.deleteById(userId);
+ }
+
+ public List listUsers(String filterLogin) {
+ if (filterLogin == null || filterLogin.isBlank()) {
+ return userRepository.findAll();
+ }
+ return userRepository.findByLoginContaining(filterLogin);
+ }
+
public void assignRoleToUser(UserId userId, RoleId roleId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java b/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
index 86a90b6..7e110f0 100644
--- a/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserRepository.java
@@ -1,9 +1,14 @@
package com.ecmsp.userservice.user.domain;
+import java.util.List;
import java.util.Optional;
public interface UserRepository {
void save(User user);
Optional findById(UserId userId);
Optional findByLogin(String login);
+ void deleteById(UserId userId);
+ List findAll();
+ List findByLoginContaining(String loginFilter);
+ void updateLogin(UserId userId, String newLogin);
}
diff --git a/src/main/java/com/ecmsp/userservice/user/domain/UserView.java b/src/main/java/com/ecmsp/userservice/user/domain/UserView.java
new file mode 100644
index 0000000..864841a
--- /dev/null
+++ b/src/main/java/com/ecmsp/userservice/user/domain/UserView.java
@@ -0,0 +1,6 @@
+package com.ecmsp.userservice.user.domain;
+
+import java.util.Set;
+
+public record UserView(UserId id, String login, Set roles) {
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 7aad341..e85d34a 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -16,10 +16,17 @@ spring:
auth:
token-generator:
type: jwt
- secret-key-file: classpath:local/secrets/local.private.key
+ secret-key-file: classpath:local/secrets/local.private.pkcs8.key
user:
repository:
type: db
+
server:
- port: 8500
\ No newline at end of file
+ port: 8500
+
+grpc:
+ server:
+ port: 7500
+ security:
+ enabled: false # Set to true to enable TLS
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql b/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql
index 2a463b9..54f67a2 100644
--- a/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql
+++ b/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql
@@ -25,14 +25,11 @@ CREATE TABLE user_roles (
-- Insert predefined permissions
INSERT INTO permissions (permission_name) VALUES
- ('READ_PRODUCTS'),
('WRITE_PRODUCTS'),
('DELETE_PRODUCTS'),
('READ_ORDERS'),
('WRITE_ORDERS'),
('CANCEL_ORDERS'),
- ('READ_CARTS'),
- ('WRITE_CARTS'),
('MANAGE_USERS'),
('MANAGE_ROLES'),
('PROCESS_PAYMENTS'),
diff --git a/src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql b/src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql
deleted file mode 100644
index bd07d79..0000000
--- a/src/main/resources/db/migration/V4__Insert_roles_and_permissions_example_data.sql
+++ /dev/null
@@ -1,67 +0,0 @@
--- Insert example roles
-
-INSERT INTO roles (role_id, role_name) VALUES
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'ADMIN'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'USER'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'MANAGER'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'CUSTOMER_SUPPORT');
-
--- Assign permissions to ADMIN role (full access)
-INSERT INTO role_permissions (role_id, permission_name) VALUES
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'READ_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'WRITE_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'DELETE_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'READ_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'WRITE_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'CANCEL_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'READ_CARTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'WRITE_CARTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'MANAGE_USERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'MANAGE_ROLES'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'PROCESS_PAYMENTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'REFUND_PAYMENTS');
-
--- Assign permissions to USER role (basic access)
-INSERT INTO role_permissions (role_id, permission_name) VALUES
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'READ_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'READ_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'WRITE_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'READ_CARTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', 'WRITE_CARTS');
-
--- Assign permissions to MANAGER role (moderate access)
-INSERT INTO role_permissions (role_id, permission_name) VALUES
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'READ_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'WRITE_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'READ_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'WRITE_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'CANCEL_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'READ_CARTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'PROCESS_PAYMENTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', 'REFUND_PAYMENTS');
-
--- Assign permissions to CUSTOMER_SUPPORT role (support access)
-INSERT INTO role_permissions (role_id, permission_name) VALUES
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'READ_PRODUCTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'READ_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'CANCEL_ORDERS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'READ_CARTS'),
- ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', 'REFUND_PAYMENTS');
-
--- Assign roles to existing users
--- admin_user gets ADMIN role
-INSERT INTO user_roles (user_id, role_id) VALUES
- ('550e8400-e29b-41d4-a716-446655440002', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11');
-
--- john_doe gets USER role
-INSERT INTO user_roles (user_id, role_id) VALUES
- ('550e8400-e29b-41d4-a716-446655440000', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12');
-
--- jane_smith gets MANAGER role
-INSERT INTO user_roles (user_id, role_id) VALUES
- ('550e8400-e29b-41d4-a716-446655440001', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13');
-
--- andy gets USER and CUSTOMER_SUPPORT roles (example of multiple roles)
-INSERT INTO user_roles (user_id, role_id) VALUES
- ('123e4567-e89b-12d3-a456-426614174001', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12'),
- ('123e4567-e89b-12d3-a456-426614174001', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14');
\ No newline at end of file
diff --git a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
index 3febdca..a3f93db 100644
--- a/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
+++ b/src/test/java/com/ecmsp/userservice/user/adapter/repository/db/DbUserRepositoryTest.java
@@ -94,6 +94,7 @@ void should_find_user_by_id() {
.userId(USER_1_ID.value())
.login("user1")
.password("hashedPassword1")
+ .roles(Set.of())
.build()
);
@@ -116,6 +117,7 @@ void should_return_empty_optional_when_user_with_given_id_not_exist() {
.userId(USER_1_ID.value())
.login("user1")
.password("hashedPassword1")
+ .roles(Set.of())
.build()
);
@@ -137,6 +139,7 @@ void should_find_user_by_login() {
.userId(USER_1_ID.value())
.login("user1")
.password("hashedPassword1")
+ .roles(Set.of())
.build()
);
@@ -180,6 +183,7 @@ void should_find_correct_user_when_multiple_users_exist() {
.userId(USER_1_ID.value())
.login("user1")
.password("hashedPassword1")
+ .roles(Set.of())
.build()
);
testEntityManager.persist(
@@ -187,6 +191,7 @@ void should_find_correct_user_when_multiple_users_exist() {
.userId(USER_2_ID.value())
.login("user2")
.password("hashedPassword2")
+ .roles(Set.of())
.build()
);
testEntityManager.flush();