diff --git a/.github/workflows/JavaMavenCI.yml b/.github/workflows/JavaMavenCI.yml
new file mode 100644
index 0000000..690e6e9
--- /dev/null
+++ b/.github/workflows/JavaMavenCI.yml
@@ -0,0 +1,114 @@
+name: Java CI with Maven
+
+on:
+ pull_request:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+ - name: Compile with maven
+ run: mvn clean compile
+
+ - name: Update dependency graph
+ uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
+
+ checkstyle:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+ - name: Compile with maven
+ run: mvn checkstyle:check
+
+ testing:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+ - name: Compile maven project
+ run: mvn clean compile
+ - name: Test maven project
+ run: mvn test
+ - name: Add coverage to PR
+ id: jacoco
+ uses: madrapps/jacoco-report@v1.6.1
+ with:
+ paths: |
+ ${{ github.workspace }}/**/target/site/jacoco/*.xml
+ token: ${{ secrets.TEST_SECRET }}
+ min-coverage-overall: 50
+ min-coverage-changed-files: 50
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ with:
+ name: jacoco-report
+ path: ${{ github.workspace }}/**/target/site/jacoco/
+
+ - name: Fail PR if overall coverage is less than 50%
+ if: ${{ steps.jacoco.outputs.coverage-overall < 50.0 }}
+ uses: actions/github-script@v6
+ with:
+ script: |
+ core.setFailed('Overall coverage is less than 50%!')
+
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ -
+ name: Checkout
+ uses: actions/checkout@v4
+ -
+ name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+ -
+ name: Package
+ run: mvn clean package
+ -
+ name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ -
+ name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_PASSWORD }}
+ -
+ name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: ./api/.
+ push: true
+ tags: asavershin/api:latest
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ed0d6b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..5f0536e
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/README.md b/README.md
index d14d000..8b17dda 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,7 @@
-# Images
\ No newline at end of file
+# Start api
+
+./mvnw clean package
+В папке api: docker compose up -d
+
+
+
diff --git a/api/.env b/api/.env
new file mode 100644
index 0000000..c2d0dce
--- /dev/null
+++ b/api/.env
@@ -0,0 +1,36 @@
+SERVER_PORT=8081
+
+# DATABASE
+HOST=db-api
+PORT_DB=5432
+POSTGRES_SCHEMA=public
+POSTGRES_DB=postgres
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=postgres
+LOG_SQL=true
+
+# REDIS
+REDIS_HOST=redis-api
+REDIS_PORT=6379
+REDIS_PASSWORD=cGFzc3dvcmxk
+REDIS_CACHE_TIME=86400000
+
+# MINIO
+MINIO_BUCKET=files
+MINIO_URL=http://minio-api:9000
+MINIO_ROOT_USER=minioadmin
+MINIO_ROOT_PASSWORD=minioadmin
+MINIO_CONSOLE_PORT=9090
+MINIO_PORT=9000
+
+# MONGO
+MONGO_INITDB_ROOT_USERNAME=admin
+MONGO_INITDB_ROOT_PASSWORD=admin
+MONGO_DB=mongo
+MONGO_PORT=27017
+MONGO_HOST=mongodb-note
+
+# AUTH
+ACCESS_EXPIRATION=86400000
+REFRESH_EXPIRATION=604800000
+JWT_SECRET=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
\ No newline at end of file
diff --git a/api/Dockerfile b/api/Dockerfile
new file mode 100644
index 0000000..79aace5
--- /dev/null
+++ b/api/Dockerfile
@@ -0,0 +1,7 @@
+FROM openjdk:21
+
+WORKDIR /app
+
+COPY target/*.jar app.jar
+
+CMD ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..9ebc64d
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,5 @@
+# Info
+.env файл со всем окружением/портами
+
+
+
diff --git a/api/docker-compose.yml b/api/docker-compose.yml
new file mode 100644
index 0000000..aaf3a8f
--- /dev/null
+++ b/api/docker-compose.yml
@@ -0,0 +1,52 @@
+version: '3.8'
+
+services:
+
+ backend-api:
+ container_name: backend-api
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "8081:8081"
+ depends_on:
+ - db-api
+ - redis-api
+ env_file:
+ - .env
+ db-api:
+ image: postgres:15.1-alpine
+ container_name: db-api
+ env_file:
+ - .env
+ ports:
+ - "5440:5432"
+ volumes:
+ - db-api-data:/var/lib/postgresql/data/
+
+ redis-api:
+ image: redis:7.2-rc-alpine
+ restart: always
+ container_name: redis-api
+ ports:
+ - '6379:6379'
+ command: redis-server --save 20 1 --loglevel debug --requirepass ${REDIS_PASSWORD}
+ volumes:
+ - redis-api-data:/data
+
+ minio-api:
+ image: minio/minio:latest
+ container_name: minio-api
+ env_file:
+ - .env
+ command: server ~/minio --console-address :9090
+ ports:
+ - '9090:9090'
+ - '9000:9000'
+ volumes:
+ - minio-api-data:/minio
+
+volumes:
+ db-api-data:
+ redis-api-data:
+ minio-api-data:
\ No newline at end of file
diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644
index 0000000..8fbee14
--- /dev/null
+++ b/api/pom.xml
@@ -0,0 +1,260 @@
+
+
+ 4.0.0
+
+ com.github.asavershin.images
+ images
+ 0.0.1-SNAPSHOT
+
+
+ api
+
+
+ 21
+ 21
+ UTF-8
+ 3.19.3
+ 2.1.0
+ 0.11.5
+ 8.5.7
+
+
+ 1.19.1
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.starter.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+
+
+ io.minio
+ minio
+ ${minio.version}
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jooq
+
+
+ org.jooq
+ jooq
+ ${jooq.version}
+
+
+ org.jooq
+ jooq-meta
+ ${jooq.version}
+
+
+ org.jooq
+ jooq-codegen
+ ${jooq.version}
+
+
+ org.jooq
+ jooq-meta-extensions
+ ${jooq.version}
+
+
+
+
+ org.liquibase
+ liquibase-core
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jsonwebtoken.version}
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jsonwebtoken.version}
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jsonwebtoken.version}
+ runtime
+
+
+
+
+
+
+
+
+
+ org.testcontainers
+ postgresql
+ ${containers.version}
+ test
+
+
+
+ org.testcontainers
+ minio
+ ${containers.version}
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ ${containers.version}
+ test
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.jooq
+ jooq-codegen-maven
+ ${jooq.version}
+
+
+ generate-jooq-sources
+ generate-sources
+
+ generate
+
+
+
+
+ org.jooq.meta.extensions.ddl.DDLDatabase
+
+
+ scripts
+ src/main/resources/db/changelog/migrations/*create*.sql
+
+
+ sort
+ semantic
+
+
+ unqualifiedSchema
+ none
+
+
+ defaultNameCase
+ lower
+
+
+
+
+ asavershin.generated.package
+ target/generated-sources/jooq
+
+
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.10
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+
+ com/github/asavershin/api/domain/**
+ com/github/asavershin/api/application/in/**
+ com/github/asavershin/api/infrastructure/out/**
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/src/main/java/com/github/asavershin/api/ApiMain.java b/api/src/main/java/com/github/asavershin/api/ApiMain.java
new file mode 100644
index 0000000..0ed4b22
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/ApiMain.java
@@ -0,0 +1,22 @@
+package com.github.asavershin.api;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class ApiMain {
+ /**
+ * The main method that starts the Spring Boot application.
+ *
+ * @param args the command-line arguments
+ */
+ public static void main(final String[] args) {
+ SpringApplication.run(ApiMain.class, args);
+ }
+
+ private void foo() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/image/ImageService.java b/api/src/main/java/com/github/asavershin/api/application/in/services/image/ImageService.java
new file mode 100644
index 0000000..d2c4f5d
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/image/ImageService.java
@@ -0,0 +1,33 @@
+package com.github.asavershin.api.application.in.services.image;
+
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.user.UserId;
+import org.springframework.web.multipart.MultipartFile;
+
+public interface ImageService {
+ /**
+ * Stores an image for a given user.
+ *
+ * @param userId the user who owns the image
+ * @param multipartFile the image file to be stored
+ * @return the unique identifier of the stored image
+ */
+ ImageId storeImage(UserId userId, MultipartFile multipartFile);
+ /**
+ * Deletes an image by its unique identifier. Validate that
+ * image belongs to user.
+ *
+ * @param userId the user who owns the image
+ * @param imageId the unique identifier of the image to be deleted
+ */
+ void deleteImageByImageId(UserId userId, ImageId imageId);
+ /**
+ * Downloads an image by its unique identifier. Validate that
+ * image belongs to user.
+ *
+ * @param imageId the unique identifier of the image to be downloaded
+ * @param userId the user who owns the image
+ * @return the byte array representation of the image
+ */
+ byte[] downloadImage(ImageId imageId, UserId userId);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/ImageServiceImpl.java b/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/ImageServiceImpl.java
new file mode 100644
index 0000000..eb2fc9a
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/ImageServiceImpl.java
@@ -0,0 +1,108 @@
+package com.github.asavershin.api.application.in.services.image.impl;
+
+import com.github.asavershin.api.application.in.services.image.ImageService;
+import com.github.asavershin.api.application.out.MinioService;
+import com.github.asavershin.api.domain.image.DeleteImageOfUser;
+import com.github.asavershin.api.domain.image.GetImageOfUser;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.image.ImageNameWithExtension;
+import com.github.asavershin.api.domain.image.MetaData;
+import com.github.asavershin.api.domain.image.StoreImageOfUser;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ImageServiceImpl implements ImageService, Serializable {
+ /**
+ * The MinioService is used to interact with the Minio storage service.
+ */
+ private final MinioService minioService;
+
+ /**
+ * The GetImageOfUser service is used to retrieve an image
+ * by its ID and user ID.
+ */
+ private final GetImageOfUser getImageOfUser;
+
+ /**
+ * The DeleteImageOfUser service is used to delete an image
+ * by its ID and user ID.
+ */
+ private final DeleteImageOfUser deleteImageOfUser;
+
+ /**
+ * The StoreImageOfUser service is used to store an image
+ * with its metadata and associate it with a user.
+ */
+ private final StoreImageOfUser storeImageOfUser;
+
+ /**
+ * Method not marked as final to allow Spring
+ * to create a proxy first for transactional purposes.
+ * @param userId the user who owns the image
+ * @param multipartFile the image file to be stored
+ * @return ID of new stored image
+ */
+ @Override
+ @Transactional
+ public ImageId storeImage(final UserId userId,
+ final MultipartFile multipartFile) {
+ var metaInfo = new MetaData(
+ ImageNameWithExtension
+ .fromOriginalFileName(
+ multipartFile.getOriginalFilename()
+ ),
+ multipartFile.getSize()
+ );
+ var imageId = ImageId.nextIdentity();
+ storeImageOfUser.storeImageOfUser(
+ new Image(
+ imageId,
+ metaInfo,
+ userId
+ )
+ );
+ minioService.saveFile(multipartFile, imageId.value().toString());
+ return imageId;
+ }
+
+ /**
+ * Method not marked as final to allow Spring
+ * * to create a proxy first for transactional purposes.
+ * @param userId the user who owns the image
+ * @param imageId the unique identifier of the image to be deleted
+ */
+ @Override
+ @Transactional
+ public void deleteImageByImageId(final UserId userId,
+ final ImageId imageId) {
+ deleteImageOfUser.removeImageOfUser(imageId, userId);
+ minioService.deleteFiles(List.of(imageId.value().toString()));
+ }
+
+ /**
+ * Method not marked as final to allow Spring make CGLIB proxy.
+ * @param imageId the unique identifier of the image to be downloaded
+ * @param userId the user who owns the image
+ * @return bytes of the image
+ */
+ @Override
+ public byte[] downloadImage(final ImageId imageId,
+ final UserId userId) {
+ return minioService.getFile(
+ getImageOfUser.getImageOfUser(
+ userId,
+ imageId
+ )
+ .imageId().value().toString()
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/package-info.java b/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/package-info.java
new file mode 100644
index 0000000..98dee3f
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains implementation application services for images.
+ * @author asavershin
+ */
+package com.github.asavershin.api.application.in.services.image.impl;
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/image/package-info.java b/api/src/main/java/com/github/asavershin/api/application/in/services/image/package-info.java
new file mode 100644
index 0000000..9480e7d
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/image/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains application services for images.
+ * @author asavershin
+ */
+package com.github.asavershin.api.application.in.services.image;
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/ApplicationCredentials.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/ApplicationCredentials.java
new file mode 100644
index 0000000..8ab9109
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/ApplicationCredentials.java
@@ -0,0 +1,29 @@
+package com.github.asavershin.api.application.in.services.user;
+
+import lombok.Getter;
+
+@Getter
+public class ApplicationCredentials {
+ /**
+ * The access token for user.
+ */
+ private final String accessToken;
+
+ /**
+ * The refresh token for user.
+ */
+ private final String refreshToken;
+
+ /**
+ * Constructs an instance of {@link ApplicationCredentials}
+ * with the provided access token and refresh token.
+ *
+ * @param aAccessToken The access token for user.
+ * @param aRefreshToken The refresh token for user.
+ */
+ public ApplicationCredentials(final String aAccessToken,
+ final String aRefreshToken) {
+ this.accessToken = aAccessToken;
+ this.refreshToken = aRefreshToken;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentials.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentials.java
new file mode 100644
index 0000000..cf788e9
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentials.java
@@ -0,0 +1,14 @@
+package com.github.asavershin.api.application.in.services.user;
+
+import com.github.asavershin.api.domain.user.Credentials;
+
+@FunctionalInterface
+public interface GetNewCredentials {
+ /**
+ * Generates new credentials for the user based on the provided credentials.
+ *
+ * @param credentials The current credentials of the user.
+ * @return The new credentials for the user.
+ */
+ ApplicationCredentials get(Credentials credentials);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentialsUsingRefreshToken.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentialsUsingRefreshToken.java
new file mode 100644
index 0000000..1cdda6b
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentialsUsingRefreshToken.java
@@ -0,0 +1,14 @@
+package com.github.asavershin.api.application.in.services.user;
+
+import com.github.asavershin.api.domain.user.Credentials;
+
+@FunctionalInterface
+public interface GetNewCredentialsUsingRefreshToken {
+ /**
+ * Generates new credentials using the provided refresh token.
+ *
+ * @param credentials The credentials that contain the refresh token.
+ * @return The new credentials generated using the refresh token.
+ */
+ ApplicationCredentials get(Credentials credentials);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/JwtService.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/JwtService.java
new file mode 100644
index 0000000..ee436d2
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/JwtService.java
@@ -0,0 +1,30 @@
+package com.github.asavershin.api.application.in.services.user;
+
+import com.github.asavershin.api.domain.user.Credentials;
+
+public interface JwtService {
+
+ /**
+ * Generates an access token for the provided credentials.
+ *
+ * @param credentials The credentials used to generate the access token.
+ * @return A string representing the generated access token.
+ */
+ String generateAccessToken(Credentials credentials);
+
+ /**
+ * Generates a refresh token for the provided credentials.
+ *
+ * @param credentials The credentials used to generate the refresh token.
+ * @return A string representing the generated refresh token.
+ */
+ String generateRefreshToken(Credentials credentials);
+
+ /**
+ * Extracts the subject from the provided JWT.
+ *
+ * @param jwt The JWT from which the subject should be extracted.
+ * @return A string representing the extracted subject.
+ */
+ String extractSub(String jwt);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsImpl.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsImpl.java
new file mode 100644
index 0000000..71b306f
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsImpl.java
@@ -0,0 +1,52 @@
+package com.github.asavershin.api.application.in.services.user.impl;
+
+import com.github.asavershin.api.application.in.services.user.ApplicationCredentials;
+import com.github.asavershin.api.application.in.services.user.GetNewCredentials;
+import com.github.asavershin.api.application.out.TokenRepository;
+import com.github.asavershin.api.application.in.services.user.JwtService;
+import com.github.asavershin.api.config.properties.JwtProperties;
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.TryToLogin;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class GetNewCredentialsImpl implements GetNewCredentials {
+ /**
+ * Dependency for get,save,delete tokens in redis.
+ */
+ private final TokenRepository tokenRepository;
+ /**
+ * Domain service that allow user try to log in.
+ */
+ private final TryToLogin tryToLogin;
+ /**
+ * Application service that allow to do some manipulations with JWT tokens.
+ */
+ private final JwtService jwtService;
+ /**
+ * Property that contains secret and access, refresh expirations.
+ */
+ private final JwtProperties jwtProperties;
+
+ @Override
+ public final ApplicationCredentials get(final Credentials credentials) {
+ var authenticatedUser = tryToLogin.login(credentials);
+ var accessToken = jwtService
+ .generateAccessToken(authenticatedUser.userCredentials());
+ var refreshToken = jwtService
+ .generateRefreshToken(authenticatedUser.userCredentials());
+ var email = authenticatedUser.userCredentials().email();
+ tokenRepository.deleteAllTokensByUserEmail(email);
+ tokenRepository.saveRefreshToken(email,
+ refreshToken,
+ jwtProperties.getRefreshExpiration());
+ tokenRepository.saveAccessToken(email,
+ accessToken,
+ jwtProperties.getAccessExpiration());
+ return new ApplicationCredentials(accessToken, refreshToken);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsUsingRefreshTokenImpl.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsUsingRefreshTokenImpl.java
new file mode 100644
index 0000000..7a057b0
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsUsingRefreshTokenImpl.java
@@ -0,0 +1,50 @@
+package com.github.asavershin.api.application.in.services.user.impl;
+
+import com.github.asavershin.api.application.in.services.user.ApplicationCredentials;
+import com.github.asavershin.api.application.in.services.user.GetNewCredentialsUsingRefreshToken;
+import com.github.asavershin.api.application.in.services.user.JwtService;
+import com.github.asavershin.api.application.out.TokenRepository;
+import com.github.asavershin.api.config.properties.JwtProperties;
+import com.github.asavershin.api.domain.user.Credentials;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class GetNewCredentialsUsingRefreshTokenImpl
+ implements GetNewCredentialsUsingRefreshToken {
+ /**
+ * The repository for storing and retrieving tokens.
+ */
+ private final TokenRepository tokenRepository;
+
+ /**
+ * The service for generating and validating JWT tokens.
+ */
+ private final JwtService jwtService;
+
+ /**
+ * The properties containing the configuration for JWT tokens.
+ */
+ private final JwtProperties jwtProperties;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public ApplicationCredentials get(final Credentials credentials) {
+ tokenRepository.deleteAllTokensByUserEmail(credentials.email());
+ var at = jwtService.generateAccessToken(credentials);
+ var rt = jwtService.generateRefreshToken(credentials);
+ tokenRepository.saveAccessToken(
+ credentials.email(),
+ at,
+ jwtProperties.getAccessExpiration()
+ );
+ tokenRepository.saveRefreshToken(
+ credentials.email(),
+ rt,
+ jwtProperties.getRefreshExpiration()
+ );
+ return new ApplicationCredentials(at, rt);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/JwtServiceIml.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/JwtServiceIml.java
new file mode 100644
index 0000000..675b938
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/JwtServiceIml.java
@@ -0,0 +1,69 @@
+package com.github.asavershin.api.application.in.services.user.impl;
+
+import com.github.asavershin.api.application.in.services.user.JwtService;
+import com.github.asavershin.api.config.properties.JwtProperties;
+import com.github.asavershin.api.domain.user.Credentials;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.security.Key;
+import java.util.Date;
+
+@Service
+@RequiredArgsConstructor
+public class JwtServiceIml implements JwtService {
+
+ /**
+ * The JwtProperties object contains the properties
+ * for generating and validating JWT tokens.
+ */
+ private final JwtProperties jwtProperties;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public String generateAccessToken(final Credentials credentials) {
+ return buildToken(credentials, jwtProperties.getAccessExpiration());
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public String generateRefreshToken(final Credentials credentials) {
+ return buildToken(credentials, jwtProperties.getRefreshExpiration());
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public String extractSub(final String jwt) {
+ return Jwts.parserBuilder()
+ .setSigningKey(getSignInKey())
+ .build()
+ .parseClaimsJws(jwt)
+ .getBody()
+ .getSubject();
+ }
+
+ private String buildToken(final Credentials credentials,
+ final long expiration) {
+ return Jwts
+ .builder()
+ .setSubject(credentials.email())
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(
+ new Date(System.currentTimeMillis() + expiration)
+ )
+ .signWith(getSignInKey(), SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ private Key getSignInKey() {
+ byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecret());
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/package-info.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/package-info.java
new file mode 100644
index 0000000..b0db315
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains implementation application services for users.
+ * @author asavershin
+ */
+package com.github.asavershin.api.application.in.services.user.impl;
diff --git a/api/src/main/java/com/github/asavershin/api/application/in/services/user/package-info.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/package-info.java
new file mode 100644
index 0000000..6bdc8a8
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains application services for users.
+ * @author asavershin
+ */
+package com.github.asavershin.api.application.in.services.user;
diff --git a/api/src/main/java/com/github/asavershin/api/application/out/CacheRepository.java b/api/src/main/java/com/github/asavershin/api/application/out/CacheRepository.java
new file mode 100644
index 0000000..8c22291
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/out/CacheRepository.java
@@ -0,0 +1,28 @@
+package com.github.asavershin.api.application.out;
+
+public interface CacheRepository {
+ /**
+ * Adds a cache entry with the specified key,
+ * token, and expiration time.
+ *
+ * @param key the unique identifier for the cache entry
+ * @param token the token associated with the cache entry
+ * @param expiration the time in milliseconds after
+ * which the cache entry will expire
+ */
+ void addCache(String key, String token, long expiration);
+ /**
+ * Retrieves the cache entry associated with the specified key.
+ *
+ * @param key the unique identifier for the cache entry
+ * @return the token associated with the cache entry,
+ * or null if the entry does not exist or has expired
+ */
+ String getCache(String key);
+ /**
+ * Deletes the cache entry associated with the specified key.
+ *
+ * @param key the unique identifier for the cache entry
+ */
+ void deleteCache(String key);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/out/FileService.java b/api/src/main/java/com/github/asavershin/api/application/out/FileService.java
new file mode 100644
index 0000000..2691519
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/out/FileService.java
@@ -0,0 +1,27 @@
+package com.github.asavershin.api.application.out;
+
+import java.util.List;
+
+public interface FileService {
+ /**
+ * Saves a file to the storage.
+ *
+ * @param file The file object to be saved.
+ * @param filename The name of the file to be saved.
+ * Must be unique.
+ */
+ void saveFile(K file, String filename);
+ /**
+ * Retrieves the content of a file from the storage.
+ *
+ * @param link The unique identifier or link of the file.
+ * @return The content of the file as a byte array.
+ */
+ byte[] getFile(String link);
+ /**
+ * Deletes multiple files from the storage.
+ *
+ * @param files A list of file links to be deleted.
+ */
+ void deleteFiles(List files);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/out/MinioService.java b/api/src/main/java/com/github/asavershin/api/application/out/MinioService.java
new file mode 100644
index 0000000..93835cb
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/out/MinioService.java
@@ -0,0 +1,6 @@
+package com.github.asavershin.api.application.out;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public interface MinioService extends FileService {
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/out/TokenRepository.java b/api/src/main/java/com/github/asavershin/api/application/out/TokenRepository.java
new file mode 100644
index 0000000..2f03cb0
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/out/TokenRepository.java
@@ -0,0 +1,46 @@
+package com.github.asavershin.api.application.out;
+
+public interface TokenRepository {
+ /**
+ * Retrieves the access token for the given email.
+ *
+ * @param email the email of the user
+ * @return the access token for the given email
+ */
+ String getAccessToken(String email);
+
+ /**
+ * Retrieves the refresh token for the given email.
+ *
+ * @param email the email of the user
+ * @return the refresh token for the given email
+ */
+ String getRefreshToken(String email);
+
+ /**
+ * Saves the refresh token for the given user with
+ * the specified JWT token and expiration time.
+ *
+ * @param username the username of the user
+ * @param jwtToken the JWT token to be saved
+ * @param expiration the expiration time of the token
+ */
+ void saveRefreshToken(String username, String jwtToken, Long expiration);
+
+ /**
+ * Saves the access token for the given user with
+ * the specified JWT token and expiration time.
+ *
+ * @param username the username of the user
+ * @param jwtToken the JWT token to be saved
+ * @param expiration the expiration time of the token
+ */
+ void saveAccessToken(String username, String jwtToken, Long expiration);
+
+ /**
+ * Deletes all tokens associated with the given user email.
+ *
+ * @param username the username of the user
+ */
+ void deleteAllTokensByUserEmail(String username);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/application/out/package-info.java b/api/src/main/java/com/github/asavershin/api/application/out/package-info.java
new file mode 100644
index 0000000..c70aea2
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/application/out/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains storage interfaces that are not domain specific.
+ * @author asavershin
+ */
+package com.github.asavershin.api.application.out;
diff --git a/api/src/main/java/com/github/asavershin/api/common/NotFoundException.java b/api/src/main/java/com/github/asavershin/api/common/NotFoundException.java
new file mode 100644
index 0000000..a2a8110
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/NotFoundException.java
@@ -0,0 +1,10 @@
+package com.github.asavershin.api.common;
+
+public class NotFoundException extends RuntimeException {
+ /**
+ * @param message specifies information about the object not found
+ */
+ public NotFoundException(final String message) {
+ super(message);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/common/Validator.java b/api/src/main/java/com/github/asavershin/api/common/Validator.java
new file mode 100644
index 0000000..f0f70dc
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/Validator.java
@@ -0,0 +1,141 @@
+package com.github.asavershin.api.common;
+
+import java.util.Objects;
+
+/**
+ * A utility class for performing basic validation checks.
+ *
+ * @author asavershin
+ */
+public abstract class Validator {
+
+ /**
+ * Asserts that the given string has a length within
+ * the specified range.
+ *
+ * @param aString the string to validate
+ * @param aMinimum the minimum length
+ * @param aMaximum the maximum length
+ * @param aMessage the message to throw if the length is invalid
+ * @throws IllegalArgumentException if the length of the string
+ * is less than {@code aMinimum} or greater than {@code aMaximum}
+ */
+ public static void assertArgumentLength(final String aString,
+ final int aMinimum,
+ final int aMaximum,
+ final String aMessage) {
+ int length = aString.length();
+ if (length < aMinimum || length > aMaximum) {
+ throw new IllegalArgumentException(aMessage);
+ }
+ }
+ /**
+ * Asserts that the given string has a length greater than or equal
+ * to the specified minimum.
+ *
+ * @param aString the string to validate
+ * @param aMinimum the minimum length
+ * @param aMessage the message to throw if the length is invalid
+ * @throws IllegalArgumentException if the length of the string is
+ * less than {@code aMinimum}
+ */
+ public static void assertArgumentLength(final String aString,
+ final int aMinimum,
+ final String aMessage) {
+ int length = aString.length();
+ if (length < aMinimum) {
+ throw new IllegalArgumentException(aMessage);
+ }
+ }
+
+ /**
+ * Asserts that the given string matches the specified regular
+ * expression.
+ *
+ * @param email the string to validate
+ * @param regex the regular expression to match against
+ * @param aMessage the message to throw if the string does not
+ * match the regular expression
+ * @throws IllegalArgumentException if the string does not
+ * match the regular expression
+ */
+ public static void assertStringFormat(final String email,
+ final String regex,
+ final String aMessage) {
+ if (!email.matches(regex)) {
+ throw new IllegalArgumentException(aMessage);
+ }
+ }
+
+ /**
+ * Asserts that the given array has a length greater than or
+ * equal to the specified minimum.
+ *
+ * @param array the array to validate
+ * @param minLength the minimum length
+ * @param aMessage the message to throw if the length of the
+ * array is less than {@code minLength}
+ * @throws IllegalArgumentException if the length of the array
+ * is less than {@code minLength}
+ */
+ public static void assertArrayLength(final Object[] array,
+ final Integer minLength,
+ final String aMessage) {
+ if (array.length < minLength) {
+ throw new IllegalArgumentException(aMessage);
+ }
+ }
+
+ /**
+ * Asserts that the given value is within the specified range.
+ *
+ * @param value the value to validate
+ * @param minLength the minimum length
+ * @param maxLength the maximum length
+ * @param aMessage the message to throw if the value is outside
+ * the specified range
+ * @throws IllegalArgumentException if the value is less than
+ * {@code minLength} or greater than {@code maxLength}
+ */
+ public static void assertLongSize(final Long value,
+ final Long minLength,
+ final Long maxLength,
+ final String aMessage) {
+ if (value < minLength || value > maxLength) {
+ throw new IllegalArgumentException(aMessage);
+ }
+ }
+
+ /**
+ * Asserts that the given value is greater than or equal to the
+ * specified minimum.
+ *
+ * @param value the value to validate
+ * @param minLength the minimum length
+ * @param aMessage the message to throw if the value is less
+ * than {@code minLength}
+ * @throws IllegalArgumentException if the value is less
+ * than {@code minLength}
+ */
+ public static void assertLongSize(final Long value,
+ final Long minLength,
+ final String aMessage) {
+ if (value < minLength) {
+ throw new IllegalArgumentException(aMessage);
+ }
+ }
+
+ /**
+ * Asserts that the given object is not null.
+ *
+ * @param object the object to validate
+ * @param aMessage the message to throw if the object is null
+ * @throws IllegalArgumentException if the object is null
+ */
+ public static void assertNotFound(final Object object,
+ final String aMessage) {
+ if (Objects.isNull(object)) {
+ throw new NotFoundException(aMessage);
+ }
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/common/annotations/Command.java b/api/src/main/java/com/github/asavershin/api/common/annotations/Command.java
new file mode 100644
index 0000000..51b88d9
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/annotations/Command.java
@@ -0,0 +1,13 @@
+package com.github.asavershin.api.common.annotations;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Inherited;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface Command {
+}
diff --git a/api/src/main/java/com/github/asavershin/api/common/annotations/DomainService.java b/api/src/main/java/com/github/asavershin/api/common/annotations/DomainService.java
new file mode 100644
index 0000000..30f2f55
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/annotations/DomainService.java
@@ -0,0 +1,13 @@
+package com.github.asavershin.api.common.annotations;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Inherited;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface DomainService {
+}
diff --git a/api/src/main/java/com/github/asavershin/api/common/annotations/Query.java b/api/src/main/java/com/github/asavershin/api/common/annotations/Query.java
new file mode 100644
index 0000000..1b83bf0
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/annotations/Query.java
@@ -0,0 +1,13 @@
+package com.github.asavershin.api.common.annotations;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Inherited;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface Query {
+}
diff --git a/api/src/main/java/com/github/asavershin/api/common/annotations/package-info.java b/api/src/main/java/com/github/asavershin/api/common/annotations/package-info.java
new file mode 100644
index 0000000..15967d2
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/annotations/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package annotations for classes that characterize their activities.
+ * @author asavershin
+ */
+package com.github.asavershin.api.common.annotations;
diff --git a/api/src/main/java/com/github/asavershin/api/common/package-info.java b/api/src/main/java/com/github/asavershin/api/common/package-info.java
new file mode 100644
index 0000000..74606bc
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/common/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * This package contains code that could be useful for coding.
+ * It contains validators and exceptions that often found.
+ * It annotations for classes that characterize their activities.
+ * @author asavershin
+ */
+package com.github.asavershin.api.common;
diff --git a/api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java b/api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java
new file mode 100644
index 0000000..6a084c2
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java
@@ -0,0 +1,23 @@
+package com.github.asavershin.api.config;
+
+import com.github.asavershin.api.common.annotations.Command;
+import com.github.asavershin.api.common.annotations.DomainService;
+import com.github.asavershin.api.common.annotations.Query;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+
+@Configuration
+@ComponentScan(
+ basePackages = "com.github.asavershin.api",
+ includeFilters = {
+ @ComponentScan.Filter(type = FilterType.ANNOTATION,
+ value = DomainService.class),
+ @ComponentScan.Filter(type = FilterType.ANNOTATION,
+ value = Query.class),
+ @ComponentScan.Filter(type = FilterType.ANNOTATION,
+ value = Command.class)
+ }
+)
+public class AnnotationsConfig {
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/AuthConfig.java b/api/src/main/java/com/github/asavershin/api/config/AuthConfig.java
new file mode 100644
index 0000000..69da912
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/AuthConfig.java
@@ -0,0 +1,21 @@
+package com.github.asavershin.api.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@RequiredArgsConstructor
+public class AuthConfig {
+ /**
+ * Configures a BCryptPasswordEncoder bean for password encoding.
+ *
+ * @return a new instance of BCryptPasswordEncoder
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java b/api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java
new file mode 100644
index 0000000..3249974
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java
@@ -0,0 +1,40 @@
+package com.github.asavershin.api.config;
+
+import com.github.asavershin.api.config.properties.MinIOProperties;
+import io.minio.MinioClient;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration class for MinIO client.
+ *
+ * @author Asavershin
+ */
+@Configuration
+@RequiredArgsConstructor
+@Slf4j
+public class MinIOConfig {
+
+ /**
+ * The MinIO properties.
+ */
+ private final MinIOProperties minioProperties;
+
+ /**
+ * Creates a MinioClient instance.
+ *
+ * @return A MinioClient instance configured with the provided
+ * MinIO properties.
+ */
+ @Bean
+ public MinioClient minioClient() {
+ log.info(minioProperties.toString());
+ return MinioClient.builder()
+ .endpoint(minioProperties.getUrl())
+ .credentials(minioProperties.getUser(),
+ minioProperties.getPassword())
+ .build();
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/RedisConfig.java b/api/src/main/java/com/github/asavershin/api/config/RedisConfig.java
new file mode 100644
index 0000000..5b920ef
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/RedisConfig.java
@@ -0,0 +1,36 @@
+package com.github.asavershin.api.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * Configuration class for setting up the RedisTemplate.
+ *
+ * @author Asavershin
+ */
+@Configuration
+public class RedisConfig {
+
+ /**
+ * Creates a new instance of {@link RedisTemplate} with the provided
+ * {@link RedisConnectionFactory}.
+ *
+ * @param connectionFactory the RedisConnectionFactory to use
+ * @return a new instance of {@link RedisTemplate}
+ */
+ @Bean
+ public RedisTemplate redisTemplate(
+ final RedisConnectionFactory connectionFactory) {
+ final RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(
+ new GenericToStringSerializer<>(Object.class)
+ );
+ return template;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/package-info.java b/api/src/main/java/com/github/asavershin/api/config/package-info.java
new file mode 100644
index 0000000..3d5fb9e
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains configs for infrastructure of service.
+ * @author asavershin
+ */
+package com.github.asavershin.api.config;
diff --git a/api/src/main/java/com/github/asavershin/api/config/properties/JwtProperties.java b/api/src/main/java/com/github/asavershin/api/config/properties/JwtProperties.java
new file mode 100644
index 0000000..5cb2e2b
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/properties/JwtProperties.java
@@ -0,0 +1,30 @@
+package com.github.asavershin.api.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@Data
+@ConfigurationProperties(prefix = "jwt")
+public class JwtProperties {
+ /**
+ * Secret key used for signing JWT tokens.
+ */
+ private String secret;
+
+ /**
+ * Expiration time for access tokens in milliseconds.
+ */
+ private Long accessExpiration;
+
+ /**
+ * Expiration time for refresh tokens in milliseconds.
+ */
+ private Long refreshExpiration;
+
+ /**
+ * Constants for the start of the token in the Authorization header.
+ */
+ public static final int START_OF_TOKEN = 7;
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/properties/MinIOProperties.java b/api/src/main/java/com/github/asavershin/api/config/properties/MinIOProperties.java
new file mode 100644
index 0000000..805e730
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/properties/MinIOProperties.java
@@ -0,0 +1,32 @@
+package com.github.asavershin.api.config.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@Data
+@ConfigurationProperties(prefix = "minio")
+@NoArgsConstructor
+public class MinIOProperties {
+ /**
+ * The name of the bucket in the MinIO service.
+ */
+ private String bucket;
+
+ /**
+ * The URL of the MinIO service.
+ */
+ private String url;
+
+ /**
+ * The username for the MinIO service.
+ */
+ private String user;
+
+ /**
+ * The password for the MinIO service.
+ */
+ private String password;
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/properties/UserProperties.java b/api/src/main/java/com/github/asavershin/api/config/properties/UserProperties.java
new file mode 100644
index 0000000..a4bbd51
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/properties/UserProperties.java
@@ -0,0 +1,18 @@
+package com.github.asavershin.api.config.properties;
+
+public final class UserProperties {
+ /**
+ * Minimum password length.
+ */
+ public static final int MIN_PASSWORD_LENGTH = 8;
+ /**
+ * Maximum firstname length.
+ */
+ public static final int MAX_FIRSTNAME_LENGTH = 20;
+ /**
+ * Maximum lastname length.
+ */
+ public static final int MAX_LASTNAME_LENGTH = 20;
+
+ private UserProperties() { }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/config/properties/package-info.java b/api/src/main/java/com/github/asavershin/api/config/properties/package-info.java
new file mode 100644
index 0000000..1109013
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/config/properties/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * This package contains parameters that can be used in different
+ * parts of the application. The values are taken from .yml/.properties file
+ * @author asavershin
+ */
+package com.github.asavershin.api.config.properties;
diff --git a/api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java b/api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java
new file mode 100644
index 0000000..837edcf
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java
@@ -0,0 +1,13 @@
+package com.github.asavershin.api.domain;
+
+import com.github.asavershin.api.common.Validator;
+
+public abstract class IsEntityFound {
+ protected final void isEntityFound(final Object entity,
+ final String entityName,
+ final String idName,
+ final String entityId) {
+ Validator.assertNotFound(entity,
+ entityName + " with " + idName + entityId + " not found");
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java b/api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java
new file mode 100644
index 0000000..1c0f0ba
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java
@@ -0,0 +1,40 @@
+package com.github.asavershin.api.domain;
+
+import com.github.asavershin.api.common.Validator;
+
+import java.util.Objects;
+
+/**
+ * A record representing a part of resources to be fetched from a
+ * data source.
+ * It contains the page number and the page size for pagination purposes.
+ *
+ * @param pageNumber The zero-based index of the first record
+ * to retrieve.
+ * @param pageSize The maximum number of records to retrieve.
+ */
+public record PartOfResources(Long pageNumber, Long pageSize) {
+
+ /**
+ * Constructor for the PartOfResources record.
+ *
+ * @param pageNumber The zero-based index of the first record
+ * to retrieve.
+ * @param pageSize The maximum number of records to retrieve.
+ * @throws IllegalArgumentException if the pageSize is less than
+ * or equal to zero.
+ */
+ public PartOfResources {
+ Objects.requireNonNull(pageNumber,
+ "PageNumber must not be empty");
+ Objects.requireNonNull(pageSize,
+ "PageSize must not be empty");
+ Validator.assertLongSize(pageNumber,
+ 0L, "PageNumber must not be negative");
+ Validator.assertLongSize(
+ pageSize,
+ 1L,
+ "PageSize must be positive"
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java b/api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java
new file mode 100644
index 0000000..3db43d2
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java
@@ -0,0 +1,20 @@
+package com.github.asavershin.api.domain;
+
+/**
+ * Represents an exception that is thrown
+ * when there is an issue with resource ownership.
+ * For example, a user requests someone else's picture
+ * @author Asavershin
+ */
+public class ResourceOwnershipException extends RuntimeException {
+
+ /**
+ * Constructs a new instance of the ResourceOwnershipException
+ * with the specified error message.
+ *
+ * @param message the error message
+ */
+ public ResourceOwnershipException(final String message) {
+ super(message);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/DeleteImageOfUser.java b/api/src/main/java/com/github/asavershin/api/domain/image/DeleteImageOfUser.java
new file mode 100644
index 0000000..404c0ab
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/DeleteImageOfUser.java
@@ -0,0 +1,14 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.domain.user.UserId;
+
+@FunctionalInterface
+public interface DeleteImageOfUser {
+ /**
+ * Removes the specified image of the specified user.
+ *
+ * @param imageId the unique identifier of the image to be removed
+ * @param userId the unique identifier of the user who owns the image
+ */
+ void removeImageOfUser(ImageId imageId, UserId userId);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/GetImageOfUser.java b/api/src/main/java/com/github/asavershin/api/domain/image/GetImageOfUser.java
new file mode 100644
index 0000000..a116ae6
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/GetImageOfUser.java
@@ -0,0 +1,15 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.domain.user.UserId;
+
+@FunctionalInterface
+public interface GetImageOfUser {
+ /**
+ * Method that retrieves an image of a user.
+ *
+ * @param userId the unique identifier of the user
+ * @param imageId the unique identifier of the image
+ * @return the image of the specified user
+ */
+ Image getImageOfUser(UserId userId, ImageId imageId);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/GetPartImagesOfUser.java b/api/src/main/java/com/github/asavershin/api/domain/image/GetPartImagesOfUser.java
new file mode 100644
index 0000000..db0cb2b
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/GetPartImagesOfUser.java
@@ -0,0 +1,20 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.user.UserId;
+
+import java.util.List;
+
+@FunctionalInterface
+public interface GetPartImagesOfUser {
+ /**
+ * This interface represents a function that retrieves a list of
+ * images for a specific user and part of resources.
+ *
+ * @param userId the unique identifier of the user
+ * @param partOfResources pagination
+ * @return a list of images belonging to the specified
+ * user and part of resources as pagination
+ */
+ List get(UserId userId, PartOfResources partOfResources);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/Image.java b/api/src/main/java/com/github/asavershin/api/domain/image/Image.java
new file mode 100644
index 0000000..f8c7063
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/Image.java
@@ -0,0 +1,79 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.domain.ResourceOwnershipException;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.util.Objects;
+
+@ToString
+@Getter
+@EqualsAndHashCode
+public class Image {
+ /**
+ * The unique identifier for the image.
+ */
+ private ImageId imageId;
+
+ /**
+ * The meta information associated with the image.
+ */
+ private MetaData metaInfo;
+
+ /**
+ * The user who owns the image.
+ */
+ private UserId userId;
+
+ /**
+ * Constructor for creating a new Image object.
+ *
+ * @param aImageId the unique identifier for the image
+ * @param aMetaInfo the meta information associated with the image
+ * @param aUserId the user who owns the image
+ */
+ public Image(final ImageId aImageId,
+ final MetaData aMetaInfo,
+ final UserId aUserId) {
+ setImageId(aImageId);
+ setMetaInfo(aMetaInfo);
+ setUserId(aUserId);
+ }
+
+ /**
+ * Checks if the given user owns the image.
+ *
+ * @param aUserId the user to check ownership for
+ * @return the same image instance if the user owns the image,
+ * otherwise throws a {@link ResourceOwnershipException}
+ * @throws ResourceOwnershipException if the image does not belong
+ * to the given user
+ */
+ public Image belongsToUser(final UserId aUserId) {
+ if (!this.userId.equals(aUserId)) {
+ throw new ResourceOwnershipException(
+ "Image with id " + imageId.value().toString()
+ + " does not belong to user with id "
+ + aUserId.value().toString()
+ );
+ }
+ return this;
+ }
+
+ private void setImageId(final ImageId aImageId) {
+ Objects.requireNonNull(aImageId, "ImageId must not be null");
+ this.imageId = aImageId;
+ }
+
+ private void setMetaInfo(final MetaData aMetaInfo) {
+ Objects.requireNonNull(aMetaInfo, "MetaInfo must not be null");
+ this.metaInfo = aMetaInfo;
+ }
+
+ private void setUserId(final UserId aUserId) {
+ Objects.requireNonNull(aUserId, "UserId must not be null");
+ this.userId = aUserId;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/ImageExtension.java b/api/src/main/java/com/github/asavershin/api/domain/image/ImageExtension.java
new file mode 100644
index 0000000..2b367f1
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageExtension.java
@@ -0,0 +1,70 @@
+package com.github.asavershin.api.domain.image;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * Enum representing different image file extensions.
+ *
+ * @author asavershin
+ */
+public enum ImageExtension {
+ /**
+ * Represents the JPG image file extension.
+ */
+ JPG(".jpg"),
+
+ /**
+ * Represents the PNG image file extension.
+ */
+ PNG(".png"),
+
+ /**
+ * Represents the JPEG image file extension.
+ */
+ JPEG(".jpeg");
+
+ /**
+ * Represents the string image file extension
+ * in format .extension.
+ */
+ private final String extension;
+
+ ImageExtension(final String aExtension) {
+ this.extension = aExtension;
+ }
+
+ @Override
+ public String toString() {
+ return extension;
+ }
+
+ /**
+ * Map that stores a string representation of an extension in keys,
+ * and their corresponding ENUM in values.
+ */
+ private static final Map STRING_TO_ENUM
+ = Stream.of(values()).collect(toMap(Object::toString, e -> e));
+
+ /**
+ * Converts a string representation of an image extension
+ * to the corresponding enum instance.
+ *
+ * @param extension the string representation of the image extension
+ * @return the enum instance corresponding to the given
+ * string representation
+ * @throws IllegalArgumentException if the given string representation
+ * does not match any known image extension
+ */
+ public static ImageExtension fromString(final String extension) {
+ var imageExtension = STRING_TO_ENUM.get(extension);
+ if (imageExtension == null) {
+ throw new IllegalArgumentException(
+ "Invalid extension: " + extension
+ );
+ }
+ return imageExtension;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/ImageId.java b/api/src/main/java/com/github/asavershin/api/domain/image/ImageId.java
new file mode 100644
index 0000000..f6ef6c7
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageId.java
@@ -0,0 +1,29 @@
+package com.github.asavershin.api.domain.image;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * A value object representing an image ID.
+ *
+ * @param value The unique identifier for the image.
+ */
+public record ImageId(UUID value) {
+ /**
+ * Constructs an ImageId instance with the provided UUID.
+ *
+ * @param value The unique identifier for the image.
+ * @throws NullPointerException if the provided value is null.
+ */
+ public ImageId {
+ Objects.requireNonNull(value, "Image ID must not be null");
+ }
+ /**
+ * Generates a new, unique ImageId.
+ *
+ * @return A new ImageId instance with a randomly generated UUID.
+ */
+ public static ImageId nextIdentity() {
+ return new ImageId(UUID.randomUUID());
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/ImageNameWithExtension.java b/api/src/main/java/com/github/asavershin/api/domain/image/ImageNameWithExtension.java
new file mode 100644
index 0000000..5b0f88f
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageNameWithExtension.java
@@ -0,0 +1,92 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.common.Validator;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+@Getter
+@EqualsAndHashCode
+public final class ImageNameWithExtension {
+
+ /**
+ * The maximum length of an image name.
+ */
+ private static final int MAX_IMAGE_NAME = 50;
+
+ /**
+ * The image name.
+ */
+ private final String imageName;
+
+ /**
+ * The image extension.
+ */
+ private final ImageExtension imageExtension;
+
+ /**
+ * Constructs an {@code ImageNameWithExtension} object.
+ *
+ * @param aImageName the image name without last extension
+ * @param aImageExt the image extension
+ */
+ private ImageNameWithExtension(final String aImageName,
+ final ImageExtension aImageExt) {
+ this.imageName = aImageName;
+ this.imageExtension = aImageExt;
+ }
+ /**
+ * Creates an {@code ImageNameWithExtension}
+ * object from an original file name.
+ *
+ * @param originalFileName the original file name
+ * @return the created {@code ImageNameWithExtension} object
+ */
+ public static ImageNameWithExtension fromOriginalFileName(
+ final String originalFileName
+ ) {
+ notNullValidate(originalFileName);
+ String[] parts = originalFileName.split("\\.");
+ Validator.assertArrayLength(parts, 2, "Incorrect image format");
+ var extension = ImageExtension
+ .fromString("." + parts[parts.length - 1]);
+ var imageName = String
+ .join(".", Arrays.copyOfRange(parts, 0, parts.length - 1));
+ lengthNameValidate(imageName);
+
+ return new ImageNameWithExtension(imageName, extension);
+ }
+
+ /**
+ * Creates an {@code ImageNameWithExtension}
+ * object that founded in repository from an image name and an extension.
+ *
+ * @param imageName the image name
+ * @param extension the image extension
+ * @return the created {@code ImageNameWithExtension} object
+ */
+ public static ImageNameWithExtension founded(
+ final String imageName,
+ final String extension) {
+ notNullValidate(imageName);
+ lengthNameValidate(imageName);
+ return new ImageNameWithExtension(imageName,
+ ImageExtension.fromString(extension));
+ }
+
+ private static void notNullValidate(final String name) {
+ Objects.requireNonNull(name, "ImageName must not be null");
+ }
+
+ private static void lengthNameValidate(final String name) {
+ Validator.assertArgumentLength(name, 0, MAX_IMAGE_NAME,
+ "ImageName must be " + 0 + "-" + MAX_IMAGE_NAME + " in length");
+ }
+
+ @Override
+ public String toString() {
+ return imageName + imageExtension;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/ImageRepository.java b/api/src/main/java/com/github/asavershin/api/domain/image/ImageRepository.java
new file mode 100644
index 0000000..5998b6b
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageRepository.java
@@ -0,0 +1,40 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.user.UserId;
+
+import java.util.List;
+
+public interface ImageRepository {
+ /**
+ * Saves an Image object to the database.
+ *
+ * @param image the Image object to be saved
+ */
+ void save(Image image);
+ /**
+ * Finds all Images associated with the given UserId and PartOfResources.
+ *
+ * @param userId the UserId of the user whose images are to be retrieved
+ * @param partOfResources the PartOfResources to filter the images by
+ * @return a list of Images associated with the given UserId
+ * and PartOfResources
+ */
+ List findImagesByUserId(UserId userId,
+ PartOfResources partOfResources);
+
+ /**
+ * Finds an Image by its ImageId.
+ *
+ * @param imageId the ImageId of the Image to be retrieved
+ * @return the Image object with the given ImageId, or null if not found
+ */
+ Image findImageByImageId(ImageId imageId);
+
+ /**
+ * Deletes an Image by its ImageId.
+ *
+ * @param imageId the ImageId of the Image to be deleted
+ */
+ void deleteImageByImageId(Image imageId);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/MetaData.java b/api/src/main/java/com/github/asavershin/api/domain/image/MetaData.java
new file mode 100644
index 0000000..1a34051
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/MetaData.java
@@ -0,0 +1,43 @@
+package com.github.asavershin.api.domain.image;
+
+import com.github.asavershin.api.common.Validator;
+
+import java.util.Objects;
+
+public record MetaData(ImageNameWithExtension imageNameWithExtension,
+ Long imageSize) {
+ /**
+ * The maximum length of an image name.
+ */
+ private static final int MAX_IMAGE_NAME = 50;
+ /**
+ * Size is in bytes. 10MB
+ */
+ private static final long MAX_IMAGE_SIZE = 10485760;
+
+ /**
+ * Constructs a new instance of MetaInfo
+ * and validates the provided image name and size.
+ */
+ public MetaData {
+ validateImageName(imageNameWithExtension);
+ validateImageSize(imageSize);
+ }
+
+ private void validateImageName(
+ final ImageNameWithExtension aImageNameWithExtension
+ ) {
+ Objects.requireNonNull(aImageNameWithExtension,
+ "Name and extension must not be null");
+ Validator.assertArgumentLength(aImageNameWithExtension.toString(),
+ 0, MAX_IMAGE_NAME,
+ "Image name must be "
+ + "0-" + MAX_IMAGE_NAME + " in length");
+ }
+
+ private void validateImageSize(final Long aImageSize) {
+ Objects.requireNonNull(aImageSize, "Image size must not be null");
+ Validator.assertLongSize(aImageSize, 0L, MAX_IMAGE_SIZE,
+ "Image size must be " + "0-" + MAX_IMAGE_SIZE);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/StoreImageOfUser.java b/api/src/main/java/com/github/asavershin/api/domain/image/StoreImageOfUser.java
new file mode 100644
index 0000000..ab0d3d4
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/StoreImageOfUser.java
@@ -0,0 +1,11 @@
+package com.github.asavershin.api.domain.image;
+
+@FunctionalInterface
+public interface StoreImageOfUser {
+ /**
+ * Stores the provided image of a user.
+ *
+ * @param image The image to be stored.
+ */
+ void storeImageOfUser(Image image);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/impl/DeleteImageOfUserImpl.java b/api/src/main/java/com/github/asavershin/api/domain/image/impl/DeleteImageOfUserImpl.java
new file mode 100644
index 0000000..569b793
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/DeleteImageOfUserImpl.java
@@ -0,0 +1,35 @@
+package com.github.asavershin.api.domain.image.impl;
+
+import com.github.asavershin.api.common.annotations.Command;
+import com.github.asavershin.api.domain.IsEntityFound;
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.image.DeleteImageOfUser;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+
+@Command
+@RequiredArgsConstructor
+public class DeleteImageOfUserImpl extends IsEntityFound
+ implements DeleteImageOfUser {
+ /**
+ * The ImageRepository dependency for accessing image data.
+ */
+ private final ImageRepository imageRepository;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void removeImageOfUser(
+ final ImageId imageId,
+ final UserId userId
+ ) {
+ var image = imageRepository.findImageByImageId(imageId);
+ isEntityFound(image, "Image", "Id", imageId.value().toString());
+ image.belongsToUser(userId);
+ imageRepository.deleteImageByImageId(
+ imageRepository.findImageByImageId(imageId)
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetImageOfUserImpl.java b/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetImageOfUserImpl.java
new file mode 100644
index 0000000..674e5ac
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetImageOfUserImpl.java
@@ -0,0 +1,37 @@
+package com.github.asavershin.api.domain.image.impl;
+
+import com.github.asavershin.api.common.annotations.DomainService;
+import com.github.asavershin.api.domain.IsEntityFound;
+import com.github.asavershin.api.domain.image.GetImageOfUser;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+
+@DomainService
+@RequiredArgsConstructor
+public class GetImageOfUserImpl extends IsEntityFound
+ implements GetImageOfUser {
+ /**
+ * The ImageRepository dependency for accessing image data.
+ */
+ private final ImageRepository imageRepository;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public Image getImageOfUser(final UserId userId,
+ final ImageId imageId) {
+ var image = imageRepository.findImageByImageId(imageId);
+ isEntityFound(
+ image,
+ "Image",
+ "Id",
+ imageId.value().toString()
+ );
+ image.belongsToUser(userId);
+ return image;
+ }
+
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetPartImagesOfUserImpl.java b/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetPartImagesOfUserImpl.java
new file mode 100644
index 0000000..9a85c58
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetPartImagesOfUserImpl.java
@@ -0,0 +1,28 @@
+package com.github.asavershin.api.domain.image.impl;
+
+import com.github.asavershin.api.common.annotations.Query;
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.image.GetPartImagesOfUser;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+@Query
+@RequiredArgsConstructor
+public class GetPartImagesOfUserImpl implements GetPartImagesOfUser {
+ /**
+ * The ImageRepository dependency for accessing image data.
+ */
+ private final ImageRepository imageRepository;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public List get(final UserId userId,
+ final PartOfResources partOfResources) {
+ return imageRepository.findImagesByUserId(userId, partOfResources);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/impl/StoreImageOfUserImpl.java b/api/src/main/java/com/github/asavershin/api/domain/image/impl/StoreImageOfUserImpl.java
new file mode 100644
index 0000000..d48c082
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/StoreImageOfUserImpl.java
@@ -0,0 +1,23 @@
+package com.github.asavershin.api.domain.image.impl;
+
+import com.github.asavershin.api.common.annotations.Command;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.image.StoreImageOfUser;
+import lombok.RequiredArgsConstructor;
+
+@Command
+@RequiredArgsConstructor
+public class StoreImageOfUserImpl implements StoreImageOfUser {
+ /**
+ * The ImageRepository dependency for accessing image data.
+ */
+ private final ImageRepository imageRepository;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void storeImageOfUser(final Image image) {
+ imageRepository.save(image);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/impl/package-info.java b/api/src/main/java/com/github/asavershin/api/domain/image/impl/package-info.java
new file mode 100644
index 0000000..65e29ee
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * This package contains implementations of domain services,
+ * queries, commands for image entity.
+ *
+ * @author asavershin
+ */
+package com.github.asavershin.api.domain.image.impl;
diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/package-info.java b/api/src/main/java/com/github/asavershin/api/domain/image/package-info.java
new file mode 100644
index 0000000..3e4dfaa
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/image/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains the domain model classes related to image processing.
+ * @author asavershin
+ */
+package com.github.asavershin.api.domain.image;
diff --git a/api/src/main/java/com/github/asavershin/api/domain/lombok.config b/api/src/main/java/com/github/asavershin/api/domain/lombok.config
new file mode 100644
index 0000000..f4387a7
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/lombok.config
@@ -0,0 +1 @@
+lombok.accessors.fluent=true
\ No newline at end of file
diff --git a/api/src/main/java/com/github/asavershin/api/domain/package-info.java b/api/src/main/java/com/github/asavershin/api/domain/package-info.java
new file mode 100644
index 0000000..9ee4b6d
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * This package contains the entities, value objects
+ * entity repositories, queries, commands and domain
+ * services used in the application.
+ *
+ * @author Asavershin
+ */
+package com.github.asavershin.api.domain;
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/AuthException.java b/api/src/main/java/com/github/asavershin/api/domain/user/AuthException.java
new file mode 100644
index 0000000..e6f8635
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/AuthException.java
@@ -0,0 +1,21 @@
+package com.github.asavershin.api.domain.user;
+
+/**
+ * Represents an exception that occurs during authentication process.
+ *
+ * @author asavershin
+ */
+public class AuthException extends RuntimeException {
+
+ /**
+ * Constructs an instance of AuthException with
+ * the specified detail message.
+ *
+ * @param message the detail message.
+ * The detail message is saved for
+ * later retrieval by the {@link #getMessage()} method.
+ */
+ public AuthException(final String message) {
+ super(message);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUser.java b/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUser.java
new file mode 100644
index 0000000..28bfc2e
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUser.java
@@ -0,0 +1,59 @@
+package com.github.asavershin.api.domain.user;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.util.Objects;
+
+@Getter
+@ToString
+@EqualsAndHashCode
+public final class AuthenticatedUser {
+ /**
+ * The unique identifier of the user.
+ */
+ private UserId userId;
+
+ /**
+ * The credentials of the user.
+ */
+ private Credentials userCredentials;
+
+ /**
+ * Constructs an instance of AuthenticatedUser
+ * with the provided userId and userCredentials.
+ *
+ * @param aUserId the unique identifier of the user
+ * @param aUserCredentials the credentials of the user
+ */
+ private AuthenticatedUser(final UserId aUserId,
+ final Credentials aUserCredentials) {
+ setUserId(aUserId);
+ setUserCredentials(aUserCredentials);
+ }
+
+ /**
+ * Creates a new instance of AuthenticatedUser that becomes from repository
+ * with the provided userId and userCredentials.
+ *
+ * @param userId the unique identifier of the user
+ * @param credentials the credentials of the user
+ * @return a new instance of AuthenticatedUser
+ */
+ public static AuthenticatedUser founded(final UserId userId,
+ final Credentials credentials) {
+ return new AuthenticatedUser(userId, credentials);
+ }
+
+ private void setUserId(final UserId aUserId) {
+ Objects.requireNonNull(aUserId, "UserId must not be null");
+ this.userId = aUserId;
+ }
+
+ private void setUserCredentials(final Credentials aUserCredentials) {
+ Objects.requireNonNull(aUserCredentials,
+ "UserCredentials must not be null");
+ this.userCredentials = aUserCredentials;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUserRepository.java b/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUserRepository.java
new file mode 100644
index 0000000..5ac94d9
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUserRepository.java
@@ -0,0 +1,14 @@
+package com.github.asavershin.api.domain.user;
+
+/**
+ * Represents a repository for finding authenticated users by their email.
+ */
+public interface AuthenticatedUserRepository {
+ /**
+ * Finds an authenticated user by their email.
+ *
+ * @param email the email of the user to find
+ * @return the authenticated user with the given email, or null if not found
+ */
+ AuthenticatedUser findByEmail(String email);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/Credentials.java b/api/src/main/java/com/github/asavershin/api/domain/user/Credentials.java
new file mode 100644
index 0000000..96a59e4
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/Credentials.java
@@ -0,0 +1,53 @@
+package com.github.asavershin.api.domain.user;
+
+
+import com.github.asavershin.api.common.Validator;
+
+import java.util.Objects;
+
+public record Credentials(String email, String password) {
+ /**
+ * The maximum length of an email.
+ */
+ private static final int MAX_EMAIL_LENGTH = 50;
+
+ /**
+ * The minimum length of an email.
+ */
+ private static final int MIN_EMAIL_LENGTH = 5;
+
+ /**
+ * The minimum length of a password.
+ */
+ private static final int MIN_PASSWORD_LENGTH = 8;
+
+ /**
+ * Constructs a new instance of Credentials.
+ * Validates the email and password before creating the instance.
+ */
+ public Credentials {
+ validateEmail(email);
+ validatePassword(password);
+ }
+
+ private void validateEmail(final String aEmail) {
+ Objects.requireNonNull(aEmail, "Email must not be null");
+ Validator.assertArgumentLength(aEmail,
+ MIN_EMAIL_LENGTH, MAX_EMAIL_LENGTH,
+ "Email must be "
+ + MIN_EMAIL_LENGTH + "-"
+ + MAX_EMAIL_LENGTH + " in length");
+
+ Validator.assertStringFormat(aEmail,
+ "[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\\.[A-Za-z0-9.-]+",
+ "Email is not in the correct format");
+ }
+
+ private void validatePassword(final String aPassword) {
+ Objects.requireNonNull(aPassword, "Password must not be null");
+ Validator.assertArgumentLength(aPassword,
+ MIN_PASSWORD_LENGTH,
+ "Password must be greater than " + MIN_PASSWORD_LENGTH);
+ }
+}
+
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/FullName.java b/api/src/main/java/com/github/asavershin/api/domain/user/FullName.java
new file mode 100644
index 0000000..162a715
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/FullName.java
@@ -0,0 +1,45 @@
+package com.github.asavershin.api.domain.user;
+
+
+import com.github.asavershin.api.common.Validator;
+
+import java.util.Objects;
+
+/**
+ * A value object representing a user's full name,
+ * consisting of a first name and a last name.
+ * The length of each name is limited to 20 characters.
+ *
+ * @param firstname The first name of the user
+ * @param lastname The last name of the user
+ */
+public record FullName(String firstname, String lastname) {
+ /**
+ * The maximum length of a first name.
+ */
+ private static final int MAX_FIRST_NAME_LENGTH = 20;
+ /**
+ * The maximum length of a last name.
+ */
+ private static final int MAX_LAST_NAME_LENGTH = 20;
+
+ /**
+ * Constructs a new instance of FullName and validates the length of
+ * the first and last names.
+ */
+ public FullName {
+ validateName(firstname, MAX_FIRST_NAME_LENGTH);
+ validateName(lastname, MAX_LAST_NAME_LENGTH);
+ }
+
+ private void validateName(final String name, final int maxLength) {
+ Objects.requireNonNull(name, "Firstname must not be null");
+ Validator.assertArgumentLength(
+ name,
+ 0,
+ MAX_FIRST_NAME_LENGTH,
+ "Name must be " + "0-" + maxLength + " in length"
+ );
+ }
+}
+
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/GetPartOfImagesForAuthenticatedUser.java b/api/src/main/java/com/github/asavershin/api/domain/user/GetPartOfImagesForAuthenticatedUser.java
new file mode 100644
index 0000000..a4ede32
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/GetPartOfImagesForAuthenticatedUser.java
@@ -0,0 +1,26 @@
+package com.github.asavershin.api.domain.user;
+
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.image.Image;
+
+import java.util.List;
+
+/**
+ * This interface represents a function that retrieves a list of images
+ * for a given authenticated user and a specific part of resources.
+ *
+ */
+@FunctionalInterface
+public interface GetPartOfImagesForAuthenticatedUser {
+ /**
+ * Retrieves a list of images for a given authenticated user and
+ * a specific part of resources.
+ *
+ * @param userId the unique identifier of the authenticated user
+ * @param partOfResources the specific part of resources
+ * for which the images are requested
+ * @return a list of images associated with the specified
+ * user and part of resources
+ */
+ List get(UserId userId, PartOfResources partOfResources);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/RegisterNewUser.java b/api/src/main/java/com/github/asavershin/api/domain/user/RegisterNewUser.java
new file mode 100644
index 0000000..a3a4b5f
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/RegisterNewUser.java
@@ -0,0 +1,18 @@
+package com.github.asavershin.api.domain.user;
+
+/**
+ * A functional interface that represents a method to register a new user.
+ *
+ */
+@FunctionalInterface
+public interface RegisterNewUser {
+ /**
+ * Registers a new user with the provided full name and credentials.
+ *
+ * @param fullName The FullName value object
+ * containing the user's full name.
+ * @param credentials The Credentials value object
+ * containing the user's credentials.
+ */
+ void register(FullName fullName, Credentials credentials);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/TryToLogin.java b/api/src/main/java/com/github/asavershin/api/domain/user/TryToLogin.java
new file mode 100644
index 0000000..374164d
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/TryToLogin.java
@@ -0,0 +1,20 @@
+package com.github.asavershin.api.domain.user;
+
+/**
+ * A functional interface representing a login operation for user
+ * using login, password.
+ *
+ * @author asavershin
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface TryToLogin {
+ /**
+ * Attempts to log the user in using the provided credentials.
+ *
+ * @param credentials The credentials to use for login.
+ * @return An instance of {@link AuthenticatedUser}
+ * if the login is successful, otherwise null.
+ */
+ AuthenticatedUser login(Credentials credentials);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/User.java b/api/src/main/java/com/github/asavershin/api/domain/user/User.java
new file mode 100644
index 0000000..d1b2133
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/User.java
@@ -0,0 +1,66 @@
+/**
+ * User class represents a user entity in the system.
+ * It contains the user's unique identifier, full name, and credentials.
+ *
+ * @author asavershin
+ */
+package com.github.asavershin.api.domain.user;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.util.Objects;
+
+@ToString
+@EqualsAndHashCode
+@Getter
+public class User {
+ /**
+ * The unique identifier for the user.
+ */
+ private UserId userId;
+
+ /**
+ * The full name of the user.
+ */
+ private FullName userFullName;
+
+ /**
+ * The credentials of the user.
+ */
+ private Credentials userCredentials;
+
+ /**
+ * Constructor for User class.
+ *
+ * @param id the unique identifier for the user
+ * @param fullname the full name of the user
+ * @param credentials the credentials of the user
+ */
+ public User(final UserId id,
+ final FullName fullname,
+ final Credentials credentials) {
+ setUserId(id);
+ setUserCredentials(credentials);
+ setUserFullName(fullname);
+ }
+
+ private void setUserId(final UserId id) {
+ Objects.requireNonNull(id,
+ "UserId must not be null");
+ this.userId = id;
+ }
+
+ private void setUserFullName(final FullName fullName) {
+ Objects.requireNonNull(fullName,
+ "UserFullName must not be null");
+ this.userFullName = fullName;
+ }
+
+ private void setUserCredentials(final Credentials credentials) {
+ Objects.requireNonNull(credentials,
+ "UserCredentials must not be null");
+ this.userCredentials = credentials;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/UserId.java b/api/src/main/java/com/github/asavershin/api/domain/user/UserId.java
new file mode 100644
index 0000000..c463fcf
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/UserId.java
@@ -0,0 +1,37 @@
+/**
+ * A unique identifier for a user.
+ *
+ * @author asavershin
+ */
+package com.github.asavershin.api.domain.user;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * A value object representing a unique identifier for a user.
+ *
+ * @param value The unique identifier for the user.
+ */
+public record UserId(UUID value) {
+
+ /**
+ * Constructs a new instance of {@code UserId} with the provided
+ * unique identifier.
+ *
+ * @param value The unique identifier for the user.
+ * @throws NullPointerException if the provided value is null.
+ */
+ public UserId {
+ Objects.requireNonNull(value, "User ID must not be null");
+ }
+
+ /**
+ * Generates a new unique identifier for a user.
+ *
+ * @return A new unique identifier for a user.
+ */
+ public static UserId nextIdentity() {
+ return new UserId(UUID.randomUUID());
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/UserRepository.java b/api/src/main/java/com/github/asavershin/api/domain/user/UserRepository.java
new file mode 100644
index 0000000..e21f666
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/UserRepository.java
@@ -0,0 +1,27 @@
+package com.github.asavershin.api.domain.user;
+
+/**
+ * UserRepository interface provides methods to interact with the User
+ * data.
+ *
+ * @author Tabnine
+ */
+public interface UserRepository {
+
+ /**
+ * Saves a new User object to the repository.
+ *
+ * @param newUser the User object to be saved
+ */
+ void save(User newUser);
+
+ /**
+ * Checks if a User with the given email already exists in the
+ * repository.
+ *
+ * @param email the email of the User to be checked
+ * @return true if a User with the given email already exists,
+ * false otherwise
+ */
+ boolean existByUserEmail(String email);
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/impl/GetPartOfImagesForAuthenticatedUserImpl.java b/api/src/main/java/com/github/asavershin/api/domain/user/impl/GetPartOfImagesForAuthenticatedUserImpl.java
new file mode 100644
index 0000000..d3eb2e8
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/GetPartOfImagesForAuthenticatedUserImpl.java
@@ -0,0 +1,37 @@
+package com.github.asavershin.api.domain.user.impl;
+
+import com.github.asavershin.api.common.annotations.Query;
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.user.GetPartOfImagesForAuthenticatedUser;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Implementation of the {@link GetPartOfImagesForAuthenticatedUser} interface.
+ * This class provides methods to retrieve a part of
+ * images for an authenticated user.
+ *
+ * @author asavershin
+ */
+@Query
+@RequiredArgsConstructor
+public class GetPartOfImagesForAuthenticatedUserImpl
+ implements GetPartOfImagesForAuthenticatedUser {
+ /**
+ * The ImageRepository dependency for fetching images.
+ */
+ private final ImageRepository imageRepository;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public List get(final UserId userId,
+ final PartOfResources partOfResources) {
+ return imageRepository.findImagesByUserId(userId, partOfResources);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/impl/RegisterNewUserImpl.java b/api/src/main/java/com/github/asavershin/api/domain/user/impl/RegisterNewUserImpl.java
new file mode 100644
index 0000000..82fe96f
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/RegisterNewUserImpl.java
@@ -0,0 +1,53 @@
+package com.github.asavershin.api.domain.user.impl;
+
+import com.github.asavershin.api.common.annotations.Command;
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.FullName;
+import com.github.asavershin.api.domain.user.RegisterNewUser;
+import com.github.asavershin.api.domain.user.User;
+import com.github.asavershin.api.domain.user.UserId;
+import com.github.asavershin.api.domain.user.UserRepository;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import lombok.RequiredArgsConstructor;
+
+
+@Command
+@RequiredArgsConstructor
+public class RegisterNewUserImpl implements RegisterNewUser {
+ /**
+ * Dependency that allow crud with User entity.
+ */
+ private final UserRepository userRepository;
+ /**
+ * is used to encode and decode passwords securely.
+ */
+ private final PasswordEncoder passwordEncoder;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void register(final FullName fullName,
+ final Credentials credentials) {
+ checkEmailForUnique(credentials.email());
+ userRepository.save(
+ new User(
+ UserId.nextIdentity(),
+ fullName,
+ new Credentials(credentials.email(),
+ protectPassword(credentials.password()))
+ )
+ );
+ }
+
+ private void checkEmailForUnique(final String email) {
+ if (userRepository.existByUserEmail(email)) {
+ throw new IllegalArgumentException("Email is not unique");
+ }
+ }
+
+ private String protectPassword(final String unprotectedPassword) {
+ return passwordEncoder.encode(unprotectedPassword);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/impl/TryToLoginImpl.java b/api/src/main/java/com/github/asavershin/api/domain/user/impl/TryToLoginImpl.java
new file mode 100644
index 0000000..3823a7d
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/TryToLoginImpl.java
@@ -0,0 +1,42 @@
+package com.github.asavershin.api.domain.user.impl;
+
+import com.github.asavershin.api.domain.IsEntityFound;
+import com.github.asavershin.api.domain.user.AuthException;
+import com.github.asavershin.api.common.annotations.DomainService;
+import com.github.asavershin.api.domain.user.AuthenticatedUser;
+import com.github.asavershin.api.domain.user.AuthenticatedUserRepository;
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.TryToLogin;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@DomainService
+@RequiredArgsConstructor
+public class TryToLoginImpl extends IsEntityFound implements TryToLogin {
+ /**
+ * The {@link PasswordEncoder}
+ * is used to encode and decode passwords securely.
+ */
+ private final PasswordEncoder passwordEncoder;
+ /**
+ * The {@link AuthenticatedUserRepository} is used
+ * to retrieve user data from the database.
+ */
+ private final AuthenticatedUserRepository authenticatedUserRepository;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public AuthenticatedUser login(final Credentials credentials) {
+
+ var authenticatedUser = authenticatedUserRepository
+ .findByEmail(credentials.email());
+
+ isEntityFound(authenticatedUser, "User", "email", credentials.email());
+ if (passwordEncoder.matches(credentials.password(),
+ authenticatedUser.userCredentials().password())) {
+ return authenticatedUser;
+ }
+ throw new AuthException("Wrong password");
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/impl/package-info.java b/api/src/main/java/com/github/asavershin/api/domain/user/impl/package-info.java
new file mode 100644
index 0000000..a591eda
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * This package contains implementations of domain services,
+ * queries, commands for user entity.
+ *
+ * @author asavershin
+ */
+package com.github.asavershin.api.domain.user.impl;
diff --git a/api/src/main/java/com/github/asavershin/api/domain/user/package-info.java b/api/src/main/java/com/github/asavershin/api/domain/user/package-info.java
new file mode 100644
index 0000000..859ec6c
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/domain/user/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * This package contains the domain model for the user entity.
+ *
+ * @author asavershin
+ */
+package com.github.asavershin.api.domain.user;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AdviceController.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AdviceController.java
new file mode 100644
index 0000000..eb5e5e7
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AdviceController.java
@@ -0,0 +1,125 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers;
+
+import com.github.asavershin.api.common.NotFoundException;
+import com.github.asavershin.api.domain.ResourceOwnershipException;
+import com.github.asavershin.api.domain.user.AuthException;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.ExceptionBody;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.UISuccessContainer;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import org.springframework.context.support.DefaultMessageSourceResolvable;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+@RestControllerAdvice
+public class AdviceController {
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param ex is handled exception
+ * @return Exception representation for user.
+ */
+ @ExceptionHandler({ResourceOwnershipException.class, AuthException.class})
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public UISuccessContainer handleValidationException(
+ final RuntimeException ex
+ ) {
+ return new UISuccessContainer(false, ex.getMessage());
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param ex is handled exception
+ * @return Exception representation for user.
+ */
+ @ExceptionHandler({NotFoundException.class})
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public UISuccessContainer handleNotFoundException(
+ final RuntimeException ex
+ ) {
+ return new UISuccessContainer(false, ex.getMessage());
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param ex is handled exception
+ * @return Exception representation for user.
+ */
+ @ExceptionHandler({Exception.class})
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public UISuccessContainer handleInnerException(final Exception ex) {
+ ex.printStackTrace();
+ return new UISuccessContainer(false, "Very bad exception");
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param ex is handled exception
+ * @return Exception representation for user.
+ */
+ @ExceptionHandler({IllegalArgumentException.class})
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public UISuccessContainer handleIllegalImageExtension(
+ final IllegalArgumentException ex
+ ) {
+ return new UISuccessContainer(false, ex.getMessage());
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param ex is handled exception
+ * @return Exception representation for user.
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ExceptionBody handleValidationException(
+ final MethodArgumentNotValidException ex
+ ) {
+ var exceptionBody = new ExceptionBody("Validation failed: ");
+ Map body = new HashMap<>();
+ body.put("errors",
+ ex.getBindingResult().getAllErrors().stream()
+ .map(
+ DefaultMessageSourceResolvable
+ ::getDefaultMessage
+ )
+ .filter(Objects::nonNull)
+ .toList());
+ exceptionBody.setErrors(body);
+ return exceptionBody;
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param ex is handled exception
+ * @return Exception representation for user.
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ExceptionBody handleConstraintViolationException(
+ final ConstraintViolationException ex
+ ) {
+ var exceptionBody = new ExceptionBody("Validation failed: ");
+ Map body = new HashMap<>();
+ body.put("errors",
+ ex.getConstraintViolations().stream()
+ .map(ConstraintViolation::getMessage)
+ .filter(Objects::nonNull)
+ .toList());
+ exceptionBody.setErrors(body);
+ return exceptionBody;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AuthController.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AuthController.java
new file mode 100644
index 0000000..e654795
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AuthController.java
@@ -0,0 +1,77 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers;
+
+import com.github.asavershin.api.application.in.services.user.ApplicationCredentials;
+import com.github.asavershin.api.application.in.services.user.GetNewCredentials;
+import com.github.asavershin.api.application.in.services.user.GetNewCredentialsUsingRefreshToken;
+import com.github.asavershin.api.domain.user.RegisterNewUser;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.user.UserLoginRequest;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.user.UserRegistrationRequest;
+import com.github.asavershin.api.infrastructure.in.security.CustomUserDetails;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+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/v1/auth")
+@Tag(name = "auth", description = "Аутентификация и регистрация")
+@RequiredArgsConstructor
+public class AuthController {
+ /**
+ * Dependency that allows to register new user.
+ */
+ private final RegisterNewUser register;
+ /**
+ * Dependency that allows to get new tokens during authentication.
+ */
+ private final GetNewCredentials getNewCredentials;
+ /**
+ * Dependency that allows to get new tokens using refresh token.
+ */
+ private final GetNewCredentialsUsingRefreshToken getRefreshToken;
+
+ /**
+ * Not final to allows Spring use proxy.
+ * @param request DTO that contains user data like email, password
+ * firstname, etc.
+ */
+ @PostMapping("/register")
+ @Operation(description = "Регистрация нового пользователя")
+ public void register(
+ final @RequestBody @Valid UserRegistrationRequest request
+ ) {
+ register.register(request.toFullName(),
+ request.toCredentials());
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ * @param userLoginRequest DTO that represents login, password.
+ * @return DTO that contains access, refresh tokens
+ */
+ @PostMapping("/login")
+ @Operation(description = "Аутентификация пользователя")
+ public ApplicationCredentials login(
+ final @RequestBody @Valid UserLoginRequest userLoginRequest
+ ) {
+ return getNewCredentials.get(userLoginRequest.toCredentials());
+ }
+ /**
+ * Not final to allows Spring use proxy.
+ * @param user Is param that injects by spring and contains
+ * current authenticated spring user.
+ * @return DTO that contains access, refresh tokens.
+ */
+ @PostMapping("/refresh-token")
+ @Operation(description = "Использовать рефреш токен")
+ public ApplicationCredentials refreshToken(
+ final @AuthenticationPrincipal CustomUserDetails user
+ ) {
+ return getRefreshToken.get(user.authenticatedUser().userCredentials());
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/ImageController.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/ImageController.java
new file mode 100644
index 0000000..88d7211
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/ImageController.java
@@ -0,0 +1,130 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers;
+
+import com.github.asavershin.api.application.in.services.image.ImageService;
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.user.GetPartOfImagesForAuthenticatedUser;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image.GetImagesResponse;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.UISuccessContainer;
+import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image.UploadImageResponse;
+import com.github.asavershin.api.infrastructure.in.security.CustomUserDetails;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/image")
+@Tag(name = "image", description = "Работа с изображениями")
+@RequiredArgsConstructor
+public class ImageController {
+ /**
+ * Domain query that allows you take images of specific user.
+ */
+ private final GetPartOfImagesForAuthenticatedUser getImages;
+ /**
+ * Application service that contains logic for images get, post, delete.
+ * It contains some others dependencies for databases and jwt service.
+ */
+ private final ImageService imageService;
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param user Is param that injects by spring and contains
+ * current authenticated spring user.
+ * @param pageNumber Page number for pagination in DB
+ * @param pageSize Page size for pagination in DB
+ * @return List of images for specific user.
+ */
+ @GetMapping("/images")
+ @Operation(description = "Получить изображения пользователя")
+ public GetImagesResponse getImages(
+ final @AuthenticationPrincipal CustomUserDetails user,
+ final Long pageNumber,
+ final Long pageSize
+ ) {
+ return GetImagesResponse.getImagesResponseFromImages(
+ getImages.get(user.authenticatedUser().userId(),
+ new PartOfResources(pageNumber, pageSize)
+ )
+ );
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param user Is param that injects by spring and contains
+ * current authenticated spring user.
+ * @param file Is image with data and metadata.
+ * @return ImageId of the new image
+ */
+ @PostMapping
+ @Operation(description = "Загрузить новую картинку")
+ public UploadImageResponse uploadImage(
+ final @AuthenticationPrincipal CustomUserDetails user,
+ final @RequestPart("file") MultipartFile file
+ ) {
+ return new UploadImageResponse(
+ imageService.storeImage(
+ user.authenticatedUser().userId(),
+ file
+ )
+ );
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param user Is param that injects by spring and contains
+ * current authenticated spring user.
+ * @param imageId The id of the image
+ * @return The response that contains status of response(true, false)
+ * and message for user about successful.
+ */
+ @DeleteMapping("/{image-id}")
+ @Operation(description = "Удалить картинку")
+ public UISuccessContainer deleteImage(
+ final @AuthenticationPrincipal CustomUserDetails user,
+ final @PathVariable("image-id") String imageId
+ ) {
+ imageService.deleteImageByImageId(
+ user.authenticatedUser().userId(),
+ new ImageId(UUID.fromString(imageId))
+ );
+ return new UISuccessContainer(
+ true,
+ "Image with id " + imageId + " deleted successfully"
+ );
+ }
+
+ /**
+ * Not final to allows Spring use proxy.
+ *
+ * @param user Is param that injects by spring and contains
+ * current authenticated spring user.
+ * @param imageId The id of the image
+ * @return bytes of the image
+ */
+ @GetMapping("/{image-id}")
+ @Operation(description = "Получить картинку по id")
+ public byte[] downloadImage(
+ final @AuthenticationPrincipal CustomUserDetails user,
+ final @PathVariable("image-id") String imageId
+ ) {
+ return imageService.downloadImage(
+ new ImageId(UUID.fromString(imageId)),
+ user.authenticatedUser().userId()
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/ExceptionBody.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/ExceptionBody.java
new file mode 100644
index 0000000..12b74e1
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/ExceptionBody.java
@@ -0,0 +1,33 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Map;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class ExceptionBody {
+ /**
+ * Message for user about exception.
+ */
+ private String message;
+ /**
+ * Map of error for a lot of exceptions.
+ */
+ private Map errors;
+ /**
+ * Constructs an instance of {@link ExceptionBody}
+ * with the provided message.
+ *
+ * @param aMessage Message for user about exception.
+ */
+ public ExceptionBody(
+ final String aMessage
+ ) {
+ this.message = aMessage;
+ }
+
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/UISuccessContainer.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/UISuccessContainer.java
new file mode 100644
index 0000000..c80f9a8
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/UISuccessContainer.java
@@ -0,0 +1,17 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class UISuccessContainer {
+ /**
+ * Indicates whether the operation was successful.
+ */
+ private boolean success;
+ /**
+ * The message to be displayed to the user.
+ */
+ private String message;
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/GetImagesResponse.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/GetImagesResponse.java
new file mode 100644
index 0000000..7792a11
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/GetImagesResponse.java
@@ -0,0 +1,33 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image;
+
+import com.github.asavershin.api.domain.image.Image;
+import lombok.Getter;
+
+import java.util.List;
+
+@Getter
+public final class GetImagesResponse {
+ /**
+ * List of metadata of images.
+ */
+ private final List images;
+
+ private GetImagesResponse(final List aImages) {
+ this.images = aImages;
+ }
+
+ /**
+ * Static fabric that map List of images to list of DTO.
+ *
+ * @param images List of images from domain
+ * @return Instance of GetImagesResponse
+ */
+ public static GetImagesResponse getImagesResponseFromImages(
+ final List images
+ ) {
+ return new GetImagesResponse(
+ images.stream()
+ .map(ImageResponse::imageResponseFromEntity).toList()
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/ImageResponse.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/ImageResponse.java
new file mode 100644
index 0000000..9600724
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/ImageResponse.java
@@ -0,0 +1,42 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image;
+
+import com.github.asavershin.api.domain.image.Image;
+import lombok.Getter;
+
+@Getter
+public final class ImageResponse {
+ /**
+ * UUID of the image.
+ */
+ private final String imageId;
+ /**
+ * Image name in format "name.extension".
+ */
+ private final String imageName;
+ /**
+ * Count of image bytes.
+ */
+ private final Long imageSize;
+
+ private ImageResponse(final String aImageId,
+ final String aImageName,
+ final Long aImageSize) {
+ this.imageId = aImageId;
+ this.imageName = aImageName;
+ this.imageSize = aImageSize;
+ }
+
+ /**
+ * Static fabric for creating ImageResponse from image entity.
+ *
+ * @param image is domain entity
+ * @return DTO
+ */
+ public static ImageResponse imageResponseFromEntity(final Image image) {
+ return new ImageResponse(
+ image.imageId().value().toString(),
+ image.metaInfo().imageNameWithExtension().toString(),
+ image.metaInfo().imageSize()
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/UploadImageResponse.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/UploadImageResponse.java
new file mode 100644
index 0000000..3e05d1a
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/UploadImageResponse.java
@@ -0,0 +1,18 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image;
+
+import com.github.asavershin.api.domain.image.ImageId;
+import lombok.Getter;
+
+@Getter
+public class UploadImageResponse {
+ /**
+ * UUID of the image.
+ */
+ private final String imageId;
+ /**
+ * @param aImageId UUID of the image
+ */
+ public UploadImageResponse(final ImageId aImageId) {
+ this.imageId = aImageId.value().toString();
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/package-info.java
new file mode 100644
index 0000000..d513554
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains image DTO's for HTTP reuqests/respons.
+ * @author asavershin
+ */
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/package-info.java
new file mode 100644
index 0000000..417722c
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains DTO's for HTTP reuqests/respons.
+ * @author asavershin
+ */
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserLoginRequest.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserLoginRequest.java
new file mode 100644
index 0000000..03abb31
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserLoginRequest.java
@@ -0,0 +1,39 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.user;
+
+import com.github.asavershin.api.domain.user.Credentials;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class UserLoginRequest {
+ /**
+ * Minimum password length.
+ */
+ private static final int MIN_PASSWORD_LENGTH = 8;
+ /**
+ * User email using as login.
+ */
+ @NotEmpty(message = "Не заполнен email")
+ @Email(message = "Некорректная почта")
+ private String userEmail;
+
+ /**
+ * User password.
+ */
+ @NotEmpty(message = "Не заполнен пароль")
+ @Size(min = MIN_PASSWORD_LENGTH,
+ message = "Длина пароля должна быть не менее 8")
+ private String userPassword;
+
+ /**
+ * Fabric using for mapping DTO to credentials.
+ * @return User credentials
+ */
+ public final Credentials toCredentials() {
+ return new Credentials(userEmail, userPassword);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserRegistrationRequest.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserRegistrationRequest.java
new file mode 100644
index 0000000..55132c4
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserRegistrationRequest.java
@@ -0,0 +1,64 @@
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.user;
+
+
+import com.github.asavershin.api.config.properties.UserProperties;
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.FullName;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+public class UserRegistrationRequest {
+
+
+ /**
+ * User firstname.
+ */
+ @NotEmpty(message = "Не заполнено имя")
+ @Size(min = 1, max = UserProperties.MAX_FIRSTNAME_LENGTH,
+ message = "Недопустимая длина имени")
+ private String userFirstname;
+
+ /**
+ * User lastname.
+ */
+ @NotEmpty(message = "Не заполнена фамилия")
+ @Size(min = 1, max = UserProperties.MAX_LASTNAME_LENGTH,
+ message = "Недопустимая длина фамилии")
+ private String userLastname;
+ /**
+ * User email using as login.
+ */
+ @NotEmpty(message = "Не заполнен email")
+ @Email(message = "Некорректная почта")
+ @Getter
+ private String userEmail;
+
+ /**
+ * User password.
+ */
+ @NotEmpty(message = "Не заполнен пароль")
+ @Size(min = UserProperties.MIN_PASSWORD_LENGTH,
+ message = "Длина пароля должна быть не менее 8")
+ @Getter
+ private String userPassword;
+
+ /**
+ * Fabric method to create FullName from DTO.
+ * @return FullName value object
+ */
+ public FullName toFullName() {
+ return new FullName(userFirstname, userLastname);
+ }
+
+ /**
+ * Fabric method to create credentials.
+ * @return Credentials value object
+ */
+ public Credentials toCredentials() {
+ return new Credentials(userEmail, userPassword);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/package-info.java
new file mode 100644
index 0000000..c81e2bd
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains user DTO's for HTTP reuqests/respons.
+ * @author asavershin
+ */
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.user;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/package-info.java
new file mode 100644
index 0000000..0b178e4
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This package contains controllers that handle http requests.
+ */
+package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/CustomUserDetails.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/CustomUserDetails.java
new file mode 100644
index 0000000..0a130ef
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/CustomUserDetails.java
@@ -0,0 +1,47 @@
+package com.github.asavershin.api.infrastructure.in.security;
+
+import com.github.asavershin.api.domain.user.AuthenticatedUser;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+public record CustomUserDetails(AuthenticatedUser authenticatedUser)
+ implements UserDetails {
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+
+ @Override
+ public String getPassword() {
+ return authenticatedUser.userCredentials().password();
+ }
+
+ @Override
+ public String getUsername() {
+ return authenticatedUser.userCredentials().email();
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/JwtAuthenticationFilter.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..81b6bea
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/JwtAuthenticationFilter.java
@@ -0,0 +1,90 @@
+package com.github.asavershin.api.infrastructure.in.security;
+
+import com.github.asavershin.api.application.out.TokenRepository;
+import com.github.asavershin.api.application.in.services.user.JwtService;
+import com.github.asavershin.api.config.properties.JwtProperties;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.lang.NonNull;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+ /**
+ * JwtService dependency injection.
+ *
+ * @see com.github.asavershin.api.application.in.services.user.JwtService
+ */
+ private final JwtService jwtService;
+
+ /**
+ * UserDetailsService dependency injection.
+ *
+ * @see com.github.asavershin.api.application.out.TokenRepository
+ */
+ private final UserDetailsService userDetailsService;
+
+ /**
+ * TokenRepository dependency injection.
+ *
+ * @see com.github.asavershin.api.application.out.TokenRepository
+ */
+ private final TokenRepository tokenRepository;
+
+ @Override
+ protected final void doFilterInternal(
+ final @NonNull HttpServletRequest request,
+ final @NonNull HttpServletResponse response,
+ final @NonNull FilterChain filterChain
+ ) throws ServletException, IOException {
+ var path = request.getServletPath();
+
+ if (path.contains("/register") || path.contains("/login")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ final String authHeader = request.getHeader("Authorization");
+ final String jwt;
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ jwt = authHeader.substring(JwtProperties.START_OF_TOKEN);
+ var email = jwtService.extractSub(jwt);
+
+ String token;
+ var pathContainsRefreshToken = path.contains("/refresh-token");
+ if (pathContainsRefreshToken) {
+ token = tokenRepository.getRefreshToken(email);
+ } else {
+ token = tokenRepository.getAccessToken(email);
+ }
+ if (!token.equals(jwt)) {
+ tokenRepository.deleteAllTokensByUserEmail(email);
+ return;
+ }
+
+ var authToken = new UsernamePasswordAuthenticationToken(
+ userDetailsService.loadUserByUsername(email),
+ null,
+ null
+ );
+ authToken.setDetails(
+ new WebAuthenticationDetailsSource().buildDetails(request)
+ );
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/LogautHandlerImpl.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/LogautHandlerImpl.java
new file mode 100644
index 0000000..34eefa0
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/LogautHandlerImpl.java
@@ -0,0 +1,46 @@
+package com.github.asavershin.api.infrastructure.in.security;
+
+import com.github.asavershin.api.application.out.TokenRepository;
+import com.github.asavershin.api.application.in.services.user.JwtService;
+import com.github.asavershin.api.config.properties.JwtProperties;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class LogautHandlerImpl implements LogoutHandler {
+ /**
+ * TokenRepository dependency.
+ */
+ private final TokenRepository tokenRepository;
+ /**
+ * JwtService dependency.
+ */
+ private final JwtService jwtService;
+
+ /**
+ * Not final to allow spring using proxy.
+ *
+ * @param request the HTTP request
+ * @param response the HTTP response
+ * @param authentication the current principal details
+ */
+ @Override
+ public void logout(
+ final HttpServletRequest request,
+ final HttpServletResponse response,
+ final Authentication authentication
+ ) {
+ final String authHeader = request.getHeader("Authorization");
+ final String jwt;
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ return;
+ }
+ jwt = authHeader.substring(JwtProperties.START_OF_TOKEN);
+ tokenRepository.deleteAllTokensByUserEmail(jwtService.extractSub(jwt));
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/SecurityConfiguration.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/SecurityConfiguration.java
new file mode 100644
index 0000000..b39bcdc
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/SecurityConfiguration.java
@@ -0,0 +1,94 @@
+package com.github.asavershin.api.infrastructure.in.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+
+import static org.springframework.security.config.http.SessionCreationPolicy.NEVER;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+@EnableMethodSecurity
+public class SecurityConfiguration {
+ /**
+ * Routes that will not be subject to spring security.
+ */
+ private static final String[] WHITE_LIST_URL = {
+ "api/v1/auth/register",
+ "api/v1/auth/login",
+ "/v2/api-docs",
+ "/v3/api-docs",
+ "/v3/api-docs/**",
+ "/swagger-resources",
+ "/swagger-resources/**",
+ "/configuration/ui",
+ "/configuration/security",
+ "/swagger-ui/**",
+ "/webjars/**",
+ "/swagger-ui.html",
+ "/docs"};
+ /**
+ * A reference to the JwtAuthenticationFilter instance.
+ */
+ private final JwtAuthenticationFilter jwtAuthFilter;
+
+ /**
+ * A reference to the LogoutHandler instance.
+ */
+ private final LogoutHandler logoutHandler;
+
+ /**
+ * Creates a SecurityFilterChain instance,
+ * which is used to secure the application.
+ * It configures the security filter chain to disable CSRF protection,
+ * allow access to the specified URLs without authentication,
+ * and add the JwtAuthenticationFilter
+ * before the UsernamePasswordAuthenticationFilter.
+ * It also configures the logout functionality
+ * to use the specified logout handler and clear
+ * the security context after logout.
+ *
+ * @param http the HttpSecurity instance to configure
+ * @return the SecurityFilterChain instance
+ * @throws Exception if an error occurs during configuration
+ */
+ @Bean
+ public SecurityFilterChain securityFilterChain(
+ final HttpSecurity http
+ ) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(req ->
+ req.requestMatchers(WHITE_LIST_URL)
+ .permitAll()
+ .anyRequest()
+ .authenticated()
+ )
+ .sessionManagement(
+ session -> session.sessionCreationPolicy(NEVER)
+ )
+ .addFilterBefore(
+ jwtAuthFilter,
+ UsernamePasswordAuthenticationFilter.class
+ )
+ .logout(logout ->
+ logout.logoutUrl("/auth/logout")
+ .addLogoutHandler(logoutHandler)
+ .logoutSuccessHandler(
+ (request, response, authentication)
+ -> SecurityContextHolder
+ .clearContext()
+ )
+ );
+ return http.build();
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/UserDetailsServiceImpl.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..b34c970
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/UserDetailsServiceImpl.java
@@ -0,0 +1,41 @@
+package com.github.asavershin.api.infrastructure.in.security;
+
+import com.github.asavershin.api.domain.IsEntityFound;
+import com.github.asavershin.api.domain.user.AuthenticatedUserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserDetailsServiceImpl
+ extends IsEntityFound implements UserDetailsService {
+ /**
+ * The AuthenticatedUserRepository
+ * dependency is used to fetch the User entity from the database.
+ */
+ private final AuthenticatedUserRepository repository;
+ /**
+ * The loadUserByUsername method is an implementation
+ * of the UserDetailsService interface.
+ * It is responsible for loading a UserDetails object
+ * based on the provided username.
+ * Not final to allow spring use proxy.
+ *
+ * @param username The username of the User to be loaded.
+ * @return A UserDetails object
+ * representing the User with the provided username.
+ * @throws UsernameNotFoundException If the User with
+ * the provided username is not found.
+ */
+ @Override
+ public UserDetails loadUserByUsername(
+ final String username
+ ) throws UsernameNotFoundException {
+ var authenticatedUser = repository.findByEmail(username);
+ isEntityFound(authenticatedUser, "User", "email", username);
+ return new CustomUserDetails(authenticatedUser);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/package-info.java
new file mode 100644
index 0000000..3f89ddd
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains logic for security like filter chains,
+ * user details ant others.
+ */
+package com.github.asavershin.api.infrastructure.in.security;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/AuthenticatedUserRepositoryImpl.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/AuthenticatedUserRepositoryImpl.java
new file mode 100644
index 0000000..e381f04
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/AuthenticatedUserRepositoryImpl.java
@@ -0,0 +1,51 @@
+package com.github.asavershin.api.infrastructure.out.persistence;
+
+import com.github.asavershin.api.domain.user.AuthenticatedUser;
+import com.github.asavershin.api.domain.user.AuthenticatedUserRepository;
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+import org.jooq.DSLContext;
+import org.jooq.Record;
+import org.jooq.RecordMapper;
+import org.springframework.stereotype.Repository;
+
+import static asavershin.generated.package_.tables.Users.USERS;
+
+@Repository
+@RequiredArgsConstructor
+public class AuthenticatedUserRepositoryImpl
+ implements AuthenticatedUserRepository,
+ RecordMapper {
+ /**
+ * The DSLContext object is used to interact with the database.
+ */
+ private final DSLContext dslContext;
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public AuthenticatedUser findByEmail(final String email) {
+ return dslContext.select(
+ USERS.USER_ID,
+ USERS.USER_EMAIL,
+ USERS.USER_PASSWORD
+ )
+ .from(USERS)
+ .where(USERS.USER_EMAIL.eq(email))
+ .fetchOne(this);
+ }
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public AuthenticatedUser map(final Record record) {
+ var userId = record.get(USERS.USER_ID);
+ var email = record.get(USERS.USER_EMAIL);
+ var password = record.get(USERS.USER_PASSWORD);
+ return AuthenticatedUser.founded(
+ new UserId(userId), new Credentials(email, password)
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/ImageRepositoryImpl.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/ImageRepositoryImpl.java
new file mode 100644
index 0000000..3cddeba
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/ImageRepositoryImpl.java
@@ -0,0 +1,108 @@
+package com.github.asavershin.api.infrastructure.out.persistence;
+
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.image.ImageNameWithExtension;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.image.MetaData;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+import org.jooq.DSLContext;
+import org.jooq.Record;
+import org.jooq.RecordMapper;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+import static asavershin.generated.package_.Tables.USER_IMAGES;
+import static asavershin.generated.package_.Tables.IMAGE;
+
+@Repository
+@RequiredArgsConstructor
+public class ImageRepositoryImpl
+ implements ImageRepository, RecordMapper {
+ /**
+ * The DSLContext object is used to interact with the database.
+ */
+ private final DSLContext dslContext;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void save(final Image image) {
+ dslContext.insertInto(IMAGE)
+ .set(IMAGE.IMAGE_ID, image.imageId().value())
+ .set(
+ IMAGE.IMAGE_NAME,
+ image.metaInfo().imageNameWithExtension().imageName()
+ )
+ .set(IMAGE.IMAGE_SIZE, image.metaInfo().imageSize())
+ .set(IMAGE.IMAGE_EXTENSION,
+ image.metaInfo().imageNameWithExtension()
+ .imageExtension().toString())
+ .execute();
+ dslContext.insertInto(USER_IMAGES)
+ .set(USER_IMAGES.IMAGE_ID, image.imageId().value())
+ .set(USER_IMAGES.USER_ID, image.userId().value())
+ .execute();
+ }
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public List findImagesByUserId(final UserId userId,
+ final PartOfResources page) {
+ return dslContext.select(IMAGE.fields()).select(USER_IMAGES.USER_ID)
+ .from(IMAGE)
+ .join(USER_IMAGES).using(IMAGE.IMAGE_ID)
+ .offset(page.pageNumber() * page.pageSize())
+ .limit(page.pageSize())
+ .fetch(this);
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public Image findImageByImageId(final ImageId imageId) {
+ return dslContext.select(IMAGE.fields()).select(USER_IMAGES.USER_ID)
+ .from(IMAGE)
+ .join(USER_IMAGES).using(IMAGE.IMAGE_ID)
+ .where(IMAGE.IMAGE_ID.eq(imageId.value()))
+ .fetchOne(this);
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void deleteImageByImageId(final Image imageId) {
+ dslContext.deleteFrom(IMAGE)
+ .where(IMAGE.IMAGE_ID.eq(imageId.imageId().value()))
+ .execute();
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public Image map(final Record record) {
+ var imageId = record.get(IMAGE.IMAGE_ID);
+ var imageName = record.get(IMAGE.IMAGE_NAME);
+ var userId = record.get(USER_IMAGES.USER_ID);
+ var imageSize = record.get(IMAGE.IMAGE_SIZE);
+ var imageExtension = record.get(IMAGE.IMAGE_EXTENSION);
+ return new Image(
+ new ImageId(imageId),
+ new MetaData(
+ ImageNameWithExtension.founded(
+ imageName,
+ imageExtension
+ ),
+ imageSize
+ ),
+ new UserId(userId)
+ );
+ }
+}
+
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/UserRepositoryImpl.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/UserRepositoryImpl.java
new file mode 100644
index 0000000..4b43728
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/UserRepositoryImpl.java
@@ -0,0 +1,45 @@
+package com.github.asavershin.api.infrastructure.out.persistence;
+
+import asavershin.generated.package_.tables.records.UsersRecord;
+import com.github.asavershin.api.domain.user.UserRepository;
+import com.github.asavershin.api.domain.user.User;
+import lombok.RequiredArgsConstructor;
+import org.jooq.DSLContext;
+import org.springframework.stereotype.Repository;
+
+import static asavershin.generated.package_.Tables.USERS;
+
+@Repository
+@RequiredArgsConstructor
+public class UserRepositoryImpl implements UserRepository {
+ /**
+ * The DSLContext object is used to interact with the database.
+ */
+ private final DSLContext dslContext;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void save(final User newUser) {
+ UsersRecord userRecord = dslContext.newRecord(USERS);
+ userRecord.setUserId(newUser.userId().value());
+ userRecord.setUserFirstname(newUser.userCredentials().email());
+ userRecord.setUserLastname(newUser.userFullName().lastname());
+ userRecord.setUserEmail(newUser.userCredentials().email());
+ userRecord.setUserPassword(newUser.userCredentials().password());
+
+ dslContext.insertInto(USERS)
+ .set(userRecord)
+ .execute();
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public boolean existByUserEmail(final String email) {
+ return dslContext.fetchExists(
+ USERS, USERS.USER_EMAIL.eq(email)
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/package-info.java
new file mode 100644
index 0000000..5ee66da
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains SQL repositories.
+ * @author asavershin
+ */
+package com.github.asavershin.api.infrastructure.out.persistence;
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/CacheRepositoryIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/CacheRepositoryIml.java
new file mode 100644
index 0000000..51e0422
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/CacheRepositoryIml.java
@@ -0,0 +1,45 @@
+package com.github.asavershin.api.infrastructure.out.storage;
+
+import com.github.asavershin.api.application.out.CacheRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.concurrent.TimeUnit;
+
+@Repository
+@RequiredArgsConstructor
+public class CacheRepositoryIml implements CacheRepository {
+ /**
+ * Redis template is used to interact with redis server.
+ */
+ private final RedisTemplate redisTemplate;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void addCache(final String key,
+ final String token,
+ final long expiration) {
+ redisTemplate.opsForValue().set(
+ key, token, expiration, TimeUnit.MILLISECONDS
+ );
+ }
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public String getCache(final String key) {
+ return (String) redisTemplate.opsForValue().get(key);
+ }
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void deleteCache(final String key) {
+ redisTemplate.delete(key);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/FileException.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/FileException.java
new file mode 100644
index 0000000..e7c3567
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/FileException.java
@@ -0,0 +1,12 @@
+package com.github.asavershin.api.infrastructure.out.storage;
+
+public class FileException extends RuntimeException {
+ /**
+ * Constructs a new FileException with the specified detail message.
+ *
+ * @param s the detail message
+ */
+ public FileException(final String s) {
+ super(s);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/MinioServiceIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/MinioServiceIml.java
new file mode 100644
index 0000000..055f46b
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/MinioServiceIml.java
@@ -0,0 +1,141 @@
+package com.github.asavershin.api.infrastructure.out.storage;
+
+
+import com.github.asavershin.api.application.out.MinioService;
+import com.github.asavershin.api.config.properties.MinIOProperties;
+import io.minio.BucketExistsArgs;
+import io.minio.GetObjectArgs;
+import io.minio.MakeBucketArgs;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import io.minio.RemoveObjectArgs;
+import lombok.SneakyThrows;
+import org.apache.commons.compress.utils.IOUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.List;
+
+@Service
+public class MinioServiceIml implements MinioService {
+ /**
+ * The MinioClient is used to interact with the MinIO server.
+ */
+ private final MinioClient minioClient;
+ /**
+ * The MinIO properties from .yml.
+ */
+ private final MinIOProperties minioProperties;
+ /**
+ * Constructor for {@link MinioServiceIml}.
+ *
+ * @param aMinioClient The {@link MinioClient}
+ * instance to interact with the MinIO server.
+ * @param aMinioProperties The {@link MinIOProperties}
+ * instance containing the configuration
+ * for the MinIO server.
+ */
+ public MinioServiceIml(final MinioClient aMinioClient,
+ final MinIOProperties aMinioProperties) {
+ this.minioClient = aMinioClient;
+ this.minioProperties = aMinioProperties;
+ createBucket();
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void saveFile(final MultipartFile image, final String filename) {
+
+ if (!bucketExists(minioProperties.getBucket())) {
+ throw new FileException(
+ "File upload failed: bucket does not exist"
+ );
+ }
+
+ if (image.isEmpty() || image.getOriginalFilename() == null) {
+ throw new FileException("File must have name");
+ }
+ InputStream inputStream;
+ try {
+ inputStream = image.getInputStream();
+ } catch (Exception e) {
+ throw new FileException("File upload failed: "
+ + e.getMessage());
+ }
+ saveFile(inputStream, filename);
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public byte[] getFile(final String link) {
+ if (link == null) {
+ throw new FileException("File download failed: link is nullable");
+ }
+ try {
+ return IOUtils.toByteArray(
+ minioClient.getObject(GetObjectArgs.builder()
+ .bucket(minioProperties.getBucket())
+ .object(link)
+ .build()));
+ } catch (Exception e) {
+ throw new FileException("File download failed: " + e.getMessage());
+ }
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void deleteFiles(final List links) {
+ if (links == null || links.isEmpty()) {
+ return;
+ }
+ if (!bucketExists(minioProperties.getBucket())) {
+ throw new FileException("Minio bucket doesn't exist");
+ }
+ try {
+ for (var link : links) {
+ minioClient.removeObject(
+ RemoveObjectArgs.builder()
+ .bucket(minioProperties.getBucket())
+ .object(link)
+ .build());
+ }
+ } catch (Exception e) {
+ throw new FileException("Failed to delete file: " + e.getMessage());
+ }
+ }
+
+ @SneakyThrows
+ private void createBucket() {
+ boolean found = minioClient.bucketExists(BucketExistsArgs.builder()
+ .bucket(minioProperties.getBucket())
+ .build());
+ if (!found) {
+ minioClient.makeBucket(MakeBucketArgs.builder()
+ .bucket(minioProperties.getBucket())
+ .build());
+ }
+ }
+
+ @SneakyThrows
+ private void saveFile(
+ final InputStream inputStream,
+ final String fileName
+ ) {
+ minioClient.putObject(PutObjectArgs.builder()
+ .stream(inputStream, inputStream.available(), -1)
+ .bucket(minioProperties.getBucket())
+ .object(fileName)
+ .build());
+ }
+
+ @SneakyThrows(Exception.class)
+ private boolean bucketExists(final String bucketName) {
+ return minioClient.bucketExists(
+ BucketExistsArgs.builder().bucket(bucketName).build()
+ );
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/TokenRepositoryIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/TokenRepositoryIml.java
new file mode 100644
index 0000000..6344c09
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/TokenRepositoryIml.java
@@ -0,0 +1,67 @@
+package com.github.asavershin.api.infrastructure.out.storage;
+
+import com.github.asavershin.api.application.out.CacheRepository;
+import com.github.asavershin.api.application.out.TokenRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class TokenRepositoryIml implements TokenRepository {
+ /**
+ * The prefix for access tokens in the cache.
+ */
+ private final String accessKey = "ACCESS_TOKEN_";
+
+ /**
+ * The prefix for refresh tokens in the cache.
+ */
+ private final String refreshKey = "REFRESH_TOKEN_";
+
+ /**
+ * CacheRepository instance for storing and retrieving tokens.
+ */
+ private final CacheRepository cacheRepository;
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public String getAccessToken(final String email) {
+ return cacheRepository.getCache(accessKey + email);
+ }
+
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public String getRefreshToken(final String email) {
+ return cacheRepository.getCache(refreshKey + email);
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void saveRefreshToken(final String username,
+ final String jwtToken,
+ final Long expiration) {
+ cacheRepository.addCache(refreshKey + username, jwtToken, expiration);
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void saveAccessToken(final String username,
+ final String jwtToken,
+ final Long expiration) {
+ cacheRepository.addCache(accessKey + username, jwtToken, expiration);
+ }
+ /**
+ * Not final to allow spring use proxy.
+ */
+ @Override
+ public void deleteAllTokensByUserEmail(final String username) {
+ cacheRepository.deleteCache(refreshKey + username);
+ cacheRepository.deleteCache(accessKey + username);
+ }
+}
diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/package-info.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/package-info.java
new file mode 100644
index 0000000..a094d40
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains impl of noSQL storages.
+ * @author asavershin
+ */
+package com.github.asavershin.api.infrastructure.out.storage;
diff --git a/api/src/main/java/com/github/asavershin/api/package-info.java b/api/src/main/java/com/github/asavershin/api/package-info.java
new file mode 100644
index 0000000..a3a5220
--- /dev/null
+++ b/api/src/main/java/com/github/asavershin/api/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * This package contains the API for the application.
+ * Implementation of a microservice that directly interacts with the user.
+ * @author asavershin
+ */
+package com.github.asavershin.api;
diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml
new file mode 100644
index 0000000..ee33663
--- /dev/null
+++ b/api/src/main/resources/application.yml
@@ -0,0 +1,46 @@
+server:
+ port: 8081
+
+spring:
+ servlet:
+ multipart:
+ max-file-size: 10MB
+ config:
+ import: optional:file:.env[.properties]
+ datasource:
+ url: jdbc:postgresql://${HOST}:${PORT_DB}/${POSTGRES_DB}?currentSchema=${POSTGRES_SCHEMA}
+ username: ${POSTGRES_USER}
+ password: ${POSTGRES_PASSWORD}
+ liquibase:
+ enabled: true
+ drop-first: false
+ change-log: classpath:db/changelog/db.changelog-master.yaml
+ default-schema: ${POSTGRES_SCHEMA}
+ data:
+ redis:
+ host: ${REDIS_HOST}
+ port: ${REDIS_PORT}
+ password: ${REDIS_PASSWORD}
+ cache:
+ type: redis
+ cache-names: redis-cache
+ redis:
+ time-to-live: ${REDIS_CACHE_TIME}
+
+springdoc:
+ swagger-ui:
+ path: /docs
+
+jwt:
+ access-expiration: ${ACCESS_EXPIRATION}
+ refresh-expiration: ${REFRESH_EXPIRATION}
+ secret: ${JWT_SECRET}
+
+minio:
+ url: ${MINIO_URL}
+ user: ${MINIO_ROOT_USER}
+ password: ${MINIO_ROOT_PASSWORD}
+ bucket: ${MINIO_BUCKET}
+ console-port: ${MINIO_CONSOLE_PORT}
+ port: ${MINIO_PORT}
+
diff --git a/api/src/main/resources/db/changelog/db.changelog-master.yaml b/api/src/main/resources/db/changelog/db.changelog-master.yaml
new file mode 100644
index 0000000..fc3e65d
--- /dev/null
+++ b/api/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -0,0 +1,4 @@
+databaseChangeLog:
+ - includeAll:
+ path: /migrations
+ relativeToChangelogFile: true
\ No newline at end of file
diff --git a/api/src/main/resources/db/changelog/migrations/1-create-user.sql b/api/src/main/resources/db/changelog/migrations/1-create-user.sql
new file mode 100644
index 0000000..64239a0
--- /dev/null
+++ b/api/src/main/resources/db/changelog/migrations/1-create-user.sql
@@ -0,0 +1,14 @@
+-- liquibase formatted sql
+
+-- changeset asavershin:createUser
+CREATE TABLE users
+(
+ user_id UUID PRIMARY KEY,
+ user_firstname varchar(20) NOT NULL,
+ user_lastname varchar(20) NOT NULL,
+ user_email varchar(50) UNIQUE NOT NULL,
+ user_password TEXT not null
+);
+
+CREATE INDEX idx_user_id ON users(user_id);
+CREATE INDEX idx_user_email ON users(user_email);
\ No newline at end of file
diff --git a/api/src/main/resources/db/changelog/migrations/2-create-image.sql b/api/src/main/resources/db/changelog/migrations/2-create-image.sql
new file mode 100644
index 0000000..5f99b97
--- /dev/null
+++ b/api/src/main/resources/db/changelog/migrations/2-create-image.sql
@@ -0,0 +1,24 @@
+-- liquibase formatted sql
+
+-- changeset asavershin:createImage
+CREATE TABLE image
+(
+ image_id UUID PRIMARY KEY,
+ image_name varchar(50) NOT NULL,
+ image_extension varchar(10) NOT NULL,
+ image_size bigint not null
+);
+
+CREATE INDEX idx_image_id ON image(image_id);
+
+CREATE TABLE user_images
+(
+ image_id UUID,
+ user_id UUID,
+ CONSTRAINT fk_user_images_user FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE,
+ CONSTRAINT fk_users_image_image FOREIGN KEY (image_id) REFERENCES image (image_id) ON DELETE CASCADE
+);
+
+CREATE INDEX idx_users_image_user_id ON user_images (user_id);
+CREATE INDEX idx_users_image_image_id ON user_images (image_id);
+
diff --git a/api/src/main/resources/logback.xml b/api/src/main/resources/logback.xml
new file mode 100644
index 0000000..fa47f73
--- /dev/null
+++ b/api/src/main/resources/logback.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/api/src/test/java/com/github/asavershin/api/common/ImageHelper.java b/api/src/test/java/com/github/asavershin/api/common/ImageHelper.java
new file mode 100644
index 0000000..6cd6c04
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/common/ImageHelper.java
@@ -0,0 +1,31 @@
+package com.github.asavershin.api.common;
+
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.image.ImageNameWithExtension;
+import com.github.asavershin.api.domain.image.MetaData;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+public class ImageHelper {
+ public static String illegalExtension = ".pdf";
+ public static ImageId imageId(){
+ return ImageId.nextIdentity();
+ }
+ public static MetaData metaInfo1(){
+ return new MetaData(ImageNameWithExtension.fromOriginalFileName("image.jpg"), 1L);
+ }
+
+ public static MetaData metaInfo3(){
+ return new MetaData(ImageNameWithExtension.fromOriginalFileName("image3.jpg"), 3L);
+ }
+
+ public static MultipartFile multipartFile1() {
+ return new MockMultipartFile("image.jpg", "image.jpg", "image/jpeg", new byte[]{0, 1});
+ }
+
+ public static MultipartFile multipartFileWithIllegalException() {
+ return new MockMultipartFile("image" + illegalExtension,
+ "image" + illegalExtension,
+ "image/jpeg", new byte[]{0, 1});
+ }
+}
diff --git a/api/src/test/java/com/github/asavershin/api/common/TestUserRepository.java b/api/src/test/java/com/github/asavershin/api/common/TestUserRepository.java
new file mode 100644
index 0000000..1261574
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/common/TestUserRepository.java
@@ -0,0 +1,47 @@
+package com.github.asavershin.api.common;
+
+
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.FullName;
+import com.github.asavershin.api.domain.user.UserId;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.Nullable;
+import org.jooq.DSLContext;
+import org.jooq.Record;
+import org.jooq.RecordMapper;
+import org.jooq.SelectConditionStep;
+import org.springframework.stereotype.Repository;
+import com.github.asavershin.api.domain.user.User;
+
+import java.util.UUID;
+
+import static asavershin.generated.package_.tables.Users.USERS;
+
+@Repository
+@RequiredArgsConstructor
+public class TestUserRepository implements RecordMapper {
+ private final DSLContext dslContext;
+ public SelectConditionStep findUserById(UUID id){
+ return dslContext.select()
+ .from(USERS)
+ .where(USERS.USER_ID.eq(id));
+ }
+
+ public Long countUsers(){
+ return dslContext.selectCount()
+ .from(USERS)
+ .fetchOne(0, Long.class);
+ }
+
+ @Override
+ public @Nullable User map(Record record) {
+ var userId = record.get(USERS.USER_ID);
+ var email = record.get(USERS.USER_EMAIL);
+ var password = record.get(USERS.USER_PASSWORD);
+ var firstName = record.get(USERS.USER_FIRSTNAME);
+ var lastName = record.get(USERS.USER_LASTNAME);
+ return new User(new UserId(userId),
+ new FullName(firstName, lastName),
+ new Credentials(email, password));
+ }
+}
diff --git a/api/src/test/java/com/github/asavershin/api/common/UserHelper.java b/api/src/test/java/com/github/asavershin/api/common/UserHelper.java
new file mode 100644
index 0000000..64a8fae
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/common/UserHelper.java
@@ -0,0 +1,46 @@
+package com.github.asavershin.api.common;
+
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.FullName;
+import com.github.asavershin.api.domain.user.User;
+import com.github.asavershin.api.domain.user.UserId;
+
+public class UserHelper {
+ public static FullName fullName1(){
+ return new FullName("Alexander", "Avershin");
+ }
+
+ public static FullName fullName2(){
+ return new FullName("Avershin", "Alexander");
+ }
+
+ public static Credentials credentials1(){
+ return new Credentials("test@test.test", "verysecretpassword");
+ }
+
+ public static Credentials credentials2(){
+ return new Credentials("test2@test.test", "verysecretpassword");
+ }
+
+ public static Credentials invalidEmail(){
+ return new Credentials("invalid", "verysecretpassword");
+ }
+
+ public static UserId UserId(){
+ return UserId.nextIdentity();
+ }
+
+ public static User user1(UserId userId, FullName userFullName, Credentials userCredentials){
+ return new User(userId, userFullName, userCredentials);
+ }
+
+ public static FullName longFirstName(){
+ return new FullName("TooLongFirstNameTooLongFirTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNamestNameTooLongFirstNameTooLongFirstName",
+ "Lastname");
+ };
+
+ public static FullName longLastName(){
+ return new FullName("Firstname",
+ "TooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongFirstNameTooLongLastNameTooLongLastNameTooLongLastNameTooLongLastName");
+ };
+}
diff --git a/api/src/test/java/com/github/asavershin/api/config/CacheRedisConfig.java b/api/src/test/java/com/github/asavershin/api/config/CacheRedisConfig.java
new file mode 100644
index 0000000..6a76e8f
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/config/CacheRedisConfig.java
@@ -0,0 +1,44 @@
+package com.github.asavershin.api.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.stereotype.Component;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import static java.util.Objects.isNull;
+
+@Slf4j
+public class CacheRedisConfig {
+
+ private static volatile GenericContainer> redisContainer = null;
+
+ private static GenericContainer> getRedisContainer() {
+ var instance = redisContainer;
+ if (isNull(redisContainer)) {
+ synchronized (GenericContainer.class) {
+ redisContainer = instance = new GenericContainer<>(
+ DockerImageName.parse("redis:7.2-rc-alpine"))
+ .withExposedPorts(6379);
+ redisContainer.start();
+ }
+ }
+ return instance;
+ }
+
+ @Component("RedisInitializer")
+ public static class Initializer implements ApplicationContextInitializer {
+ @Override
+ public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
+ var redisContainer = getRedisContainer();
+ TestPropertyValues.of(
+ "spring.data.redis.host=" + redisContainer.getHost(),
+ "spring.data.redis.port=" + redisContainer.getMappedPort(6379)
+ ).applyTo(configurableApplicationContext.getEnvironment());
+ }
+ }
+
+}
+
diff --git a/api/src/test/java/com/github/asavershin/api/config/MinioConfig.java b/api/src/test/java/com/github/asavershin/api/config/MinioConfig.java
new file mode 100644
index 0000000..42412a8
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/config/MinioConfig.java
@@ -0,0 +1,52 @@
+package com.github.asavershin.api.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.stereotype.Component;
+import org.testcontainers.containers.MinIOContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+
+import java.time.Duration;
+
+import static java.util.Objects.isNull;
+
+@Slf4j
+public class MinioConfig {
+
+ private static volatile MinIOContainer minioContainer = null;
+
+
+ private static MinIOContainer getMinioContainer() {
+ MinIOContainer instance = minioContainer;
+ if (isNull(minioContainer)) {
+ synchronized (MinIOContainer.class) {
+ minioContainer = instance =
+ new MinIOContainer("minio/minio:latest")
+ .withReuse(true)
+ .withLogConsumer(new Slf4jLogConsumer(log))
+ .withExposedPorts(9000)
+ .withUserName("minioadmin")
+ .withPassword("minioadmin")
+ .withStartupTimeout(Duration.ofSeconds(60));
+ minioContainer.start();
+ }
+ }
+ return instance;
+ }
+
+ @Component("MinioInitializer")
+ public static class Initializer implements ApplicationContextInitializer {
+ @Override
+ public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
+ var minioContainer = getMinioContainer();
+ TestPropertyValues.of(
+ "minio.url=" + minioContainer.getS3URL(),
+ "minio.access-key=" + minioContainer.getUserName(),
+ "minio.secret-key=" + minioContainer.getPassword()
+ ).applyTo(configurableApplicationContext.getEnvironment());
+ }
+ }
+
+}
diff --git a/api/src/test/java/com/github/asavershin/api/config/PostgreConfig.java b/api/src/test/java/com/github/asavershin/api/config/PostgreConfig.java
new file mode 100644
index 0000000..94faf77
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/config/PostgreConfig.java
@@ -0,0 +1,52 @@
+package com.github.asavershin.api.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.stereotype.Component;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+
+import java.time.Duration;
+
+import static java.util.Objects.isNull;
+
+@Slf4j
+public class PostgreConfig {
+ private static volatile PostgreSQLContainer> postgreSQLContainer = null;
+
+
+ private static PostgreSQLContainer> getPostgreSQLContainer(){
+ PostgreSQLContainer> instance = postgreSQLContainer;
+ if(isNull(instance)){
+ synchronized (PostgreSQLContainer.class){
+ postgreSQLContainer = instance =
+ new PostgreSQLContainer<>("postgres:15.1")
+ .withDatabaseName("public")
+ .withUsername("postgres")
+ .withPassword("postgres")
+ .withReuse(true)
+ .withLogConsumer(new Slf4jLogConsumer(log))
+ .withStartupTimeout(Duration.ofSeconds(60));
+ postgreSQLContainer.start();
+ }
+ }
+ return instance;
+ }
+
+ @Component("PostgresInitializer")
+ public static class Initializer
+ implements ApplicationContextInitializer {
+ @Override
+ public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
+ var postgreSQLContainer = getPostgreSQLContainer();
+ TestPropertyValues.of(
+ "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
+ "spring.datasource.username=" + postgreSQLContainer.getUsername(),
+ "spring.datasource.password=" + postgreSQLContainer.getPassword(),
+ "spring.liquibase.change_log=classpath:db/changelog/db.changelog-master.yaml"
+ ).applyTo(configurableApplicationContext.getEnvironment());
+ }
+ }
+}
diff --git a/api/src/test/java/com/github/asavershin/api/domaintest/AuthenticatedUserTest.java b/api/src/test/java/com/github/asavershin/api/domaintest/AuthenticatedUserTest.java
new file mode 100644
index 0000000..0a070ef
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/domaintest/AuthenticatedUserTest.java
@@ -0,0 +1,47 @@
+package com.github.asavershin.api.domaintest;
+
+import com.github.asavershin.api.common.UserHelper;
+import com.github.asavershin.api.domain.user.*;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class AuthenticatedUserTest {
+
+ @Test
+ public void testFoundedUser() {
+ UserId userId = UserHelper.UserId();
+ Credentials credentials = UserHelper.credentials1();
+ AuthenticatedUser authenticatedUser = AuthenticatedUser.founded(userId, credentials);
+ assertEquals(userId, authenticatedUser.userId());
+ assertEquals(credentials, authenticatedUser.userCredentials());
+ }
+
+ @Test
+ public void testFoundedUserWithEmailEmpty() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ AuthenticatedUser.founded(UserHelper.UserId(), new Credentials("", "password"));
+ });
+ assertEquals("Email must be 5-50 in length", exception.getMessage());
+ }
+
+ @Test
+ public void testFoundedUserWithPasswordNull() {
+ NullPointerException exception = assertThrows(NullPointerException.class, () -> {
+ AuthenticatedUser.founded(UserHelper.UserId(), new Credentials("test@test.com", null));
+ });
+ assertEquals("Password must not be null", exception.getMessage());
+ }
+
+ @Test
+ public void testFoundedUserWithPasswordTooShort() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ AuthenticatedUser.founded(UserHelper.UserId(), new Credentials("test@test.com", "short"));
+ });
+ assertEquals("Password must be greater than 8", exception.getMessage());
+ }
+
+
+
+}
+
diff --git a/api/src/test/java/com/github/asavershin/api/domaintest/ImageTest.java b/api/src/test/java/com/github/asavershin/api/domaintest/ImageTest.java
new file mode 100644
index 0000000..0790394
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/domaintest/ImageTest.java
@@ -0,0 +1,93 @@
+package com.github.asavershin.api.domaintest;
+
+import com.github.asavershin.api.common.ImageHelper;
+import com.github.asavershin.api.common.UserHelper;
+import com.github.asavershin.api.domain.ResourceOwnershipException;
+import com.github.asavershin.api.domain.image.Image;
+import com.github.asavershin.api.domain.image.ImageId;
+import com.github.asavershin.api.domain.image.MetaData;
+import com.github.asavershin.api.domain.user.UserId;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ImageTest {
+
+ @Test
+ void testAddNewImage() {
+ //Given
+ ImageId imageId = ImageHelper.imageId();
+ MetaData metaInfo = ImageHelper.metaInfo1();
+ UserId userId = UserHelper.UserId();
+
+ //When
+ Image image = new Image(imageId, metaInfo, userId);
+
+ //Then
+ assertEquals(image.imageId(), imageId);
+ assertEquals(image.metaInfo(), metaInfo);
+ assertEquals(image.userId(), userId);
+ }
+
+ @Test
+ void testFoundedImage() {
+ //Given
+ ImageId imageId = ImageHelper.imageId();
+ MetaData metaInfo = ImageHelper.metaInfo1();
+ UserId userId = UserHelper.UserId();
+ // When
+ Image image = new Image(imageId, metaInfo, userId);
+ // Then
+ assertEquals(image.imageId(), imageId);
+ assertEquals(image.metaInfo(), metaInfo);
+ assertEquals(image.userId(), userId);
+ }
+
+ @Test
+ void testBelongsToUser() {
+ // Given
+ ImageId imageId = ImageHelper.imageId();
+ MetaData metaInfo = ImageHelper.metaInfo1();
+ UserId userId = UserHelper.UserId();
+ UserId otherUserId = UserHelper.UserId();
+
+ // When
+ Image image = new Image(imageId, metaInfo, userId);
+ assertDoesNotThrow(() -> image.belongsToUser(userId));
+ var exception = assertThrows(ResourceOwnershipException.class, () -> image.belongsToUser(otherUserId));
+
+ // Then
+ assertDoesNotThrow(() -> image.belongsToUser(userId));
+ assertEquals("Image with id " + imageId.value().toString() +
+ " does not belong to user with id " + otherUserId.value().toString(), exception.getMessage());
+ }
+
+ @Test
+ void testEquals() {
+ var imageId = ImageId.nextIdentity();
+ var userId = UserId.nextIdentity();
+
+ MetaData metaInfo1 = ImageHelper.metaInfo1();
+ Image image1 = new Image(imageId, metaInfo1, userId);
+
+ MetaData metaInfo2 = ImageHelper.metaInfo1();
+ Image image2 = new Image(imageId, metaInfo2, userId);
+
+ assertTrue(image1.equals(image2));
+ assertTrue(image2.equals(image1));
+
+ assertTrue(image1.equals(image1));
+
+ assertFalse(image1.equals(null));
+
+ ImageId imageId3 = ImageId.nextIdentity();
+ MetaData metaInfo3 = ImageHelper.metaInfo3();
+ UserId userId3 = UserHelper.UserId();
+ Image image3 = new Image(imageId3, metaInfo3, userId3);
+
+ assertFalse(image1.equals(image3));
+ assertFalse(image3.equals(image1));
+ }
+
+}
+
+
diff --git a/api/src/test/java/com/github/asavershin/api/domaintest/UserTest.java b/api/src/test/java/com/github/asavershin/api/domaintest/UserTest.java
new file mode 100644
index 0000000..4c02076
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/domaintest/UserTest.java
@@ -0,0 +1,86 @@
+package com.github.asavershin.api.domaintest;
+
+import com.github.asavershin.api.common.UserHelper;
+import com.github.asavershin.api.domain.user.Credentials;
+import com.github.asavershin.api.domain.user.FullName;
+import com.github.asavershin.api.domain.user.User;
+import com.github.asavershin.api.domain.user.UserId;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class UserTest {
+
+ @Test
+ public void testCreateNewUserForRegistration() {
+ UserId userId = UserHelper.UserId();
+ FullName fullName = UserHelper.fullName1();
+ Credentials credentials = UserHelper.credentials1();
+ User user = new User(userId, fullName, credentials);
+ assertEquals(userId, user.userId());
+ assertEquals(fullName, user.userFullName());
+ assertEquals(credentials, user.userCredentials());
+ }
+
+ @Test
+ public void testCreateNewUserForRegistrationWithNullUserId() {
+ NullPointerException exception = assertThrows(NullPointerException.class, () -> {
+ new User(null, UserHelper.fullName1(), UserHelper.credentials1());
+ });
+ assertEquals("UserId must not be null", exception.getMessage());
+ }
+
+ @Test
+ public void testCreateNewUserForRegistrationWithNullFullName() {
+ NullPointerException exception = assertThrows(NullPointerException.class, () -> {
+ new User(UserHelper.UserId(), null, UserHelper.credentials1());
+ });
+ assertEquals("UserFullName must not be null", exception.getMessage());
+ }
+
+ @Test
+ public void testCreateNewUserForRegistrationWithNullCredentials() {
+ NullPointerException exception = assertThrows(NullPointerException.class, () -> {
+ new User(UserHelper.UserId(), UserHelper.fullName1(), null);
+ });
+ assertEquals("UserCredentials must not be null", exception.getMessage());
+ }
+
+ @Test
+ public void testCreateNewUserForRegistrationWithInvalidFullName() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ new User(UserHelper.UserId(), UserHelper.longFirstName(), UserHelper.credentials1());
+ });
+ assertEquals("Name must be 0-20 in length", exception.getMessage());
+ }
+
+ @Test
+ void testEquals() {
+
+ var userId = UserHelper.UserId();
+
+ FullName fullName1 = UserHelper.fullName1();
+ Credentials credentials1 = UserHelper.credentials1();
+ User user1 = new User(userId, fullName1, credentials1);
+
+ FullName fullName2 = UserHelper.fullName1();
+ Credentials credentials2 = UserHelper.credentials1();
+ User user2 = new User(userId, fullName2, credentials2);
+
+ UserId userId3 = UserId.nextIdentity();
+ FullName fullName3 = UserHelper.fullName2();
+ Credentials credentials3 = UserHelper.credentials2();
+ User user3 = new User(userId3, fullName3, credentials3);
+
+ assertTrue(user1.equals(user2));
+ assertTrue(user2.equals(user1));
+
+ assertTrue(user1.equals(user1));
+
+ assertFalse(user1.equals(null));
+
+
+ assertFalse(user1.equals(user3));
+ assertFalse(user3.equals(user1));
+ }
+}
diff --git a/api/src/test/java/com/github/asavershin/api/integrations/AbstractTest.java b/api/src/test/java/com/github/asavershin/api/integrations/AbstractTest.java
new file mode 100644
index 0000000..42d1621
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/integrations/AbstractTest.java
@@ -0,0 +1,10 @@
+package com.github.asavershin.api.integrations;
+
+import com.github.asavershin.api.config.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+
+@SpringBootTest
+@ContextConfiguration(initializers = {PostgreConfig.Initializer.class, MinioConfig.Initializer.class, CacheRedisConfig.Initializer.class})
+public abstract class AbstractTest {
+}
diff --git a/api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java b/api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java
new file mode 100644
index 0000000..48b77a0
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java
@@ -0,0 +1,219 @@
+package com.github.asavershin.api.integrations;
+
+import asavershin.generated.package_.tables.Image;
+import asavershin.generated.package_.tables.Users;
+import com.github.asavershin.api.application.in.services.image.ImageService;
+import com.github.asavershin.api.application.out.TokenRepository;
+import com.github.asavershin.api.common.ImageHelper;
+import com.github.asavershin.api.common.UserHelper;
+import com.github.asavershin.api.config.properties.MinIOProperties;
+import com.github.asavershin.api.domain.PartOfResources;
+import com.github.asavershin.api.domain.ResourceOwnershipException;
+import com.github.asavershin.api.domain.image.GetPartImagesOfUser;
+import com.github.asavershin.api.domain.image.ImageRepository;
+import com.github.asavershin.api.domain.user.AuthenticatedUserRepository;
+import com.github.asavershin.api.domain.user.RegisterNewUser;
+import io.minio.*;
+import io.minio.messages.Item;
+import lombok.extern.slf4j.Slf4j;
+import org.jooq.DSLContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+@Slf4j
+public class ImageLogicTest extends AbstractTest{
+ @Autowired
+ private DSLContext dslContext;
+
+ @Autowired
+ private TokenRepository tokenRepository;
+
+ @Autowired
+ private ImageService imageService;
+
+ @Autowired
+ private ImageRepository imageRepository;
+
+ @Autowired
+ private RegisterNewUser registerNewUser;
+
+ @Autowired
+ private AuthenticatedUserRepository authenticatedUserRepository;
+
+ @Autowired
+ private GetPartImagesOfUser getPartImagesOfUser;
+
+ @Autowired
+ private MinioClient minioClient;
+
+ @Autowired
+ private MinIOProperties minioProperties;
+
+ @BeforeEach
+ public void clearDB(){
+ dslContext.delete(Users.USERS).execute();
+ dslContext.delete(Image.IMAGE).execute();
+ tokenRepository.deleteAllTokensByUserEmail(UserHelper.credentials1().email());
+ tokenRepository.deleteAllTokensByUserEmail(UserHelper.credentials1().email());
+
+ try {
+ Iterable> files = minioClient.listObjects(ListObjectsArgs.builder().bucket(minioProperties.getBucket()).build());
+
+ for (var file : files) {
+ minioClient.removeObject(
+ RemoveObjectArgs.builder()
+ .bucket(minioProperties.getBucket())
+ .object(file.get().objectName())
+ .build());
+ }
+ } catch (Exception e) {
+ log.info("Clean bucket fail");
+ e.printStackTrace();
+ }
+ }
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ @Test
+ public void storeImageTest(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var multipartFile = ImageHelper.multipartFile1();
+
+ registerNewUser.register(fullName,credentials);
+
+ var user = authenticatedUserRepository.findByEmail(credentials.email());
+ // When
+ var imageId = imageService.storeImage(user.userId(), ImageHelper.multipartFile1());
+ log.info("asdasd");
+ log.info(applicationContext.getBean(ImageService.class, "foo").getClass().toString());
+
+ // Then
+ var image = imageRepository.findImageByImageId(imageId);
+ assertNotNull(image);
+ assertEquals(user.userId(), image.userId());
+ assertEquals(multipartFile.getSize(), image.metaInfo().imageSize());
+ assertEquals(multipartFile.getOriginalFilename(),
+ image.metaInfo().imageNameWithExtension().toString());
+ assertEquals(".jpg", image.metaInfo().imageNameWithExtension().imageExtension().toString());
+
+ var objectsInMinio = minioClient.listObjects(ListObjectsArgs.builder().bucket("files").build());
+ AtomicInteger countObjectsInMinio = new AtomicInteger();
+ objectsInMinio.forEach( it -> countObjectsInMinio.addAndGet(1));
+ assertEquals(1, countObjectsInMinio.get());
+ }
+
+ @Test
+ public void storeImageWithIllegalExtensionTest(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var multipartFile = ImageHelper.multipartFile1();
+
+ registerNewUser.register(fullName,credentials);
+
+ var user = authenticatedUserRepository.findByEmail(credentials.email());
+ assertNotNull(user);
+ // When
+ var ex = assertThrows(IllegalArgumentException.class, () -> imageService.storeImage(user.userId(), ImageHelper.multipartFileWithIllegalException()));
+
+ // Then
+ assertEquals(ex.getMessage(), "Invalid extension: " + ImageHelper.illegalExtension);
+ }
+
+ @Test
+ public void getPartImagesOfUserTest(){
+ //Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var multipartFile = ImageHelper.multipartFile1();
+
+ registerNewUser.register(fullName,credentials);
+
+ var user = authenticatedUserRepository.findByEmail(credentials.email());
+
+ var imageId1 = imageService.storeImage(user.userId(), ImageHelper.multipartFile1());
+ var imageId2 = imageService.storeImage(user.userId(), ImageHelper.multipartFile1());
+
+ // When
+
+ var images = getPartImagesOfUser.get(user.userId(), new PartOfResources(1L,1L));
+
+ // Then
+
+ assertEquals(images.size(), 1);
+ }
+
+ @Test
+ public void deleteImageByImageIdTest(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var multipartFile = ImageHelper.multipartFile1();
+
+ registerNewUser.register(fullName,credentials);
+
+ var user = authenticatedUserRepository.findByEmail(credentials.email());
+
+ var imageId1 = imageService.storeImage(user.userId(), ImageHelper.multipartFile1());
+ var imageId2 = imageService.storeImage(user.userId(), ImageHelper.multipartFile1());
+
+ // When
+ imageService.deleteImageByImageId(user.userId(), imageId1);
+
+ // Then
+ var images = imageRepository.findImagesByUserId(user.userId(), new PartOfResources(0L, 2L));
+ assertEquals(images.size(), 1);
+ assertEquals(images.get(0).imageId(), imageId2);
+ }
+
+ @Test
+ public void deleteImageByImageIdAnotherUser(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var multipartFile = ImageHelper.multipartFile1();
+ var userId = UserHelper.UserId();
+
+ registerNewUser.register(fullName,credentials);
+
+ var user = authenticatedUserRepository.findByEmail(credentials.email());
+
+ var imageId1 = imageService.storeImage(user.userId(), multipartFile);
+
+ // When
+ var ex = assertThrows(ResourceOwnershipException.class, () -> imageService.deleteImageByImageId(userId, imageId1));
+ assertEquals("Image with id " + imageId1.value().toString()
+ + " does not belong to user with id " + userId.value().toString(), ex.getMessage());
+ }
+
+ @Test
+ public void downloadImageTest() throws IOException {
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var multipartFile = ImageHelper.multipartFile1();
+ var userId = UserHelper.UserId();
+
+ registerNewUser.register(fullName,credentials);
+
+ var user = authenticatedUserRepository.findByEmail(credentials.email());
+
+ var imageId1 = imageService.storeImage(user.userId(), multipartFile);
+
+ // When
+ var image = imageService.downloadImage(imageId1, user.userId());
+
+ // Then
+ assertArrayEquals(multipartFile.getBytes(), image);
+ }
+}
diff --git a/api/src/test/java/com/github/asavershin/api/integrations/UserLogicTest.java b/api/src/test/java/com/github/asavershin/api/integrations/UserLogicTest.java
new file mode 100644
index 0000000..734af26
--- /dev/null
+++ b/api/src/test/java/com/github/asavershin/api/integrations/UserLogicTest.java
@@ -0,0 +1,157 @@
+package com.github.asavershin.api.integrations;
+
+import asavershin.generated.package_.tables.Image;
+import asavershin.generated.package_.tables.Users;
+import com.github.asavershin.api.application.in.services.user.GetNewCredentials;
+import com.github.asavershin.api.application.in.services.user.JwtService;
+import com.github.asavershin.api.application.in.services.user.impl.GetNewCredentialsUsingRefreshTokenImpl;
+import com.github.asavershin.api.application.out.TokenRepository;
+import com.github.asavershin.api.common.NotFoundException;
+import com.github.asavershin.api.common.TestUserRepository;
+import com.github.asavershin.api.common.UserHelper;
+import com.github.asavershin.api.domain.user.*;
+import lombok.extern.slf4j.Slf4j;
+import org.jooq.DSLContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.hibernate.validator.internal.util.Contracts.assertNotNull;
+import static org.junit.jupiter.api.Assertions.*;
+
+@Slf4j
+public class UserLogicTest extends AbstractTest {
+ @Autowired
+ private DSLContext dslContext;
+
+ @Autowired
+ private RegisterNewUser registerNewUser;
+
+ @Autowired
+ private TestUserRepository testUserRepository;
+
+ @Autowired
+ private AuthenticatedUserRepository authenticatedUserRepository;
+
+ @Autowired
+ private GetNewCredentials getNewCredentials;
+
+ @Autowired
+ private JwtService jwtService;
+
+ @Autowired
+ private TokenRepository tokenRepository;
+
+ @Autowired
+ private GetNewCredentialsUsingRefreshTokenImpl getNewCredentialsUsingRefreshToken;
+
+
+
+ @BeforeEach
+ public void clearDB(){
+ dslContext.delete(Users.USERS).execute();
+ dslContext.delete(Image.IMAGE).execute();
+ tokenRepository.deleteAllTokensByUserEmail(UserHelper.credentials1().email());
+ tokenRepository.deleteAllTokensByUserEmail(UserHelper.credentials1().email());
+ }
+
+ @Test
+ public void testRegisterNewUser(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ var fullName2 = UserHelper.fullName1();
+ var credentials2 = UserHelper.credentials2();
+
+ // When
+ registerNewUser.register(fullName, credentials);
+ registerNewUser.register(fullName2, credentials2);
+
+
+ // Then
+ assertEquals(2, testUserRepository.countUsers());
+ assertNotNull(authenticatedUserRepository.findByEmail(credentials.email()));
+ assertNotNull(authenticatedUserRepository.findByEmail(credentials2.email()));
+
+ }
+
+ @Test
+ public void testRegisterNewUserWithoutUniqueEmail(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+
+ // When
+ registerNewUser.register(fullName, credentials);
+
+ // Then
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ registerNewUser.register(UserHelper.fullName1(), UserHelper.credentials1());
+ });
+
+ assertEquals("Email is not unique", exception.getMessage());
+ }
+
+ @Test
+ public void testGetNewCredentials(){
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+
+ // When
+ registerNewUser.register(fullName, credentials);
+ var newCredentials = getNewCredentials.get(credentials);
+ assertDoesNotThrow(() -> jwtService.extractSub(newCredentials.getAccessToken()));
+ assertDoesNotThrow(() -> jwtService.extractSub(newCredentials.getRefreshToken()));
+
+ var access = jwtService.extractSub(newCredentials.getAccessToken());
+ var refresh = jwtService.extractSub(newCredentials.getRefreshToken());
+
+ assertDoesNotThrow(() -> tokenRepository.getAccessToken(access));
+ assertDoesNotThrow(() -> tokenRepository.getRefreshToken(refresh));
+
+ var accessFromRedis = tokenRepository.getAccessToken(access);
+ var refreshFromRedis = tokenRepository.getRefreshToken(refresh);
+
+ // Then
+ assertNotNull(access);
+ assertNotNull(refresh);
+ assertEquals(credentials.email(), access);
+ assertEquals(credentials.email(), refresh);
+
+ assertNotNull(accessFromRedis);
+ assertNotNull(refreshFromRedis);
+
+ assertEquals(newCredentials.getAccessToken(), accessFromRedis);
+ assertEquals(newCredentials.getRefreshToken(), refreshFromRedis);
+ }
+
+ @Test
+ public void testGetNewCredentialsForNotRegisteredUser(){
+ // When
+ registerNewUser.register(UserHelper.fullName1(), UserHelper.credentials1());
+ var ex = assertThrows(NotFoundException.class, () -> getNewCredentials.get(UserHelper.credentials2()));
+
+ // Then
+ assertEquals(ex.getMessage(),
+ "User" + " with " + "email" + UserHelper.credentials2().email() + " not found");
+ }
+
+ @Test
+ public void getNewCredentialsUsingRefreshTokenTest() throws InterruptedException {
+ // Given
+ var fullName = UserHelper.fullName1();
+ var credentials = UserHelper.credentials1();
+ registerNewUser.register(fullName, credentials);
+ var newCredentials = getNewCredentials.get(credentials);
+
+ // When
+ Thread.sleep(2000L);
+ var refreshedCredentials = getNewCredentialsUsingRefreshToken.get(credentials);
+
+ // Then
+ assertNotNull(refreshedCredentials);
+ assertNotEquals(newCredentials.getAccessToken(), refreshedCredentials.getAccessToken());
+ assertNotEquals(newCredentials.getRefreshToken(), refreshedCredentials.getRefreshToken());
+ }
+}
diff --git a/api/src/test/resources/application.properties b/api/src/test/resources/application.properties
new file mode 100644
index 0000000..28c3f00
--- /dev/null
+++ b/api/src/test/resources/application.properties
@@ -0,0 +1,29 @@
+spring.profiles.active=test
+spring.datasource.url=jdbc:postgresql://db-api:5432/$postgres?currentSchema=public
+spring.datasource.username=postgres
+spring.datasource.password=postgres
+spring.liquibase.enabled=true
+spring.liquibase.drop-first=false
+spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
+spring.liquibase.default-schema=public
+
+spring.data.redis.host=redis-api
+spring.data.redis.port=6379
+spring.cache.type=redis
+spring.cache.cache-names=redis-cache
+spring.cache.redis.time-to-live=86400000
+
+jwt.access-expiration=86400000
+jwt.refresh-expiration=604800000
+jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
+
+minio.url=http://minio-api:9000
+minio.user=minioadmin
+minio.password=minioadmin
+minio.bucket=files
+minio.console-port=9090
+minio.port=9000
+
+testconf=test
+
+
diff --git a/api/src/test/resources/changelog/db.changelog-master.yaml b/api/src/test/resources/changelog/db.changelog-master.yaml
new file mode 100644
index 0000000..fc3e65d
--- /dev/null
+++ b/api/src/test/resources/changelog/db.changelog-master.yaml
@@ -0,0 +1,4 @@
+databaseChangeLog:
+ - includeAll:
+ path: /migrations
+ relativeToChangelogFile: true
\ No newline at end of file
diff --git a/api/src/test/resources/changelog/migrations/1-create-user.sql b/api/src/test/resources/changelog/migrations/1-create-user.sql
new file mode 100644
index 0000000..64239a0
--- /dev/null
+++ b/api/src/test/resources/changelog/migrations/1-create-user.sql
@@ -0,0 +1,14 @@
+-- liquibase formatted sql
+
+-- changeset asavershin:createUser
+CREATE TABLE users
+(
+ user_id UUID PRIMARY KEY,
+ user_firstname varchar(20) NOT NULL,
+ user_lastname varchar(20) NOT NULL,
+ user_email varchar(50) UNIQUE NOT NULL,
+ user_password TEXT not null
+);
+
+CREATE INDEX idx_user_id ON users(user_id);
+CREATE INDEX idx_user_email ON users(user_email);
\ No newline at end of file
diff --git a/api/src/test/resources/changelog/migrations/2-create-image.sql b/api/src/test/resources/changelog/migrations/2-create-image.sql
new file mode 100644
index 0000000..2667cf5
--- /dev/null
+++ b/api/src/test/resources/changelog/migrations/2-create-image.sql
@@ -0,0 +1,23 @@
+-- liquibase formatted sql
+
+-- changeset asavershin:createImage
+CREATE TABLE image
+(
+ image_id UUID PRIMARY KEY,
+ image_name varchar(50) NOT NULL,
+ image_size bigint not null
+);
+
+CREATE INDEX idx_image_id ON image(image_id);
+
+CREATE TABLE user_images
+(
+ image_id UUID,
+ user_id UUID,
+ CONSTRAINT fk_user_images_user FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE,
+ CONSTRAINT fk_users_image_image FOREIGN KEY (image_id) REFERENCES image (image_id) ON DELETE CASCADE
+);
+
+CREATE INDEX idx_users_image_user_id ON user_images (user_id);
+CREATE INDEX idx_users_image_image_id ON user_images (image_id);
+
diff --git a/api/src/test/resources/logback.xml b/api/src/test/resources/logback.xml
new file mode 100644
index 0000000..fa47f73
--- /dev/null
+++ b/api/src/test/resources/logback.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..66df285
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..95ba6f5
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..8a09635
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.4
+
+
+ com.github.asavershin.images
+ images
+ 0.0.1-SNAPSHOT
+ pom
+ images
+
+
+ api
+
+
+
+ 21
+ 21
+ UTF-8
+ 1.18.30
+ 3.3.1
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 3.2.0
+ import
+ pom
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${org.projectlombok.version}
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ ${maven-checkstyle-plugin.version}
+
+
+
+
+