From 14cc2e78fc7dc56981842dfdad90c7ab8832e7fc Mon Sep 17 00:00:00 2001 From: asavershin Date: Tue, 2 Apr 2024 22:01:07 +0900 Subject: [PATCH 1/9] Add api --- .gitignore | 32 ++ .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .mvn/wrapper/maven-wrapper.properties | 2 + README.md | 8 +- api/.env | 36 ++ api/Dockerfile | 7 + api/README.md | 5 + api/docker-compose.yml | 52 +++ api/pom.xml | 259 +++++++++++++++ .../com/github/asavershin/api/ApiMain.java | 11 + .../in/services/image/ImageService.java | 11 + .../services/image/impl/ImageServiceImpl.java | 61 ++++ .../services/user/ApplicationCredentials.java | 14 + .../in/services/user/GetNewCredentials.java | 8 + .../GetNewCredentialsUsingRefreshToken.java | 8 + .../in/services/user/JwtService.java | 14 + .../user/impl/GetNewCredentialsImpl.java | 34 ++ ...etNewCredentialsUsingRefreshTokenImpl.java | 28 ++ .../in/services/user/impl/JwtServiceIml.java | 57 ++++ .../api/application/out/CacheRepository.java | 9 + .../api/application/out/FileService.java | 9 + .../api/application/out/MinioService.java | 6 + .../api/application/out/TokenRepository.java | 13 + .../api/common/NotFoundException.java | 7 + .../asavershin/api/common/Validator.java | 48 +++ .../api/common/annotations/Command.java | 9 + .../api/common/annotations/DomainService.java | 9 + .../api/common/annotations/Query.java | 9 + .../api/config/AnnotationsConfig.java | 20 ++ .../asavershin/api/config/AuthConfig.java | 21 ++ .../asavershin/api/config/MinIOConfig.java | 25 ++ .../asavershin/api/config/RedisConfig.java | 20 ++ .../api/config/properties/JwtProperties.java | 16 + .../config/properties/MinIOProperties.java | 17 + .../asavershin/api/domain/IsEntityFound.java | 10 + .../api/domain/PartOfResources.java | 15 + .../domain/ResourceOwnershipException.java | 7 + .../api/domain/image/DeleteImageOfUser.java | 8 + .../api/domain/image/GetImageOfUser.java | 8 + .../api/domain/image/GetPartImagesOfUser.java | 11 + .../asavershin/api/domain/image/Image.java | 48 +++ .../api/domain/image/ImageExtension.java | 35 ++ .../asavershin/api/domain/image/ImageId.java | 14 + .../domain/image/ImageNameWithExtension.java | 51 +++ .../api/domain/image/ImageRepository.java | 15 + .../asavershin/api/domain/image/MetaInfo.java | 29 ++ .../api/domain/image/StoreImageOfUser.java | 6 + .../image/impl/DeleteImageOfUserImpl.java | 22 ++ .../domain/image/impl/GetImageOfUserImpl.java | 24 ++ .../image/impl/GetPartImagesOfUserImpl.java | 21 ++ .../image/impl/StoreImageOfUserImpl.java | 18 + .../asavershin/api/domain/lombok.config | 1 + .../api/domain/user/AuthException.java | 7 + .../api/domain/user/AuthenticatedUser.java | 35 ++ .../user/AuthenticatedUserRepository.java | 7 + .../api/domain/user/Credentials.java | 33 ++ .../asavershin/api/domain/user/FullName.java | 23 ++ .../GetPartOfImagesForAuthenticatedUser.java | 11 + .../api/domain/user/RegisterNewUser.java | 6 + .../api/domain/user/TryToLogin.java | 6 + .../asavershin/api/domain/user/User.java | 39 +++ .../asavershin/api/domain/user/UserId.java | 16 + .../api/domain/user/UserRepository.java | 9 + ...tPartOfImagesForAuthenticatedUserImpl.java | 21 ++ .../domain/user/impl/RegisterNewUserImpl.java | 38 +++ .../api/domain/user/impl/TryToLoginImpl.java | 31 ++ .../controllers/AdviceController.java | 77 +++++ .../controllers/AuthController.java | 52 +++ .../controllers/ImageController.java | 73 +++++ .../controllers/dto/ExceptionBody.java | 23 ++ .../controllers/dto/UISuccessContainer.java | 11 + .../dto/image/GetImagesResponse.java | 22 ++ .../controllers/dto/image/ImageResponse.java | 25 ++ .../dto/image/UploadImageResponse.java | 13 + .../dto/user/UserLoginRequest.java | 24 ++ .../dto/user/UserRegistrationRequest.java | 40 +++ .../in/security/CustomUserDetails.java | 46 +++ .../in/security/JwtAuthenticationFilter.java | 82 +++++ .../in/security/LogautHandlerImpl.java | 33 ++ .../in/security/SecurityConfiguration.java | 62 ++++ .../in/security/UserDetailsServiceImpl.java | 21 ++ .../AuthenticatedUserRepositoryImpl.java | 39 +++ .../out/persistence/CacheRepositoryIml.java | 28 ++ .../out/persistence/ImageRepositoryImpl.java | 77 +++++ .../out/persistence/TokenRepositoryIml.java | 40 +++ .../out/persistence/UserRepositoryImpl.java | 36 ++ .../out/storage/FileException.java | 7 + .../out/storage/MinioServiceIml.java | 118 +++++++ api/src/main/resources/application.yml | 46 +++ .../db/changelog/db.changelog-master.yaml | 4 + .../db/changelog/migrations/1-create-user.sql | 14 + .../changelog/migrations/2-create-image.sql | 24 ++ api/src/main/resources/logback.xml | 22 ++ .../asavershin/api/common/ImageHelper.java | 31 ++ .../api/common/TestUserRepository.java | 47 +++ .../asavershin/api/common/UserHelper.java | 46 +++ .../api/config/CacheRedisConfig.java | 44 +++ .../asavershin/api/config/MinioConfig.java | 52 +++ .../asavershin/api/config/PostgreConfig.java | 52 +++ .../api/domaintest/AuthenticatedUserTest.java | 47 +++ .../asavershin/api/domaintest/ImageTest.java | 93 ++++++ .../asavershin/api/domaintest/UserTest.java | 86 +++++ .../api/integrations/AbstractTest.java | 10 + .../api/integrations/ImageLogicTest.java | 213 ++++++++++++ .../api/integrations/UserLogicTest.java | 157 +++++++++ api/src/test/resources/application.properties | 29 ++ .../changelog/db.changelog-master.yaml | 4 + .../changelog/migrations/1-create-user.sql | 14 + .../changelog/migrations/2-create-image.sql | 23 ++ api/src/test/resources/logback.xml | 22 ++ mvnw | 308 ++++++++++++++++++ mvnw.cmd | 205 ++++++++++++ pom.xml | 49 +++ 113 files changed, 4100 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 api/.env create mode 100644 api/Dockerfile create mode 100644 api/README.md create mode 100644 api/docker-compose.yml create mode 100644 api/pom.xml create mode 100644 api/src/main/java/com/github/asavershin/api/ApiMain.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/image/ImageService.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/ImageServiceImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/ApplicationCredentials.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentials.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentialsUsingRefreshToken.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/JwtService.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsUsingRefreshTokenImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/JwtServiceIml.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/out/CacheRepository.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/out/FileService.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/out/MinioService.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/out/TokenRepository.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/NotFoundException.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/Validator.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/annotations/Command.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/annotations/DomainService.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/annotations/Query.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/AuthConfig.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/RedisConfig.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/properties/JwtProperties.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/properties/MinIOProperties.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/DeleteImageOfUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/GetImageOfUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/GetPartImagesOfUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/Image.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/ImageExtension.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/ImageId.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/ImageNameWithExtension.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/ImageRepository.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/StoreImageOfUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/impl/DeleteImageOfUserImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/impl/GetImageOfUserImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/impl/GetPartImagesOfUserImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/impl/StoreImageOfUserImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/lombok.config create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/AuthException.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUserRepository.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/Credentials.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/FullName.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/GetPartOfImagesForAuthenticatedUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/RegisterNewUser.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/TryToLogin.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/User.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/UserId.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/UserRepository.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/impl/GetPartOfImagesForAuthenticatedUserImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/impl/RegisterNewUserImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/impl/TryToLoginImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AdviceController.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AuthController.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/ImageController.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/ExceptionBody.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/UISuccessContainer.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/GetImagesResponse.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/ImageResponse.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/UploadImageResponse.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserLoginRequest.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserRegistrationRequest.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/security/CustomUserDetails.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/security/JwtAuthenticationFilter.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/security/LogautHandlerImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/security/SecurityConfiguration.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/security/UserDetailsServiceImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/AuthenticatedUserRepositoryImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/ImageRepositoryImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/UserRepositoryImpl.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/FileException.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/MinioServiceIml.java create mode 100644 api/src/main/resources/application.yml create mode 100644 api/src/main/resources/db/changelog/db.changelog-master.yaml create mode 100644 api/src/main/resources/db/changelog/migrations/1-create-user.sql create mode 100644 api/src/main/resources/db/changelog/migrations/2-create-image.sql create mode 100644 api/src/main/resources/logback.xml create mode 100644 api/src/test/java/com/github/asavershin/api/common/ImageHelper.java create mode 100644 api/src/test/java/com/github/asavershin/api/common/TestUserRepository.java create mode 100644 api/src/test/java/com/github/asavershin/api/common/UserHelper.java create mode 100644 api/src/test/java/com/github/asavershin/api/config/CacheRedisConfig.java create mode 100644 api/src/test/java/com/github/asavershin/api/config/MinioConfig.java create mode 100644 api/src/test/java/com/github/asavershin/api/config/PostgreConfig.java create mode 100644 api/src/test/java/com/github/asavershin/api/domaintest/AuthenticatedUserTest.java create mode 100644 api/src/test/java/com/github/asavershin/api/domaintest/ImageTest.java create mode 100644 api/src/test/java/com/github/asavershin/api/domaintest/UserTest.java create mode 100644 api/src/test/java/com/github/asavershin/api/integrations/AbstractTest.java create mode 100644 api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java create mode 100644 api/src/test/java/com/github/asavershin/api/integrations/UserLogicTest.java create mode 100644 api/src/test/resources/application.properties create mode 100644 api/src/test/resources/changelog/db.changelog-master.yaml create mode 100644 api/src/test/resources/changelog/migrations/1-create-user.sql create mode 100644 api/src/test/resources/changelog/migrations/2-create-image.sql create mode 100644 api/src/test/resources/logback.xml create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml 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 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 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..fd4a080 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:22 + +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..e145ce1 --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,259 @@ + + + 4.0.0 + + com.github.asavershin.images + images + 0.0.1-SNAPSHOT + + + api + + + 22 + 22 + 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..5c2aba3 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/ApiMain.java @@ -0,0 +1,11 @@ +package com.github.asavershin.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiMain { + public static void main(String[] args) { + SpringApplication.run(ApiMain.class, args); + } +} 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..de452c7 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/image/ImageService.java @@ -0,0 +1,11 @@ +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 { + ImageId storeImage(UserId userId, MultipartFile multipartFile); + void deleteImageByImageId(UserId userId, ImageId imageId); + 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..6f35ae9 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/ImageServiceImpl.java @@ -0,0 +1,61 @@ +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.common.Validator; +import com.github.asavershin.api.domain.image.*; +import com.github.asavershin.api.domain.user.UserId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ImageServiceImpl implements ImageService { + private final MinioService minioService; + private final GetImageOfUser getImageOfUser; + private final DeleteImageOfUser deleteImageOfUser; + private final StoreImageOfUser storeImageOfUser; + @Override + @Transactional + public ImageId storeImage(UserId userId, MultipartFile multipartFile) { + log.info("Store image: {}", multipartFile.getOriginalFilename()); + var metaInfo = new MetaInfo( + ImageNameWithExtension + .fromOriginalFileName(multipartFile.getOriginalFilename()), + multipartFile.getSize() + ); + log.info("Store image: check on ex"); + var imageId = new ImageId(UUID.fromString(minioService.saveFile(multipartFile))); + storeImageOfUser.storeImageOfUser( + new Image( + imageId, + metaInfo, + userId + ) + ); + return imageId; + } + + @Override + @Transactional + public void deleteImageByImageId(UserId userId, ImageId imageId) { + deleteImageOfUser.removeImageOfUser(imageId, userId); + minioService.deleteFiles(List.of(imageId.value().toString())); + } + + @Override + public byte[] downloadImage(ImageId imageId, 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/user/ApplicationCredentials.java b/api/src/main/java/com/github/asavershin/api/application/in/services/user/ApplicationCredentials.java new file mode 100644 index 0000000..e8e288a --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/ApplicationCredentials.java @@ -0,0 +1,14 @@ +package com.github.asavershin.api.application.in.services.user; + +import lombok.Getter; + +@Getter +public class ApplicationCredentials { + private final String accessToken; + private final String refreshToken; + + public ApplicationCredentials(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} 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..7765672 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentials.java @@ -0,0 +1,8 @@ +package com.github.asavershin.api.application.in.services.user; + +import com.github.asavershin.api.domain.user.Credentials; + +@FunctionalInterface +public interface GetNewCredentials { + 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..f0ed0cc --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/GetNewCredentialsUsingRefreshToken.java @@ -0,0 +1,8 @@ +package com.github.asavershin.api.application.in.services.user; + +import com.github.asavershin.api.domain.user.Credentials; + +@FunctionalInterface +public interface GetNewCredentialsUsingRefreshToken { + 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..02e3646 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/JwtService.java @@ -0,0 +1,14 @@ +package com.github.asavershin.api.application.in.services.user; + +import com.github.asavershin.api.domain.user.Credentials; + +public interface JwtService { + + String generateAccessToken(Credentials credentials); + + String generateRefreshToken( + Credentials credentials + ); + + 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..75a5838 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsImpl.java @@ -0,0 +1,34 @@ +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 { + private final TokenRepository tokenRepository; + private final TryToLogin tryToLogin; + private final JwtService jwtService; + private final JwtProperties jwtProperties; + + @Override + public ApplicationCredentials get(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..1d16949 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/GetNewCredentialsUsingRefreshTokenImpl.java @@ -0,0 +1,28 @@ +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 { + private final TokenRepository tokenRepository; + private final JwtService jwtService; + private final JwtProperties jwtProperties; + + @Override + public ApplicationCredentials get(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..c229ea7 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/JwtServiceIml.java @@ -0,0 +1,57 @@ +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.AuthenticatedUser; +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 { + + private final JwtProperties jwtProperties; + + @Override + public String generateAccessToken(Credentials credentials) { + return buildToken(credentials, jwtProperties.getAccessExpiration()); + } + + @Override + public String generateRefreshToken(Credentials credentials) { + return buildToken(credentials, jwtProperties.getRefreshExpiration()); + } + + @Override + public String extractSub(String jwt) { + return Jwts.parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(jwt) + .getBody() + .getSubject(); + } + + private String buildToken(Credentials credentials, 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/out/CacheRepository.java b/api/src/main/java/com/github/asavershin/api/application/out/CacheRepository.java new file mode 100644 index 0000000..7ba8d35 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/out/CacheRepository.java @@ -0,0 +1,9 @@ +package com.github.asavershin.api.application.out; + +public interface CacheRepository { + void addCache(String key, String token, long expiration); + + String getCache(String key); + + 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..fb837cb --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/out/FileService.java @@ -0,0 +1,9 @@ +package com.github.asavershin.api.application.out; + +import java.util.List; + +public interface FileService { + T saveFile(K file); + byte[] getFile(String link); + 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..b829a63 --- /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..d57436b --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/application/out/TokenRepository.java @@ -0,0 +1,13 @@ +package com.github.asavershin.api.application.out; + +public interface TokenRepository { + String getAccessToken(String email); + + String getRefreshToken(String email); + + void saveRefreshToken(String username, String jwtToken, Long expiration); + + void saveAccessToken(String username, String jwtToken, Long expiration); + + void deleteAllTokensByUserEmail(String username); +} 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..2584611 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/common/NotFoundException.java @@ -0,0 +1,7 @@ +package com.github.asavershin.api.common; + +public class NotFoundException extends RuntimeException{ + public NotFoundException(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..afd6770 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/common/Validator.java @@ -0,0 +1,48 @@ +package com.github.asavershin.api.common; + +import java.util.Objects; + +public class Validator { + public static void assertArgumentLength(String aString, int aMinimum, int aMaximum, String aMessage) { + int length = aString.length(); + if (length < aMinimum || length > aMaximum) { + throw new IllegalArgumentException(aMessage); + } + } + public static void assertArgumentLength(String aString, int aMinimum, String aMessage) { + int length = aString.length(); + if (length < aMinimum) { + throw new IllegalArgumentException(aMessage); + } + } + + public static void assertStringFormat(String email, String regex, String aMessage) { + if (!email.matches(regex)) { + throw new IllegalArgumentException(aMessage); + } + } + + public static void assertArrayLength(Object[] array, Integer minLength, String aMessage) { + if (array.length < minLength) { + throw new IllegalArgumentException(aMessage); + } + } + + public static void assertLongSize(Long value, Long minLength, Long maxLength, String aMessage) { + if (value < minLength || value > maxLength) { + throw new IllegalArgumentException(aMessage); + } + } + + public static void assertLongSize(Long value, Long minLength, String aMessage) { + if (value < minLength) { + throw new IllegalArgumentException(aMessage); + } + } + + public static void assertNotFound(Object object, 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..0725e3a --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/common/annotations/Command.java @@ -0,0 +1,9 @@ +package com.github.asavershin.api.common.annotations; + +import java.lang.annotation.*; + +@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..6b0666c --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/common/annotations/DomainService.java @@ -0,0 +1,9 @@ +package com.github.asavershin.api.common.annotations; + +import java.lang.annotation.*; + +@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..1d1483d --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/common/annotations/Query.java @@ -0,0 +1,9 @@ +package com.github.asavershin.api.common.annotations; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Query { +} 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..216bfe6 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java @@ -0,0 +1,20 @@ +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..d72aeb3 --- /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.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class AuthConfig { + @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..7bb2900 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java @@ -0,0 +1,25 @@ +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 +@RequiredArgsConstructor +@Slf4j +public class MinIOConfig { + private final MinIOProperties minioProperties; + @Bean + public MinioClient minioClient() { + log.info("MinIOConfigLog"); + 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..5003bf1 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/config/RedisConfig.java @@ -0,0 +1,20 @@ +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 +public class RedisConfig { + @Bean + public RedisTemplate redisTemplate(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/properties/JwtProperties.java b/api/src/main/java/com/github/asavershin/api/config/properties/JwtProperties.java new file mode 100644 index 0000000..22f8438 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/config/properties/JwtProperties.java @@ -0,0 +1,16 @@ +package com.github.asavershin.api.config.properties; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Data +public class JwtProperties { + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.access-expiration}") + private Long accessExpiration; + @Value("${jwt.refresh-expiration}") + private Long refreshExpiration; +} 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..7369083 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/config/properties/MinIOProperties.java @@ -0,0 +1,17 @@ +package com.github.asavershin.api.config.properties; + +import lombok.*; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Data +@ConfigurationProperties(prefix = "minio") +@NoArgsConstructor +public class MinIOProperties { + private String bucket; + private String url; + private String user; + private String password; +} 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..9aca1af --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java @@ -0,0 +1,10 @@ +package com.github.asavershin.api.domain; + +import com.github.asavershin.api.common.Validator; + +public abstract class IsEntityFound { + protected void isEntityFound(Object entity, String entityName, String idName, 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..0c8262f --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java @@ -0,0 +1,15 @@ +package com.github.asavershin.api.domain; + +import com.github.asavershin.api.common.Validator; + +import java.util.Objects; + +public record PartOfResources(Long pageNumber, Long pageSize) { + 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"); + if(pageSize <= 0) + throw new IllegalArgumentException("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..b9395ee --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java @@ -0,0 +1,7 @@ +package com.github.asavershin.api.domain; + +public class ResourceOwnershipException extends RuntimeException { + public ResourceOwnershipException(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..4b425b0 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/DeleteImageOfUser.java @@ -0,0 +1,8 @@ +package com.github.asavershin.api.domain.image; + +import com.github.asavershin.api.domain.user.UserId; + +@FunctionalInterface +public interface DeleteImageOfUser { + 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..ce7911c --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/GetImageOfUser.java @@ -0,0 +1,8 @@ +package com.github.asavershin.api.domain.image; + +import com.github.asavershin.api.domain.user.UserId; + +@FunctionalInterface +public interface GetImageOfUser { + 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..534413f --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/GetPartImagesOfUser.java @@ -0,0 +1,11 @@ +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 { + 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..b6a9c90 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/Image.java @@ -0,0 +1,48 @@ +package com.github.asavershin.api.domain.image; + +import com.github.asavershin.api.domain.IsEntityFound; +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 { + private ImageId imageId; + private MetaInfo metaInfo; + private UserId userId; + public Image(ImageId imageId, MetaInfo metaInfo, UserId userId){ + setImageId(imageId); + setMetaInfo(metaInfo); + setUserId(userId); + } + + public Image belongsToUser(UserId userId){ + if (!this.userId.equals(userId)){ + throw new ResourceOwnershipException( + "Image with id " + imageId.value().toString()+ " does not belong to user with id " + userId.value().toString() + ); + } + return this; + } + + private void setImageId(ImageId imageId) { + Objects.requireNonNull(imageId, "ImageId must not be null"); + this.imageId = imageId; + } + + private void setMetaInfo(MetaInfo metaInfo) { + Objects.requireNonNull(metaInfo, "MetaInfo must not be null"); + this.metaInfo = metaInfo; + } + + private void setUserId(UserId userId) { + Objects.requireNonNull(userId, "UserId must not be null"); + this.userId = userId; + } +} 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..91b163f --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageExtension.java @@ -0,0 +1,35 @@ +package com.github.asavershin.api.domain.image; + +import lombok.EqualsAndHashCode; + +import java.util.Map; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toMap; +public enum ImageExtension { + JPG(".jpg"), + PNG(".png"), + JPEG(".jpeg"); + + + private final String extension; + private ImageExtension(String extension){ + this.extension = extension; + } + + @Override + public String toString() { + return extension; + } + + private static final Map stringToEnum + = Stream.of(values()).collect(toMap(Object::toString, e -> e)); + + public static ImageExtension fromString(String extension){ + var imageExtension = stringToEnum.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..690ac67 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageId.java @@ -0,0 +1,14 @@ +package com.github.asavershin.api.domain.image; + +import java.util.Objects; +import java.util.UUID; + +public record ImageId(UUID value) { + public ImageId { + Objects.requireNonNull(value, "Image ID must not be null"); + } + + 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..c3e321e --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageNameWithExtension.java @@ -0,0 +1,51 @@ +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 class ImageNameWithExtension { + + private static final int MAX_IMAGE_NAME = 50; + private final String imageName; + private final ImageExtension imageExtension; + + private ImageNameWithExtension(String imageName, ImageExtension imageExt){ + this.imageName = imageName; + this.imageExtension = imageExt; + } + + public static ImageNameWithExtension fromOriginalFileName(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); + } + public static ImageNameWithExtension founded(String imageName, String extension) { + notNullValidate(imageName); + lengthNameValidate(imageName); + return new ImageNameWithExtension(imageName, ImageExtension.fromString(extension)); + } + + private static void notNullValidate(String name){ + Objects.requireNonNull(name, "ImageName must not be null"); + } + + private static void lengthNameValidate(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..6669ca9 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/ImageRepository.java @@ -0,0 +1,15 @@ +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 { + void save(Image image); + List findImagesByUserId(UserId userId, PartOfResources partOfResources); + + Image findImageByImageId(ImageId imageId); + + void deleteImageByImageId(Image ImageId); +} diff --git a/api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java b/api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java new file mode 100644 index 0000000..3fc7fd3 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java @@ -0,0 +1,29 @@ +package com.github.asavershin.api.domain.image; + +import com.github.asavershin.api.common.Validator; + +import java.util.Objects; + +public record MetaInfo(ImageNameWithExtension imageNameWithExtension, Long imageSize) { + private static final int MAX_IMAGE_NAME = 50; + /** + * @param imageName + * Size is in bytes. 10MB + */ + private static final long MAX_IMAGE_SIZE = 10485760; + + public MetaInfo { + validateImageName(imageNameWithExtension); + validateImageSize(imageSize); + } + + private void validateImageName(ImageNameWithExtension imageNameWithExtension) { + Objects.requireNonNull(imageNameWithExtension, "Name and extension must not be null"); + } + + private void validateImageSize(Long imageSize){ + Objects.requireNonNull(imageSize, "Image size must not be null"); + Validator.assertLongSize(imageSize, 0L, MAX_IMAGE_SIZE, + "Image size must be " + "0-" + MAX_IMAGE_SIZE + " in length"); + } +} 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..e292586 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/StoreImageOfUser.java @@ -0,0 +1,6 @@ +package com.github.asavershin.api.domain.image; + +@FunctionalInterface +public interface StoreImageOfUser { + 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..fffc7e8 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/DeleteImageOfUserImpl.java @@ -0,0 +1,22 @@ +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 { + private final ImageRepository imageRepository; + @Override + public void removeImageOfUser(ImageId imageId, 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..23b8074 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetImageOfUserImpl.java @@ -0,0 +1,24 @@ +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 { + private final ImageRepository imageRepository; + @Override + public Image getImageOfUser(UserId userId, 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..34de1c8 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/GetPartImagesOfUserImpl.java @@ -0,0 +1,21 @@ +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 { + private final ImageRepository imageRepository; + @Override + public List get(UserId userId, 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..6712b25 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/image/impl/StoreImageOfUserImpl.java @@ -0,0 +1,18 @@ +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 { + private final ImageRepository imageRepository; + + @Override + public void storeImageOfUser(Image image) { + imageRepository.save(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/user/AuthException.java b/api/src/main/java/com/github/asavershin/api/domain/user/AuthException.java new file mode 100644 index 0000000..4203fe8 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/AuthException.java @@ -0,0 +1,7 @@ +package com.github.asavershin.api.domain.user; + +public class AuthException extends RuntimeException { + public AuthException(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..e6c4184 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUser.java @@ -0,0 +1,35 @@ +package com.github.asavershin.api.domain.user; + +import com.github.asavershin.api.domain.IsEntityFound; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.util.Objects; + +@Getter +@ToString +@EqualsAndHashCode +public class AuthenticatedUser { + private UserId userId; + private Credentials userCredentials; + + private AuthenticatedUser(UserId userId, Credentials userCredentials) { + setUserId(userId); + setUserCredentials(userCredentials); + } + + public static AuthenticatedUser founded(UserId userId, Credentials credentials) { + return new AuthenticatedUser(userId, credentials); + } + + private void setUserId(UserId userId) { + Objects.requireNonNull(userId, "UserId must not be null"); + this.userId = userId; + } + + private void setUserCredentials(Credentials userCredentials) { + Objects.requireNonNull(userCredentials, "UserCredentials must not be null"); + this.userCredentials = userCredentials; + } +} 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..037c05e --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/AuthenticatedUserRepository.java @@ -0,0 +1,7 @@ +package com.github.asavershin.api.domain.user; + +import java.util.Optional; + +public interface AuthenticatedUserRepository { + 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..e593614 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/Credentials.java @@ -0,0 +1,33 @@ +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) { + private static final int MAX_EMAIL_LENGTH = 50; + private static final int MIN_EMAIL_LENGTH = 5; + private static final int MIN_PASSWORD_LENGTH = 8; + + public Credentials { + validateEmail(email); + validatePassword(password); + } + + private void validateEmail(String email) { + Objects.requireNonNull(email, "Email must not be null"); + Validator.assertArgumentLength(email, + MIN_EMAIL_LENGTH, MAX_EMAIL_LENGTH, + "Email must be " + MIN_EMAIL_LENGTH + "-" + MAX_EMAIL_LENGTH + " in length"); + + Validator.assertStringFormat(email, "[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\\.[A-Za-z0-9.-]+", + "Email is not in the correct format"); + } + + private void validatePassword(String password) { + Objects.requireNonNull(password, "Password must not be null"); + Validator.assertArgumentLength(password, 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..1190c92 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/FullName.java @@ -0,0 +1,23 @@ +package com.github.asavershin.api.domain.user; + + + +import com.github.asavershin.api.common.Validator; + +import java.util.Objects; + +public record FullName(String firstname, String lastname) { + private static final int MAX_FIRST_NAME_LENGTH = 20; + private static final int MAX_LAST_NAME_LENGTH = 20; + + public FullName { + validateName(firstname, MAX_FIRST_NAME_LENGTH); + validateName(lastname, MAX_LAST_NAME_LENGTH); + } + + private void validateName(String name, 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..946eefa --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/GetPartOfImagesForAuthenticatedUser.java @@ -0,0 +1,11 @@ +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; + +@FunctionalInterface +public interface GetPartOfImagesForAuthenticatedUser { + public 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..a3caf54 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/RegisterNewUser.java @@ -0,0 +1,6 @@ +package com.github.asavershin.api.domain.user; + +@FunctionalInterface +public interface RegisterNewUser { + 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..a6a17f8 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/TryToLogin.java @@ -0,0 +1,6 @@ +package com.github.asavershin.api.domain.user; + +@FunctionalInterface +public interface TryToLogin { + 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..061f25a --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/User.java @@ -0,0 +1,39 @@ +package com.github.asavershin.api.domain.user; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.util.Objects; + +@ToString +@EqualsAndHashCode +public class User{ + @Getter + private UserId userId; + @Getter + private FullName userFullName; + @Getter + private Credentials userCredentials; + + public User(UserId userId, FullName userFullName, Credentials userCredentials) { + setUserId(userId); + setUserCredentials(userCredentials); + setUserFullName(userFullName); + } + + private void setUserId(UserId userId) { + Objects.requireNonNull(userId, "UserId must not be null"); + this.userId = userId; + } + + private void setUserFullName(FullName userFullName) { + Objects.requireNonNull(userFullName, "UserFullName must not be null"); + this.userFullName = userFullName; + } + + private void setUserCredentials(Credentials userCredentials) { + Objects.requireNonNull(userCredentials, "UserCredentials must not be null"); + this.userCredentials = userCredentials; + } +} 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..1f672dd --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/UserId.java @@ -0,0 +1,16 @@ +package com.github.asavershin.api.domain.user; + +import lombok.EqualsAndHashCode; + +import java.util.Objects; +import java.util.UUID; + +public record UserId(UUID value) { + public UserId { + Objects.requireNonNull(value, "User ID must not be null"); + } + + 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..df27a0c --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/UserRepository.java @@ -0,0 +1,9 @@ +package com.github.asavershin.api.domain.user; + +import java.util.Optional; + +public interface UserRepository { + void save(User newUser); + + 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..bf6a572 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/GetPartOfImagesForAuthenticatedUserImpl.java @@ -0,0 +1,21 @@ +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; + +@Query +@RequiredArgsConstructor +public class GetPartOfImagesForAuthenticatedUserImpl implements GetPartOfImagesForAuthenticatedUser { + private final ImageRepository imageRepository; + @Override + public List get(UserId userId, 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..4a2984b --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/RegisterNewUserImpl.java @@ -0,0 +1,38 @@ +package com.github.asavershin.api.domain.user.impl; + +import com.github.asavershin.api.common.annotations.Command; +import org.springframework.security.crypto.password.PasswordEncoder; + +import com.github.asavershin.api.domain.user.*; +import com.github.asavershin.api.domain.user.RegisterNewUser; +import lombok.RequiredArgsConstructor; + + +@Command +@RequiredArgsConstructor +public class RegisterNewUserImpl implements RegisterNewUser { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Override + public void register(FullName fullName, Credentials credentials) { + checkEmailForUnique(credentials.email()); + userRepository.save( + new User( + UserId.nextIdentity(), + fullName, + new Credentials(credentials.email(), protectPassword(credentials.password())) + ) + ); + } + + private void checkEmailForUnique(String email) { + if (userRepository.existByUserEmail(email)) { + throw new IllegalArgumentException("Email is not unique"); + } + } + + private String protectPassword(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..be33cb2 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/domain/user/impl/TryToLoginImpl.java @@ -0,0 +1,31 @@ +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 { + + private final PasswordEncoder passwordEncoder; + private final AuthenticatedUserRepository authenticatedUserRepository; + + @Override + public AuthenticatedUser login(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/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..8b4fa5b --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AdviceController.java @@ -0,0 +1,77 @@ +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 lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@RestControllerAdvice +@Slf4j +public class AdviceController { + + + @ExceptionHandler({ResourceOwnershipException.class, AuthException.class}) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public UISuccessContainer handleValidationException(RuntimeException ex) { + return new UISuccessContainer(false, ex.getMessage()); + } + + @ExceptionHandler({NotFoundException.class}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public UISuccessContainer handleNotFoundException(RuntimeException ex) { + return new UISuccessContainer(false, ex.getMessage()); + } + + @ExceptionHandler({Exception.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public UISuccessContainer handleInnerException(Exception ex) { + return new UISuccessContainer(false, "Very bad exception"); + } + + @ExceptionHandler({IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public UISuccessContainer handleIllegalImageExtension(IllegalArgumentException ex) { + log.info("IllegaArgumentException: " + ex); + return new UISuccessContainer(false, ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ExceptionBody handleValidationException(MethodArgumentNotValidException ex) { + log.info("handleValidationException"); + 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; + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ExceptionBody handleConstraintViolationException(ConstraintViolationException ex) { + log.info("handleConstraintViolationException"); + 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..35c3cf6 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/AuthController.java @@ -0,0 +1,52 @@ +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 lombok.extern.slf4j.Slf4j; +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 { + + private final RegisterNewUser register; + private final GetNewCredentials getNewCredentials; + private final GetNewCredentialsUsingRefreshToken getNewCredentialsUsingRefreshToken; + + @PostMapping("/register") + @Operation(description = "Регистрация нового пользователя") + public void register( + @RequestBody @Valid UserRegistrationRequest userRegistrationRequest + ) { + register.register(userRegistrationRequest.ToFullName(), + userRegistrationRequest.toCredentials()); + } + + @PostMapping("/login") + @Operation(description = "Аутентификация пользователя") + public ApplicationCredentials login(@RequestBody @Valid UserLoginRequest userLoginRequest) { + + return getNewCredentials.get(userLoginRequest.toCredentials()); + } + + @PostMapping("/refresh-token") + @Operation(description = "Использовать рефреш токен") + public ApplicationCredentials refreshToken(@AuthenticationPrincipal CustomUserDetails user) { + return getNewCredentialsUsingRefreshToken.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..cfc5229 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/ImageController.java @@ -0,0 +1,73 @@ +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 lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/image") +@Tag(name = "image", description = "Работа с изображениями") +@RequiredArgsConstructor +public class ImageController { + + private final GetPartOfImagesForAuthenticatedUser getImages; + private final ImageService imageService; + + @GetMapping("/images") + @Operation(description = "Получить изображения пользователя") + public GetImagesResponse getImages(@AuthenticationPrincipal CustomUserDetails user, + Long pageNumber, + Long pageSize) { + return GetImagesResponse.GetImagesResponseFromImages( + getImages.get(user.authenticatedUser().userId(), new PartOfResources(pageNumber, pageSize)) + ); + } + + @PostMapping + @Operation(description = "Загрузить новую картинку") + public UploadImageResponse uploadImage( + @AuthenticationPrincipal CustomUserDetails user, + @RequestPart("file") MultipartFile file + ) { + return new UploadImageResponse(imageService.storeImage(user.authenticatedUser().userId(), file)); + } + + @DeleteMapping("/{image-id}") + @Operation(description = "Удалить картинку") + public UISuccessContainer deleteImage( + @AuthenticationPrincipal CustomUserDetails user, + @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" + ); + } + + @GetMapping("/{image-id}") + @Operation(description = "Получить картинку по id") + public byte[] downloadImage( + @AuthenticationPrincipal CustomUserDetails user, + @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..3c094b6 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/ExceptionBody.java @@ -0,0 +1,23 @@ +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 { + + private String message; + private Map errors; + + public ExceptionBody( + final String message + ) { + this.message = message; + } + +} 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..f7ab7b7 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/UISuccessContainer.java @@ -0,0 +1,11 @@ +package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UISuccessContainer { + private boolean success; + 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..2b7d65a --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/GetImagesResponse.java @@ -0,0 +1,22 @@ +package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image; + +import com.github.asavershin.api.domain.image.Image; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +public class GetImagesResponse { + private List images; + private GetImagesResponse(List images){ + this.images = images; + } + public static GetImagesResponse GetImagesResponseFromImages(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..7b2a6a8 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/ImageResponse.java @@ -0,0 +1,25 @@ +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 class ImageResponse { + private String ImageId; + private String ImageName; + private Long imageSize; + + private ImageResponse(String ImageId, String ImageName, Long imageSize){ + this.ImageId = ImageId; + this.ImageName = ImageName; + this.imageSize = imageSize; + } + + public static ImageResponse imageResponseFromEntity(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..165a742 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/UploadImageResponse.java @@ -0,0 +1,13 @@ +package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image; + +import com.github.asavershin.api.domain.image.ImageId; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +public class UploadImageResponse { + private String imageId; + public UploadImageResponse(ImageId imageId){ + this.imageId = imageId.value().toString(); + } +} 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..d3ca761 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserLoginRequest.java @@ -0,0 +1,24 @@ +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 { + @NotEmpty(message = "Не заполнен email") + @Email(message = "Некорректная почта") + private String userEmail; + + @NotEmpty(message = "Не заполнен пароль") + @Size(min = 8, message = "Длина пароля должна быть не менее 8") + private String userPassword; + + 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/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..0165581 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/UserRegistrationRequest.java @@ -0,0 +1,40 @@ +package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.user; + + +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 { + + @NotEmpty(message = "Не заполнено имя") + @Size(min = 1, max = 20, message = "Недопустимая длина имени") + private String userFirstname; + + @NotEmpty(message = "Не заполнена фамилия") + @Size(min = 1, max = 20, message = "Недопустимая длина фамилии") + private String userLastname; + + @NotEmpty(message = "Не заполнен email") + @Email(message = "Некорректная почта") + @Getter + private String userEmail; + + @NotEmpty(message = "Не заполнен пароль") + @Size(min = 8, message = "Длина пароля должна быть не менее 8") + @Getter + private String userPassword; + + public FullName ToFullName() { + return new FullName(userFirstname, userLastname); + } + + public Credentials toCredentials() { + return new Credentials(userEmail, userPassword); + } +} 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..a14e57f --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/CustomUserDetails.java @@ -0,0 +1,46 @@ +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 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..f41e747 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/JwtAuthenticationFilter.java @@ -0,0 +1,82 @@ +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.domain.user.Credentials; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + private final TokenRepository tokenRepository; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + log.info("Start JWT authentication filter"); + var path = request.getServletPath(); + + if (path.contains("/register") || path.contains("/login")) { + log.info("REGISTER OR 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(7); + var email = jwtService.extractSub(jwt); + + String token; + var pathContainsRefreshToken = path.contains("/refresh-token"); + if(pathContainsRefreshToken){ + log.info("Refresh token getting"); + token = tokenRepository.getRefreshToken(email); + }else{ + log.info("Access token getting"); + token = tokenRepository.getAccessToken(email); + } + log.info("Token: " + token); + log.info("JWT: " + jwt); + if (!token.equals(jwt)) { + tokenRepository.deleteAllTokensByUserEmail(email); + return; + } + + UsernamePasswordAuthenticationToken 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..9808396 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/LogautHandlerImpl.java @@ -0,0 +1,33 @@ +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 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 { + + private final TokenRepository tokenService; + private final JwtService jwtService; + + @Override + public void logout( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + if (authHeader == null ||!authHeader.startsWith("Bearer ")) { + return; + } + jwt = authHeader.substring(7); + tokenService.deleteAllTokensByUserEmail("asd"); + } +} 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..a718077 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/SecurityConfiguration.java @@ -0,0 +1,62 @@ +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.authentication.AuthenticationProvider; +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 { + + 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"}; + private final JwtAuthenticationFilter jwtAuthFilter; + private final LogoutHandler logoutHandler; + + @Bean + public SecurityFilterChain securityFilterChain(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..cebe0f0 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/in/security/UserDetailsServiceImpl.java @@ -0,0 +1,21 @@ +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 { + private final AuthenticatedUserRepository repository; + @Override + public UserDetails loadUserByUsername(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/out/persistence/AuthenticatedUserRepositoryImpl.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/AuthenticatedUserRepositoryImpl.java new file mode 100644 index 0000000..3402186 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/AuthenticatedUserRepositoryImpl.java @@ -0,0 +1,39 @@ +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 { + private final DSLContext dslContext; + @Override + public AuthenticatedUser findByEmail(String email) { + return dslContext.select( + USERS.USER_ID, + USERS.USER_EMAIL, + USERS.USER_PASSWORD + ) + .from(USERS) + .where(USERS.USER_EMAIL.eq(email)) + .fetchOne(this); + } + + + @Override + public AuthenticatedUser map(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/CacheRepositoryIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java new file mode 100644 index 0000000..48ea8ba --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java @@ -0,0 +1,28 @@ +package com.github.asavershin.api.infrastructure.out.persistence; + +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 { + private final RedisTemplate redisTemplate; + @Override + public void addCache(String key, String token, long expiration) { + redisTemplate.opsForValue().set(key, token, expiration, TimeUnit.MILLISECONDS); + } + + @Override + public String getCache(String key) { + return (String) redisTemplate.opsForValue().get(key); + } + + @Override + public void deleteCache(String key) { + redisTemplate.delete(key); + } +} 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..d462f00 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/ImageRepositoryImpl.java @@ -0,0 +1,77 @@ +package com.github.asavershin.api.infrastructure.out.persistence; + +import com.github.asavershin.api.domain.PartOfResources; +import com.github.asavershin.api.domain.image.*; +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 { + private final DSLContext dslContext; + + @Override + public void save(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(); + } + + @Override + public List findImagesByUserId(UserId userId, PartOfResources partOfResources) { + return dslContext.select(IMAGE.fields()).select(USER_IMAGES.USER_ID) + .from(IMAGE) + .join(USER_IMAGES).using(IMAGE.IMAGE_ID) + .offset(partOfResources.pageNumber() * partOfResources.pageSize()) + .limit(partOfResources.pageSize()) + .fetch(this); + } + + @Override + public Image findImageByImageId(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); + } + + @Override + public void deleteImageByImageId(Image imageId) { + dslContext.deleteFrom(IMAGE) + .where(IMAGE.IMAGE_ID.eq(imageId.imageId().value())) + .execute(); + } + + @Override + public Image map(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 MetaInfo(ImageNameWithExtension.founded(imageName, imageExtension), imageSize), + new UserId(userId) + ); + } +} + diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java new file mode 100644 index 0000000..63095fd --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java @@ -0,0 +1,40 @@ +package com.github.asavershin.api.infrastructure.out.persistence; + +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 { + private final String refreshKey = "REFRESH_TOKEN_"; + private final String accessKey = "ACCESS_TOKEN_"; + private final CacheRepository cacheRepository; + + @Override + public String getAccessToken(String email){ + return cacheRepository.getCache(accessKey + email); + } + + @Override + public String getRefreshToken(String email){ + return cacheRepository.getCache(refreshKey + email); + } + + @Override + public void saveRefreshToken(String username, String jwtToken, Long expiration) { + cacheRepository.addCache(refreshKey + username, jwtToken, expiration); + } + + @Override + public void saveAccessToken(String username, String jwtToken, Long expiration) { + cacheRepository.addCache(accessKey + username, jwtToken, expiration); + } + + @Override + public void deleteAllTokensByUserEmail(String username) { + cacheRepository.deleteCache(refreshKey + username); + cacheRepository.deleteCache(accessKey+ username); + } +} 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..a20bac2 --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/UserRepositoryImpl.java @@ -0,0 +1,36 @@ +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 { + + private final DSLContext dslContext; + + @Override + public void save(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(); + } + + @Override + public boolean existByUserEmail(String email) { + return dslContext.fetchExists(USERS, USERS.USER_EMAIL.eq(email)); + } +} 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..9904ffe --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/FileException.java @@ -0,0 +1,7 @@ +package com.github.asavershin.api.infrastructure.out.storage; + +public class FileException extends RuntimeException { + public FileException(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..8eef78b --- /dev/null +++ b/api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/MinioServiceIml.java @@ -0,0 +1,118 @@ +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.*; +import lombok.RequiredArgsConstructor; +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; +import java.util.UUID; + +@Service +public class MinioServiceIml implements MinioService { + private final MinioClient minioClient; + private final MinIOProperties minioProperties; + + public MinioServiceIml(MinioClient minioClient, MinIOProperties minioProperties) { + this.minioClient = minioClient; + this.minioProperties = minioProperties; + createBucket(); + } + + @Override + public String saveFile(final MultipartFile image) { + + 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"); + } + var link = generateFileName(); + InputStream inputStream; + try { + inputStream = image.getInputStream(); + } catch (Exception e) { + throw new FileException("File upload failed: " + + e.getMessage()); + } + saveImage(inputStream, link); + return link; + } + + @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()); + } + } + + @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 saveImage( + final InputStream inputStream, + final String fileName + ) { + minioClient.putObject(PutObjectArgs.builder() + .stream(inputStream, inputStream.available(), -1) + .bucket(minioProperties.getBucket()) + .object(fileName) + .build()); + } + + private String generateFileName() { + return UUID.randomUUID().toString(); + } + + @SneakyThrows(Exception.class) + private boolean bucketExists(String bucketName) { + return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + } +} 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..004de07 --- /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.MetaInfo; +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 MetaInfo metaInfo1(){ + return new MetaInfo(ImageNameWithExtension.fromOriginalFileName("image.jpg"), 1L); + } + + public static MetaInfo metaInfo3(){ + return new MetaInfo(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..24d597c --- /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.MetaInfo; +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(); + MetaInfo 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(); + MetaInfo 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(); + MetaInfo 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(); + + MetaInfo metaInfo1 = ImageHelper.metaInfo1(); + Image image1 = new Image(imageId, metaInfo1, userId); + + MetaInfo 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(); + MetaInfo 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..a1ca5f8 --- /dev/null +++ b/api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java @@ -0,0 +1,213 @@ +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 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(); + } + } + + @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()); + + // 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()); + // 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..a9fba13 --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + + 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 + + + + 22 + 22 + UTF-8 + 1.18.30 + + + + + + org.springframework.boot + spring-boot-dependencies + 3.2.0 + import + pom + + + + + + + org.projectlombok + lombok + ${org.projectlombok.version} + true + + + + From 852ae46b78e4d0023aefba95bbfad814a71fea80 Mon Sep 17 00:00:00 2001 From: asavershin Date: Thu, 4 Apr 2024 10:13:45 +0300 Subject: [PATCH 2/9] Format to sun code style --- api/pom.xml | 1 + .../com/github/asavershin/api/ApiMain.java | 13 ++- .../in/services/image/ImageService.java | 22 ++++ .../services/image/impl/ImageServiceImpl.java | 82 ++++++++++--- .../in/services/image/impl/package-info.java | 5 + .../in/services/image/package-info.java | 5 + .../services/user/ApplicationCredentials.java | 21 +++- .../in/services/user/GetNewCredentials.java | 6 + .../GetNewCredentialsUsingRefreshToken.java | 6 + .../in/services/user/JwtService.java | 22 +++- .../user/impl/GetNewCredentialsImpl.java | 28 ++++- ...etNewCredentialsUsingRefreshTokenImpl.java | 32 ++++- .../in/services/user/impl/JwtServiceIml.java | 30 +++-- .../in/services/user/impl/package-info.java | 5 + .../in/services/user/package-info.java | 5 + .../api/application/out/CacheRepository.java | 23 +++- .../api/application/out/FileService.java | 17 +++ .../api/application/out/MinioService.java | 2 +- .../api/application/out/TokenRepository.java | 33 ++++++ .../api/application/out/package-info.java | 5 + .../api/common/NotFoundException.java | 7 +- .../asavershin/api/common/Validator.java | 109 ++++++++++++++++-- .../api/common/annotations/Command.java | 6 +- .../api/common/annotations/DomainService.java | 6 +- .../api/common/annotations/Query.java | 6 +- .../api/common/annotations/package-info.java | 5 + .../asavershin/api/common/package-info.java | 7 ++ .../api/config/AnnotationsConfig.java | 9 +- .../asavershin/api/config/AuthConfig.java | 10 +- .../asavershin/api/config/MinIOConfig.java | 17 ++- .../asavershin/api/config/RedisConfig.java | 20 +++- .../asavershin/api/config/package-info.java | 5 + .../api/config/properties/JwtProperties.java | 22 +++- .../config/properties/MinIOProperties.java | 19 ++- .../api/config/properties/UserProperties.java | 18 +++ .../api/config/properties/package-info.java | 6 + .../asavershin/api/domain/IsEntityFound.java | 5 +- .../api/domain/PartOfResources.java | 37 +++++- .../domain/ResourceOwnershipException.java | 15 ++- .../api/domain/image/DeleteImageOfUser.java | 6 + .../api/domain/image/GetImageOfUser.java | 7 ++ .../api/domain/image/GetPartImagesOfUser.java | 9 ++ .../asavershin/api/domain/image/Image.java | 67 ++++++++--- .../api/domain/image/ImageExtension.java | 55 +++++++-- .../asavershin/api/domain/image/ImageId.java | 17 ++- .../domain/image/ImageNameWithExtension.java | 67 ++++++++--- .../api/domain/image/ImageRepository.java | 29 ++++- .../asavershin/api/domain/image/MetaData.java | 43 +++++++ .../asavershin/api/domain/image/MetaInfo.java | 29 ----- .../api/domain/image/StoreImageOfUser.java | 5 + .../image/impl/DeleteImageOfUserImpl.java | 19 ++- .../domain/image/impl/GetImageOfUserImpl.java | 19 ++- .../image/impl/GetPartImagesOfUserImpl.java | 9 +- .../image/impl/StoreImageOfUserImpl.java | 9 +- .../api/domain/image/impl/package-info.java | 7 ++ .../api/domain/image/package-info.java | 5 + .../asavershin/api/domain/package-info.java | 8 ++ .../api/domain/user/AuthException.java | 16 ++- .../api/domain/user/AuthenticatedUser.java | 48 ++++++-- .../user/AuthenticatedUserRepository.java | 11 +- .../api/domain/user/Credentials.java | 36 ++++-- .../asavershin/api/domain/user/FullName.java | 28 ++++- .../GetPartOfImagesForAuthenticatedUser.java | 17 ++- .../api/domain/user/RegisterNewUser.java | 12 ++ .../api/domain/user/TryToLogin.java | 14 +++ .../asavershin/api/domain/user/User.java | 61 +++++++--- .../asavershin/api/domain/user/UserId.java | 25 +++- .../api/domain/user/UserRepository.java | 22 +++- ...tPartOfImagesForAuthenticatedUserImpl.java | 20 +++- .../domain/user/impl/RegisterNewUserImpl.java | 27 ++++- .../api/domain/user/impl/TryToLoginImpl.java | 23 +++- .../api/domain/user/impl/package-info.java | 7 ++ .../api/domain/user/package-info.java | 6 + .../controllers/AdviceController.java | 78 ++++++++++--- .../controllers/AuthController.java | 47 ++++++-- .../controllers/ImageController.java | 91 ++++++++++++--- .../controllers/dto/ExceptionBody.java | 18 ++- .../controllers/dto/UISuccessContainer.java | 6 + .../dto/image/GetImagesResponse.java | 27 +++-- .../controllers/dto/image/ImageResponse.java | 35 ++++-- .../dto/image/UploadImageResponse.java | 13 ++- .../controllers/dto/image/package-info.java | 5 + .../controllers/dto/package-info.java | 5 + .../dto/user/UserLoginRequest.java | 19 ++- .../dto/user/UserRegistrationRequest.java | 34 +++++- .../controllers/dto/user/package-info.java | 5 + .../controllers/controllers/package-info.java | 4 + .../in/security/CustomUserDetails.java | 3 +- .../in/security/JwtAuthenticationFilter.java | 44 ++++--- .../in/security/LogautHandlerImpl.java | 29 +++-- .../in/security/SecurityConfiguration.java | 50 ++++++-- .../in/security/UserDetailsServiceImpl.java | 24 +++- .../in/security/package-info.java | 5 + .../AuthenticatedUserRepositoryImpl.java | 22 +++- .../out/persistence/CacheRepositoryIml.java | 28 ----- .../out/persistence/ImageRepositoryImpl.java | 61 +++++++--- .../out/persistence/TokenRepositoryIml.java | 40 ------- .../out/persistence/UserRepositoryImpl.java | 19 ++- .../out/persistence/package-info.java | 5 + .../out/storage/CacheRepositoryIml.java | 45 ++++++++ .../out/storage/FileException.java | 7 +- .../out/storage/MinioServiceIml.java | 56 ++++++--- .../out/storage/TokenRepositoryIml.java | 67 +++++++++++ .../out/storage/package-info.java | 5 + .../github/asavershin/api/package-info.java | 6 + .../asavershin/api/common/ImageHelper.java | 10 +- .../asavershin/api/domaintest/ImageTest.java | 14 +-- .../api/integrations/ImageLogicTest.java | 6 + 108 files changed, 1949 insertions(+), 459 deletions(-) create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/image/impl/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/image/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/impl/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/in/services/user/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/application/out/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/annotations/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/common/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/properties/UserProperties.java create mode 100644 api/src/main/java/com/github/asavershin/api/config/properties/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/MetaData.java delete mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/impl/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/image/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/impl/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/domain/user/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/image/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/dto/user/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/impl/controllers/controllers/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/in/security/package-info.java delete mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java delete mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/CacheRepositoryIml.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/TokenRepositoryIml.java create mode 100644 api/src/main/java/com/github/asavershin/api/infrastructure/out/storage/package-info.java create mode 100644 api/src/main/java/com/github/asavershin/api/package-info.java diff --git a/api/pom.xml b/api/pom.xml index e145ce1..38e9914 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -256,4 +256,5 @@ + \ 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 index 5c2aba3..0ed4b22 100644 --- a/api/src/main/java/com/github/asavershin/api/ApiMain.java +++ b/api/src/main/java/com/github/asavershin/api/ApiMain.java @@ -1,11 +1,22 @@ package com.github.asavershin.api; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; + @SpringBootApplication public class ApiMain { - public static void main(String[] args) { + /** + * 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 index de452c7..d2c4f5d 100644 --- 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 @@ -5,7 +5,29 @@ 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 index 6f35ae9..0dcfe82 100644 --- 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 @@ -2,39 +2,71 @@ import com.github.asavershin.api.application.in.services.image.ImageService; import com.github.asavershin.api.application.out.MinioService; -import com.github.asavershin.api.common.Validator; -import com.github.asavershin.api.domain.image.*; +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 lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.Arrays; +import java.io.Serializable; import java.util.List; -import java.util.Objects; import java.util.UUID; @Service @RequiredArgsConstructor -@Slf4j -public class ImageServiceImpl implements ImageService { +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(UserId userId, MultipartFile multipartFile) { - log.info("Store image: {}", multipartFile.getOriginalFilename()); - var metaInfo = new MetaInfo( + public ImageId storeImage(final UserId userId, + final MultipartFile multipartFile) { + var metaInfo = new MetaData( ImageNameWithExtension - .fromOriginalFileName(multipartFile.getOriginalFilename()), + .fromOriginalFileName( + multipartFile.getOriginalFilename() + ), multipartFile.getSize() ); - log.info("Store image: check on ex"); - var imageId = new ImageId(UUID.fromString(minioService.saveFile(multipartFile))); + // TODO Why minio service store before postgreSQL? + var imageId = new ImageId( + UUID.fromString(minioService.saveFile(multipartFile)) + ); storeImageOfUser.storeImageOfUser( new Image( imageId, @@ -45,17 +77,35 @@ public ImageId storeImage(UserId userId, MultipartFile multipartFile) { 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(UserId userId, ImageId imageId) { + 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(ImageId imageId, UserId userId) { + public byte[] downloadImage(final ImageId imageId, + final UserId userId) { return minioService.getFile( - getImageOfUser.getImageOfUser(userId, imageId).imageId().value().toString() + 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 index e8e288a..8ab9109 100644 --- 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 @@ -4,11 +4,26 @@ @Getter public class ApplicationCredentials { + /** + * The access token for user. + */ private final String accessToken; + + /** + * The refresh token for user. + */ private final String refreshToken; - public ApplicationCredentials(String accessToken, String refreshToken) { - this.accessToken = accessToken; - this.refreshToken = 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 index 7765672..cf788e9 100644 --- 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 @@ -4,5 +4,11 @@ @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 index f0ed0cc..1cdda6b 100644 --- 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 @@ -4,5 +4,11 @@ @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 index 02e3646..ee436d2 100644 --- 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 @@ -4,11 +4,27 @@ 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); - String generateRefreshToken( - 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 index 75a5838..71b306f 100644 --- 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 @@ -15,20 +15,38 @@ @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 ApplicationCredentials get(Credentials credentials) { + 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 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()); + 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 index 1d16949..7a057b0 100644 --- 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 @@ -11,18 +11,40 @@ @Service @RequiredArgsConstructor -public class GetNewCredentialsUsingRefreshTokenImpl implements GetNewCredentialsUsingRefreshToken { +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; - private final JwtProperties jwtProperties; + /** + * The properties containing the configuration for JWT tokens. + */ + private final JwtProperties jwtProperties; + /** + * Not final to allow spring use proxy. + */ @Override - public ApplicationCredentials get(Credentials credentials) { + 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()); + 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 index c229ea7..675b938 100644 --- 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 @@ -2,7 +2,6 @@ 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.AuthenticatedUser; import com.github.asavershin.api.domain.user.Credentials; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -18,20 +17,30 @@ @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(Credentials credentials) { + public String generateAccessToken(final Credentials credentials) { return buildToken(credentials, jwtProperties.getAccessExpiration()); } - + /** + * Not final to allow spring use proxy. + */ @Override - public String generateRefreshToken(Credentials credentials) { + public String generateRefreshToken(final Credentials credentials) { return buildToken(credentials, jwtProperties.getRefreshExpiration()); } - + /** + * Not final to allow spring use proxy. + */ @Override - public String extractSub(String jwt) { + public String extractSub(final String jwt) { return Jwts.parserBuilder() .setSigningKey(getSignInKey()) .build() @@ -40,12 +49,15 @@ public String extractSub(String jwt) { .getSubject(); } - private String buildToken(Credentials credentials, long expiration) { + 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)) + .setExpiration( + new Date(System.currentTimeMillis() + expiration) + ) .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); } 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 index 7ba8d35..8c22291 100644 --- 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 @@ -1,9 +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 index fb837cb..03d3d05 100644 --- 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 @@ -3,7 +3,24 @@ import java.util.List; public interface FileService { + /** + * Saves a file to the storage. + * + * @param file The file object to be saved. + * @return The saved file object. + */ T saveFile(K file); + /** + * 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 index b829a63..5fb906e 100644 --- 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 @@ -2,5 +2,5 @@ import org.springframework.web.multipart.MultipartFile; -public interface MinioService extends FileService{ +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 index d57436b..2f03cb0 100644 --- 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 @@ -1,13 +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 index 2584611..a2a8110 100644 --- a/api/src/main/java/com/github/asavershin/api/common/NotFoundException.java +++ b/api/src/main/java/com/github/asavershin/api/common/NotFoundException.java @@ -1,7 +1,10 @@ package com.github.asavershin.api.common; -public class NotFoundException extends RuntimeException{ - public NotFoundException(String message){ +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 index afd6770..f0f70dc 100644 --- a/api/src/main/java/com/github/asavershin/api/common/Validator.java +++ b/api/src/main/java/com/github/asavershin/api/common/Validator.java @@ -2,45 +2,138 @@ import java.util.Objects; -public class Validator { - public static void assertArgumentLength(String aString, int aMinimum, int aMaximum, String aMessage) { +/** + * 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); } } - public static void assertArgumentLength(String aString, int aMinimum, String 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); } } - public static void assertStringFormat(String email, String regex, String 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); } } - public static void assertArrayLength(Object[] array, Integer minLength, String 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); } } - public static void assertLongSize(Long value, Long minLength, Long maxLength, String 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); } } - public static void assertLongSize(Long value, Long minLength, String 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); } } - public static void assertNotFound(Object object, String 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 index 0725e3a..51b88d9 100644 --- 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 @@ -1,6 +1,10 @@ package com.github.asavershin.api.common.annotations; -import java.lang.annotation.*; +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) 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 index 6b0666c..30f2f55 100644 --- 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 @@ -1,6 +1,10 @@ package com.github.asavershin.api.common.annotations; -import java.lang.annotation.*; +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) 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 index 1d1483d..1b83bf0 100644 --- 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 @@ -1,6 +1,10 @@ package com.github.asavershin.api.common.annotations; -import java.lang.annotation.*; +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) 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 index 216bfe6..6a084c2 100644 --- a/api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java +++ b/api/src/main/java/com/github/asavershin/api/config/AnnotationsConfig.java @@ -11,9 +11,12 @@ @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) + @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 index d72aeb3..69da912 100644 --- a/api/src/main/java/com/github/asavershin/api/config/AuthConfig.java +++ b/api/src/main/java/com/github/asavershin/api/config/AuthConfig.java @@ -3,17 +3,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.core.userdetails.UserDetailsService; 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 index 7bb2900..3249974 100644 --- a/api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java +++ b/api/src/main/java/com/github/asavershin/api/config/MinIOConfig.java @@ -7,14 +7,29 @@ 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("MinIOConfigLog"); log.info(minioProperties.toString()); return MinioClient.builder() .endpoint(minioProperties.getUrl()) 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 index 5003bf1..5b920ef 100644 --- a/api/src/main/java/com/github/asavershin/api/config/RedisConfig.java +++ b/api/src/main/java/com/github/asavershin/api/config/RedisConfig.java @@ -7,14 +7,30 @@ 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(RedisConnectionFactory connectionFactory) { + public RedisTemplate redisTemplate( + final RedisConnectionFactory connectionFactory) { final RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new GenericToStringSerializer<>(Object.class)); + 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 index 22f8438..5cb2e2b 100644 --- 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 @@ -1,16 +1,30 @@ package com.github.asavershin.api.config.properties; import lombok.Data; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data +@ConfigurationProperties(prefix = "jwt") public class JwtProperties { - @Value("${jwt.secret}") + /** + * Secret key used for signing JWT tokens. + */ private String secret; - @Value("${jwt.access-expiration}") + + /** + * Expiration time for access tokens in milliseconds. + */ private Long accessExpiration; - @Value("${jwt.refresh-expiration}") + + /** + * 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 index 7369083..805e730 100644 --- 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 @@ -1,8 +1,8 @@ package com.github.asavershin.api.config.properties; -import lombok.*; +import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; @Component @@ -10,8 +10,23 @@ @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 index 9aca1af..837edcf 100644 --- a/api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java +++ b/api/src/main/java/com/github/asavershin/api/domain/IsEntityFound.java @@ -3,7 +3,10 @@ import com.github.asavershin.api.common.Validator; public abstract class IsEntityFound { - protected void isEntityFound(Object entity, String entityName, String idName, String entityId){ + 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 index 0c8262f..1c0f0ba 100644 --- a/api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java +++ b/api/src/main/java/com/github/asavershin/api/domain/PartOfResources.java @@ -4,12 +4,37 @@ 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) { - 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"); - if(pageSize <= 0) - throw new IllegalArgumentException("PageSize must be positive"); + + /** + * 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 index b9395ee..3db43d2 100644 --- a/api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java +++ b/api/src/main/java/com/github/asavershin/api/domain/ResourceOwnershipException.java @@ -1,7 +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 { - public ResourceOwnershipException(String message) { + + /** + * 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 index 4b425b0..404c0ab 100644 --- 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 @@ -4,5 +4,11 @@ @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 index ce7911c..a116ae6 100644 --- 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 @@ -4,5 +4,12 @@ @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 index 534413f..db0cb2b 100644 --- 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 @@ -7,5 +7,14 @@ @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 index b6a9c90..f8c7063 100644 --- 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 @@ -1,6 +1,5 @@ package com.github.asavershin.api.domain.image; -import com.github.asavershin.api.domain.IsEntityFound; import com.github.asavershin.api.domain.ResourceOwnershipException; import com.github.asavershin.api.domain.user.UserId; import lombok.EqualsAndHashCode; @@ -13,36 +12,68 @@ @Getter @EqualsAndHashCode public class Image { + /** + * The unique identifier for the image. + */ private ImageId imageId; - private MetaInfo metaInfo; + + /** + * The meta information associated with the image. + */ + private MetaData metaInfo; + + /** + * The user who owns the image. + */ private UserId userId; - public Image(ImageId imageId, MetaInfo metaInfo, UserId userId){ - setImageId(imageId); - setMetaInfo(metaInfo); - setUserId(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); } - public Image belongsToUser(UserId userId){ - if (!this.userId.equals(userId)){ + /** + * 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 " + userId.value().toString() + "Image with id " + imageId.value().toString() + + " does not belong to user with id " + + aUserId.value().toString() ); } return this; } - private void setImageId(ImageId imageId) { - Objects.requireNonNull(imageId, "ImageId must not be null"); - this.imageId = imageId; + private void setImageId(final ImageId aImageId) { + Objects.requireNonNull(aImageId, "ImageId must not be null"); + this.imageId = aImageId; } - private void setMetaInfo(MetaInfo metaInfo) { - Objects.requireNonNull(metaInfo, "MetaInfo must not be null"); - this.metaInfo = metaInfo; + private void setMetaInfo(final MetaData aMetaInfo) { + Objects.requireNonNull(aMetaInfo, "MetaInfo must not be null"); + this.metaInfo = aMetaInfo; } - private void setUserId(UserId userId) { - Objects.requireNonNull(userId, "UserId must not be null"); - this.userId = userId; + 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 index 91b163f..2b367f1 100644 --- 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 @@ -1,20 +1,39 @@ package com.github.asavershin.api.domain.image; -import lombok.EqualsAndHashCode; - 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"), - JPEG(".jpeg"); + /** + * Represents the JPEG image file extension. + */ + JPEG(".jpeg"); + /** + * Represents the string image file extension + * in format .extension. + */ private final String extension; - private ImageExtension(String extension){ - this.extension = extension; + + ImageExtension(final String aExtension) { + this.extension = aExtension; } @Override @@ -22,13 +41,29 @@ public String toString() { return extension; } - private static final Map stringToEnum + /** + * 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)); - public static ImageExtension fromString(String extension){ - var imageExtension = stringToEnum.get(extension); - if(imageExtension == null){ - throw new IllegalArgumentException("Invalid extension: " + extension); + /** + * 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 index 690ac67..f6ef6c7 100644 --- 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 @@ -3,11 +3,26 @@ 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 index c3e321e..5b0f88f 100644 --- 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 @@ -6,41 +6,82 @@ import java.util.Arrays; import java.util.Objects; + @Getter @EqualsAndHashCode -public class ImageNameWithExtension { +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; - private ImageNameWithExtension(String imageName, ImageExtension imageExt){ - this.imageName = imageName; - this.imageExtension = imageExt; + /** + * 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; } - - public static ImageNameWithExtension fromOriginalFileName(String originalFileName){ + /** + * 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)); + 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); } - public static ImageNameWithExtension founded(String imageName, String 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)); + return new ImageNameWithExtension(imageName, + ImageExtension.fromString(extension)); } - private static void notNullValidate(String name){ + private static void notNullValidate(final String name) { Objects.requireNonNull(name, "ImageName must not be null"); } - private static void lengthNameValidate(String name){ - Validator.assertArgumentLength(name,0, MAX_IMAGE_NAME, + private static void lengthNameValidate(final String name) { + Validator.assertArgumentLength(name, 0, MAX_IMAGE_NAME, "ImageName must be " + 0 + "-" + MAX_IMAGE_NAME + " in length"); } 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 index 6669ca9..5998b6b 100644 --- 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 @@ -6,10 +6,35 @@ 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); - List findImagesByUserId(UserId userId, PartOfResources partOfResources); + /** + * 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); - void deleteImageByImageId(Image 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/MetaInfo.java b/api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java deleted file mode 100644 index 3fc7fd3..0000000 --- a/api/src/main/java/com/github/asavershin/api/domain/image/MetaInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.asavershin.api.domain.image; - -import com.github.asavershin.api.common.Validator; - -import java.util.Objects; - -public record MetaInfo(ImageNameWithExtension imageNameWithExtension, Long imageSize) { - private static final int MAX_IMAGE_NAME = 50; - /** - * @param imageName - * Size is in bytes. 10MB - */ - private static final long MAX_IMAGE_SIZE = 10485760; - - public MetaInfo { - validateImageName(imageNameWithExtension); - validateImageSize(imageSize); - } - - private void validateImageName(ImageNameWithExtension imageNameWithExtension) { - Objects.requireNonNull(imageNameWithExtension, "Name and extension must not be null"); - } - - private void validateImageSize(Long imageSize){ - Objects.requireNonNull(imageSize, "Image size must not be null"); - Validator.assertLongSize(imageSize, 0L, MAX_IMAGE_SIZE, - "Image size must be " + "0-" + MAX_IMAGE_SIZE + " in length"); - } -} 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 index e292586..ab0d3d4 100644 --- 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 @@ -2,5 +2,10 @@ @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 index fffc7e8..569b793 100644 --- 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 @@ -10,13 +10,26 @@ @Command @RequiredArgsConstructor -public class DeleteImageOfUserImpl extends IsEntityFound implements DeleteImageOfUser { +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(ImageId imageId, UserId userId) { + 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)); + 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 index 23b8074..674e5ac 100644 --- 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 @@ -11,12 +11,25 @@ @DomainService @RequiredArgsConstructor -public class GetImageOfUserImpl extends IsEntityFound implements GetImageOfUser { +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(UserId userId, ImageId imageId) { + public Image getImageOfUser(final UserId userId, + final ImageId imageId) { var image = imageRepository.findImageByImageId(imageId); - isEntityFound(image, "Image", "Id", imageId.value().toString()); + 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 index 34de1c8..9a85c58 100644 --- 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 @@ -13,9 +13,16 @@ @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(UserId userId, PartOfResources partOfResources) { + 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 index 6712b25..d48c082 100644 --- 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 @@ -9,10 +9,15 @@ @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(Image image) { + 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/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 index 4203fe8..e6f8635 100644 --- 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 @@ -1,7 +1,21 @@ package com.github.asavershin.api.domain.user; +/** + * Represents an exception that occurs during authentication process. + * + * @author asavershin + */ public class AuthException extends RuntimeException { - public AuthException(String message){ + + /** + * 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 index e6c4184..28bfc2e 100644 --- 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 @@ -1,6 +1,5 @@ package com.github.asavershin.api.domain.user; -import com.github.asavershin.api.domain.IsEntityFound; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -10,26 +9,51 @@ @Getter @ToString @EqualsAndHashCode -public class AuthenticatedUser { +public final class AuthenticatedUser { + /** + * The unique identifier of the user. + */ private UserId userId; + + /** + * The credentials of the user. + */ private Credentials userCredentials; - private AuthenticatedUser(UserId userId, Credentials userCredentials) { - setUserId(userId); - setUserCredentials(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); } - public static AuthenticatedUser founded(UserId userId, Credentials credentials) { + /** + * 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(UserId userId) { - Objects.requireNonNull(userId, "UserId must not be null"); - this.userId = userId; + private void setUserId(final UserId aUserId) { + Objects.requireNonNull(aUserId, "UserId must not be null"); + this.userId = aUserId; } - private void setUserCredentials(Credentials userCredentials) { - Objects.requireNonNull(userCredentials, "UserCredentials must not be null"); - this.userCredentials = userCredentials; + 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 index 037c05e..5ac94d9 100644 --- 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 @@ -1,7 +1,14 @@ package com.github.asavershin.api.domain.user; -import java.util.Optional; - +/** + * 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 index e593614..96a59e4 100644 --- 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 @@ -6,28 +6,48 @@ 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(String email) { - Objects.requireNonNull(email, "Email must not be null"); - Validator.assertArgumentLength(email, + 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"); + "Email must be " + + MIN_EMAIL_LENGTH + "-" + + MAX_EMAIL_LENGTH + " in length"); - Validator.assertStringFormat(email, "[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\\.[A-Za-z0-9.-]+", + 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(String password) { - Objects.requireNonNull(password, "Password must not be null"); - Validator.assertArgumentLength(password, MIN_PASSWORD_LENGTH, "Password must be greater than " + MIN_PASSWORD_LENGTH); + 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 index 1190c92..162a715 100644 --- 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 @@ -1,23 +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(String name, int maxLength) { + 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"); + 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 index 946eefa..a4ede32 100644 --- 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 @@ -5,7 +5,22 @@ 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 { - public List get(UserId userId, PartOfResources partOfResources); + /** + * 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 index a3caf54..a3a4b5f 100644 --- 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 @@ -1,6 +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 index a6a17f8..374164d 100644 --- 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 @@ -1,6 +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 index 061f25a..d1b2133 100644 --- 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 @@ -1,3 +1,9 @@ +/** + * 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; @@ -8,32 +14,53 @@ @ToString @EqualsAndHashCode -public class User{ - @Getter +@Getter +public class User { + /** + * The unique identifier for the user. + */ private UserId userId; - @Getter + + /** + * The full name of the user. + */ private FullName userFullName; - @Getter + + /** + * The credentials of the user. + */ private Credentials userCredentials; - public User(UserId userId, FullName userFullName, Credentials userCredentials) { - setUserId(userId); - setUserCredentials(userCredentials); - setUserFullName(userFullName); + /** + * 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(UserId userId) { - Objects.requireNonNull(userId, "UserId must not be null"); - this.userId = userId; + private void setUserId(final UserId id) { + Objects.requireNonNull(id, + "UserId must not be null"); + this.userId = id; } - private void setUserFullName(FullName userFullName) { - Objects.requireNonNull(userFullName, "UserFullName must not be null"); - this.userFullName = userFullName; + private void setUserFullName(final FullName fullName) { + Objects.requireNonNull(fullName, + "UserFullName must not be null"); + this.userFullName = fullName; } - private void setUserCredentials(Credentials userCredentials) { - Objects.requireNonNull(userCredentials, "UserCredentials must not be null"); - this.userCredentials = userCredentials; + 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 index 1f672dd..c463fcf 100644 --- 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 @@ -1,15 +1,36 @@ +/** + * A unique identifier for a user. + * + * @author asavershin + */ package com.github.asavershin.api.domain.user; -import lombok.EqualsAndHashCode; - 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 index df27a0c..e21f666 100644 --- 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 @@ -1,9 +1,27 @@ package com.github.asavershin.api.domain.user; -import java.util.Optional; - +/** + * 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 index bf6a572..d3eb2e8 100644 --- 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 @@ -10,12 +10,28 @@ 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 { +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(UserId userId, PartOfResources partOfResources) { + 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 index 4a2984b..82fe96f 100644 --- 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 @@ -1,38 +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 com.github.asavershin.api.domain.user.*; -import com.github.asavershin.api.domain.user.RegisterNewUser; 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(FullName fullName, Credentials credentials) { + 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())) + new Credentials(credentials.email(), + protectPassword(credentials.password())) ) ); } - private void checkEmailForUnique(String email) { + private void checkEmailForUnique(final String email) { if (userRepository.existByUserEmail(email)) { throw new IllegalArgumentException("Email is not unique"); } } - private String protectPassword(String unprotectedPassword) { + 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 index be33cb2..3823a7d 100644 --- 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 @@ -13,17 +13,28 @@ @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(Credentials credentials) { + public AuthenticatedUser login(final Credentials credentials) { - var authenticatedUser = authenticatedUserRepository.findByEmail(credentials.email()); + var authenticatedUser = authenticatedUserRepository + .findByEmail(credentials.email()); - isEntityFound(authenticatedUser,"User", "email", credentials.email()); - if(passwordEncoder.matches(credentials.password(), authenticatedUser.userCredentials().password())) { + 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 index 8b4fa5b..eb5e5e7 100644 --- 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 @@ -7,69 +7,117 @@ import com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.UISuccessContainer; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.*; +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 -@Slf4j 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(RuntimeException ex) { + 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(RuntimeException ex) { + 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(Exception ex) { + 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(IllegalArgumentException ex) { - log.info("IllegaArgumentException: " + ex); + 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(MethodArgumentNotValidException ex) { - log.info("handleValidationException"); + 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) + .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(ConstraintViolationException ex) { - log.info("handleConstraintViolationException"); + 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) + .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 index 35c3cf6..e654795 100644 --- 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 @@ -11,7 +11,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,30 +22,56 @@ @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; - private final GetNewCredentialsUsingRefreshToken getNewCredentialsUsingRefreshToken; + /** + * 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( - @RequestBody @Valid UserRegistrationRequest userRegistrationRequest + final @RequestBody @Valid UserRegistrationRequest request ) { - register.register(userRegistrationRequest.ToFullName(), - userRegistrationRequest.toCredentials()); + 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(@RequestBody @Valid UserLoginRequest userLoginRequest) { - + 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(@AuthenticationPrincipal CustomUserDetails user) { - return getNewCredentialsUsingRefreshToken.get(user.authenticatedUser().userCredentials()); + 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 index cfc5229..88d7211 100644 --- 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 @@ -11,9 +11,14 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +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; @@ -23,47 +28,99 @@ @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(@AuthenticationPrincipal CustomUserDetails user, - Long pageNumber, - Long pageSize) { - return GetImagesResponse.GetImagesResponseFromImages( - getImages.get(user.authenticatedUser().userId(), new PartOfResources(pageNumber, pageSize)) + 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( - @AuthenticationPrincipal CustomUserDetails user, - @RequestPart("file") MultipartFile file + final @AuthenticationPrincipal CustomUserDetails user, + final @RequestPart("file") MultipartFile file ) { - return new UploadImageResponse(imageService.storeImage(user.authenticatedUser().userId(), 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( - @AuthenticationPrincipal CustomUserDetails user, - @PathVariable("image-id") String imageId + final @AuthenticationPrincipal CustomUserDetails user, + final @PathVariable("image-id") String imageId ) { - imageService.deleteImageByImageId(user.authenticatedUser().userId(), new ImageId(UUID.fromString(imageId))); + imageService.deleteImageByImageId( + user.authenticatedUser().userId(), + new ImageId(UUID.fromString(imageId)) + ); return new UISuccessContainer( true, - "Image with id " + imageId +" deleted successfully" + "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( - @AuthenticationPrincipal CustomUserDetails user, - @PathVariable("image-id") String imageId + final @AuthenticationPrincipal CustomUserDetails user, + final @PathVariable("image-id") String imageId ) { return imageService.downloadImage( new ImageId(UUID.fromString(imageId)), 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 index 3c094b6..12b74e1 100644 --- 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 @@ -10,14 +10,24 @@ @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 message + final String aMessage ) { - this.message = message; + 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 index f7ab7b7..c80f9a8 100644 --- 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 @@ -6,6 +6,12 @@ @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 index 2b7d65a..7792a11 100644 --- 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 @@ -1,22 +1,33 @@ package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image; import com.github.asavershin.api.domain.image.Image; -import lombok.AllArgsConstructor; import lombok.Getter; import java.util.List; -import java.util.stream.Collectors; @Getter -public class GetImagesResponse { - private List images; - private GetImagesResponse(List images){ - this.images = images; +public final class GetImagesResponse { + /** + * List of metadata of images. + */ + private final List images; + + private GetImagesResponse(final List aImages) { + this.images = aImages; } - public static GetImagesResponse GetImagesResponseFromImages(List images) { + + /** + * 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() + .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 index 7b2a6a8..9600724 100644 --- 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 @@ -4,18 +4,35 @@ import lombok.Getter; @Getter -public class ImageResponse { - private String ImageId; - private String ImageName; - private Long imageSize; +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(String ImageId, String ImageName, Long imageSize){ - this.ImageId = ImageId; - this.ImageName = ImageName; - this.imageSize = imageSize; + private ImageResponse(final String aImageId, + final String aImageName, + final Long aImageSize) { + this.imageId = aImageId; + this.imageName = aImageName; + this.imageSize = aImageSize; } - public static ImageResponse imageResponseFromEntity(Image image){ + /** + * 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(), 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 index 165a742..3e05d1a 100644 --- 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 @@ -1,13 +1,18 @@ package com.github.asavershin.api.infrastructure.in.impl.controllers.controllers.dto.image; import com.github.asavershin.api.domain.image.ImageId; -import lombok.AllArgsConstructor; import lombok.Getter; @Getter public class UploadImageResponse { - private String imageId; - public UploadImageResponse(ImageId imageId){ - this.imageId = imageId.value().toString(); + /** + * 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 index d3ca761..03abb31 100644 --- 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 @@ -10,15 +10,30 @@ @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 = 8, message = "Длина пароля должна быть не менее 8") + @Size(min = MIN_PASSWORD_LENGTH, + message = "Длина пароля должна быть не менее 8") private String userPassword; - public Credentials toCredentials() { + /** + * 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 index 0165581..55132c4 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -12,28 +13,51 @@ @Setter public class UserRegistrationRequest { + + /** + * User firstname. + */ @NotEmpty(message = "Не заполнено имя") - @Size(min = 1, max = 20, message = "Недопустимая длина имени") + @Size(min = 1, max = UserProperties.MAX_FIRSTNAME_LENGTH, + message = "Недопустимая длина имени") private String userFirstname; + /** + * User lastname. + */ @NotEmpty(message = "Не заполнена фамилия") - @Size(min = 1, max = 20, 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 = 8, message = "Длина пароля должна быть не менее 8") + @Size(min = UserProperties.MIN_PASSWORD_LENGTH, + message = "Длина пароля должна быть не менее 8") @Getter private String userPassword; - public FullName ToFullName() { + /** + * 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 index a14e57f..0a130ef 100644 --- 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 @@ -7,7 +7,8 @@ import java.util.Collection; import java.util.List; -public record CustomUserDetails(AuthenticatedUser authenticatedUser) implements UserDetails { +public record CustomUserDetails(AuthenticatedUser authenticatedUser) + implements UserDetails { @Override public Collection getAuthorities() { 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 index f41e747..81b6bea 100644 --- 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 @@ -2,13 +2,12 @@ import com.github.asavershin.api.application.out.TokenRepository; import com.github.asavershin.api.application.in.services.user.JwtService; -import com.github.asavershin.api.domain.user.Credentials; +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 lombok.extern.slf4j.Slf4j; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -21,24 +20,37 @@ @Component @RequiredArgsConstructor -@Slf4j 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 void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain + protected final void doFilterInternal( + final @NonNull HttpServletRequest request, + final @NonNull HttpServletResponse response, + final @NonNull FilterChain filterChain ) throws ServletException, IOException { - log.info("Start JWT authentication filter"); var path = request.getServletPath(); if (path.contains("/register") || path.contains("/login")) { - log.info("REGISTER OR LOGIN"); filterChain.doFilter(request, response); return; } @@ -48,26 +60,22 @@ protected void doFilterInternal( filterChain.doFilter(request, response); return; } - jwt = authHeader.substring(7); + jwt = authHeader.substring(JwtProperties.START_OF_TOKEN); var email = jwtService.extractSub(jwt); String token; var pathContainsRefreshToken = path.contains("/refresh-token"); - if(pathContainsRefreshToken){ - log.info("Refresh token getting"); + if (pathContainsRefreshToken) { token = tokenRepository.getRefreshToken(email); - }else{ - log.info("Access token getting"); + } else { token = tokenRepository.getAccessToken(email); } - log.info("Token: " + token); - log.info("JWT: " + jwt); if (!token.equals(jwt)) { tokenRepository.deleteAllTokensByUserEmail(email); return; } - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + var authToken = new UsernamePasswordAuthenticationToken( userDetailsService.loadUserByUsername(email), null, null 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 index 9808396..34eefa0 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -12,22 +13,34 @@ @Service @RequiredArgsConstructor public class LogautHandlerImpl implements LogoutHandler { - - private final TokenRepository tokenService; + /** + * 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( - HttpServletRequest request, - HttpServletResponse response, - Authentication authentication + final HttpServletRequest request, + final HttpServletResponse response, + final Authentication authentication ) { final String authHeader = request.getHeader("Authorization"); final String jwt; - if (authHeader == null ||!authHeader.startsWith("Bearer ")) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { return; } - jwt = authHeader.substring(7); - tokenService.deleteAllTokensByUserEmail("asd"); + 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 index a718077..b39bcdc 100644 --- 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 @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationProvider; 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; @@ -20,7 +19,9 @@ @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", @@ -35,11 +36,35 @@ public class SecurityConfiguration { "/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(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain( + final HttpSecurity http + ) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(req -> @@ -48,15 +73,22 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .anyRequest() .authenticated() ) - .sessionManagement(session -> session.sessionCreationPolicy(NEVER)) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .sessionManagement( + session -> session.sessionCreationPolicy(NEVER) + ) + .addFilterBefore( + jwtAuthFilter, + UsernamePasswordAuthenticationFilter.class + ) .logout(logout -> logout.logoutUrl("/auth/logout") .addLogoutHandler(logoutHandler) - .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()) - ) - ; - + .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 index cebe0f0..b34c970 100644 --- 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 @@ -10,10 +10,30 @@ @Service @RequiredArgsConstructor -public class UserDetailsServiceImpl extends IsEntityFound implements UserDetailsService { +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(String username) throws UsernameNotFoundException { + 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 index 3402186..e381f04 100644 --- 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 @@ -14,10 +14,18 @@ @Repository @RequiredArgsConstructor -public class AuthenticatedUserRepositoryImpl implements AuthenticatedUserRepository, RecordMapper { +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(String email) { + public AuthenticatedUser findByEmail(final String email) { return dslContext.select( USERS.USER_ID, USERS.USER_EMAIL, @@ -28,12 +36,16 @@ public AuthenticatedUser findByEmail(String email) { .fetchOne(this); } - + /** + * Not final to allow spring use proxy. + */ @Override - public AuthenticatedUser map(Record record) { + 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)); + return AuthenticatedUser.founded( + new UserId(userId), new Credentials(email, password) + ); } } diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java deleted file mode 100644 index 48ea8ba..0000000 --- a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/CacheRepositoryIml.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.asavershin.api.infrastructure.out.persistence; - -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 { - private final RedisTemplate redisTemplate; - @Override - public void addCache(String key, String token, long expiration) { - redisTemplate.opsForValue().set(key, token, expiration, TimeUnit.MILLISECONDS); - } - - @Override - public String getCache(String key) { - return (String) redisTemplate.opsForValue().get(key); - } - - @Override - public void deleteCache(String key) { - redisTemplate.delete(key); - } -} 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 index d462f00..3cddeba 100644 --- 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 @@ -1,7 +1,11 @@ package com.github.asavershin.api.infrastructure.out.persistence; import com.github.asavershin.api.domain.PartOfResources; -import com.github.asavershin.api.domain.image.*; +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; @@ -16,17 +20,28 @@ @Repository @RequiredArgsConstructor -public class ImageRepositoryImpl implements ImageRepository, RecordMapper { +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(Image image) { + 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_NAME, + image.metaInfo().imageNameWithExtension().imageName() + ) .set(IMAGE.IMAGE_SIZE, image.metaInfo().imageSize()) .set(IMAGE.IMAGE_EXTENSION, - image.metaInfo().imageNameWithExtension().imageExtension().toString()) + image.metaInfo().imageNameWithExtension() + .imageExtension().toString()) .execute(); dslContext.insertInto(USER_IMAGES) .set(USER_IMAGES.IMAGE_ID, image.imageId().value()) @@ -34,34 +49,44 @@ public void save(Image image) { .execute(); } + /** + * Not final to allow spring use proxy. + */ @Override - public List findImagesByUserId(UserId userId, PartOfResources partOfResources) { + 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(partOfResources.pageNumber() * partOfResources.pageSize()) - .limit(partOfResources.pageSize()) + .offset(page.pageNumber() * page.pageSize()) + .limit(page.pageSize()) .fetch(this); } - + /** + * Not final to allow spring use proxy. + */ @Override - public Image findImageByImageId(ImageId imageId) { + 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(Image imageId) { + 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(Record record) { + 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); @@ -69,7 +94,13 @@ public Image map(Record record) { var imageExtension = record.get(IMAGE.IMAGE_EXTENSION); return new Image( new ImageId(imageId), - new MetaInfo(ImageNameWithExtension.founded(imageName, imageExtension), imageSize), + new MetaData( + ImageNameWithExtension.founded( + imageName, + imageExtension + ), + imageSize + ), new UserId(userId) ); } diff --git a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java b/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java deleted file mode 100644 index 63095fd..0000000 --- a/api/src/main/java/com/github/asavershin/api/infrastructure/out/persistence/TokenRepositoryIml.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.asavershin.api.infrastructure.out.persistence; - -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 { - private final String refreshKey = "REFRESH_TOKEN_"; - private final String accessKey = "ACCESS_TOKEN_"; - private final CacheRepository cacheRepository; - - @Override - public String getAccessToken(String email){ - return cacheRepository.getCache(accessKey + email); - } - - @Override - public String getRefreshToken(String email){ - return cacheRepository.getCache(refreshKey + email); - } - - @Override - public void saveRefreshToken(String username, String jwtToken, Long expiration) { - cacheRepository.addCache(refreshKey + username, jwtToken, expiration); - } - - @Override - public void saveAccessToken(String username, String jwtToken, Long expiration) { - cacheRepository.addCache(accessKey + username, jwtToken, expiration); - } - - @Override - public void deleteAllTokensByUserEmail(String username) { - cacheRepository.deleteCache(refreshKey + username); - cacheRepository.deleteCache(accessKey+ username); - } -} 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 index a20bac2..4b43728 100644 --- 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 @@ -12,11 +12,16 @@ @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(User newUser) { + public void save(final User newUser) { UsersRecord userRecord = dslContext.newRecord(USERS); userRecord.setUserId(newUser.userId().value()); userRecord.setUserFirstname(newUser.userCredentials().email()); @@ -28,9 +33,13 @@ public void save(User newUser) { .set(userRecord) .execute(); } - + /** + * Not final to allow spring use proxy. + */ @Override - public boolean existByUserEmail(String email) { - return dslContext.fetchExists(USERS, USERS.USER_EMAIL.eq(email)); + 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 index 9904ffe..e7c3567 100644 --- 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 @@ -1,7 +1,12 @@ package com.github.asavershin.api.infrastructure.out.storage; public class FileException extends RuntimeException { - public FileException(String s) { + /** + * 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 index 8eef78b..87454b2 100644 --- 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 @@ -3,8 +3,12 @@ import com.github.asavershin.api.application.out.MinioService; import com.github.asavershin.api.config.properties.MinIOProperties; -import io.minio.*; -import lombok.RequiredArgsConstructor; +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; @@ -16,20 +20,39 @@ @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; - - public MinioServiceIml(MinioClient minioClient, MinIOProperties minioProperties) { - this.minioClient = minioClient; - this.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 String saveFile(final MultipartFile image) { if (!bucketExists(minioProperties.getBucket())) { - throw new FileException("File upload failed: bucket does not exist"); + throw new FileException( + "File upload failed: bucket does not exist" + ); } if (image.isEmpty() || image.getOriginalFilename() == null) { @@ -46,14 +69,17 @@ public String saveFile(final MultipartFile image) { saveImage(inputStream, link); return link; } - + /** + * 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() + return IOUtils.toByteArray( + minioClient.getObject(GetObjectArgs.builder() .bucket(minioProperties.getBucket()) .object(link) .build())); @@ -61,7 +87,9 @@ public byte[] getFile(final String link) { 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()) { @@ -112,7 +140,9 @@ private String generateFileName() { } @SneakyThrows(Exception.class) - private boolean bucketExists(String bucketName) { - return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + 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/test/java/com/github/asavershin/api/common/ImageHelper.java b/api/src/test/java/com/github/asavershin/api/common/ImageHelper.java index 004de07..6cd6c04 100644 --- a/api/src/test/java/com/github/asavershin/api/common/ImageHelper.java +++ b/api/src/test/java/com/github/asavershin/api/common/ImageHelper.java @@ -2,7 +2,7 @@ import com.github.asavershin.api.domain.image.ImageId; import com.github.asavershin.api.domain.image.ImageNameWithExtension; -import com.github.asavershin.api.domain.image.MetaInfo; +import com.github.asavershin.api.domain.image.MetaData; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -11,12 +11,12 @@ public class ImageHelper { public static ImageId imageId(){ return ImageId.nextIdentity(); } - public static MetaInfo metaInfo1(){ - return new MetaInfo(ImageNameWithExtension.fromOriginalFileName("image.jpg"), 1L); + public static MetaData metaInfo1(){ + return new MetaData(ImageNameWithExtension.fromOriginalFileName("image.jpg"), 1L); } - public static MetaInfo metaInfo3(){ - return new MetaInfo(ImageNameWithExtension.fromOriginalFileName("image3.jpg"), 3L); + public static MetaData metaInfo3(){ + return new MetaData(ImageNameWithExtension.fromOriginalFileName("image3.jpg"), 3L); } public static MultipartFile multipartFile1() { 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 index 24d597c..0790394 100644 --- a/api/src/test/java/com/github/asavershin/api/domaintest/ImageTest.java +++ b/api/src/test/java/com/github/asavershin/api/domaintest/ImageTest.java @@ -5,7 +5,7 @@ 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.MetaInfo; +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.*; @@ -16,7 +16,7 @@ public class ImageTest { void testAddNewImage() { //Given ImageId imageId = ImageHelper.imageId(); - MetaInfo metaInfo = ImageHelper.metaInfo1(); + MetaData metaInfo = ImageHelper.metaInfo1(); UserId userId = UserHelper.UserId(); //When @@ -32,7 +32,7 @@ void testAddNewImage() { void testFoundedImage() { //Given ImageId imageId = ImageHelper.imageId(); - MetaInfo metaInfo = ImageHelper.metaInfo1(); + MetaData metaInfo = ImageHelper.metaInfo1(); UserId userId = UserHelper.UserId(); // When Image image = new Image(imageId, metaInfo, userId); @@ -46,7 +46,7 @@ void testFoundedImage() { void testBelongsToUser() { // Given ImageId imageId = ImageHelper.imageId(); - MetaInfo metaInfo = ImageHelper.metaInfo1(); + MetaData metaInfo = ImageHelper.metaInfo1(); UserId userId = UserHelper.UserId(); UserId otherUserId = UserHelper.UserId(); @@ -66,10 +66,10 @@ void testEquals() { var imageId = ImageId.nextIdentity(); var userId = UserId.nextIdentity(); - MetaInfo metaInfo1 = ImageHelper.metaInfo1(); + MetaData metaInfo1 = ImageHelper.metaInfo1(); Image image1 = new Image(imageId, metaInfo1, userId); - MetaInfo metaInfo2 = ImageHelper.metaInfo1(); + MetaData metaInfo2 = ImageHelper.metaInfo1(); Image image2 = new Image(imageId, metaInfo2, userId); assertTrue(image1.equals(image2)); @@ -80,7 +80,7 @@ void testEquals() { assertFalse(image1.equals(null)); ImageId imageId3 = ImageId.nextIdentity(); - MetaInfo metaInfo3 = ImageHelper.metaInfo3(); + MetaData metaInfo3 = ImageHelper.metaInfo3(); UserId userId3 = UserHelper.UserId(); Image image3 = new Image(imageId3, metaInfo3, userId3); 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 index a1ca5f8..48b77a0 100644 --- a/api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java +++ b/api/src/test/java/com/github/asavershin/api/integrations/ImageLogicTest.java @@ -20,6 +20,7 @@ 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; @@ -78,6 +79,8 @@ public void clearDB(){ e.printStackTrace(); } } + @Autowired + private ApplicationContext applicationContext; @Test public void storeImageTest(){ @@ -91,6 +94,8 @@ public void storeImageTest(){ 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); @@ -117,6 +122,7 @@ public void storeImageWithIllegalExtensionTest(){ registerNewUser.register(fullName,credentials); var user = authenticatedUserRepository.findByEmail(credentials.email()); + assertNotNull(user); // When var ex = assertThrows(IllegalArgumentException.class, () -> imageService.storeImage(user.userId(), ImageHelper.multipartFileWithIllegalException())); From efa5e0c53bf83f948a3fad82a07e5b82900edfce Mon Sep 17 00:00:00 2001 From: asavershin Date: Thu, 4 Apr 2024 21:23:33 +0300 Subject: [PATCH 3/9] Add checkstyle in pom --- pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pom.xml b/pom.xml index a9fba13..ebe7e31 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 22 UTF-8 1.18.30 + 3.3.1 @@ -46,4 +47,14 @@ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + + From c51666e5f6466d1ecb2ca722cd76c725148afd14 Mon Sep 17 00:00:00 2001 From: asavershin Date: Thu, 4 Apr 2024 21:29:20 +0200 Subject: [PATCH 4/9] Set up on 21 java, add action for compile --- .github/workflows/JavaMavenCI.yml | 24 ++++++++++++++++++++++++ api/Dockerfile | 2 +- api/pom.xml | 4 ++-- pom.xml | 4 ++-- 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/JavaMavenCI.yml diff --git a/.github/workflows/JavaMavenCI.yml b/.github/workflows/JavaMavenCI.yml new file mode 100644 index 0000000..889e948 --- /dev/null +++ b/.github/workflows/JavaMavenCI.yml @@ -0,0 +1,24 @@ +name: Java CI with Maven + +on: + push: + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + 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 \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index fd4a080..79aace5 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:22 +FROM openjdk:21 WORKDIR /app diff --git a/api/pom.xml b/api/pom.xml index 38e9914..8fbee14 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -12,8 +12,8 @@ api - 22 - 22 + 21 + 21 UTF-8 3.19.3 2.1.0 diff --git a/pom.xml b/pom.xml index ebe7e31..8a09635 100644 --- a/pom.xml +++ b/pom.xml @@ -19,8 +19,8 @@ - 22 - 22 + 21 + 21 UTF-8 1.18.30 3.3.1 From 1dee4afc522b790546ed7934cc8602cb7cddb44e Mon Sep 17 00:00:00 2001 From: asavershin Date: Thu, 4 Apr 2024 22:36:07 +0300 Subject: [PATCH 5/9] Update action --- .github/workflows/JavaMavenCI.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/JavaMavenCI.yml b/.github/workflows/JavaMavenCI.yml index 889e948..2adaf03 100644 --- a/.github/workflows/JavaMavenCI.yml +++ b/.github/workflows/JavaMavenCI.yml @@ -10,9 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v4 - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' From 9ac81e9b95bdec87cb76fa11b67b144aea142aeb Mon Sep 17 00:00:00 2001 From: asavershin Date: Fri, 5 Apr 2024 11:29:07 +0200 Subject: [PATCH 6/9] Action for jacoco --- .github/workflows/JavaMavenCI.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/JavaMavenCI.yml b/.github/workflows/JavaMavenCI.yml index 2adaf03..82a23f5 100644 --- a/.github/workflows/JavaMavenCI.yml +++ b/.github/workflows/JavaMavenCI.yml @@ -1,7 +1,6 @@ name: Java CI with Maven on: - push: pull_request: jobs: @@ -22,4 +21,28 @@ jobs: run: mvn clean compile - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 \ No newline at end of file + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + + 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 & Test with maven + run: mvn clean 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 \ No newline at end of file From 4a55d55dd34a90a1cf42fd8a9e06141a24dee82a Mon Sep 17 00:00:00 2001 From: asavershin Date: Fri, 5 Apr 2024 12:56:25 +0200 Subject: [PATCH 7/9] Add checkstyle & docker push image --- .github/workflows/JavaMavenCI.yml | 71 +++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/.github/workflows/JavaMavenCI.yml b/.github/workflows/JavaMavenCI.yml index 82a23f5..1688bcf 100644 --- a/.github/workflows/JavaMavenCI.yml +++ b/.github/workflows/JavaMavenCI.yml @@ -23,6 +23,21 @@ jobs: - 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: @@ -36,8 +51,10 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven - - name: Compile & Test with maven - run: mvn clean test + - 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 @@ -45,4 +62,52 @@ jobs: paths: | ${{ github.workspace }}/**/target/site/jacoco/*.xml token: ${{ secrets.TEST_SECRET }} - min-coverage-overall: 50 \ No newline at end of file + min-coverage-overall: 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 80% + if: ${{ steps.jacoco.outputs.coverage-overall < 50.0 }} + uses: actions/github-script@v6 + with: + script: | + core.setFailed('Overall coverage is less than 80%!') + + 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 From 06016e56ad1056004535aa5635524311571dc87d Mon Sep 17 00:00:00 2001 From: asavershin Date: Fri, 5 Apr 2024 13:20:17 +0200 Subject: [PATCH 8/9] Update testing action --- .github/workflows/JavaMavenCI.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/JavaMavenCI.yml b/.github/workflows/JavaMavenCI.yml index 1688bcf..690e6e9 100644 --- a/.github/workflows/JavaMavenCI.yml +++ b/.github/workflows/JavaMavenCI.yml @@ -63,18 +63,19 @@ jobs: ${{ 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 80% + - 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 80%!') + core.setFailed('Overall coverage is less than 50%!') docker: runs-on: ubuntu-latest From 673ec012d50e685ca7d2c24c3ac1530973b64e50 Mon Sep 17 00:00:00 2001 From: asavershin Date: Sun, 7 Apr 2024 09:32:31 +0300 Subject: [PATCH 9/9] Save file using minio after postgresql --- .../in/services/image/impl/ImageServiceImpl.java | 7 ++----- .../asavershin/api/application/out/FileService.java | 9 +++++---- .../api/application/out/MinioService.java | 2 +- .../infrastructure/out/storage/MinioServiceIml.java | 13 +++---------- 4 files changed, 11 insertions(+), 20 deletions(-) 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 index 0dcfe82..eb2fc9a 100644 --- 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 @@ -17,7 +17,6 @@ import java.io.Serializable; import java.util.List; -import java.util.UUID; @Service @RequiredArgsConstructor @@ -63,10 +62,7 @@ public ImageId storeImage(final UserId userId, ), multipartFile.getSize() ); - // TODO Why minio service store before postgreSQL? - var imageId = new ImageId( - UUID.fromString(minioService.saveFile(multipartFile)) - ); + var imageId = ImageId.nextIdentity(); storeImageOfUser.storeImageOfUser( new Image( imageId, @@ -74,6 +70,7 @@ public ImageId storeImage(final UserId userId, userId ) ); + minioService.saveFile(multipartFile, imageId.value().toString()); return imageId; } 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 index 03d3d05..2691519 100644 --- 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 @@ -2,14 +2,15 @@ import java.util.List; -public interface FileService { +public interface FileService { /** * Saves a file to the storage. * - * @param file The file object to be saved. - * @return The saved file object. + * @param file The file object to be saved. + * @param filename The name of the file to be saved. + * Must be unique. */ - T saveFile(K file); + void saveFile(K file, String filename); /** * Retrieves the content of a file from the storage. * 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 index 5fb906e..93835cb 100644 --- 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 @@ -2,5 +2,5 @@ import org.springframework.web.multipart.MultipartFile; -public interface MinioService extends FileService { +public interface MinioService extends FileService { } 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 index 87454b2..055f46b 100644 --- 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 @@ -16,7 +16,6 @@ import java.io.InputStream; import java.util.List; -import java.util.UUID; @Service public class MinioServiceIml implements MinioService { @@ -47,7 +46,7 @@ public MinioServiceIml(final MinioClient aMinioClient, * Not final to allow spring use proxy. */ @Override - public String saveFile(final MultipartFile image) { + public void saveFile(final MultipartFile image, final String filename) { if (!bucketExists(minioProperties.getBucket())) { throw new FileException( @@ -58,7 +57,6 @@ public String saveFile(final MultipartFile image) { if (image.isEmpty() || image.getOriginalFilename() == null) { throw new FileException("File must have name"); } - var link = generateFileName(); InputStream inputStream; try { inputStream = image.getInputStream(); @@ -66,8 +64,7 @@ public String saveFile(final MultipartFile image) { throw new FileException("File upload failed: " + e.getMessage()); } - saveImage(inputStream, link); - return link; + saveFile(inputStream, filename); } /** * Not final to allow spring use proxy. @@ -124,7 +121,7 @@ private void createBucket() { } @SneakyThrows - private void saveImage( + private void saveFile( final InputStream inputStream, final String fileName ) { @@ -135,10 +132,6 @@ private void saveImage( .build()); } - private String generateFileName() { - return UUID.randomUUID().toString(); - } - @SneakyThrows(Exception.class) private boolean bucketExists(final String bucketName) { return minioClient.bucketExists(