From 8c2e7e51832ef60a0493c73b402f6c349375910d Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:04:19 +0200 Subject: [PATCH 01/41] configured actuator (#8) --- pom.xml | 5 ++++- src/main/resources/application.properties | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c8f9aa8..7b5288f 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,10 @@ h2 test - + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e400403..34f5576 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,3 +13,7 @@ spring.jpa.hibernate.ddl-auto=none server.servlet.context-path=/api rawg.key=${RAWG_KEY} + +management.endpoints.web.exposure.include=health +management.endpoint.health.probes.enabled=true +management.endpoint.health.show-details=always From f4d43743d6c2a09086f4cc9db9aa078aa0cfedd0 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:45:57 +0200 Subject: [PATCH 02/41] Improve filtering (#9) * implemented search games method * refactored getById to use apiId * ran mvnclean package --- pom.xml | 4 + .../backend/controller/GameController.java | 82 +++++++++---------- .../dto/internal/GameSearchParameters.java | 11 +++ ...pecificationProviderNotFoundException.java | 7 ++ .../backend/model/Game.java | 14 ++++ .../backend/model/Genre.java | 1 + .../backend/model/Platform.java | 1 + .../backend/repository/GameRepository.java | 14 +--- .../repository/SpecificationBuilder.java | 7 ++ .../repository/SpecificationProvider.java | 9 ++ .../SpecificationProviderManager.java | 5 ++ .../game/GameSpecificationBuilder.java | 60 ++++++++++++++ .../GameSpecificationProviderManager.java | 28 +++++++ .../spec/GameGenresSpecificationProvider.java | 47 +++++++++++ .../spec/GameNameSpecificationProvider.java | 37 +++++++++ .../GamePlatformsSpecificationProvider.java | 48 +++++++++++ .../spec/GameYearSpecificationProvider.java | 35 ++++++++ .../backend/service/GameService.java | 11 +-- .../backend/service/GameServiceImpl.java | 29 +++---- src/main/resources/application.properties | 4 + 20 files changed, 372 insertions(+), 82 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java diff --git a/pom.xml b/pom.xml index 7b5288f..42c589c 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,10 @@ org.springframework.boot spring-boot-starter-jdbc + + org.springframework.boot + spring-boot-starter-validation + mysql diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index ba1d65b..1ca3d37 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -1,17 +1,19 @@ package com.videogamescatalogue.backend.controller; import com.videogamescatalogue.backend.dto.internal.GameDto; +import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.model.Genre; import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.service.GameService; import java.util.Arrays; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,11 +22,12 @@ @RequestMapping("/games") @RestController public class GameController { + public static final int DEFAULT_PAGE_SIZE = 30; private final GameService gameService; @GetMapping("/local") public Page getAllGamesFromDb( - @PageableDefault(size = 30, sort = "apiRating", + @PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "apiRating", direction = Sort.Direction.DESC) Pageable pageable ) { @@ -32,55 +35,44 @@ public Page getAllGamesFromDb( } @GetMapping("/local/id/{id}") - public GameDto getFromDbById(@PathVariable Long id) { - return gameService.getFromDbById(id); + public GameDto getFromDbByApiId(@PathVariable Long id) { + return gameService.getFromDbByApiId(id); } - @GetMapping("/local/genre/{genre}") - public ResponseEntity getFromDbByGenre( - @PathVariable String genre, - @PageableDefault(size = 30) + @GetMapping("/local/search") + public Page search( + @ModelAttribute GameSearchParameters searchParameters, + @PageableDefault(size = DEFAULT_PAGE_SIZE) Pageable pageable ) { - Genre.Name name; - try { - name = Genre.Name.valueOf(genre.toUpperCase()); - } catch (IllegalArgumentException ex) { - return ResponseEntity - .badRequest() - .body("There is no genre by name: " + genre - + ". Allowed values: " + Arrays.asList(Genre.Name.values()) - ); - } - return ResponseEntity.ok(gameService.getFromDbByGenre(name, pageable)); - } - - @GetMapping("/local/year/{year}") - public Page getFromDbByYear( - @PathVariable int year, - @PageableDefault(size = 30) - Pageable pageable - ) { - return gameService.getByYear(year, pageable); + validateSearchParams(searchParameters); + return gameService.search(searchParameters, pageable); } - @GetMapping("/local/platform/{platform}") - public ResponseEntity getFromDbByPlatform( - @PathVariable String platform, - @PageableDefault(size = 30) - Pageable pageable - ) { - Platform.GeneralName name; - try { - name = Platform.GeneralName.valueOf(platform.toUpperCase()); - - } catch (IllegalArgumentException ex) { - return ResponseEntity - .badRequest() - .body("There is no platform by name: " + platform - + ". Allowed values: " + Arrays.asList(Platform.GeneralName.values()) - ); + private void validateSearchParams(GameSearchParameters searchParameters) { + if (searchParameters.platforms() != null) { + try { + searchParameters.platforms() + .forEach( + p -> Platform.GeneralName.valueOf(p.toUpperCase()) + ); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid platforms provided. Valid platforms: " + + Arrays.stream(Platform.GeneralName.values()) + .map(Enum::toString) + .collect(Collectors.joining(", ")), e); + } + } + if (searchParameters.genres() != null) { + try { + searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase())); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid genres provided. Valid genres: " + + Arrays.stream(Genre.Name.values()) + .map(Enum::toString) + .collect(Collectors.joining(", ")), e); + } } - return ResponseEntity.ok(gameService.getFromDbByPlatform(name, pageable)); } } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java new file mode 100644 index 0000000..ad56359 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java @@ -0,0 +1,11 @@ +package com.videogamescatalogue.backend.dto.internal; + +import java.util.List; + +public record GameSearchParameters( + String name, + Integer year, + List platforms, + List genres +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java b/src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java new file mode 100644 index 0000000..06b67c4 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.exception; + +public class SpecificationProviderNotFoundException extends RuntimeException { + public SpecificationProviderNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 41f8ab0..00f762c 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -60,4 +60,18 @@ public class Game { @Column(nullable = false, name = "is_deleted") private boolean isDeleted = false; + + @Getter + public enum SpecificationKey { + NAME("name"), + YEAR("year"), + PLATFORMS("platforms"), + GENRES("genres"); + + private final String value; + + SpecificationKey(String value) { + this.value = value; + } + } } diff --git a/src/main/java/com/videogamescatalogue/backend/model/Genre.java b/src/main/java/com/videogamescatalogue/backend/model/Genre.java index a7fddd9..094735f 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Genre.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Genre.java @@ -31,6 +31,7 @@ public class Genre { @Column(nullable = false, name = "is_deleted") private boolean isDeleted = false; + @Getter public enum Name { ACTION("Action"), INDIE("Indie"), diff --git a/src/main/java/com/videogamescatalogue/backend/model/Platform.java b/src/main/java/com/videogamescatalogue/backend/model/Platform.java index 7c4a3bd..7c5120d 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Platform.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Platform.java @@ -31,6 +31,7 @@ public class Platform { @Column(nullable = false, name = "is_deleted") private boolean isDeleted = false; + @Getter public enum GeneralName { PC("PC"), PLAYSTATION("PlayStation"), diff --git a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java index 635aa16..eda9e08 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java @@ -1,16 +1,10 @@ package com.videogamescatalogue.backend.repository; import com.videogamescatalogue.backend.model.Game; -import com.videogamescatalogue.backend.model.Genre; -import com.videogamescatalogue.backend.model.Platform; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface GameRepository extends JpaRepository { - Page findByGenresName(Genre.Name genre, Pageable pageable); - - Page findByYear(int year, Pageable pageable); - - Page findByPlatformsGeneralName(Platform.GeneralName generalName, Pageable pageable); +public interface GameRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByApiId(Long apiId); } diff --git a/src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java new file mode 100644 index 0000000..460beb7 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.repository; + +import org.springframework.data.jpa.domain.Specification; + +public interface SpecificationBuilder { + Specification build(T searchParameters); +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java new file mode 100644 index 0000000..7873128 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java @@ -0,0 +1,9 @@ +package com.videogamescatalogue.backend.repository; + +import org.springframework.data.jpa.domain.Specification; + +public interface SpecificationProvider { + String getKey(); + + Specification getSpecification(P param); +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java new file mode 100644 index 0000000..d0a55bf --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java @@ -0,0 +1,5 @@ +package com.videogamescatalogue.backend.repository; + +public interface SpecificationProviderManager { + SpecificationProvider getSpecificationProvider(String key); +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java new file mode 100644 index 0000000..ad5ee03 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java @@ -0,0 +1,60 @@ +package com.videogamescatalogue.backend.repository.game; + +import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.repository.SpecificationBuilder; +import com.videogamescatalogue.backend.repository.SpecificationProvider; +import com.videogamescatalogue.backend.repository.SpecificationProviderManager; +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GameSpecificationBuilder + implements SpecificationBuilder { + private final SpecificationProviderManager specificationProviderManager; + + @Override + public Specification build(GameSearchParameters searchParameters) { + if (searchParameters == null) { + throw new IllegalArgumentException("Search Parameters cannot be null"); + } + Specification specification = Specification.where(null); + specification = getSpecificationForParam( + searchParameters.name(), + Game.SpecificationKey.NAME.getValue(), specification + ); + specification = getSpecificationForParam( + searchParameters.year(), + Game.SpecificationKey.YEAR.getValue(), specification + ); + specification = getSpecificationForParam( + searchParameters.platforms(), + Game.SpecificationKey.PLATFORMS.getValue(), specification + ); + specification = getSpecificationForParam( + searchParameters.genres(), + Game.SpecificationKey.GENRES.getValue(), specification + ); + + return specification; + } + + @SuppressWarnings("unchecked") + private Specification getSpecificationForParam( + T searchParameter, + String key, + Specification specification + ) { + if (searchParameter != null) { + SpecificationProvider provider = + (SpecificationProvider) specificationProviderManager + .getSpecificationProvider(key); + return specification.and( + provider.getSpecification(searchParameter) + ); + } + return specification; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java new file mode 100644 index 0000000..ea06f5b --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java @@ -0,0 +1,28 @@ +package com.videogamescatalogue.backend.repository.game; + +import com.videogamescatalogue.backend.exception.SpecificationProviderNotFoundException; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.repository.SpecificationProvider; +import com.videogamescatalogue.backend.repository.SpecificationProviderManager; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class GameSpecificationProviderManager + implements SpecificationProviderManager { + private final List> specificationProvidersList; + + @Override + public SpecificationProvider getSpecificationProvider(String key) { + return specificationProvidersList.stream() + .filter(provider -> provider.getKey().equals(key)) + .findFirst() + .orElseThrow( + () -> new SpecificationProviderNotFoundException( + "Can't find SpecificationProvider for key: " + + key) + ); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java new file mode 100644 index 0000000..e17716f --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java @@ -0,0 +1,47 @@ +package com.videogamescatalogue.backend.repository.game.spec; + +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Genre; +import com.videogamescatalogue.backend.repository.SpecificationProvider; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.util.List; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class GameGenresSpecificationProvider implements SpecificationProvider> { + + public static final String KEY = Game.SpecificationKey.GENRES.getValue(); + + @Override + public String getKey() { + return KEY; + } + + @Override + public Specification getSpecification(List param) { + return new Specification() { + @Override + public Predicate toPredicate( + Root root, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder + ) { + query.distinct(true); + + List names = param.stream() + .map(String::toUpperCase) + .map(Genre.Name::valueOf) + .toList(); + + Join genreJoin = root.join(KEY); + + return genreJoin.get("name").in(names); + } + }; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java new file mode 100644 index 0000000..cb7f2a0 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java @@ -0,0 +1,37 @@ +package com.videogamescatalogue.backend.repository.game.spec; + +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.repository.SpecificationProvider; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class GameNameSpecificationProvider implements SpecificationProvider { + + public static final String KEY = Game.SpecificationKey.NAME.getValue(); + + @Override + public String getKey() { + return KEY; + } + + @Override + public Specification getSpecification(String param) { + return new Specification() { + @Override + public Predicate toPredicate( + Root root, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder + ) { + return criteriaBuilder.like( + criteriaBuilder.lower(root.get(KEY)), + "%" + param.toLowerCase() + "%"); + } + }; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java new file mode 100644 index 0000000..c72ceae --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java @@ -0,0 +1,48 @@ +package com.videogamescatalogue.backend.repository.game.spec; + +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Platform; +import com.videogamescatalogue.backend.repository.SpecificationProvider; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.util.List; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class GamePlatformsSpecificationProvider + implements SpecificationProvider> { + + public static final String KEY = Game.SpecificationKey.PLATFORMS.getValue(); + + @Override + public String getKey() { + return KEY; + } + + @Override + public Specification getSpecification(List param) { + return new Specification() { + @Override + public Predicate toPredicate( + Root root, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder + ) { + query.distinct(true); + + List names = param.stream() + .map(String::toUpperCase) + .map(Platform.GeneralName::valueOf) + .toList(); + + Join platromJoin = root.join(KEY); + + return platromJoin.get("generalName").in(names); + } + }; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java new file mode 100644 index 0000000..7ed36b2 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java @@ -0,0 +1,35 @@ +package com.videogamescatalogue.backend.repository.game.spec; + +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.repository.SpecificationProvider; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class GameYearSpecificationProvider implements SpecificationProvider { + + public static final String KEY = Game.SpecificationKey.YEAR.getValue(); + + @Override + public String getKey() { + return KEY; + } + + @Override + public Specification getSpecification(Integer param) { + return new Specification() { + @Override + public Predicate toPredicate( + Root root, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder + ) { + return criteriaBuilder.equal(root.get(KEY), param); + } + }; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/GameService.java index 169384d..0e57ae5 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/GameService.java @@ -1,8 +1,7 @@ package com.videogamescatalogue.backend.service; import com.videogamescatalogue.backend.dto.internal.GameDto; -import com.videogamescatalogue.backend.model.Genre; -import com.videogamescatalogue.backend.model.Platform; +import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -11,11 +10,7 @@ public interface GameService { Page getAllGamesFromDb(Pageable pageable); - GameDto getFromDbById(Long id); + GameDto getFromDbByApiId(Long id); - Page getFromDbByGenre(Genre.Name genre, Pageable pageable); - - Page getByYear(int year, Pageable pageable); - - Page getFromDbByPlatform(Platform.GeneralName platform, Pageable pageable); + Page search(GameSearchParameters searchParameters, Pageable pageable); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java index 088f8e1..def5699 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java @@ -2,17 +2,18 @@ import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.GameDto; +import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.mapper.GameMapper; import com.videogamescatalogue.backend.model.Game; -import com.videogamescatalogue.backend.model.Genre; -import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.repository.GameRepository; +import com.videogamescatalogue.backend.repository.SpecificationBuilder; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -21,6 +22,7 @@ public class GameServiceImpl implements GameService { private final RawgApiClient apiClient; private final GameMapper gameMapper; private final GameRepository gameRepository; + private final SpecificationBuilder specificationBuilder; @Override public void fetchFromDb() { @@ -38,30 +40,19 @@ public Page getAllGamesFromDb(Pageable pageable) { } @Override - public GameDto getFromDbById(Long id) { - Optional gameOptional = gameRepository.findById(id); + public GameDto getFromDbByApiId(Long apiId) { + Optional gameOptional = gameRepository.findByApiId(apiId); if (gameOptional.isEmpty()) { - throw new EntityNotFoundException("There is no game in DB by id:" + id); + throw new EntityNotFoundException("There is no game in DB by api id:" + apiId); } - return gameMapper.toDto(gameOptional.get()); } @Override - public Page getFromDbByGenre(Genre.Name genre, Pageable pageable) { - return gameRepository.findByGenresName(genre, pageable) - .map(gameMapper::toDto); - } - - @Override - public Page getByYear(int year, Pageable pageable) { - return gameRepository.findByYear(year, pageable) - .map(gameMapper::toDto); - } + public Page search(GameSearchParameters searchParameters, Pageable pageable) { + Specification specification = specificationBuilder.build(searchParameters); - @Override - public Page getFromDbByPlatform(Platform.GeneralName platform, Pageable pageable) { - return gameRepository.findByPlatformsGeneralName(platform, pageable) + return gameRepository.findAll(specification, pageable) .map(gameMapper::toDto); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 34f5576..6eba6a0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,3 +17,7 @@ rawg.key=${RAWG_KEY} management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true management.endpoint.health.show-details=always + +logging.level.org.springframework.web.bind.WebDataBinder=DEBUG +logging.level.org.springframework.web.method.annotation=DEBUG +logging.level.org.springframework.web.servlet.mvc.method.annotation=DEBUG From 389d612fb5a3d6c8aea45760365273117a7c6b5c Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 13 Dec 2025 13:57:51 +0200 Subject: [PATCH 03/41] Add user registration login (#11) * implemented registration and login * renamed username field, corrected structure of files * implemented user auth --- pom.xml | 21 +++++- ...VideogamescatalogueBackendApplication.java | 2 +- .../backend/config/SecurityConfig.java | 70 +++++++++++++++++++ .../controller/AuthenticationController.java | 34 +++++++++ .../backend/controller/GameController.java | 4 +- .../backend/dto/internal/GenreDto.java | 6 -- .../backend/dto/internal/PlatformDto.java | 6 -- .../dto/internal/{ => game}/GameDto.java | 4 +- .../backend/dto/internal/genre/GenreDto.java | 6 ++ .../dto/internal/platform/PlatformDto.java | 6 ++ .../internal/user/UserLoginRequestDto.java | 16 +++++ .../internal/user/UserLoginResponseDto.java | 4 ++ .../user/UserRegistrationRequestDto.java | 20 ++++++ .../dto/internal/user/UserResponseDto.java | 8 +++ .../exception/JwtAuthenticationException.java | 7 ++ .../exception/RegistrationException.java | 7 ++ .../backend/mapper/{ => game}/GameMapper.java | 6 +- .../mapper/{ => genre}/GenreProvider.java | 2 +- .../mapper/{ => platform}/PlatformMapper.java | 4 +- .../{ => platform}/PlatformProvider.java | 4 +- .../backend/mapper/user/UserMapper.java | 16 +++++ .../backend/model/User.java | 69 ++++++++++++++++++ .../backend/repository/UserRepository.java | 11 +++ .../security/AuthenticationService.java | 8 +++ .../security/AuthenticationServiceImpl.java | 26 +++++++ .../security/CustomUserDetailsService.java | 22 ++++++ .../security/JwtAuthenticationFilter.java | 60 ++++++++++++++++ .../backend/security/JwtUtil.java | 59 ++++++++++++++++ .../service/{ => game}/GameService.java | 4 +- .../service/{ => game}/GameServiceImpl.java | 7 +- .../backend/service/user/UserService.java | 10 +++ .../backend/service/user/UserServiceImpl.java | 37 ++++++++++ src/main/resources/application.properties | 3 + .../changes/08-create-users-table.yaml | 45 ++++++++++++ .../db/changelog/db.changelog-master.yaml | 2 + 35 files changed, 587 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java create mode 100644 src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java delete mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/GenreDto.java delete mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/PlatformDto.java rename src/main/java/com/videogamescatalogue/backend/dto/internal/{ => game}/GameDto.java (55%) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/genre/GenreDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/platform/PlatformDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginRequestDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/JwtAuthenticationException.java create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/RegistrationException.java rename src/main/java/com/videogamescatalogue/backend/mapper/{ => game}/GameMapper.java (84%) rename src/main/java/com/videogamescatalogue/backend/mapper/{ => genre}/GenreProvider.java (97%) rename src/main/java/com/videogamescatalogue/backend/mapper/{ => platform}/PlatformMapper.java (80%) rename src/main/java/com/videogamescatalogue/backend/mapper/{ => platform}/PlatformProvider.java (95%) create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java create mode 100644 src/main/java/com/videogamescatalogue/backend/model/User.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java create mode 100644 src/main/java/com/videogamescatalogue/backend/security/AuthenticationService.java create mode 100644 src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java create mode 100644 src/main/java/com/videogamescatalogue/backend/security/CustomUserDetailsService.java create mode 100644 src/main/java/com/videogamescatalogue/backend/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java rename src/main/java/com/videogamescatalogue/backend/service/{ => game}/GameService.java (75%) rename src/main/java/com/videogamescatalogue/backend/service/{ => game}/GameServiceImpl.java (87%) create mode 100644 src/main/java/com/videogamescatalogue/backend/service/user/UserService.java create mode 100644 src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java create mode 100644 src/main/resources/db/changelog/changes/08-create-users-table.yaml diff --git a/pom.xml b/pom.xml index 42c589c..caa89ab 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ checkstyle.xml 1.6.2 0.2.0 + 0.11.5 @@ -54,6 +55,10 @@ org.springframework.boot spring-boot-starter-jdbc + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-validation @@ -64,7 +69,6 @@ mysql-connector-java 8.0.32 - org.projectlombok lombok @@ -88,6 +92,21 @@ org.springframework.boot spring-boot-starter-actuator + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + diff --git a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java index cf4de2e..0538206 100644 --- a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java +++ b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java @@ -1,6 +1,6 @@ package com.videogamescatalogue.backend; -import com.videogamescatalogue.backend.service.GameService; +import com.videogamescatalogue.backend.service.game.GameService; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; diff --git a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java new file mode 100644 index 0000000..0a95573 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java @@ -0,0 +1,70 @@ +package com.videogamescatalogue.backend.config; + +import com.videogamescatalogue.backend.security.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableMethodSecurity +@RequiredArgsConstructor +@Configuration +public class SecurityConfig { + public static final String[] PUBLIC_ENDPOINTS = {"/auth/**", "/games/**", "/error", + "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health"}; + private final UserDetailsService userDetailsService; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + auth -> auth + .requestMatchers(PUBLIC_ENDPOINTS) + .permitAll() + .anyRequest() + .authenticated() + ) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy( + SessionCreationPolicy.STATELESS + )) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class) + .userDetailsService(userDetailsService) + .exceptionHandling(ex -> ex.authenticationEntryPoint( + new HttpStatusEntryPoint( + HttpStatus.UNAUTHORIZED + ) + ) + ) + .build(); + } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java new file mode 100644 index 0000000..e4fd185 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java @@ -0,0 +1,34 @@ +package com.videogamescatalogue.backend.controller; + +import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.security.AuthenticationService; +import com.videogamescatalogue.backend.service.user.UserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthenticationController { + private final UserService userService; + private final AuthenticationService authenticationService; + + @PostMapping("/registration") + UserResponseDto registerUser( + @RequestBody @Valid UserRegistrationRequestDto requestDto + ) { + return userService.registerUser(requestDto); + } + + @PostMapping("/login") + UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) { + return authenticationService.authenticate(requestDto); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index 1ca3d37..f3db478 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -1,10 +1,10 @@ package com.videogamescatalogue.backend.controller; -import com.videogamescatalogue.backend.dto.internal.GameDto; import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.model.Genre; import com.videogamescatalogue.backend.model.Platform; -import com.videogamescatalogue.backend.service.GameService; +import com.videogamescatalogue.backend.service.game.GameService; import java.util.Arrays; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/GenreDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/GenreDto.java deleted file mode 100644 index c90efbd..0000000 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/GenreDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.videogamescatalogue.backend.dto.internal; - -public record GenreDto( - String name -) { -} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/PlatformDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/PlatformDto.java deleted file mode 100644 index c2bfa78..0000000 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/PlatformDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.videogamescatalogue.backend.dto.internal; - -public record PlatformDto( - String generalName -) { -} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java similarity index 55% rename from src/main/java/com/videogamescatalogue/backend/dto/internal/GameDto.java rename to src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java index b8cb409..61a16fd 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java @@ -1,5 +1,7 @@ -package com.videogamescatalogue.backend.dto.internal; +package com.videogamescatalogue.backend.dto.internal.game; +import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; +import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import java.math.BigDecimal; import java.util.Set; diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/genre/GenreDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/genre/GenreDto.java new file mode 100644 index 0000000..00b7141 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/genre/GenreDto.java @@ -0,0 +1,6 @@ +package com.videogamescatalogue.backend.dto.internal.genre; + +public record GenreDto( + String name +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/platform/PlatformDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/platform/PlatformDto.java new file mode 100644 index 0000000..a3a7167 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/platform/PlatformDto.java @@ -0,0 +1,6 @@ +package com.videogamescatalogue.backend.dto.internal.platform; + +public record PlatformDto( + String generalName +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginRequestDto.java new file mode 100644 index 0000000..85cbfbd --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginRequestDto.java @@ -0,0 +1,16 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UserLoginRequestDto( + @Email(message = "Invalid format of email") + @NotBlank(message = "Email is required. Please provide your email.") + String email, + + @NotBlank(message = "Password is required. Please provide your password.") + @Size(min = 8, max = 25, message = "Password must be between 8 and 25 digits") + String password +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java new file mode 100644 index 0000000..30a904c --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java @@ -0,0 +1,4 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +public record UserLoginResponseDto(String token) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java new file mode 100644 index 0000000..089cbc1 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java @@ -0,0 +1,20 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UserRegistrationRequestDto( + @NotBlank(message = "Username is required. Please provide username.") + String profileName, + @NotBlank(message = "Password is required. Please provide your password.") + @Size(min = 8, max = 25, message = "Password must be between 8 and 25 digits") + String password, + @NotBlank(message = "Please, repeat your password.") + @Size(min = 8, max = 25, message = "Repeat password must be between 8 and 25 digits") + String repeatPassword, + @Email(message = "Invalid format of email.") + @NotBlank(message = "Email is required. Please provide your email.") + String email +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java new file mode 100644 index 0000000..807c03c --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java @@ -0,0 +1,8 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +public record UserResponseDto( + Long id, + String profileName, + String email +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/exception/JwtAuthenticationException.java b/src/main/java/com/videogamescatalogue/backend/exception/JwtAuthenticationException.java new file mode 100644 index 0000000..29e3450 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/JwtAuthenticationException.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.exception; + +public class JwtAuthenticationException extends RuntimeException { + public JwtAuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/exception/RegistrationException.java b/src/main/java/com/videogamescatalogue/backend/exception/RegistrationException.java new file mode 100644 index 0000000..409f049 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/RegistrationException.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.exception; + +public class RegistrationException extends RuntimeException { + public RegistrationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java similarity index 84% rename from src/main/java/com/videogamescatalogue/backend/mapper/GameMapper.java rename to src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java index 1ae7d19..9eb02a1 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/GameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java @@ -1,10 +1,12 @@ -package com.videogamescatalogue.backend.mapper; +package com.videogamescatalogue.backend.mapper.game; import com.videogamescatalogue.backend.config.MapperConfig; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; -import com.videogamescatalogue.backend.dto.internal.GameDto; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.exception.ApiException; import com.videogamescatalogue.backend.exception.ParsingException; +import com.videogamescatalogue.backend.mapper.genre.GenreProvider; +import com.videogamescatalogue.backend.mapper.platform.PlatformProvider; import com.videogamescatalogue.backend.model.Game; import java.time.LocalDate; import java.time.format.DateTimeParseException; diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/GenreProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java similarity index 97% rename from src/main/java/com/videogamescatalogue/backend/mapper/GenreProvider.java rename to src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java index 77a124e..363fd7a 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/GenreProvider.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java @@ -1,4 +1,4 @@ -package com.videogamescatalogue.backend.mapper; +package com.videogamescatalogue.backend.mapper.genre; import com.videogamescatalogue.backend.dto.external.ApiResponseGenreDto; import com.videogamescatalogue.backend.model.Genre; diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/PlatformMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformMapper.java similarity index 80% rename from src/main/java/com/videogamescatalogue/backend/mapper/PlatformMapper.java rename to src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformMapper.java index 4ddeaf3..f4dbb57 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/PlatformMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformMapper.java @@ -1,7 +1,7 @@ -package com.videogamescatalogue.backend.mapper; +package com.videogamescatalogue.backend.mapper.platform; import com.videogamescatalogue.backend.config.MapperConfig; -import com.videogamescatalogue.backend.dto.internal.PlatformDto; +import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.model.Platform; import java.util.Set; import org.mapstruct.Mapper; diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/PlatformProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java similarity index 95% rename from src/main/java/com/videogamescatalogue/backend/mapper/PlatformProvider.java rename to src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java index 33a259a..2fdb590 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/PlatformProvider.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java @@ -1,8 +1,8 @@ -package com.videogamescatalogue.backend.mapper; +package com.videogamescatalogue.backend.mapper.platform; import com.videogamescatalogue.backend.dto.external.ApiPlatformWrapper; import com.videogamescatalogue.backend.dto.external.ApiResponsePlatformDto; -import com.videogamescatalogue.backend.dto.internal.PlatformDto; +import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.repository.PlatformRepository; import jakarta.annotation.PostConstruct; diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java new file mode 100644 index 0000000..47b7917 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java @@ -0,0 +1,16 @@ +package com.videogamescatalogue.backend.mapper.user; + +import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.model.User; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface UserMapper { + @Mapping(target = "password", ignore = true) + User toModel(UserRegistrationRequestDto requestDto); + + UserResponseDto toDto(User user); +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/User.java b/src/main/java/com/videogamescatalogue/backend/model/User.java new file mode 100644 index 0000000..93b3a2a --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/model/User.java @@ -0,0 +1,69 @@ +package com.videogamescatalogue.backend.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Collection; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@Entity +@Table(name = "users") +@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?") +@SQLRestriction(value = "is_deleted = false") +@Getter +@Setter +public class User implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String profileName; + + @Column(nullable = false) + private String password; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false, name = "is_deleted") + private boolean isDeleted = false; + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return !isDeleted; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java new file mode 100644 index 0000000..8ba976d --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.videogamescatalogue.backend.repository; + +import com.videogamescatalogue.backend.model.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/videogamescatalogue/backend/security/AuthenticationService.java b/src/main/java/com/videogamescatalogue/backend/security/AuthenticationService.java new file mode 100644 index 0000000..1473206 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/security/AuthenticationService.java @@ -0,0 +1,8 @@ +package com.videogamescatalogue.backend.security; + +import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; + +public interface AuthenticationService { + UserLoginResponseDto authenticate(UserLoginRequestDto requestDto); +} diff --git a/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java new file mode 100644 index 0000000..8e6a5f4 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java @@ -0,0 +1,26 @@ +package com.videogamescatalogue.backend.security; + +import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class AuthenticationServiceImpl implements AuthenticationService { + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + @Override + public UserLoginResponseDto authenticate(UserLoginRequestDto requestDto) { + final Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + requestDto.email(), requestDto.password()) + ); + String token = jwtUtil.generateToken(authentication.getName()); + return new UserLoginResponseDto(token); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/security/CustomUserDetailsService.java b/src/main/java/com/videogamescatalogue/backend/security/CustomUserDetailsService.java new file mode 100644 index 0000000..5ac4ed5 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/security/CustomUserDetailsService.java @@ -0,0 +1,22 @@ +package com.videogamescatalogue.backend.security; + +import com.videogamescatalogue.backend.repository.UserRepository; +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; + +@RequiredArgsConstructor +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email).orElseThrow( + () -> new UsernameNotFoundException("Can't find user by email: " + + email) + ); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/security/JwtAuthenticationFilter.java b/src/main/java/com/videogamescatalogue/backend/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..dbde90a --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/security/JwtAuthenticationFilter.java @@ -0,0 +1,60 @@ +package com.videogamescatalogue.backend.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + public static final String BEARER_TOKEN_STRING_START = "Bearer "; + public static final int BEARER_STRING_LENGTH = 7; + public static final int STRING_START_INDEX = 0; + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = getToken(request); + if (token != null && jwtUtil.isValidToken(token)) { + String username = jwtUtil.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, + userDetails.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String getToken(HttpServletRequest request) { + String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(bearerToken) && bearerToken.regionMatches( + true, + STRING_START_INDEX, + BEARER_TOKEN_STRING_START, + STRING_START_INDEX, + BEARER_STRING_LENGTH + )) { + return bearerToken.substring(BEARER_STRING_LENGTH); + } + return null; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java new file mode 100644 index 0000000..87d254f --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java @@ -0,0 +1,59 @@ +package com.videogamescatalogue.backend.security; + +import com.videogamescatalogue.backend.exception.JwtAuthenticationException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.function.Function; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + @Value("${jwt.expiration}") + private long expiration; + private final Key secret; + + public JwtUtil(@Value("${jwt.secret}") String secretString) { + this.secret = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String email) { + return Jwts.builder() + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secret) + .compact(); + } + + public boolean isValidToken(String token) { + try { + Jws claims = getAllClaims(token); + return !claims.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtAuthenticationException("Expired or invalid JWT token. ", e); + } + } + + private Jws getAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(secret) + .build() + .parseClaimsJws(token); + } + + public String getUsername(String token) { + return getSpecificClaim(token, Claims::getSubject); + } + + private T getSpecificClaim(String token, Function claimsResolver) { + Jws allClaims = getAllClaims(token); + return claimsResolver.apply(allClaims.getBody()); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java similarity index 75% rename from src/main/java/com/videogamescatalogue/backend/service/GameService.java rename to src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index 0e57ae5..b59dee1 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -1,7 +1,7 @@ -package com.videogamescatalogue.backend.service; +package com.videogamescatalogue.backend.service.game; -import com.videogamescatalogue.backend.dto.internal.GameDto; import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java similarity index 87% rename from src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java rename to src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index def5699..941a592 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -1,13 +1,14 @@ -package com.videogamescatalogue.backend.service; +package com.videogamescatalogue.backend.service.game; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; -import com.videogamescatalogue.backend.dto.internal.GameDto; import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.exception.EntityNotFoundException; -import com.videogamescatalogue.backend.mapper.GameMapper; +import com.videogamescatalogue.backend.mapper.game.GameMapper; import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.repository.GameRepository; import com.videogamescatalogue.backend.repository.SpecificationBuilder; +import com.videogamescatalogue.backend.service.RawgApiClient; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java new file mode 100644 index 0000000..c4aadf3 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java @@ -0,0 +1,10 @@ +package com.videogamescatalogue.backend.service.user; + +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; + +public interface UserService { + UserResponseDto registerUser( + UserRegistrationRequestDto requestDto + ); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java new file mode 100644 index 0000000..ba78285 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -0,0 +1,37 @@ +package com.videogamescatalogue.backend.service.user; + +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.exception.RegistrationException; +import com.videogamescatalogue.backend.mapper.user.UserMapper; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final UserMapper userMapper; + private final PasswordEncoder passwordEncoder; + + @Override + public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { + checkUserAlreadyExists(requestDto.email()); + + User user = userMapper.toModel(requestDto); + user.setPassword(passwordEncoder.encode(requestDto.password())); + + User savedUser = userRepository.save(user); + return userMapper.toDto(savedUser); + } + + private void checkUserAlreadyExists(String email) { + if (userRepository.existsByEmail(email)) { + throw new RegistrationException("Can't register user. User with email: " + + email + " is already registered."); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6eba6a0..f355f85 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,3 +21,6 @@ management.endpoint.health.show-details=always logging.level.org.springframework.web.bind.WebDataBinder=DEBUG logging.level.org.springframework.web.method.annotation=DEBUG logging.level.org.springframework.web.servlet.mvc.method.annotation=DEBUG + +jwt.secret=${JWT_SECRET} +jwt.expiration=${JWT_EXPIRATION} diff --git a/src/main/resources/db/changelog/changes/08-create-users-table.yaml b/src/main/resources/db/changelog/changes/08-create-users-table.yaml new file mode 100644 index 0000000..b4edd07 --- /dev/null +++ b/src/main/resources/db/changelog/changes/08-create-users-table.yaml @@ -0,0 +1,45 @@ +databaseChangeLog: + - changeSet: + id: create-users-table + author: Yuliia + changes: + - createTable: + tableName: users + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: profile_name + type: VARCHAR(50) + constraints: + nullable: false + + - column: + name: password + type: VARCHAR(255) + constraints: + nullable: false + + - column: + name: email + type: VARCHAR(255) + constraints: + nullable: false + + - column: + name: is_deleted + type: BOOLEAN + defaultValueBoolean: false + constraints: + nullable: false + + - addUniqueConstraint: + tableName: users + columnNames: email + constraintName: uq_users_email \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index f826078..453fde1 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -13,3 +13,5 @@ databaseChangeLog: file: classpath:/db/changelog/changes/06-create-games-platforms-table.yaml - include: file: classpath:/db/changelog/changes/07-create-games-genres-table.yaml + - include: + file: classpath:/db/changelog/changes/08-create-users-table.yaml From 65af954ae94ef8ac7cc7111183b459be128b995d Mon Sep 17 00:00:00 2001 From: Yuliia Date: Sat, 13 Dec 2025 16:02:53 +0200 Subject: [PATCH 04/41] fetched description --- .../backend/controller/GameController.java | 1 + .../dto/external/ApiResponseFullGameDto.java | 20 ++++++++++++++ .../backend/dto/internal/game/GameDto.java | 4 ++- .../backend/model/Game.java | 2 ++ .../backend/service/RawgApiClient.java | 26 +++++++++++++++++-- .../backend/service/game/GameServiceImpl.java | 12 ++++++++- .../changes/05-create-games-table.yaml | 4 +++ 7 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index f3db478..6751ca3 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -36,6 +36,7 @@ public Page getAllGamesFromDb( @GetMapping("/local/id/{id}") public GameDto getFromDbByApiId(@PathVariable Long id) { + return gameService.getFromDbByApiId(id); } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java new file mode 100644 index 0000000..53741cf --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java @@ -0,0 +1,20 @@ +package com.videogamescatalogue.backend.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import java.util.List; + +public record ApiResponseFullGameDto( + Long id, + String name, + String description, + String released, + @JsonProperty(value = "background_image") + String backgroundImage, + List platforms, + + List genres, + + BigDecimal rating +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java index 61a16fd..094ef3d 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java @@ -6,12 +6,14 @@ import java.util.Set; public record GameDto( + Long id, Long apiId, String name, int year, String backgroundImage, Set platforms, Set genres, - BigDecimal apiRating + BigDecimal apiRating, + String description ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 00f762c..2c1a635 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -58,6 +58,8 @@ public class Game { private BigDecimal apiRating; + private String description; + @Column(nullable = false, name = "is_deleted") private boolean isDeleted = false; diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index dc72152..6d6cc68 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGames; import com.videogamescatalogue.backend.exception.ApiException; @@ -40,12 +41,22 @@ public List getAllGames() { .GET() .uri(URI.create(url)) .build(); - ApiResponseGames responseObject = getResponseObject(httpRequest); + ApiResponseGames responseObject = getResponseGamesList(httpRequest); return new ArrayList<>(responseObject.results()); } - private ApiResponseGames getResponseObject(HttpRequest httpRequest) { + public ApiResponseFullGameDto getGameById(Long id) { + String url = BASE_URL + GAME_URL_PART + "/" + + id + KEY_URL_PART + apiKey; + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .build(); + return getIndividualGame(httpRequest); + } + + private ApiResponseGames getResponseGamesList(HttpRequest httpRequest) { try { return objectMapper.readValue( getHttpResponse(httpRequest).body(), @@ -56,6 +67,17 @@ private ApiResponseGames getResponseObject(HttpRequest httpRequest) { } } + private ApiResponseFullGameDto getIndividualGame(HttpRequest httpRequest) { + try { + return objectMapper.readValue( + getHttpResponse(httpRequest).body(), + ApiResponseFullGameDto.class); + } catch (JacksonException e) { + throw new ObjectMapperException("URL: " + httpRequest.uri() + + " Failed to read httpResponse: ", e); + } + } + private HttpResponse getHttpResponse(HttpRequest httpRequest) { try { HttpResponse response = httpClient.send(httpRequest, diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 941a592..7cbb1aa 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -1,5 +1,6 @@ package com.videogamescatalogue.backend.service.game; +import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; @@ -46,7 +47,16 @@ public GameDto getFromDbByApiId(Long apiId) { if (gameOptional.isEmpty()) { throw new EntityNotFoundException("There is no game in DB by api id:" + apiId); } - return gameMapper.toDto(gameOptional.get()); + + Game dbGame = gameOptional.get(); + + if (dbGame.getDescription() == null) { + ApiResponseFullGameDto game = apiClient.getGameById(apiId); + dbGame.setDescription(game.description()); + Game savedGame = gameRepository.save(dbGame); + return gameMapper.toDto(savedGame); + } + return gameMapper.toDto(dbGame); } @Override diff --git a/src/main/resources/db/changelog/changes/05-create-games-table.yaml b/src/main/resources/db/changelog/changes/05-create-games-table.yaml index 67bb87f..8df3cec 100644 --- a/src/main/resources/db/changelog/changes/05-create-games-table.yaml +++ b/src/main/resources/db/changelog/changes/05-create-games-table.yaml @@ -38,6 +38,10 @@ databaseChangeLog: name: api_rating type: DECIMAL(4,2) + - column: + name: description + type: VARCHAR(5000) + - column: name: is_deleted type: BOOLEAN From 87eb0dfcc19a97c8fc946a202598e1c969c88905 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:10:11 +0200 Subject: [PATCH 05/41] Merge pull request #13 from mategames-team/add-game-lists-to-user Add game lists to user --- ...VideogamescatalogueBackendApplication.java | 2 +- .../controller/UserGameController.java | 56 +++++++++ .../internal/usergame/CreateUserGameDto.java | 9 ++ .../dto/internal/usergame/UserGameDto.java | 10 ++ .../exception/AccessNotAllowedException.java | 7 ++ .../backend/mapper/game/GameMapper.java | 10 ++ .../mapper/usergame/UserGameMapper.java | 16 +++ .../backend/model/Game.java | 1 + .../backend/model/User.java | 14 +++ .../backend/model/UserGame.java | 45 ++++++++ .../repository/UserGameRepository.java | 15 +++ .../backend/service/RawgApiClient.java | 2 +- .../backend/service/game/GameService.java | 4 +- .../backend/service/game/GameServiceImpl.java | 12 +- .../service/usergame/UserGameService.java | 20 ++++ .../service/usergame/UserGameServiceImpl.java | 107 ++++++++++++++++++ src/main/resources/application.properties | 1 - .../changes/05-create-games-table.yaml | 3 + .../changes/09-create-user-game-table.yaml | 52 +++++++++ .../db/changelog/db.changelog-master.yaml | 2 + 20 files changed, 383 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/CreateUserGameDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/AccessNotAllowedException.java create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java create mode 100644 src/main/java/com/videogamescatalogue/backend/model/UserGame.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java create mode 100644 src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java create mode 100644 src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java create mode 100644 src/main/resources/db/changelog/changes/09-create-user-game-table.yaml diff --git a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java index 0538206..e9e20b4 100644 --- a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java +++ b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java @@ -19,6 +19,6 @@ public static void main(String[] args) { @Override public void run(String... args) throws Exception { - gameService.fetchFromDb(); + gameService.fetchBestGames(); } } diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java new file mode 100644 index 0000000..517119b --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java @@ -0,0 +1,56 @@ +package com.videogamescatalogue.backend.controller; + +import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.model.UserGame; +import com.videogamescatalogue.backend.service.usergame.UserGameService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/user-games") +@RestController +public class UserGameController { + public static final int DEFAULT_PAGE_SIZE = 30; + private final UserGameService userGameService; + + @PostMapping + public UserGameDto createOrUpdate( + @RequestBody CreateUserGameDto createDto, + @AuthenticationPrincipal User user + ) { + return userGameService.createOrUpdate(createDto, user); + } + + @DeleteMapping("/{id}") + public void delete( + @PathVariable Long id, + @AuthenticationPrincipal User user + ) { + userGameService.delete(id, user); + } + + @GetMapping + public Page getByStatus( + @RequestParam UserGame.GameStatus status, + @AuthenticationPrincipal User user, + @PageableDefault(size = DEFAULT_PAGE_SIZE) + Pageable pageable + ) { + return userGameService.getByStatus( + status, user.getId(), pageable + ); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/CreateUserGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/CreateUserGameDto.java new file mode 100644 index 0000000..4dacba9 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/CreateUserGameDto.java @@ -0,0 +1,9 @@ +package com.videogamescatalogue.backend.dto.internal.usergame; + +import com.videogamescatalogue.backend.model.UserGame; + +public record CreateUserGameDto( + Long apiId, + UserGame.GameStatus status +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java new file mode 100644 index 0000000..d70a8bc --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java @@ -0,0 +1,10 @@ +package com.videogamescatalogue.backend.dto.internal.usergame; + +public record UserGameDto( + Long id, + Long userId, + Long gameId, + Long gameApiId, + String status +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/exception/AccessNotAllowedException.java b/src/main/java/com/videogamescatalogue/backend/exception/AccessNotAllowedException.java new file mode 100644 index 0000000..9aa4bb0 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/AccessNotAllowedException.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.exception; + +public class AccessNotAllowedException extends RuntimeException { + public AccessNotAllowedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java index 9eb02a1..e7960a0 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java @@ -1,6 +1,7 @@ package com.videogamescatalogue.backend.mapper.game; import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.exception.ApiException; @@ -19,6 +20,7 @@ public interface GameMapper { List toModelList(List games); + @Mapping(target = "id", ignore = true) @Mapping(source = "id", target = "apiId") @Mapping(source = "released", target = "year", qualifiedByName = "toYear") @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformsSet") @@ -26,6 +28,14 @@ public interface GameMapper { @Mapping(source = "rating", target = "apiRating") Game toModel(ApiResponseGameDto apiResponseGameDto); + @Mapping(target = "id", ignore = true) + @Mapping(source = "id", target = "apiId") + @Mapping(source = "released", target = "year", qualifiedByName = "toYear") + @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformsSet") + @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenresSet") + @Mapping(source = "rating", target = "apiRating") + Game toModel(ApiResponseFullGameDto apiResponseGameDto); + @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatfromDtosSet") GameDto toDto(Game game); diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java new file mode 100644 index 0000000..62bc9e6 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java @@ -0,0 +1,16 @@ +package com.videogamescatalogue.backend.mapper.usergame; + +import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; +import com.videogamescatalogue.backend.model.UserGame; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface UserGameMapper { + @Mapping(source = "user.id", target = "userId") + @Mapping(source = "game.id", target = "gameId") + @Mapping(source = "game.apiId", target = "gameApiId") + UserGameDto toDto(UserGame userGame); + +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 2c1a635..8c75915 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -29,6 +29,7 @@ public class Game { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, unique = true) private Long apiId; @Column(nullable = false) diff --git a/src/main/java/com/videogamescatalogue/backend/model/User.java b/src/main/java/com/videogamescatalogue/backend/model/User.java index 93b3a2a..622d6d6 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/User.java +++ b/src/main/java/com/videogamescatalogue/backend/model/User.java @@ -5,8 +5,13 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import java.util.Collection; +import java.util.Set; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.SQLDelete; @@ -34,6 +39,15 @@ public class User implements UserDetails { @Column(nullable = false, unique = true) private String email; + @ManyToMany + @JoinTable( + name = "users_games", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "game_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "game_id"}) + ) + private Set games; + @Column(nullable = false, name = "is_deleted") private boolean isDeleted = false; diff --git a/src/main/java/com/videogamescatalogue/backend/model/UserGame.java b/src/main/java/com/videogamescatalogue/backend/model/UserGame.java new file mode 100644 index 0000000..c4e9cc7 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/model/UserGame.java @@ -0,0 +1,45 @@ +package com.videogamescatalogue.backend.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "user_games", + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "game_id"})) +@Getter +@Setter +public class UserGame { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(optional = false) + @JoinColumn(name = "game_id") + private Game game; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private GameStatus status; + + @Getter + public enum GameStatus { + BACKLOG, + IN_PROGRESS, + COMPLETED + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java new file mode 100644 index 0000000..2e83633 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java @@ -0,0 +1,15 @@ +package com.videogamescatalogue.backend.repository; + +import com.videogamescatalogue.backend.model.UserGame; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserGameRepository extends JpaRepository { + Optional findByUserIdAndGameApiId(Long userId, Long apiId); + + Page findByUserIdAndStatus( + Long userId, UserGame.GameStatus status, Pageable pageable + ); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index 6d6cc68..e1adcc1 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -91,7 +91,7 @@ private HttpResponse getHttpResponse(HttpRequest httpRequest) { } catch (IOException | InterruptedException e) { throw new ApiException("URL: " + httpRequest.uri() - + " Cannot get all games from API: ", e); + + " Cannot get all game(s) from API: ", e); } } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index b59dee1..f8e4316 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -6,7 +6,9 @@ import org.springframework.data.domain.Pageable; public interface GameService { - void fetchFromDb(); + void fetchBestGames(); + + GameDto fetchSingleGame(Long id); Page getAllGamesFromDb(Pageable pageable); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 7cbb1aa..5b03e1c 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -27,7 +27,7 @@ public class GameServiceImpl implements GameService { private final SpecificationBuilder specificationBuilder; @Override - public void fetchFromDb() { + public void fetchBestGames() { List apiGames = apiClient.getAllGames(); List modelList = gameMapper.toModelList(apiGames); @@ -35,6 +35,16 @@ public void fetchFromDb() { gameRepository.saveAll(modelList); } + @Override + public GameDto fetchSingleGame(Long id) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(id); + Game game = gameMapper.toModel(apiGame); + + Game savedGame = gameRepository.save(game); + + return gameMapper.toDto(savedGame); + } + @Override public Page getAllGamesFromDb(Pageable pageable) { return gameRepository.findAll(pageable) diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java new file mode 100644 index 0000000..f5636fa --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java @@ -0,0 +1,20 @@ +package com.videogamescatalogue.backend.service.usergame; + +import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.model.UserGame; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface UserGameService { + UserGameDto createOrUpdate(CreateUserGameDto createDto, User user); + + void delete(Long id, User user); + + Page getByStatus( + UserGame.GameStatus status, + Long userId, + Pageable pageable + ); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java new file mode 100644 index 0000000..ae75c5d --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java @@ -0,0 +1,107 @@ +package com.videogamescatalogue.backend.service.usergame; + +import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; +import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; +import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.exception.EntityNotFoundException; +import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.mapper.usergame.UserGameMapper; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.model.UserGame; +import com.videogamescatalogue.backend.repository.GameRepository; +import com.videogamescatalogue.backend.repository.UserGameRepository; +import com.videogamescatalogue.backend.service.RawgApiClient; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class UserGameServiceImpl implements UserGameService { + private final GameRepository gameRepository; + private final UserGameRepository userGameRepository; + private final RawgApiClient apiClient; + private final UserGameMapper userGameMapper; + private final GameMapper gameMapper; + + @Override + public UserGameDto createOrUpdate(CreateUserGameDto createDto, User user) { + Optional userGameOptional = userGameRepository.findByUserIdAndGameApiId( + user.getId(), + createDto.apiId() + ); + if (userGameOptional.isPresent()) { + return updateUserGameStatus(createDto.status(), userGameOptional.get()); + } + + UserGame userGame = createNewUserGame(createDto, user); + + Optional gameOptional = gameRepository.findByApiId(createDto.apiId()); + if (gameOptional.isPresent()) { + return addGameAndSave(userGame, gameOptional.get()); + } + + Game game = getGameFromApi(createDto.apiId()); + Game savedGame = gameRepository.save(game); + + return addGameAndSave(userGame, savedGame); + } + + @Override + public void delete(Long id, User user) { + checkBelongsToUser(id, user.getId()); + userGameRepository.deleteById(id); + } + + @Override + public Page getByStatus( + UserGame.GameStatus status, + Long userId, + Pageable pageable) { + Page userGames = userGameRepository.findByUserIdAndStatus( + userId, status, pageable + ); + return userGames.map(userGameMapper::toDto); + } + + private void checkBelongsToUser(Long id, Long userId) { + Optional userGameOptional = userGameRepository.findById(id); + if (userGameOptional.isEmpty()) { + throw new EntityNotFoundException("There is no userGame by id: " + id); + } + + Long userGameUserId = userGameOptional.get().getUser().getId(); + if (!userGameUserId.equals(userId)) { + throw new AccessNotAllowedException("User with id: " + + userId + "is not allowed to access userGame with id: " + id); + } + } + + private UserGameDto addGameAndSave(UserGame userGame, Game game) { + userGame.setGame(game); + UserGame savedUserGame = userGameRepository.save(userGame); + return userGameMapper.toDto(savedUserGame); + } + + private UserGame createNewUserGame(CreateUserGameDto createDto, User user) { + UserGame userGame = new UserGame(); + userGame.setUser(user); + userGame.setStatus(createDto.status()); + return userGame; + } + + private UserGameDto updateUserGameStatus(UserGame.GameStatus status, UserGame userGame) { + userGame.setStatus(status); + UserGame savedUserGame = userGameRepository.save(userGame); + return userGameMapper.toDto(savedUserGame); + } + + private Game getGameFromApi(Long apiId) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + return gameMapper.toModel(apiGame); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f355f85..15c5bc4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,7 +7,6 @@ spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml -spring.liquibase.drop-first=true spring.jpa.hibernate.ddl-auto=none server.servlet.context-path=/api diff --git a/src/main/resources/db/changelog/changes/05-create-games-table.yaml b/src/main/resources/db/changelog/changes/05-create-games-table.yaml index 8df3cec..c1634a9 100644 --- a/src/main/resources/db/changelog/changes/05-create-games-table.yaml +++ b/src/main/resources/db/changelog/changes/05-create-games-table.yaml @@ -19,6 +19,7 @@ databaseChangeLog: type: BIGINT constraints: nullable: false + unique: true - column: name: name @@ -37,6 +38,8 @@ databaseChangeLog: - column: name: api_rating type: DECIMAL(4,2) + constraints: + nullable: false - column: name: description diff --git a/src/main/resources/db/changelog/changes/09-create-user-game-table.yaml b/src/main/resources/db/changelog/changes/09-create-user-game-table.yaml new file mode 100644 index 0000000..bb19ca5 --- /dev/null +++ b/src/main/resources/db/changelog/changes/09-create-user-game-table.yaml @@ -0,0 +1,52 @@ +databaseChangeLog: + - changeSet: + id: create-user-games-table + author: Yuliia + changes: + - createTable: + tableName: user_games + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: user_id + type: BIGINT + constraints: + nullable: false + + - column: + name: game_id + type: BIGINT + constraints: + nullable: false + + - column: + name: status + type: VARCHAR(50) + constraints: + nullable: false + + - addForeignKeyConstraint: + baseTableName: user_games + baseColumnNames: user_id + referencedTableName: users + referencedColumnNames: id + constraintName: fk_user_games_user + + - addForeignKeyConstraint: + baseTableName: user_games + baseColumnNames: game_id + referencedTableName: games + referencedColumnNames: id + constraintName: fk_user_games_game + + - addUniqueConstraint: + tableName: user_games + columnNames: user_id, game_id + constraintName: uk_user_game diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 453fde1..bd9bd47 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -15,3 +15,5 @@ databaseChangeLog: file: classpath:/db/changelog/changes/07-create-games-genres-table.yaml - include: file: classpath:/db/changelog/changes/08-create-users-table.yaml + - include: + file: classpath:/db/changelog/changes/09-create-user-game-table.yaml From 55cc8740eb3cac5edd61e3fc9f6b6f35ac5997ca Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:58:39 +0200 Subject: [PATCH 06/41] Improve fetch best games (#14) * changed get url * added already existing game check * fetch best games, update existing * changed sending configuration, added loggin * added logging to errors --- .../backend/config/HttpClientConfig.java | 4 +- .../backend/repository/GameRepository.java | 3 + .../backend/service/RawgApiClient.java | 65 +++++++++++++++---- .../backend/service/game/GameService.java | 2 + .../backend/service/game/GameServiceImpl.java | 51 ++++++++++++++- 5 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/config/HttpClientConfig.java b/src/main/java/com/videogamescatalogue/backend/config/HttpClientConfig.java index fa7b473..c9d5c6c 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/HttpClientConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/HttpClientConfig.java @@ -8,6 +8,8 @@ public class HttpClientConfig { @Bean public HttpClient httpClient() { - return HttpClient.newHttpClient(); + return HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); } } diff --git a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java index eda9e08..cf88999 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java @@ -1,10 +1,13 @@ package com.videogamescatalogue.backend.repository; import com.videogamescatalogue.backend.model.Game; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface GameRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByApiId(Long apiId); + + List findAllByApiIdIn(List apiIds); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index e1adcc1..98395fa 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -13,37 +13,66 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +@Slf4j @RequiredArgsConstructor @Service public class RawgApiClient { private static final String BASE_URL = "https://api.rawg.io/api/"; private static final String GAME_URL_PART = "games"; private static final String KEY_URL_PART = "?key="; - private static final String PAGE_URL_PART = "&page=1"; - private static final String PAGE_SIZE_URL_PART = "&page_size=100"; - private static final String ORDERING_URL_PART = "&ordering=-rating"; + private static final String PAGE_NUMBER_URL_PART = "&page="; + private static final String PAGE_SIZE_URL_PART = "&page_size=30"; + private static final String ORDERING_URL_PART = "&ordering=-added"; + private static final String EXCLUDE_ADDITIONS_URL_PART = "&exclude_additions=true"; + private static final String DATES_BETWEEN_URL_PART = "&dates=" + LocalDate.now() + + "%2C" + LocalDate.now().minusMonths(12); + private static final String METACRITIC_URL_PART = "metacritic=80%2C100"; + @Value("${rawg.key}") private String apiKey; private final ObjectMapper objectMapper; private final HttpClient httpClient; - public List getAllGames() { - String url = BASE_URL + GAME_URL_PART - + KEY_URL_PART + apiKey - + PAGE_URL_PART + PAGE_SIZE_URL_PART + ORDERING_URL_PART; - HttpRequest httpRequest = HttpRequest.newBuilder() - .GET() - .uri(URI.create(url)) - .build(); - ApiResponseGames responseObject = getResponseGamesList(httpRequest); + public List getBestGames() { + ArrayList result = new ArrayList<>(); + + for (int i = 1; i < 11; i++) { + log.info("Create request for page: " + i); + + String url = BASE_URL + GAME_URL_PART + + KEY_URL_PART + apiKey + + ORDERING_URL_PART + + EXCLUDE_ADDITIONS_URL_PART + + DATES_BETWEEN_URL_PART + + METACRITIC_URL_PART + + PAGE_SIZE_URL_PART + + PAGE_NUMBER_URL_PART + i; + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .header("User-Agent", "VideoGamesCatalogue") + .build(); + ApiResponseGames responseObject = getResponseGamesList(httpRequest); - return new ArrayList<>(responseObject.results()); + result.addAll(responseObject.results()); + + log.info("Added games to resul list. Result list size: " + result.size()); + } + + log.info( + "Formed result list with fetched games to return. List size: " + + result.size() + ); + + return result; } public ApiResponseFullGameDto getGameById(Long id) { @@ -52,6 +81,7 @@ public ApiResponseFullGameDto getGameById(Long id) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) + .header("User-Agent", "VideoGamesCatalogue") .build(); return getIndividualGame(httpRequest); } @@ -62,6 +92,7 @@ private ApiResponseGames getResponseGamesList(HttpRequest httpRequest) { getHttpResponse(httpRequest).body(), ApiResponseGames.class); } catch (JacksonException e) { + log.error("Failed to read httpResponse:", e); throw new ObjectMapperException("URL: " + httpRequest.uri() + " Failed to read httpResponse: ", e); } @@ -73,6 +104,7 @@ private ApiResponseFullGameDto getIndividualGame(HttpRequest httpRequest) { getHttpResponse(httpRequest).body(), ApiResponseFullGameDto.class); } catch (JacksonException e) { + log.error("Failed to read httpResponse:", e); throw new ObjectMapperException("URL: " + httpRequest.uri() + " Failed to read httpResponse: ", e); } @@ -80,18 +112,23 @@ private ApiResponseFullGameDto getIndividualGame(HttpRequest httpRequest) { private HttpResponse getHttpResponse(HttpRequest httpRequest) { try { + log.info("Send request to API"); HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { + log.error("Received non-200 status code." + + "Response status code: " + + response.statusCode()); throw new HttpResponseException("Received non-200 status code. " + "Response status code: " + response.statusCode()); } return response; } catch (IOException | InterruptedException e) { + log.error("Failed to get game(s) from API: ", e); throw new ApiException("URL: " + httpRequest.uri() - + " Cannot get all game(s) from API: ", e); + + " Cannot get game(s) from API: ", e); } } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index f8e4316..94b0a51 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -8,6 +8,8 @@ public interface GameService { void fetchBestGames(); + void fetchAndUpdateBestGames(); + GameDto fetchSingleGame(Long id); Page getAllGamesFromDb(Pageable pageable); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 5b03e1c..e87ae42 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -10,14 +10,20 @@ import com.videogamescatalogue.backend.repository.GameRepository; import com.videogamescatalogue.backend.repository.SpecificationBuilder; import com.videogamescatalogue.backend.service.RawgApiClient; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +@Slf4j @RequiredArgsConstructor @Service public class GameServiceImpl implements GameService { @@ -28,10 +34,32 @@ public class GameServiceImpl implements GameService { @Override public void fetchBestGames() { - List apiGames = apiClient.getAllGames(); + List apiGames = apiClient.getBestGames(); + log.info("Received list of games from Api. List size: " + apiGames.size()); List modelList = gameMapper.toModelList(apiGames); + Map existingGamesMap = getExistingGamesMap(modelList); + List toSaveGames = new ArrayList<>(); + + for (Game game : modelList) { + if (!existingGamesMap.containsKey(game.getApiId())) { + toSaveGames.add(game); + } + } + + gameRepository.saveAll(toSaveGames); + log.info("Saved games to DB"); + } + + @Override + public void fetchAndUpdateBestGames() { + List apiGames = apiClient.getBestGames(); + List modelList = gameMapper.toModelList(apiGames); + + Map existingGamesMap = getExistingGamesMap(modelList); + setIdIfExistingGame(modelList, existingGamesMap); + gameRepository.saveAll(modelList); } @@ -76,4 +104,25 @@ public Page search(GameSearchParameters searchParameters, Pageable page return gameRepository.findAll(specification, pageable) .map(gameMapper::toDto); } + + private Map getExistingGamesMap(List modelList) { + List apiIds = modelList.stream() + .map(Game::getApiId) + .toList(); + List existingGames = gameRepository.findAllByApiIdIn(apiIds); + return existingGames.stream() + .collect(Collectors.toMap( + Game::getApiId, Function.identity() + )); + } + + private void setIdIfExistingGame(List modelList, Map existingGamesMap) { + for (Game game : modelList) { + if (existingGamesMap.containsKey(game.getApiId())) { + game.setId( + existingGamesMap.get(game.getApiId()).getId() + ); + } + } + } } From e8d1baaab093338f2ee4be69641c1d075b9f0199 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:44:41 +0200 Subject: [PATCH 07/41] created user info update and get (#15) --- .../backend/controller/UserController.java | 47 ++++++++++++++ .../user/ChangePasswordRequestDto.java | 21 +++++++ .../internal/user/UpdateUserRequestDto.java | 15 +++++ .../user/UserRegistrationRequestDto.java | 10 ++- .../dto/internal/user/UserResponseDto.java | 4 +- .../exception/InvalidInputException.java | 7 +++ .../backend/mapper/user/UserMapper.java | 4 ++ .../backend/model/User.java | 4 ++ .../backend/service/RawgApiClient.java | 16 ++--- .../backend/service/game/GameServiceImpl.java | 3 +- .../backend/service/user/UserService.java | 9 +++ .../backend/service/user/UserServiceImpl.java | 55 ++++++++++++++++ .../backend/validation/FieldMatch.java | 21 +++++++ .../validation/FieldMatchValidator.java | 63 +++++++++++++++++++ .../resources/ValidationMessages.properties | 2 + .../changes/08-create-users-table.yaml | 8 +++ 16 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/controller/UserController.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/ChangePasswordRequestDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/InvalidInputException.java create mode 100644 src/main/java/com/videogamescatalogue/backend/validation/FieldMatch.java create mode 100644 src/main/java/com/videogamescatalogue/backend/validation/FieldMatchValidator.java create mode 100644 src/main/resources/ValidationMessages.properties diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java new file mode 100644 index 0000000..af8daf2 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java @@ -0,0 +1,47 @@ +package com.videogamescatalogue.backend.controller; + +import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.service.user.UserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/users") +@RestController +public class UserController { + private final UserService userService; + + @GetMapping("/{id}") + public UserResponseDto getUserInfo( + @PathVariable Long id, + @AuthenticationPrincipal User user + ) { + return userService.getUserInfo(id, user); + } + + @PatchMapping("/me") + public UserResponseDto updateUserInfo( + @Valid @RequestBody UpdateUserRequestDto requestDto, + @AuthenticationPrincipal User user + ) { + return userService.updateUserInfo(requestDto, user); + } + + @PatchMapping("/me/password") + public UserResponseDto changePassword( + @Valid @RequestBody ChangePasswordRequestDto requestDto, + @AuthenticationPrincipal User user + ) { + return userService.changePassword(requestDto, user); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/ChangePasswordRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/ChangePasswordRequestDto.java new file mode 100644 index 0000000..0e62318 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/ChangePasswordRequestDto.java @@ -0,0 +1,21 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +import com.videogamescatalogue.backend.validation.FieldMatch; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +@FieldMatch(first = "newPassword", + second = "repeatPassword", + message = "New password and repeat password must match") +public record ChangePasswordRequestDto( + @NotBlank(message = "Password is required. Please provide your current password.") + @Size(min = 8, max = 25, message = "Password must be between 8 and 25 digits") + String currentPassword, + @NotBlank(message = "Password is required. Please provide your password.") + @Size(min = 8, max = 25, message = "Password must be between 8 and 25 digits") + String newPassword, + @NotBlank(message = "Please, repeat your password.") + @Size(min = 8, max = 25, message = "Repeat password must be between 8 and 25 digits") + String repeatPassword +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java new file mode 100644 index 0000000..90bdc95 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java @@ -0,0 +1,15 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; + +public record UpdateUserRequestDto( + String profileName, + @Email(message = "Invalid format of email.") + String email, + @Size(max = 5000, message = "About user info must be less than 5000 digits") + String about, + @Size(max = 50, message = "Location info must be less than 50 digits") + String location +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java index 089cbc1..3c4b168 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java @@ -1,9 +1,13 @@ package com.videogamescatalogue.backend.dto.internal.user; +import com.videogamescatalogue.backend.validation.FieldMatch; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +@FieldMatch(first = "password", + second = "repeatPassword", + message = "Password and repeat password must match") public record UserRegistrationRequestDto( @NotBlank(message = "Username is required. Please provide username.") String profileName, @@ -15,6 +19,10 @@ public record UserRegistrationRequestDto( String repeatPassword, @Email(message = "Invalid format of email.") @NotBlank(message = "Email is required. Please provide your email.") - String email + String email, + @Size(max = 5000, message = "About user info must be less than 5000 digits") + String about, + @Size(max = 50, message = "Location info must be less than 50 digits") + String location ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java index 807c03c..573b7e1 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java @@ -3,6 +3,8 @@ public record UserResponseDto( Long id, String profileName, - String email + String email, + String about, + String location ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/exception/InvalidInputException.java b/src/main/java/com/videogamescatalogue/backend/exception/InvalidInputException.java new file mode 100644 index 0000000..c772b84 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/InvalidInputException.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.exception; + +public class InvalidInputException extends RuntimeException { + public InvalidInputException(String message) { + super(message); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java index 47b7917..bbe94dc 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java @@ -1,11 +1,13 @@ package com.videogamescatalogue.backend.mapper.user; import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.model.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; @Mapper(config = MapperConfig.class) public interface UserMapper { @@ -13,4 +15,6 @@ public interface UserMapper { User toModel(UserRegistrationRequestDto requestDto); UserResponseDto toDto(User user); + + User updateProfileInfo(@MappingTarget User user, UpdateUserRequestDto requestDto); } diff --git a/src/main/java/com/videogamescatalogue/backend/model/User.java b/src/main/java/com/videogamescatalogue/backend/model/User.java index 622d6d6..627bcd9 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/User.java +++ b/src/main/java/com/videogamescatalogue/backend/model/User.java @@ -39,6 +39,10 @@ public class User implements UserDetails { @Column(nullable = false, unique = true) private String email; + private String about; + + private String location; + @ManyToMany @JoinTable( name = "users_games", diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index 98395fa..f3e6a17 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -17,11 +17,11 @@ import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -@Slf4j +@Log4j2 @RequiredArgsConstructor @Service public class RawgApiClient { @@ -45,7 +45,7 @@ public List getBestGames() { ArrayList result = new ArrayList<>(); for (int i = 1; i < 11; i++) { - log.info("Create request for page: " + i); + log.info("Create request for page {}", i); String url = BASE_URL + GAME_URL_PART + KEY_URL_PART + apiKey @@ -64,12 +64,12 @@ public List getBestGames() { result.addAll(responseObject.results()); - log.info("Added games to resul list. Result list size: " + result.size()); + log.info("Added games to resul list. Result list size={}", result.size()); } log.info( - "Formed result list with fetched games to return. List size: " - + result.size() + "Formed result list with fetched games to return. List size={}", + result.size() ); return result; @@ -116,16 +116,12 @@ private HttpResponse getHttpResponse(HttpRequest httpRequest) { HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { - log.error("Received non-200 status code." - + "Response status code: " - + response.statusCode()); throw new HttpResponseException("Received non-200 status code. " + "Response status code: " + response.statusCode()); } return response; } catch (IOException | InterruptedException e) { - log.error("Failed to get game(s) from API: ", e); throw new ApiException("URL: " + httpRequest.uri() + " Cannot get game(s) from API: ", e); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index e87ae42..39c9de4 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -35,7 +35,8 @@ public class GameServiceImpl implements GameService { @Override public void fetchBestGames() { List apiGames = apiClient.getBestGames(); - log.info("Received list of games from Api. List size: " + apiGames.size()); + log.info("Received list of games from Api. List size={}", + apiGames.size()); List modelList = gameMapper.toModelList(apiGames); diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java index c4aadf3..73c69ac 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java @@ -1,10 +1,19 @@ package com.videogamescatalogue.backend.service.user; +import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.model.User; public interface UserService { UserResponseDto registerUser( UserRegistrationRequestDto requestDto ); + + UserResponseDto getUserInfo(Long userId, User authenticatedUser); + + UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser); + + UserResponseDto changePassword(ChangePasswordRequestDto requestDto, User authenticatedUser); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index ba78285..5648c6f 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -1,15 +1,21 @@ package com.videogamescatalogue.backend.service.user; +import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.exception.InvalidInputException; import com.videogamescatalogue.backend.exception.RegistrationException; import com.videogamescatalogue.backend.mapper.user.UserMapper; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +@Log4j2 @RequiredArgsConstructor @Service public class UserServiceImpl implements UserService { @@ -25,9 +31,58 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { user.setPassword(passwordEncoder.encode(requestDto.password())); User savedUser = userRepository.save(user); + + log.info("User registered successfully, id={}",savedUser.getId()); + + return userMapper.toDto(savedUser); + } + + @Override + public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { + checkUserCanAccess(userId, authenticatedUser); + return userMapper.toDto(authenticatedUser); + } + + @Override + public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser) { + User updatedUser = userMapper.updateProfileInfo(authenticatedUser, requestDto); + User savedUser = userRepository.save(updatedUser); + + log.info("User with id={} updated profile info.", authenticatedUser.getId()); + + return userMapper.toDto(savedUser); + } + + @Override + public UserResponseDto changePassword( + ChangePasswordRequestDto requestDto, User authenticatedUser + ) { + if (!passwordEncoder.matches( + requestDto.currentPassword(), + authenticatedUser.getPassword() + )) { + throw new InvalidInputException("Current password is not valid."); + } + + if (!requestDto.newPassword().equals(requestDto.repeatPassword())) { + throw new InvalidInputException("New password and repeat password must match"); + } + + authenticatedUser.setPassword(passwordEncoder.encode(requestDto.newPassword())); + User savedUser = userRepository.save(authenticatedUser); + + log.info("User with id={} changed password.", authenticatedUser.getId()); + return userMapper.toDto(savedUser); } + private void checkUserCanAccess(Long userId, User authenticatedUser) { + if (!userId.equals(authenticatedUser.getId())) { + throw new AccessNotAllowedException("User with id: " + authenticatedUser.getId() + + " is not allowed to access info of user with id: " + userId); + } + } + private void checkUserAlreadyExists(String email) { if (userRepository.existsByEmail(email)) { throw new RegistrationException("Can't register user. User with email: " diff --git a/src/main/java/com/videogamescatalogue/backend/validation/FieldMatch.java b/src/main/java/com/videogamescatalogue/backend/validation/FieldMatch.java new file mode 100644 index 0000000..1136000 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/validation/FieldMatch.java @@ -0,0 +1,21 @@ +package com.videogamescatalogue.backend.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = FieldMatchValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FieldMatch { + String message() default "{constraints.fieldmatch}"; + String errorWithValidationMessage() default "{constraints.fieldmatchValidationError}"; + Class[] groups() default {}; + Class[] payload() default {}; + + String first(); + String second(); +} diff --git a/src/main/java/com/videogamescatalogue/backend/validation/FieldMatchValidator.java b/src/main/java/com/videogamescatalogue/backend/validation/FieldMatchValidator.java new file mode 100644 index 0000000..48352a7 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/validation/FieldMatchValidator.java @@ -0,0 +1,63 @@ +package com.videogamescatalogue.backend.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.lang.reflect.Field; + +public class FieldMatchValidator implements ConstraintValidator { + private String first; + private String second; + private String notValidMessage; + private String errorWithValidationMessage; + + @Override + public void initialize(FieldMatch constraintAnnotation) { + this.first = constraintAnnotation.first(); + this.second = constraintAnnotation.second(); + this.notValidMessage = constraintAnnotation.message(); + this.errorWithValidationMessage = constraintAnnotation.errorWithValidationMessage(); + } + + @Override + public boolean isValid(Object object, ConstraintValidatorContext context) { + try { + Field firstField = getField(object, first); + Field secondField = getField(object, second); + if (firstField == null || secondField == null) { + return false; + } + if (!firstField.getType().equals(secondField.getType())) { + return false; + } + Object firstValue = firstField.get(object); + Object secondValue = secondField.get(object); + if (firstValue == null && secondValue == null) { + return true; + } + if (firstValue == null || !firstValue.equals(secondValue)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(notValidMessage) + .addPropertyNode(second) + .addConstraintViolation(); + return false; + } + return true; + } catch (IllegalAccessException e) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(errorWithValidationMessage) + .addPropertyNode(second) + .addConstraintViolation(); + return false; + } + } + + private Field getField(Object object, String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + return null; + } + } +} diff --git a/src/main/resources/ValidationMessages.properties b/src/main/resources/ValidationMessages.properties new file mode 100644 index 0000000..12631e6 --- /dev/null +++ b/src/main/resources/ValidationMessages.properties @@ -0,0 +1,2 @@ +constraints.fieldmatch=Fields do not match +constraints.fieldmatchValidationError=Validation error: cannot access fields diff --git a/src/main/resources/db/changelog/changes/08-create-users-table.yaml b/src/main/resources/db/changelog/changes/08-create-users-table.yaml index b4edd07..99ae4fb 100644 --- a/src/main/resources/db/changelog/changes/08-create-users-table.yaml +++ b/src/main/resources/db/changelog/changes/08-create-users-table.yaml @@ -32,6 +32,14 @@ databaseChangeLog: constraints: nullable: false + - column: + name: about + type: VARCHAR(5000) + + - column: + name: location + type: VARCHAR(50) + - column: name: is_deleted type: BOOLEAN From 151a67b7a746e5205fbfc0017cd55c68cefb2057 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:08:32 +0200 Subject: [PATCH 08/41] Refactor get by api id method (#16) * made method check if in db and if description is null * made method check if in db and if description is null * ran mvn cl package --- .../backend/controller/GameController.java | 7 ++-- .../backend/service/game/GameService.java | 4 +-- .../backend/service/game/GameServiceImpl.java | 33 ++++++++----------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index 6751ca3..e4a41c2 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -34,10 +34,9 @@ public Page getAllGamesFromDb( return gameService.getAllGamesFromDb(pageable); } - @GetMapping("/local/id/{id}") - public GameDto getFromDbByApiId(@PathVariable Long id) { - - return gameService.getFromDbByApiId(id); + @GetMapping("{id}") + public GameDto getByApiId(@PathVariable Long id) { + return gameService.getByApiId(id); } @GetMapping("/local/search") diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index 94b0a51..80a9f24 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -10,11 +10,9 @@ public interface GameService { void fetchAndUpdateBestGames(); - GameDto fetchSingleGame(Long id); - Page getAllGamesFromDb(Pageable pageable); - GameDto getFromDbByApiId(Long id); + GameDto getByApiId(Long id); Page search(GameSearchParameters searchParameters, Pageable pageable); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 39c9de4..86c91b9 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -4,7 +4,6 @@ import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; -import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.mapper.game.GameMapper; import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.repository.GameRepository; @@ -41,8 +40,8 @@ public void fetchBestGames() { List modelList = gameMapper.toModelList(apiGames); Map existingGamesMap = getExistingGamesMap(modelList); - List toSaveGames = new ArrayList<>(); + List toSaveGames = new ArrayList<>(); for (Game game : modelList) { if (!existingGamesMap.containsKey(game.getApiId())) { toSaveGames.add(game); @@ -64,16 +63,6 @@ public void fetchAndUpdateBestGames() { gameRepository.saveAll(modelList); } - @Override - public GameDto fetchSingleGame(Long id) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(id); - Game game = gameMapper.toModel(apiGame); - - Game savedGame = gameRepository.save(game); - - return gameMapper.toDto(savedGame); - } - @Override public Page getAllGamesFromDb(Pageable pageable) { return gameRepository.findAll(pageable) @@ -81,21 +70,25 @@ public Page getAllGamesFromDb(Pageable pageable) { } @Override - public GameDto getFromDbByApiId(Long apiId) { + public GameDto getByApiId(Long apiId) { Optional gameOptional = gameRepository.findByApiId(apiId); if (gameOptional.isEmpty()) { - throw new EntityNotFoundException("There is no game in DB by api id:" + apiId); + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + Game game = gameMapper.toModel(apiGame); + Game savedGame = gameRepository.save(game); + return gameMapper.toDto(savedGame); } - Game dbGame = gameOptional.get(); + Game game = gameOptional.get(); - if (dbGame.getDescription() == null) { - ApiResponseFullGameDto game = apiClient.getGameById(apiId); - dbGame.setDescription(game.description()); - Game savedGame = gameRepository.save(dbGame); + if (game.getDescription() == null) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + game.setDescription(apiGame.description()); + Game savedGame = gameRepository.save(game); return gameMapper.toDto(savedGame); } - return gameMapper.toDto(dbGame); + + return gameMapper.toDto(game); } @Override From 5547ad533bb3b442cf54940fa9e04ef3e714c5dd Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:03:06 +0200 Subject: [PATCH 09/41] Create get all games from api endpoint (#17) * created getAll method * made method retun page * created get all method --- .../backend/controller/GameController.java | 9 ++++ .../dto/external/ApiResponseGames.java | 1 + .../backend/mapper/game/GameMapper.java | 10 ++-- .../backend/mapper/genre/GenreProvider.java | 12 +++++ .../mapper/platform/PlatformProvider.java | 2 +- .../backend/model/Game.java | 2 +- .../backend/service/RawgApiClient.java | 53 ++++++++++++++++--- .../backend/service/game/GameService.java | 2 + .../backend/service/game/GameServiceImpl.java | 7 +++ 9 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index e4a41c2..0e92806 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -23,6 +23,7 @@ @RestController public class GameController { public static final int DEFAULT_PAGE_SIZE = 30; + public static final int DEFAULT_PAGE_NUMBER = 1; private final GameService gameService; @GetMapping("/local") @@ -39,6 +40,14 @@ public GameDto getByApiId(@PathVariable Long id) { return gameService.getByApiId(id); } + @GetMapping + public Page getAllGamesFromApi( + @PageableDefault(size = DEFAULT_PAGE_SIZE, page = DEFAULT_PAGE_NUMBER) + Pageable pageable + ) { + return gameService.getAllGamesFromApi(pageable); + } + @GetMapping("/local/search") public Page search( @ModelAttribute GameSearchParameters searchParameters, diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseGames.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseGames.java index 13a01d5..eb64a27 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseGames.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseGames.java @@ -3,6 +3,7 @@ import java.util.List; public record ApiResponseGames( + Long count, String next, List results ) { diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java index e7960a0..559f3f9 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java @@ -4,7 +4,6 @@ import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; -import com.videogamescatalogue.backend.exception.ApiException; import com.videogamescatalogue.backend.exception.ParsingException; import com.videogamescatalogue.backend.mapper.genre.GenreProvider; import com.videogamescatalogue.backend.mapper.platform.PlatformProvider; @@ -36,13 +35,14 @@ public interface GameMapper { @Mapping(source = "rating", target = "apiRating") Game toModel(ApiResponseFullGameDto apiResponseGameDto); - @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatfromDtosSet") + @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformDtosSet") + @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenreDtosSet") GameDto toDto(Game game); @Named("toYear") - default int toYear(String releasedDate) { - if (releasedDate == null) { - throw new ApiException("Release date should not be null"); + default Integer toYear(String releasedDate) { + if (releasedDate == null || releasedDate.isBlank()) { + return null; } try { LocalDate localDate = LocalDate.parse(releasedDate); diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java index 363fd7a..8b4ba75 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/genre/GenreProvider.java @@ -1,6 +1,7 @@ package com.videogamescatalogue.backend.mapper.genre; import com.videogamescatalogue.backend.dto.external.ApiResponseGenreDto; +import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.model.Genre; import com.videogamescatalogue.backend.repository.GenreRepository; import jakarta.annotation.PostConstruct; @@ -51,6 +52,17 @@ public Set toGenresSet(List apiGenres) { return gameGenres; } + @Named("toGenreDtosSet") + public Set toGenreDtosSet(Set genres) { + Set genreDtos = new HashSet<>(); + + for (Genre genre : genres) { + GenreDto genreDto = new GenreDto(genre.getName().getValue()); + genreDtos.add(genreDto); + } + return genreDtos; + } + private Genre getDefaultGenre(String name) { if (compareApiName(possibleGenreNames.get(Genre.Name.ACTION), name)) { return defaultGenres.get(Genre.Name.ACTION); diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java index 2fdb590..d90c5c0 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/platform/PlatformProvider.java @@ -56,7 +56,7 @@ public Set toPlatformSet(List apiPlatformWrappers) return gamePlatforms; } - @Named("toPlatfromDtosSet") + @Named("toPlatformDtosSet") public Set toPlatformDtosSet(Set platforms) { return platformMapper.toPlatfromDtosSet(platforms); } diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 8c75915..e4eaa2e 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -35,7 +35,7 @@ public class Game { @Column(nullable = false) private String name; - private int year; + private Integer year; private String backgroundImage; diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index f3e6a17..85cc29d 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -19,6 +19,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; @Log4j2 @@ -29,8 +33,10 @@ public class RawgApiClient { private static final String GAME_URL_PART = "games"; private static final String KEY_URL_PART = "?key="; private static final String PAGE_NUMBER_URL_PART = "&page="; - private static final String PAGE_SIZE_URL_PART = "&page_size=30"; - private static final String ORDERING_URL_PART = "&ordering=-added"; + private static final String PAGE_SIZE_URL_PART = "&page_size="; + private static final String DEFAULT_PAGE_SIZE = "30"; + private static final String ORDERING_URL_PART = "&ordering="; + private static final String ORDERING_ADDED_DESC = "-added"; private static final String EXCLUDE_ADDITIONS_URL_PART = "&exclude_additions=true"; private static final String DATES_BETWEEN_URL_PART = "&dates=" + LocalDate.now() + "%2C" + LocalDate.now().minusMonths(12); @@ -49,11 +55,11 @@ public List getBestGames() { String url = BASE_URL + GAME_URL_PART + KEY_URL_PART + apiKey - + ORDERING_URL_PART + + ORDERING_URL_PART + ORDERING_ADDED_DESC + EXCLUDE_ADDITIONS_URL_PART + DATES_BETWEEN_URL_PART + METACRITIC_URL_PART - + PAGE_SIZE_URL_PART + + PAGE_SIZE_URL_PART + DEFAULT_PAGE_SIZE + PAGE_NUMBER_URL_PART + i; HttpRequest httpRequest = HttpRequest.newBuilder() .GET() @@ -64,17 +70,37 @@ public List getBestGames() { result.addAll(responseObject.results()); - log.info("Added games to resul list. Result list size={}", result.size()); + log.info("Added games to resul list. Result list size={}", + result.size()); } - log.info( - "Formed result list with fetched games to return. List size={}", + log.info("Formed result list with fetched games to return. List size={}", result.size() ); return result; } + public Page getAllGames(Pageable pageable) { + String url = BASE_URL + GAME_URL_PART + + KEY_URL_PART + apiKey + + PAGE_SIZE_URL_PART + pageable.getPageSize() + + PAGE_NUMBER_URL_PART + pageable.getPageNumber(); + + String ordering = toRawgOrdering(pageable.getSort()); + if (ordering != null) { + url = url + ORDERING_URL_PART + ordering; + } + + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .header("User-Agent", "VideoGamesCatalogue") + .build(); + ApiResponseGames responseObject = getResponseGamesList(httpRequest); + return new PageImpl<>(responseObject.results(), pageable, responseObject.count()); + } + public ApiResponseFullGameDto getGameById(Long id) { String url = BASE_URL + GAME_URL_PART + "/" + id + KEY_URL_PART + apiKey; @@ -127,4 +153,17 @@ private HttpResponse getHttpResponse(HttpRequest httpRequest) { + " Cannot get game(s) from API: ", e); } } + + private String toRawgOrdering(Sort sort) { + if (sort.isUnsorted()) { + return null; + } + Sort.Order order = sort.iterator().next(); + String field = order.getProperty(); + Sort.Direction direction = order.getDirection(); + + return direction == Sort.Direction.DESC + ? "-" + field + : field; + } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index 80a9f24..ed43d70 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -14,5 +14,7 @@ public interface GameService { GameDto getByApiId(Long id); + Page getAllGamesFromApi(Pageable pageable); + Page search(GameSearchParameters searchParameters, Pageable pageable); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 86c91b9..4e62300 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -91,6 +91,13 @@ public GameDto getByApiId(Long apiId) { return gameMapper.toDto(game); } + @Override + public Page getAllGamesFromApi(Pageable pageable) { + return apiClient.getAllGames(pageable) + .map(gameMapper::toModel) + .map(gameMapper::toDto); + } + @Override public Page search(GameSearchParameters searchParameters, Pageable pageable) { Specification specification = specificationBuilder.build(searchParameters); From 981e8ff4190f3dfce1ff50b2751ba57150d432d6 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:56:05 +0200 Subject: [PATCH 10/41] Configure comments (#18) * added create comment endpoint * created create, get, delete endpoints * configured comments --- .../backend/controller/CommentController.java | 62 ++++++++++++ .../backend/controller/GameController.java | 9 +- .../dto/internal/comment/CommentDto.java | 11 +++ .../comment/CreateCommentRequestDto.java | 16 +++ .../comment/UpdateCommentRequestDto.java | 16 +++ .../backend/mapper/comment/CommentMapper.java | 23 +++++ .../backend/model/Comment.java | 43 ++++++++ .../backend/repository/CommentRepository.java | 12 +++ .../backend/service/RawgApiClient.java | 15 ++- .../service/comment/CommentService.java | 22 +++++ .../service/comment/CommentServiceImpl.java | 99 +++++++++++++++++++ .../changes/10-create-comments-table.yaml | 65 ++++++++++++ .../db/changelog/db.changelog-master.yaml | 2 + 13 files changed, 391 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/controller/CommentController.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java create mode 100644 src/main/java/com/videogamescatalogue/backend/model/Comment.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java create mode 100644 src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java create mode 100644 src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java create mode 100644 src/main/resources/db/changelog/changes/10-create-comments-table.yaml diff --git a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java new file mode 100644 index 0000000..8d45195 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java @@ -0,0 +1,62 @@ +package com.videogamescatalogue.backend.controller; + +import com.videogamescatalogue.backend.dto.internal.comment.CommentDto; +import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; +import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.service.comment.CommentService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping +@RestController +public class CommentController { + private final CommentService commentService; + + @PostMapping("/comments/games/{gameApiId}") + @ResponseStatus(value = HttpStatus.CREATED) + public CommentDto create( + @PathVariable Long gameApiId, + @RequestBody @Valid CreateCommentRequestDto requestDto, + @AuthenticationPrincipal User user + ) { + return commentService.create(gameApiId, requestDto, user); + } + + @GetMapping("/games/{gameApiId}/comments") + public Page getCommentsForGame(@PathVariable Long gameApiId, Pageable pageable) { + return commentService.getCommentsForGame(gameApiId, pageable); + } + + @PatchMapping("/comments/{commentId}") + public CommentDto update( + @PathVariable Long commentId, + @RequestBody @Valid UpdateCommentRequestDto requestDto, + @AuthenticationPrincipal User user + ) { + return commentService.update(commentId,requestDto, user.getId()); + } + + @DeleteMapping("/comments/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete( + @PathVariable Long commentId, + @AuthenticationPrincipal User user + ) { + commentService.delete(commentId, user.getId()); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index 0e92806..bbf06cf 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -35,9 +35,12 @@ public Page getAllGamesFromDb( return gameService.getAllGamesFromDb(pageable); } - @GetMapping("{id}") - public GameDto getByApiId(@PathVariable Long id) { - return gameService.getByApiId(id); + @GetMapping("{gameApiId}") + public GameDto getByApiId( + @PathVariable Long gameApiId, + @PageableDefault(size = DEFAULT_PAGE_SIZE) + Pageable pageable) { + return gameService.getByApiId(gameApiId); } @GetMapping diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java new file mode 100644 index 0000000..4498217 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java @@ -0,0 +1,11 @@ +package com.videogamescatalogue.backend.dto.internal.comment; + +public record CommentDto( + Long id, + Long gameApiId, + Long userId, + String text, + String localDateTime, + Integer rating +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java new file mode 100644 index 0000000..629448f --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java @@ -0,0 +1,16 @@ +package com.videogamescatalogue.backend.dto.internal.comment; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record CreateCommentRequestDto( + @Size(max = 2000, message = "Comment must be less than 2000 digits") + String text, + @NotNull + @Min(0) + @Max(5) + Integer rating +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java new file mode 100644 index 0000000..94e5059 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java @@ -0,0 +1,16 @@ +package com.videogamescatalogue.backend.dto.internal.comment; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record UpdateCommentRequestDto( + @Size(max = 2000, message = "Comment must be less than 2000 digits") + String text, + @NotNull + @Min(0) + @Max(5) + Integer rating +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java new file mode 100644 index 0000000..2504425 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java @@ -0,0 +1,23 @@ +package com.videogamescatalogue.backend.mapper.comment; + +import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.internal.comment.CommentDto; +import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; +import com.videogamescatalogue.backend.model.Comment; +import com.videogamescatalogue.backend.model.User; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapperConfig.class) +public interface CommentMapper { + Comment toModel(CreateCommentRequestDto requestDto); + + @Mapping(source = "user", target = "userId", qualifiedByName = "toUserId") + CommentDto toDto(Comment comment); + + @Named("toUserId") + default Long toUserId(User user) { + return user.getId(); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/Comment.java b/src/main/java/com/videogamescatalogue/backend/model/Comment.java new file mode 100644 index 0000000..6d95f25 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/model/Comment.java @@ -0,0 +1,43 @@ +package com.videogamescatalogue.backend.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "comments") +@Getter +@Setter +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "game_id") + private Game game; + + @Column(nullable = false) + private Long gameApiId; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false) + private String text; + + @Column(nullable = false) + private LocalDateTime localDateTime; + + @Column(nullable = false) + private Integer rating; +} diff --git a/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java new file mode 100644 index 0000000..53ccb84 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java @@ -0,0 +1,12 @@ +package com.videogamescatalogue.backend.repository; + +import com.videogamescatalogue.backend.model.Comment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { + Page findAllByGameApiId(Long gameApiId, Pageable pageable); + + boolean existsByIdAndUserId(Long id, Long userId); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index 85cc29d..0fc4588 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -48,6 +48,8 @@ public class RawgApiClient { private final HttpClient httpClient; public List getBestGames() { + log.info("Called get best games from API"); + ArrayList result = new ArrayList<>(); for (int i = 1; i < 11; i++) { @@ -82,6 +84,8 @@ public List getBestGames() { } public Page getAllGames(Pageable pageable) { + log.info("Called get all games from API"); + String url = BASE_URL + GAME_URL_PART + KEY_URL_PART + apiKey + PAGE_SIZE_URL_PART + pageable.getPageSize() @@ -98,10 +102,15 @@ public Page getAllGames(Pageable pageable) { .header("User-Agent", "VideoGamesCatalogue") .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); + + log.info("Received response from API"); + return new PageImpl<>(responseObject.results(), pageable, responseObject.count()); } public ApiResponseFullGameDto getGameById(Long id) { + log.info("Called get game by id from API"); + String url = BASE_URL + GAME_URL_PART + "/" + id + KEY_URL_PART + apiKey; HttpRequest httpRequest = HttpRequest.newBuilder() @@ -109,7 +118,11 @@ public ApiResponseFullGameDto getGameById(Long id) { .uri(URI.create(url)) .header("User-Agent", "VideoGamesCatalogue") .build(); - return getIndividualGame(httpRequest); + ApiResponseFullGameDto game = getIndividualGame(httpRequest); + + log.info("Received response from API"); + + return game; } private ApiResponseGames getResponseGamesList(HttpRequest httpRequest) { diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java new file mode 100644 index 0000000..c7b8b51 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java @@ -0,0 +1,22 @@ +package com.videogamescatalogue.backend.service.comment; + +import com.videogamescatalogue.backend.dto.internal.comment.CommentDto; +import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; +import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto; +import com.videogamescatalogue.backend.model.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CommentService { + CommentDto create( + Long gameApiId, + CreateCommentRequestDto requestDto, + User user + ); + + Page getCommentsForGame(Long id, Pageable pageable); + + CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId); + + void delete(Long commentId, Long userId); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java new file mode 100644 index 0000000..50438d3 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java @@ -0,0 +1,99 @@ +package com.videogamescatalogue.backend.service.comment; + +import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; +import com.videogamescatalogue.backend.dto.internal.comment.CommentDto; +import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; +import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto; +import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.exception.EntityNotFoundException; +import com.videogamescatalogue.backend.mapper.comment.CommentMapper; +import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Comment; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.repository.CommentRepository; +import com.videogamescatalogue.backend.repository.GameRepository; +import com.videogamescatalogue.backend.service.RawgApiClient; +import java.time.LocalDateTime; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class CommentServiceImpl implements CommentService { + private final CommentMapper commentMapper; + private final GameRepository gameRepository; + private final RawgApiClient apiClient; + private final GameMapper gameMapper; + private final CommentRepository commentRepository; + + @Override + public CommentDto create(Long gameApiId, CreateCommentRequestDto requestDto, User user) { + Comment comment = commentMapper.toModel(requestDto); + setGame(comment, gameApiId); + comment.setUser(user); + comment.setLocalDateTime(LocalDateTime.now()); + + Comment savedComment = commentRepository.save(comment); + return commentMapper.toDto(savedComment); + } + + @Override + public Page getCommentsForGame(Long id, Pageable pageable) { + Page gameComments = commentRepository.findAllByGameApiId(id, pageable); + return gameComments.map(commentMapper::toDto); + } + + @Override + public CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId) { + Optional commentOptional = commentRepository.findById(commentId); + if (commentOptional.isEmpty()) { + throw new EntityNotFoundException("There is no comment by id: " + commentId); + } + Comment comment = commentOptional.get(); + if (!comment.getUser().getId().equals(userId)) { + throw new AccessNotAllowedException("User with id: " + userId + + " is not allowed to modify comment with id: " + commentId); + } + if (requestDto.text() != null && !requestDto.text().isBlank()) { + comment.setText(requestDto.text()); + } + if (requestDto.rating() != null) { + comment.setRating(requestDto.rating()); + } + Comment savedComment = commentRepository.save(comment); + return commentMapper.toDto(savedComment); + } + + @Override + public void delete(Long commentId, Long userId) { + isCreatorOfComment(commentId, userId); + commentRepository.deleteById(commentId); + } + + private void isCreatorOfComment(Long commentId, Long userId) { + if (!commentRepository.existsByIdAndUserId(commentId, userId)) { + throw new AccessNotAllowedException("User with id: " + userId + + " is not allowed to modify comment with id: " + commentId); + } + } + + private void setGame(Comment comment, Long gameApiId) { + Optional gameOptional = gameRepository.findByApiId(gameApiId); + + if (gameOptional.isEmpty()) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(gameApiId); + Game game = gameMapper.toModel(apiGame); + Game savedGame = gameRepository.save(game); + comment.setGame(savedGame); + comment.setGameApiId(savedGame.getApiId()); + } else { + Game game = gameOptional.get(); + comment.setGame(game); + comment.setGameApiId(game.getApiId()); + } + } +} diff --git a/src/main/resources/db/changelog/changes/10-create-comments-table.yaml b/src/main/resources/db/changelog/changes/10-create-comments-table.yaml new file mode 100644 index 0000000..e62f289 --- /dev/null +++ b/src/main/resources/db/changelog/changes/10-create-comments-table.yaml @@ -0,0 +1,65 @@ +databaseChangeLog: + - changeSet: + id: create-comments-table + author: Yuliia + changes: + - createTable: + tableName: comments + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: game_id + type: BIGINT + constraints: + nullable: false + + - column: + name: game_api_id + type: BIGINT + constraints: + nullable: false + + - column: + name: user_id + type: BIGINT + constraints: + nullable: false + + - column: + name: text + type: VARCHAR(2000) + constraints: + nullable: false + + - column: + name: local_date_time + type: TIMESTAMP + constraints: + nullable: false + + - column: + name: rating + type: INTEGER + constraints: + nullable: false + + - addForeignKeyConstraint: + baseTableName: comments + baseColumnNames: game_id + referencedTableName: games + referencedColumnNames: id + constraintName: fk_comments_game + + - addForeignKeyConstraint: + baseTableName: comments + baseColumnNames: user_id + referencedTableName: users + referencedColumnNames: id + constraintName: fk_comments_user diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index bd9bd47..f885d62 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -17,3 +17,5 @@ databaseChangeLog: file: classpath:/db/changelog/changes/08-create-users-table.yaml - include: file: classpath:/db/changelog/changes/09-create-user-game-table.yaml + - include: + file: classpath:/db/changelog/changes/10-create-comments-table.yaml From 6a45e62451634000012bddb1808e3f5cb7ae87d2 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:21:23 +0200 Subject: [PATCH 11/41] refactored comments (#19) * refactored comments * refactored comments --- .../dto/internal/comment/CommentDto.java | 4 ++- .../comment/UpdateCommentRequestDto.java | 2 -- .../service/comment/CommentService.java | 2 +- .../service/comment/CommentServiceImpl.java | 34 ++++++++++++------- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java index 4498217..8a04bd2 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java @@ -1,11 +1,13 @@ package com.videogamescatalogue.backend.dto.internal.comment; +import java.time.LocalDateTime; + public record CommentDto( Long id, Long gameApiId, Long userId, String text, - String localDateTime, + LocalDateTime localDateTime, Integer rating ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java index 94e5059..5c5d3a9 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java @@ -2,13 +2,11 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public record UpdateCommentRequestDto( @Size(max = 2000, message = "Comment must be less than 2000 digits") String text, - @NotNull @Min(0) @Max(5) Integer rating diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java index c7b8b51..bf1a0f2 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java @@ -14,7 +14,7 @@ CommentDto create( User user ); - Page getCommentsForGame(Long id, Pageable pageable); + Page getCommentsForGame(Long gameApiId, Pageable pageable); CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId); diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java index 50438d3..0605dc4 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java @@ -42,39 +42,40 @@ public CommentDto create(Long gameApiId, CreateCommentRequestDto requestDto, Use } @Override - public Page getCommentsForGame(Long id, Pageable pageable) { - Page gameComments = commentRepository.findAllByGameApiId(id, pageable); + public Page getCommentsForGame(Long gameApiId, Pageable pageable) { + Page gameComments = commentRepository.findAllByGameApiId(gameApiId, pageable); return gameComments.map(commentMapper::toDto); } @Override public CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId) { - Optional commentOptional = commentRepository.findById(commentId); - if (commentOptional.isEmpty()) { - throw new EntityNotFoundException("There is no comment by id: " + commentId); - } - Comment comment = commentOptional.get(); - if (!comment.getUser().getId().equals(userId)) { - throw new AccessNotAllowedException("User with id: " + userId - + " is not allowed to modify comment with id: " + commentId); - } + Comment comment = commentRepository.findById(commentId) + .orElseThrow( + () -> new EntityNotFoundException( + "There is no comment by id: " + commentId + ) + ); + + isCreator(comment.getUser().getId(), userId); + if (requestDto.text() != null && !requestDto.text().isBlank()) { comment.setText(requestDto.text()); } if (requestDto.rating() != null) { comment.setRating(requestDto.rating()); } + Comment savedComment = commentRepository.save(comment); return commentMapper.toDto(savedComment); } @Override public void delete(Long commentId, Long userId) { - isCreatorOfComment(commentId, userId); + existsByIdAndUserId(commentId, userId); commentRepository.deleteById(commentId); } - private void isCreatorOfComment(Long commentId, Long userId) { + private void existsByIdAndUserId(Long commentId, Long userId) { if (!commentRepository.existsByIdAndUserId(commentId, userId)) { throw new AccessNotAllowedException("User with id: " + userId + " is not allowed to modify comment with id: " + commentId); @@ -96,4 +97,11 @@ private void setGame(Comment comment, Long gameApiId) { comment.setGameApiId(game.getApiId()); } } + + private void isCreator(Long commentCreatorId, Long userId) { + if (!commentCreatorId.equals(userId)) { + throw new AccessNotAllowedException("User with id: " + userId + + " is not allowed to modify comment with id: " + commentCreatorId); + } + } } From 7ea45daac32c01bf26de9e2e00af969885ef4542 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:06:57 +0200 Subject: [PATCH 12/41] added status to response (#20) --- .../backend/controller/GameController.java | 11 ++-- .../backend/dto/internal/game/GameDto.java | 1 - .../dto/internal/game/GameWithStatusDto.java | 20 ++++++ .../backend/mapper/game/GameMapper.java | 5 ++ .../backend/model/UserGame.java | 12 +++- .../backend/service/game/GameService.java | 4 +- .../backend/service/game/GameServiceImpl.java | 63 +++++++++++++------ 7 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index bbf06cf..a1b0686 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -2,8 +2,10 @@ import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.model.Genre; import com.videogamescatalogue.backend.model.Platform; +import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.game.GameService; import java.util.Arrays; import java.util.stream.Collectors; @@ -12,6 +14,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; @@ -36,11 +39,11 @@ public Page getAllGamesFromDb( } @GetMapping("{gameApiId}") - public GameDto getByApiId( + public GameWithStatusDto getByApiId( @PathVariable Long gameApiId, - @PageableDefault(size = DEFAULT_PAGE_SIZE) - Pageable pageable) { - return gameService.getByApiId(gameApiId); + @AuthenticationPrincipal User user + ) { + return gameService.getByApiId(gameApiId, user); } @GetMapping diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java index 094ef3d..69f8089 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java @@ -6,7 +6,6 @@ import java.util.Set; public record GameDto( - Long id, Long apiId, String name, int year, diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java new file mode 100644 index 0000000..1df968d --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java @@ -0,0 +1,20 @@ +package com.videogamescatalogue.backend.dto.internal.game; + +import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; +import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; +import com.videogamescatalogue.backend.model.UserGame; +import java.math.BigDecimal; +import java.util.Set; + +public record GameWithStatusDto( + Long apiId, + String name, + int year, + String backgroundImage, + Set platforms, + Set genres, + BigDecimal apiRating, + String description, + UserGame.GameStatus status +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java index 559f3f9..3458fe7 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java @@ -4,10 +4,12 @@ import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.exception.ParsingException; import com.videogamescatalogue.backend.mapper.genre.GenreProvider; import com.videogamescatalogue.backend.mapper.platform.PlatformProvider; import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.UserGame; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.List; @@ -39,6 +41,9 @@ public interface GameMapper { @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenreDtosSet") GameDto toDto(Game game); + @Mapping(target = "status", source = "status") + GameWithStatusDto toDtoWithStatus(Game game, UserGame.GameStatus status); + @Named("toYear") default Integer toYear(String releasedDate) { if (releasedDate == null || releasedDate.isBlank()) { diff --git a/src/main/java/com/videogamescatalogue/backend/model/UserGame.java b/src/main/java/com/videogamescatalogue/backend/model/UserGame.java index c4e9cc7..1ac5c26 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/UserGame.java +++ b/src/main/java/com/videogamescatalogue/backend/model/UserGame.java @@ -38,8 +38,14 @@ public class UserGame { @Getter public enum GameStatus { - BACKLOG, - IN_PROGRESS, - COMPLETED + BACKLOG("Backlog"), + IN_PROGRESS("In progress"), + COMPLETED("Completed"); + + private final String value; + + GameStatus(String value) { + this.value = value; + } } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index ed43d70..64fa64c 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -2,6 +2,8 @@ import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; +import com.videogamescatalogue.backend.model.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,7 +14,7 @@ public interface GameService { Page getAllGamesFromDb(Pageable pageable); - GameDto getByApiId(Long id); + GameWithStatusDto getByApiId(Long id, User user); Page getAllGamesFromApi(Pageable pageable); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 4e62300..6693757 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -4,10 +4,14 @@ import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.mapper.game.GameMapper; import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.model.UserGame; import com.videogamescatalogue.backend.repository.GameRepository; import com.videogamescatalogue.backend.repository.SpecificationBuilder; +import com.videogamescatalogue.backend.repository.UserGameRepository; import com.videogamescatalogue.backend.service.RawgApiClient; import java.util.ArrayList; import java.util.List; @@ -30,6 +34,7 @@ public class GameServiceImpl implements GameService { private final GameMapper gameMapper; private final GameRepository gameRepository; private final SpecificationBuilder specificationBuilder; + private final UserGameRepository userGameRepository; @Override public void fetchBestGames() { @@ -49,7 +54,7 @@ public void fetchBestGames() { } gameRepository.saveAll(toSaveGames); - log.info("Saved games to DB"); + log.info("Saved {} games to DB", toSaveGames.size()); } @Override @@ -70,25 +75,12 @@ public Page getAllGamesFromDb(Pageable pageable) { } @Override - public GameDto getByApiId(Long apiId) { - Optional gameOptional = gameRepository.findByApiId(apiId); - if (gameOptional.isEmpty()) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - Game game = gameMapper.toModel(apiGame); - Game savedGame = gameRepository.save(game); - return gameMapper.toDto(savedGame); - } + public GameWithStatusDto getByApiId(Long apiId, User user) { + Game game = findOrUpdate(apiId); - Game game = gameOptional.get(); - - if (game.getDescription() == null) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - game.setDescription(apiGame.description()); - Game savedGame = gameRepository.save(game); - return gameMapper.toDto(savedGame); - } + UserGame.GameStatus status = getGameStatus(apiId, user); - return gameMapper.toDto(game); + return gameMapper.toDtoWithStatus(game, status); } @Override @@ -126,4 +118,39 @@ private void setIdIfExistingGame(List modelList, Map existingG } } } + + private UserGame.GameStatus getGameStatus(Long apiId, User user) { + if (user == null) { + return null; + } + + Optional userGameOptional = userGameRepository.findByUserIdAndGameApiId( + user.getId(), apiId + ); + return userGameOptional.map(UserGame::getStatus) + .orElse(null); + } + + private Game findOrUpdate(Long apiId) { + Optional gameOptional = gameRepository.findByApiId(apiId); + if (gameOptional.isEmpty()) { + return findFromApi(apiId); + } + Game game = gameOptional.get(); + if (game.getDescription() == null) { + return updateGameDescription(apiId, game); + } + return game; + } + + private Game updateGameDescription(Long apiId, Game game) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + game.setDescription(apiGame.description()); + return gameRepository.save(game); + } + + private Game findFromApi(Long apiId) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + return gameMapper.toModel(apiGame); + } } From 95594989b10845219b4513599f1c1cf5ba4a7c0d Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:22:13 +0200 Subject: [PATCH 13/41] created search in api endpoint (#21) --- .../backend/controller/GameController.java | 9 +++ .../backend/service/RawgApiClient.java | 75 ++++++++++++++++--- .../backend/service/game/GameService.java | 3 + .../backend/service/game/GameServiceImpl.java | 8 ++ 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index a1b0686..ccac842 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -8,6 +8,7 @@ import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.game.GameService; import java.util.Arrays; +import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -64,6 +66,13 @@ public Page search( return gameService.search(searchParameters, pageable); } + @GetMapping("/search") + public Page apiSearch( + @RequestParam Map searchParams + ) { + return gameService.apiSearch(searchParams); + } + private void validateSearchParams(GameSearchParameters searchParameters) { if (searchParameters.platforms() != null) { try { diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index 0fc4588..d22dacb 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -10,17 +10,21 @@ import com.videogamescatalogue.backend.exception.ObjectMapperException; import java.io.IOException; import java.net.URI; +import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; @@ -86,15 +90,7 @@ public List getBestGames() { public Page getAllGames(Pageable pageable) { log.info("Called get all games from API"); - String url = BASE_URL + GAME_URL_PART - + KEY_URL_PART + apiKey - + PAGE_SIZE_URL_PART + pageable.getPageSize() - + PAGE_NUMBER_URL_PART + pageable.getPageNumber(); - - String ordering = toRawgOrdering(pageable.getSort()); - if (ordering != null) { - url = url + ORDERING_URL_PART + ordering; - } + String url = getGamesUrlWithPageable(pageable); HttpRequest httpRequest = HttpRequest.newBuilder() .GET() @@ -125,6 +121,67 @@ public ApiResponseFullGameDto getGameById(Long id) { return game; } + public Page search(Map searchParams) { + log.info("Called API search"); + + updateSearchParams(searchParams); + String url = createUrl(searchParams); + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url.toString())) + .header("User-Agent", "VideoGamesCatalogue") + .build(); + ApiResponseGames responseObject = getResponseGamesList(httpRequest); + + log.info("Received response from API"); + + Pageable pageable = createPageable(searchParams); + + return new PageImpl<>(responseObject.results(), pageable, responseObject.count()); + } + + private Pageable createPageable(Map searchParams) { + int page = Integer.parseInt(searchParams.get("page")) - 1; + int pageSize = Integer.parseInt(searchParams.get("page_size")); + return PageRequest.of(page, pageSize); + } + + private String createUrl(Map searchParams) { + StringBuilder url = new StringBuilder(BASE_URL + GAME_URL_PART + + KEY_URL_PART + apiKey); + + for (Map.Entry entry : searchParams.entrySet()) { + url.append("&") + .append(entry.getKey()) + .append("=") + .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + } + + return url.toString(); + } + + private void updateSearchParams(Map searchParams) { + if (!searchParams.containsKey("page")) { + searchParams.put("page", "1"); + } + if (!searchParams.containsKey("page_size")) { + searchParams.put("page_size", DEFAULT_PAGE_SIZE); + } + } + + private String getGamesUrlWithPageable(Pageable pageable) { + String url = BASE_URL + GAME_URL_PART + + KEY_URL_PART + apiKey + + PAGE_SIZE_URL_PART + pageable.getPageSize() + + PAGE_NUMBER_URL_PART + pageable.getPageNumber(); + + String ordering = toRawgOrdering(pageable.getSort()); + if (ordering != null) { + url = url + ORDERING_URL_PART + ordering; + } + return url; + } + private ApiResponseGames getResponseGamesList(HttpRequest httpRequest) { try { return objectMapper.readValue( diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index 64fa64c..ad2e5ab 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -4,6 +4,7 @@ import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.model.User; +import java.util.Map; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -19,4 +20,6 @@ public interface GameService { Page getAllGamesFromApi(Pageable pageable); Page search(GameSearchParameters searchParameters, Pageable pageable); + + Page apiSearch(Map searchParams); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 6693757..4b95eef 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -98,6 +98,14 @@ public Page search(GameSearchParameters searchParameters, Pageable page .map(gameMapper::toDto); } + @Override + public Page apiSearch(Map searchParams) { + Page apiGames = apiClient.search(searchParams); + + return apiGames.map(gameMapper::toModel) + .map(gameMapper::toDto); + } + private Map getExistingGamesMap(List modelList) { List apiIds = modelList.stream() .map(Game::getApiId) From 7d0e69ecbdbfba6a98f4241f321f4149085a21ad Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:46:04 +0200 Subject: [PATCH 14/41] Create get user comments (#22) * created get user comments endpoint * updated readme --- README.md | 115 +++++++++++++++--- .../backend/controller/CommentController.java | 17 ++- .../backend/repository/CommentRepository.java | 2 + .../service/comment/CommentService.java | 2 + .../service/comment/CommentServiceImpl.java | 6 + 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a0e24d6..3556b4a 100644 --- a/README.md +++ b/README.md @@ -60,34 +60,117 @@ https://git-scm.com/ mvn spring-boot:run ``` -## API Endpoint Details. GameController +## API Endpoint Details. +## GameController ### getAllGamesFromDb - - Description: Returns a page of games from DB. - URL: `http://localhost:8080/api/games/local` - Method: GET +- Authentication: Not required -### getFromDbById +### getByApiId -- Description: Returns a game from DB by its ID. -- URL: `http://localhost:8080/api/games/local/id/{id}` +- Description: Returns a game from db by its apiId if the game is in db. Fetches the game from API if there is no game by the provided apiId. +- URL: `http://localhost:8080/api/games/{gameApiId}` - Method: GET +- Authentication: Not required -### getFromDbByGenre - -- Description: Returns a page of games from DB genre. -- URL: `http://localhost:8080/api/games/local/genre/{genre}` +### getAllGamesFromApi +- Description: Returns a paginated list of games fetched from the external API. +- URL: http://localhost:8080/api/games - Method: GET +- Authentication: Not required -### getFromDbByYear - -- Description: Returns a page of games from DB by year. -- URL: `http://localhost:8080/api/games/local/year/{year}` +### search +- Description: Searches local database games using search parameters. +- URL: http://localhost:8080/api/games/local/search - Method: GET +- Authentication: Not required -### getFromDbByPlatform +### apiSearch +- Description: Searches games directly from the external API. +- URL: http://localhost:8080/api/games/search +- Method: GET +- Authentication: Not required + +## AuthenticationController +### registerUser + +- Description: Registers a new user. +- URL: http://localhost:8080/api/auth/registration +- Method: POST + +### login +_ Description: Authenticates a user and returns JWT token. +- URL: http://localhost:8080/api/auth/login +- Method: POST + +## CommentController +### createComment +- Description: Creates a new comment for a specific game. +- URL: http://localhost:8080/api/comments/games/{gameApiId} +- Method: POST +- Authentication: Required + +### getCommentsForGame +- Description: Returns a paginated list of comments for a specific game. +- URL: http://localhost:8080/api/games/{gameApiId}/comments +- Method: GET +- Authentication: Not required -- Description: Returns a page of games from DB by platform. -- URL: `http://localhost:8080/api/games/local/platform/{platform}` +### getUserComments +- Description: Returns a paginated list of comments created by the authenticated user. +- URL: http://localhost:8080/api/comments +- Method: GET +- Authentication: Required + +### updateComment +- Description: Updates an existing comment owned by the authenticated user. +- URL: http://localhost:8080/api/comments/{commentId} +- Method: PATCH +- Authentication: Required + +### deleteComment +- Description: Deletes a comment owned by the authenticated user. +- URL: http://localhost:8080/api/comments/{commentId} +- Method: DELETE +- Authentication: Required + +## UserController +### getUserInfo +- Description: Returns user information by user ID. Access is restricted based on the authenticated user. +- URL: http://localhost:8080/api/users/{id} +- Method: GET +- Authentication: Required + +### updateUserInfo +- Description: Updates the authenticated user’s profile information (profileName, email, about, location). +- URL: http://localhost:8080/api/users/me +- Method: PATCH +- Authentication: Required + +### changePassword +- Description: Changes the authenticated user’s password. +- URL: http://localhost:8080/api/users/me/password +- Method: PATCH +- Authentication: Required + +## UserGameController +### createOrUpdateUserGame +- Description: Creates or updates a game associated with the authenticated user. +- URL: http://localhost:8080/api/user-games +- Method: POST +- Authentication: Required + +### deleteUserGame +- Description: Removes a game from the authenticated user’s list. +- URL: http://localhost:8080/api/user-games/{id} +- Method: DELETE +- Authentication: Required + +### getUserGamesByStatus +- Description: Returns a paginated list of the authenticated user’s games filtered by status. +- URL: http://localhost:8080/api/user-games - Method: GET +- Authentication: Required diff --git a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java index 8d45195..9713360 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -25,6 +26,7 @@ @RequestMapping @RestController public class CommentController { + private static final int DEFAULT_PAGE_SIZE = 30; private final CommentService commentService; @PostMapping("/comments/games/{gameApiId}") @@ -38,10 +40,23 @@ public CommentDto create( } @GetMapping("/games/{gameApiId}/comments") - public Page getCommentsForGame(@PathVariable Long gameApiId, Pageable pageable) { + public Page getCommentsForGame( + @PathVariable Long gameApiId, + @PageableDefault(size = DEFAULT_PAGE_SIZE) + Pageable pageable + ) { return commentService.getCommentsForGame(gameApiId, pageable); } + @GetMapping("/comments") + public Page getUserComments( + @AuthenticationPrincipal User user, + @PageableDefault(size = DEFAULT_PAGE_SIZE) + Pageable pageable + ) { + return commentService.getUserComments(user.getId(), pageable); + } + @PatchMapping("/comments/{commentId}") public CommentDto update( @PathVariable Long commentId, diff --git a/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java index 53ccb84..5b79c22 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/CommentRepository.java @@ -8,5 +8,7 @@ public interface CommentRepository extends JpaRepository { Page findAllByGameApiId(Long gameApiId, Pageable pageable); + Page findAllByUserId(Long userId, Pageable pageable); + boolean existsByIdAndUserId(Long id, Long userId); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java index bf1a0f2..6b0e156 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java @@ -16,6 +16,8 @@ CommentDto create( Page getCommentsForGame(Long gameApiId, Pageable pageable); + Page getUserComments(Long userId, Pageable pageable); + CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId); void delete(Long commentId, Long userId); diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java index 0605dc4..45949fd 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java @@ -47,6 +47,12 @@ public Page getCommentsForGame(Long gameApiId, Pageable pageable) { return gameComments.map(commentMapper::toDto); } + @Override + public Page getUserComments(Long userId, Pageable pageable) { + Page userComments = commentRepository.findAllByUserId(userId, pageable); + return userComments.map(commentMapper::toDto); + } + @Override public CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId) { Comment comment = commentRepository.findById(commentId) From 4989e6be06447470aed087b4cc5c15b60306b5b9 Mon Sep 17 00:00:00 2001 From: Alina Bondarenko Date: Sun, 21 Dec 2025 15:55:42 +0200 Subject: [PATCH 15/41] Update Dockerfile for dev environment --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 43da892..d130b5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,11 @@ FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . + RUN mvn dependency:go-offline -B +COPY checkstyle.xml . + COPY src ./src RUN mvn clean package -DskipTests @@ -15,7 +18,6 @@ WORKDIR /app COPY --from=build /app/target/*.jar app.jar - EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] From 9b06e83a516bc82f313b98eddd38cf637d8f240a Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:30:00 +0200 Subject: [PATCH 16/41] added swagger (#23) * added swagger * added link to swagger to readme * configured swagger * configured exceptions --- README.md | 46 +++++ pom.xml | 5 + .../backend/config/OpenApiConfig.java | 26 +++ .../controller/AuthenticationController.java | 42 +++++ .../backend/controller/CommentController.java | 73 ++++++++ .../backend/controller/GameController.java | 77 +++++++++ .../backend/controller/UserController.java | 70 ++++++++ .../controller/UserGameController.java | 75 ++++++++ .../CustomGlobalExceptionHandler.java | 162 ++++++++++++++++++ .../backend/service/game/GameServiceImpl.java | 10 +- .../backend/service/user/UserServiceImpl.java | 29 ++-- 11 files changed, 603 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java diff --git a/README.md b/README.md index 3556b4a..e947255 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,52 @@ https://git-scm.com/ ```bash mvn spring-boot:run ``` +## Swagger documentation +To test endpoints use Swagger documentation by link (when running the app locally, instructions below): +http://localhost:8080/api/swagger-ui/index.html +### How to Use the API Documentation (Swagger UI) +This API uses JWT authentication. +To access protected endpoints, you must register, log in, and authorise Swagger with your token. + +1️⃣ Register a New User +- Open Swagger UI +- Find the Authentication section +- Click POST /auth/register +- Click “Try it out” +- Fill in the request body +- Click Execute +✅ If registration is successful, the user is created. + +2️⃣ Log In and Get JWT Token +- In the Authentication section +- Open POST /auth/login +- Click “Try it out” +- Enter your credentials +- Click Execute +📌 The response will contain a JWT token, for example: +{ +"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +👉 Copy this token + +3️⃣ Authorise Swagger with JWT Token +- At the top-right corner of Swagger UI, click the 🔒 Authorize button +- In the popup window: +- Paste your token in this format: +- Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +⚠️ Important: The word Bearer and a space must be included +- Click Authorize +- Click Close +✅ Swagger is now authorised + +4️⃣ Call Protected Endpoints +Once authorised, you can access endpoints that require authentication. +📌 Swagger will now automatically attach the JWT token to each authorised request. + +5️⃣ Logging Out / Token Expiry +If you refresh the page or your token expires: +- Click 🔒 Authorize again +- Paste a new token ## API Endpoint Details. ## GameController diff --git a/pom.xml b/pom.xml index caa89ab..c3ac23b 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,11 @@ jjwt-jackson ${jjwt.version} + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + diff --git a/src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java b/src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java new file mode 100644 index 0000000..ee8f154 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java @@ -0,0 +1,26 @@ +package com.videogamescatalogue.backend.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + public static final String SECURITY_SCHEME_NAME = "BearerAuth"; + public static final String BEARER_FORMAT = "JWT"; + public static final String SCHEME_TYPE = "bearer"; + + @Bean + public OpenAPI customOpenApi() { + return new OpenAPI() + .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme(SCHEME_TYPE) + .bearerFormat(BEARER_FORMAT))) + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java index e4fd185..f4f7c3f 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java @@ -6,6 +6,11 @@ import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.security.AuthenticationService; import com.videogamescatalogue.backend.service.user.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; @@ -13,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Authentication", description = "Register and login users") @RestController @RequestMapping("/auth") @RequiredArgsConstructor @@ -20,6 +26,24 @@ public class AuthenticationController { private final UserService userService; private final AuthenticationService authenticationService; + @Operation( + summary = "Register a new user", + description = "Creates a new user account and returns basic user information", + responses = { + @ApiResponse( + responseCode = "200", + description = "User registered successfully", + content = @Content(schema = @Schema( + implementation = UserResponseDto.class + )) + ), + @ApiResponse( + responseCode = "400", + description = "Validation error", + content = @Content + ) + } + ) @PostMapping("/registration") UserResponseDto registerUser( @RequestBody @Valid UserRegistrationRequestDto requestDto @@ -27,6 +51,24 @@ UserResponseDto registerUser( return userService.registerUser(requestDto); } + @Operation( + summary = "User login", + description = "Authenticates a user and returns a JWT token", + responses = { + @ApiResponse( + responseCode = "200", + description = "Authentication successful", + content = @Content(schema = @Schema( + implementation = UserLoginResponseDto.class + )) + ), + @ApiResponse( + responseCode = "401", + description = "Invalid credentials", + content = @Content + ) + } + ) @PostMapping("/login") UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) { return authenticationService.authenticate(requestDto); diff --git a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java index 9713360..f26275c 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java @@ -5,6 +5,11 @@ import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.comment.CommentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -22,6 +27,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Comments", description = "Create, update, delete, and view comments for games") @RequiredArgsConstructor @RequestMapping @RestController @@ -29,6 +35,23 @@ public class CommentController { private static final int DEFAULT_PAGE_SIZE = 30; private final CommentService commentService; + @Operation( + summary = "Create a comment for a game", + description = """ + Creates a new comment for the specified game. + User must be authenticated. + """ + ) + @ApiResponse( + responseCode = "201", + description = "Comment created successfully", + content = @Content(schema = @Schema(implementation = CommentDto.class)) + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @PostMapping("/comments/games/{gameApiId}") @ResponseStatus(value = HttpStatus.CREATED) public CommentDto create( @@ -39,6 +62,15 @@ public CommentDto create( return commentService.create(gameApiId, requestDto, user); } + @Operation( + summary = "Get comments for a game", + description = "Returns paginated comments for a specific game" + ) + @ApiResponse( + responseCode = "200", + description = "Comments retrieved successfully", + content = @Content(schema = @Schema(implementation = CommentDto.class)) + ) @GetMapping("/games/{gameApiId}/comments") public Page getCommentsForGame( @PathVariable Long gameApiId, @@ -48,6 +80,20 @@ public Page getCommentsForGame( return commentService.getCommentsForGame(gameApiId, pageable); } + @Operation( + summary = "Get authenticated user's comments", + description = "Returns paginated comments created by the authenticated user" + ) + @ApiResponse( + responseCode = "200", + description = "User comments retrieved successfully", + content = @Content(schema = @Schema(implementation = CommentDto.class)) + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @GetMapping("/comments") public Page getUserComments( @AuthenticationPrincipal User user, @@ -57,6 +103,20 @@ public Page getUserComments( return commentService.getUserComments(user.getId(), pageable); } + @Operation( + summary = "Get authenticated user's comments", + description = "Returns paginated comments created by the authenticated user" + ) + @ApiResponse( + responseCode = "200", + description = "User comments retrieved successfully", + content = @Content(schema = @Schema(implementation = CommentDto.class)) + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @PatchMapping("/comments/{commentId}") public CommentDto update( @PathVariable Long commentId, @@ -66,6 +126,19 @@ public CommentDto update( return commentService.update(commentId,requestDto, user.getId()); } + @Operation( + summary = "Delete a comment", + description = "Deletes a comment owned by the authenticated user" + ) + @ApiResponse( + responseCode = "204", + description = "Comment deleted successfully" + ) + @ApiResponse( + responseCode = "403", + description = "User is not the owner of the comment", + content = @Content + ) @DeleteMapping("/comments/{commentId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete( diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index ccac842..ef62b38 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -7,6 +7,11 @@ import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.game.GameService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; @@ -23,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Games", description = "Retrieve and search video games") @RequiredArgsConstructor @RequestMapping("/games") @RestController @@ -31,6 +37,18 @@ public class GameController { public static final int DEFAULT_PAGE_NUMBER = 1; private final GameService gameService; + @Operation( + summary = "Get games stored in local database", + description = """ + Returns paginated games from the local database + sorted by API rating starting from the highest + """ + ) + @ApiResponse( + responseCode = "200", + description = "Games retrieved successfully", + content = @Content(schema = @Schema(implementation = GameDto.class)) + ) @GetMapping("/local") public Page getAllGamesFromDb( @PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "apiRating", @@ -40,6 +58,33 @@ public Page getAllGamesFromDb( return gameService.getAllGamesFromDb(pageable); } + @Operation( + summary = "Get specific game and its details by API ID", + description = """ + Returns detailed game information using a hybrid DB + external API strategy. + + Behaviour: + - If the game exists in the local database: + - If it already has a description → data is returned from DB + - If description is missing → full data is fetched from external API, + saved to DB, and then returned + - If the game does not exist in the database: + - Data is fetched from external API and returned without saving to DB + + If the user is authenticated, the response also includes + user-specific game status. + """ + ) + @ApiResponse( + responseCode = "200", + description = "Game retrieved successfully", + content = @Content(schema = @Schema(implementation = GameWithStatusDto.class)) + ) + @ApiResponse( + responseCode = "404", + description = "Game not found", + content = @Content + ) @GetMapping("{gameApiId}") public GameWithStatusDto getByApiId( @PathVariable Long gameApiId, @@ -48,6 +93,15 @@ public GameWithStatusDto getByApiId( return gameService.getByApiId(gameApiId, user); } + @Operation( + summary = "Get games from external API", + description = "Returns paginated games fetched from the external games API" + ) + @ApiResponse( + responseCode = "200", + description = "Games retrieved successfully", + content = @Content(schema = @Schema(implementation = GameDto.class)) + ) @GetMapping public Page getAllGamesFromApi( @PageableDefault(size = DEFAULT_PAGE_SIZE, page = DEFAULT_PAGE_NUMBER) @@ -56,6 +110,20 @@ public Page getAllGamesFromApi( return gameService.getAllGamesFromApi(pageable); } + @Operation( + summary = "Search games in local database", + description = "Search games by platforms, genres, name and year" + ) + @ApiResponse( + responseCode = "200", + description = "Search results returned successfully", + content = @Content(schema = @Schema(implementation = GameDto.class)) + ) + @ApiResponse( + responseCode = "400", + description = "Invalid search parameters", + content = @Content + ) @GetMapping("/local/search") public Page search( @ModelAttribute GameSearchParameters searchParameters, @@ -66,6 +134,15 @@ public Page search( return gameService.search(searchParameters, pageable); } + @Operation( + summary = "Search games via external API", + description = "Flexible search using raw query parameters forwarded to the external API" + ) + @ApiResponse( + responseCode = "200", + description = "Search results returned successfully", + content = @Content(schema = @Schema(implementation = GameDto.class)) + ) @GetMapping("/search") public Page apiSearch( @RequestParam Map searchParams diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java index af8daf2..17ab850 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java @@ -5,6 +5,11 @@ import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.user.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -15,12 +20,34 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Users", description = "Operations related to user profiles and accounts") @RequiredArgsConstructor @RequestMapping("/users") @RestController public class UserController { private final UserService userService; + @Operation( + summary = "Get user information by ID", + description = " Returns user profile information. Requires authentication" + ) + @ApiResponse( + responseCode = "200", + description = "User information retrieved successfully", + content = @Content( + schema = @Schema(implementation = UserResponseDto.class) + ) + ) + @ApiResponse( + responseCode = "403", + description = "Access denied", + content = @Content + ) + @ApiResponse( + responseCode = "404", + description = "User not found", + content = @Content + ) @GetMapping("/{id}") public UserResponseDto getUserInfo( @PathVariable Long id, @@ -29,6 +56,27 @@ public UserResponseDto getUserInfo( return userService.getUserInfo(id, user); } + @Operation( + summary = "Update authenticated user's profile", + description = "Updates profile information of the currently authenticated user" + ) + @ApiResponse( + responseCode = "200", + description = "User profile updated successfully", + content = @Content( + schema = @Schema(implementation = UserResponseDto.class) + ) + ) + @ApiResponse( + responseCode = "400", + description = "Validation error", + content = @Content + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @PatchMapping("/me") public UserResponseDto updateUserInfo( @Valid @RequestBody UpdateUserRequestDto requestDto, @@ -37,6 +85,28 @@ public UserResponseDto updateUserInfo( return userService.updateUserInfo(requestDto, user); } + @Operation( + summary = "Change authenticated user's password", + description = "Changes the password of the currently authenticated user. " + + "This operation requires the current password and a new password." + ) + @ApiResponse( + responseCode = "200", + description = "Password changed successfully", + content = @Content( + schema = @Schema(implementation = UserResponseDto.class) + ) + ) + @ApiResponse( + responseCode = "400", + description = "Invalid current password or validation error", + content = @Content + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @PatchMapping("/me/password") public UserResponseDto changePassword( @Valid @RequestBody ChangePasswordRequestDto requestDto, diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java index 517119b..fcfc227 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java @@ -5,6 +5,11 @@ import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.model.UserGame; import com.videogamescatalogue.backend.service.usergame.UserGameService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -19,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "User Games", description = "Manage games in the authenticated user's library") @RequiredArgsConstructor @RequestMapping("/user-games") @RestController @@ -26,6 +32,34 @@ public class UserGameController { public static final int DEFAULT_PAGE_SIZE = 30; private final UserGameService userGameService; + @Operation( + summary = "Add or update a game in user's library", + description = """ + Creates or updates a game entry in the authenticated user's library. + + Behaviour: + - If the game already exists in the user's library → its status is updated + - If the game does not exist → a new entry is created + + """ + ) + @ApiResponse( + responseCode = "200", + description = "User game created or updated successfully", + content = @Content( + schema = @Schema(implementation = UserGameDto.class) + ) + ) + @ApiResponse( + responseCode = "400", + description = "Invalid request data", + content = @Content + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @PostMapping public UserGameDto createOrUpdate( @RequestBody CreateUserGameDto createDto, @@ -34,6 +68,28 @@ public UserGameDto createOrUpdate( return userGameService.createOrUpdate(createDto, user); } + @Operation( + summary = "Remove a game from user's library", + description = """ + Deletes a game entry from the authenticated user's library. + + Only the owner of the entry can delete it. + """ + ) + @ApiResponse( + responseCode = "204", + description = "User game deleted successfully" + ) + @ApiResponse( + responseCode = "403", + description = "User is not allowed to delete this entry", + content = @Content + ) + @ApiResponse( + responseCode = "404", + description = "User game not found", + content = @Content + ) @DeleteMapping("/{id}") public void delete( @PathVariable Long id, @@ -42,6 +98,25 @@ public void delete( userGameService.delete(id, user); } + @Operation( + summary = "Get user's games by status", + description = """ + Returns paginated games from the authenticated + user's library filtered by game status + """ + ) + @ApiResponse( + responseCode = "200", + description = "User games retrieved successfully", + content = @Content( + schema = @Schema(implementation = UserGameDto.class) + ) + ) + @ApiResponse( + responseCode = "401", + description = "User is not authenticated", + content = @Content + ) @GetMapping public Page getByStatus( @RequestParam UserGame.GameStatus status, diff --git a/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java new file mode 100644 index 0000000..980ea33 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java @@ -0,0 +1,162 @@ +package com.videogamescatalogue.backend.exception; + +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Log4j2 +@ControllerAdvice +public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + log.info("Validation error", ex); + List errors = ex.getBindingResult() + .getAllErrors() + .stream() + .map(this::getErrorMessage) + .toList(); + return new ResponseEntity<>(getBody(errors), headers, + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(AuthenticationException.class) + protected ResponseEntity handleAuthenticationException( + AuthenticationException ex + ) { + log.info("Authentication error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(AccessNotAllowedException.class) + protected ResponseEntity handleAccessNotAllowedException( + AccessNotAllowedException ex + ) { + log.info("AccessNotAllowed error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(ApiException.class) + protected ResponseEntity handleApiException( + ApiException ex + ) { + log.info("External API error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.BAD_GATEWAY); + } + + @ExceptionHandler(EntityNotFoundException.class) + protected ResponseEntity handleEntityNotFoundException( + EntityNotFoundException ex + ) { + log.info("Entity Not Found error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(HttpResponseException.class) + protected ResponseEntity handleHttpResponseException( + HttpResponseException ex + ) { + log.info("Http Response error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.BAD_GATEWAY); + } + + @ExceptionHandler(InvalidInputException.class) + protected ResponseEntity handleInvalidInputException( + InvalidInputException ex + ) { + log.info("Invalid Input error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(JwtAuthenticationException.class) + protected ResponseEntity handleJwtAuthenticationException( + JwtAuthenticationException ex + ) { + log.info("Jwt Authentication error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(MappingException.class) + protected ResponseEntity handleMappingException( + MappingException ex + ) { + log.info("Mapping error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ObjectMapperException.class) + protected ResponseEntity handleObjectMapperException( + ObjectMapperException ex + ) { + log.info("Object Mapper error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ParsingException.class) + protected ResponseEntity handleParsingException( + ParsingException ex + ) { + log.info("Parsing error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.BAD_GATEWAY); + } + + @ExceptionHandler(RegistrationException.class) + protected ResponseEntity handleRegistrationException( + RegistrationException ex + ) { + log.info("Registration error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(SpecificationProviderNotFoundException.class) + protected ResponseEntity handleSpecificationProviderNotFoundException( + SpecificationProviderNotFoundException ex + ) { + log.info("Specification Provider Not Found error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.NOT_FOUND); + } + + private String getErrorMessage(ObjectError error) { + if (error instanceof FieldError fieldError) { + return fieldError.getField() + ": " + error.getDefaultMessage(); + } + return error.getDefaultMessage(); + } + + private Map getBody(List message) { + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("errors", message); + return body; + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 4b95eef..9bc3779 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -20,13 +20,13 @@ import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import lombok.extern.log4j.Log4j2; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; -@Slf4j +@Log4j2 @RequiredArgsConstructor @Service public class GameServiceImpl implements GameService { @@ -59,13 +59,19 @@ public void fetchBestGames() { @Override public void fetchAndUpdateBestGames() { + log.info("fetchAndUpdateBestGames is called"); + List apiGames = apiClient.getBestGames(); + log.info("Received list of games from Api. List size={}", + apiGames.size()); + List modelList = gameMapper.toModelList(apiGames); Map existingGamesMap = getExistingGamesMap(modelList); setIdIfExistingGame(modelList, existingGamesMap); gameRepository.saveAll(modelList); + log.info("Saved and updated games"); } @Override diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index 5648c6f..797f42a 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -57,16 +57,8 @@ public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User auth public UserResponseDto changePassword( ChangePasswordRequestDto requestDto, User authenticatedUser ) { - if (!passwordEncoder.matches( - requestDto.currentPassword(), - authenticatedUser.getPassword() - )) { - throw new InvalidInputException("Current password is not valid."); - } - - if (!requestDto.newPassword().equals(requestDto.repeatPassword())) { - throw new InvalidInputException("New password and repeat password must match"); - } + validateCurrentPassword(requestDto, authenticatedUser); + checkPasswordsMatch(requestDto); authenticatedUser.setPassword(passwordEncoder.encode(requestDto.newPassword())); User savedUser = userRepository.save(authenticatedUser); @@ -76,6 +68,23 @@ public UserResponseDto changePassword( return userMapper.toDto(savedUser); } + private void checkPasswordsMatch(ChangePasswordRequestDto requestDto) { + if (!requestDto.newPassword().equals(requestDto.repeatPassword())) { + throw new InvalidInputException("New password and repeat password must match"); + } + } + + private void validateCurrentPassword( + ChangePasswordRequestDto requestDto, User authenticatedUser + ) { + if (!passwordEncoder.matches( + requestDto.currentPassword(), + authenticatedUser.getPassword() + )) { + throw new InvalidInputException("Current password is not valid."); + } + } + private void checkUserCanAccess(Long userId, User authenticatedUser) { if (!userId.equals(authenticatedUser.getId())) { throw new AccessNotAllowedException("User with id: " + authenticatedUser.getId() From afeb5d95d1d389fe2b71512464272ce1396f6f91 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:31:43 +0200 Subject: [PATCH 17/41] refactored security and health (#24) --- .../com/videogamescatalogue/backend/config/CorsConfig.java | 2 +- .../com/videogamescatalogue/backend/config/SecurityConfig.java | 2 +- src/main/resources/application.properties | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java b/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java index 65e38b8..ef756a6 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java @@ -14,7 +14,7 @@ public WebMvcConfigurer corsConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:5173") + .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*"); } diff --git a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java index 0a95573..c4c624f 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java @@ -23,7 +23,7 @@ @Configuration public class SecurityConfig { public static final String[] PUBLIC_ENDPOINTS = {"/auth/**", "/games/**", "/error", - "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health"}; + "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health/**"}; private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 15c5bc4..6c5207c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,6 +13,9 @@ server.servlet.context-path=/api rawg.key=${RAWG_KEY} +management.endpoint.health.enabled=true +management.endpoints.enabled-by-default=true +management.endpoints.web.base-path=/actuator management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true management.endpoint.health.show-details=always From ab2c77733675346295bcd0c09338a15af6627319 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:10:47 +0200 Subject: [PATCH 18/41] Refactor db update (#25) * made scheduled update * made scheduled and admin update --- README.md | 32 ++++++++++++ ...VideogamescatalogueBackendApplication.java | 2 + .../controller/AdminGameController.java | 51 +++++++++++++++++++ .../backend/model/Role.java | 37 ++++++++++++++ .../backend/model/User.java | 15 +++++- .../backend/repository/UserRepository.java | 2 + .../backend/service/game/GameService.java | 2 + .../backend/service/game/GameServiceImpl.java | 7 +++ .../service/game/GameUpdateScheduler.java | 25 +++++++++ .../changes/01-create-roles-table.yaml | 21 ++++++++ .../changes/02-insert-into-roles-table.yaml | 18 +++++++ ...-table.yaml => 03-create-users-table.yaml} | 0 .../changes/04-insert-into-users-table.yaml | 26 ++++++++++ .../changes/05-create-users-roles-table.yaml | 39 ++++++++++++++ .../06-insert-into-users-roles-table.yaml | 14 +++++ ...le.yaml => 07-create-platforms-table.yaml} | 0 ....yaml => 08-insert-default-platforms.yaml} | 0 ...table.yaml => 09-create-genres-table.yaml} | 0 ...res.yaml => 10-insert-default-genres.yaml} | 0 ...-table.yaml => 11-create-games-table.yaml} | 0 ...l => 12-create-games-platforms-table.yaml} | 0 ...yaml => 13-create-games-genres-table.yaml} | 0 ...le.yaml => 14-create-user-game-table.yaml} | 0 ...ble.yaml => 15-create-comments-table.yaml} | 0 .../db/changelog/db.changelog-master.yaml | 30 +++++++---- 25 files changed, 310 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java create mode 100644 src/main/java/com/videogamescatalogue/backend/model/Role.java create mode 100644 src/main/java/com/videogamescatalogue/backend/service/game/GameUpdateScheduler.java create mode 100644 src/main/resources/db/changelog/changes/01-create-roles-table.yaml create mode 100644 src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml rename src/main/resources/db/changelog/changes/{08-create-users-table.yaml => 03-create-users-table.yaml} (100%) create mode 100644 src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml create mode 100644 src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml create mode 100644 src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml rename src/main/resources/db/changelog/changes/{01-create-platforms-table.yaml => 07-create-platforms-table.yaml} (100%) rename src/main/resources/db/changelog/changes/{02-insert-default-platforms.yaml => 08-insert-default-platforms.yaml} (100%) rename src/main/resources/db/changelog/changes/{03-create-genres-table.yaml => 09-create-genres-table.yaml} (100%) rename src/main/resources/db/changelog/changes/{04-insert-default-genres.yaml => 10-insert-default-genres.yaml} (100%) rename src/main/resources/db/changelog/changes/{05-create-games-table.yaml => 11-create-games-table.yaml} (100%) rename src/main/resources/db/changelog/changes/{06-create-games-platforms-table.yaml => 12-create-games-platforms-table.yaml} (100%) rename src/main/resources/db/changelog/changes/{07-create-games-genres-table.yaml => 13-create-games-genres-table.yaml} (100%) rename src/main/resources/db/changelog/changes/{09-create-user-game-table.yaml => 14-create-user-game-table.yaml} (100%) rename src/main/resources/db/changelog/changes/{10-create-comments-table.yaml => 15-create-comments-table.yaml} (100%) diff --git a/README.md b/README.md index e947255..106c82a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ https://git-scm.com/ To test endpoints use Swagger documentation by link (when running the app locally, instructions below): http://localhost:8080/api/swagger-ui/index.html ### How to Use the API Documentation (Swagger UI) +Important! To run manual update of the database using AdminGameController endpoints, you need to log in as ADMIN. This API uses JWT authentication. To access protected endpoints, you must register, log in, and authorise Swagger with your token. @@ -220,3 +221,34 @@ _ Description: Authenticates a user and returns JWT token. - URL: http://localhost:8080/api/user-games - Method: GET - Authentication: Required + +## AdminGameController +Provides administrative endpoints for managing game data fetched from an external API. These endpoints are intended only for administrators and are protected by role-based security. + +### fetchBestGamesManually +- Description: Fetches the current list of best games from the external API and saves only games that do not already exist in the database. +- URL: http://localhost:8080/api/admin/fetch-best-games +- Method: POST +- Authentication: Required + +### fetchAndUpdateAllGamesManually +- Description: Fetches the best games from the external API and updates existing records, while also saving any newly discovered games. +- URL: http://localhost:8080/api/admin/fetch-update-best-games +- Method: POST +- Authentication: Required + +## Automatic Game Fetch on Application Startup +The application is configured to automatically fetch best games from the external API when the backend starts. +# How It Works +- The main Spring Boot application class implements CommandLineRunner +- On startup, Spring executes the run() method +- This triggers an automatic call to gameService +- As a result, the application ensures that the database is initially populated with best games without requiring any manual admin action. + +## Scheduled Daily Game Fetch +The application includes a scheduled task that automatically fetches best games once per day. +- Time: Every day at 06:00 AM +- Time zone: Europe/Kyiv +- Trigger mechanism: Spring’s @Scheduled with a cron expression +- The scheduler runs automatically without any manual intervention +- Only new games are saved diff --git a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java index e9e20b4..0e5ef6d 100644 --- a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java +++ b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java @@ -6,7 +6,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @Profile("!test") @RequiredArgsConstructor @SpringBootApplication diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java new file mode 100644 index 0000000..ac86d26 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java @@ -0,0 +1,51 @@ +package com.videogamescatalogue.backend.controller; + +import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.service.game.GameService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Admin", description = "Admin management of games") +@RequiredArgsConstructor +@RequestMapping("/admin") +@RestController +@PreAuthorize("hasRole('ADMIN')") +public class AdminGameController { + private final GameService gameService; + + @Operation( + summary = "Fetch best games from API", + description = "Fetches best games from API and saves only new ones" + ) + @ApiResponse( + responseCode = "200", + description = "Games fetched successfully", + content = @Content + ) + @PostMapping("/fetch-best-games") + public void fetchBestGamesManually() { + gameService.fetchBestGames(); + } + + @Operation( + summary = "Fetches best games from API and updates existing", + description = "Fetches best games from API and updates existing ones" + ) + @ApiResponse( + responseCode = "200", + description = "Games retrieved successfully", + content = @Content(schema = @Schema(implementation = GameDto.class)) + ) + @PostMapping("/fetch-update-best-games") + public void fetchAndUpdateAllGamesManually() { + gameService.fetchAndUpdateBestGames(); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/Role.java b/src/main/java/com/videogamescatalogue/backend/model/Role.java new file mode 100644 index 0000000..a4a60b1 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/model/Role.java @@ -0,0 +1,37 @@ +package com.videogamescatalogue.backend.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; + +@Table(name = "roles") +@Entity +@Getter +@Setter +public class Role implements GrantedAuthority { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private RoleName role; + + @Override + public String getAuthority() { + return role.name(); + } + + public enum RoleName { + ROLE_ADMIN, + ROLE_USER + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/User.java b/src/main/java/com/videogamescatalogue/backend/model/User.java index 627bcd9..de41352 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/User.java +++ b/src/main/java/com/videogamescatalogue/backend/model/User.java @@ -11,9 +11,12 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import java.util.Collection; +import java.util.HashSet; import java.util.Set; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import lombok.ToString; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; import org.springframework.security.core.GrantedAuthority; @@ -52,12 +55,22 @@ public class User implements UserDetails { ) private Set games; + @ManyToMany + @JoinTable( + name = "users_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + @EqualsAndHashCode.Exclude + @ToString.Exclude + private Set roles = new HashSet<>(); + @Column(nullable = false, name = "is_deleted") private boolean isDeleted = false; @Override public Collection getAuthorities() { - return null; + return roles; } @Override diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java index 8ba976d..e20ce98 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java @@ -2,10 +2,12 @@ import com.videogamescatalogue.backend.model.User; import java.util.Optional; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); + @EntityGraph(attributePaths = "roles") Optional findByEmail(String email); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index ad2e5ab..a4174f6 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -13,6 +13,8 @@ public interface GameService { void fetchAndUpdateBestGames(); + void updateDbGames(); + Page getAllGamesFromDb(Pageable pageable); GameWithStatusDto getByApiId(Long id, User user); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 9bc3779..ad8a0f4 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -38,6 +38,8 @@ public class GameServiceImpl implements GameService { @Override public void fetchBestGames() { + log.info("fetchBestGames is called"); + List apiGames = apiClient.getBestGames(); log.info("Received list of games from Api. List size={}", apiGames.size()); @@ -74,6 +76,11 @@ public void fetchAndUpdateBestGames() { log.info("Saved and updated games"); } + @Override + public void updateDbGames() { + + } + @Override public Page getAllGamesFromDb(Pageable pageable) { return gameRepository.findAll(pageable) diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameUpdateScheduler.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameUpdateScheduler.java new file mode 100644 index 0000000..161c4fc --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameUpdateScheduler.java @@ -0,0 +1,25 @@ +package com.videogamescatalogue.backend.service.game; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Log4j2 +@RequiredArgsConstructor +@Service +public class GameUpdateScheduler { + public static final String EVERY_DAY_SIX_AM = "0 0 6 * * ?"; + public static final String ZONE = "Europe/Kyiv"; + + private final GameService gameService; + + @Scheduled( + cron = EVERY_DAY_SIX_AM, + zone = ZONE + ) + public void fetchBestGames() { + log.info("Scheduled fetchBestGames is called"); + gameService.fetchBestGames(); + } +} diff --git a/src/main/resources/db/changelog/changes/01-create-roles-table.yaml b/src/main/resources/db/changelog/changes/01-create-roles-table.yaml new file mode 100644 index 0000000..a1dbcce --- /dev/null +++ b/src/main/resources/db/changelog/changes/01-create-roles-table.yaml @@ -0,0 +1,21 @@ +databaseChangeLog: + - changeSet: + id: create-roles-table + author: julia + changes: + - createTable: + tableName: roles + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: role + type: VARCHAR(50) + constraints: + nullable: false + unique: true diff --git a/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml b/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml new file mode 100644 index 0000000..0d1a4d7 --- /dev/null +++ b/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml @@ -0,0 +1,18 @@ +databaseChangeLog: + - changeSet: + id: insert-roles + author: julia + changes: + - insert: + tableName: roles + columns: + - column: + name: role + value: ROLE_ADMIN + + - insert: + tableName: roles + columns: + - column: + name: role + value: ROLE_USER diff --git a/src/main/resources/db/changelog/changes/08-create-users-table.yaml b/src/main/resources/db/changelog/changes/03-create-users-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/08-create-users-table.yaml rename to src/main/resources/db/changelog/changes/03-create-users-table.yaml diff --git a/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml b/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml new file mode 100644 index 0000000..b2a33a9 --- /dev/null +++ b/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: + - changeSet: + id: insert-users + author: julia + changes: + - insert: + tableName: users + columns: + - column: + name: profile_name + value: admin + - column: + name: password + value: $2a$10$iHLvIfNAFWubZLSUjY4kCeB9.gMurj6ePKehoCcHnn0dWEZBvnY9i + - column: + name: email + value: admin@gmail.com + - column: + name: about + value: Admin + - column: + name: location + value: Admin + - column: + name: is_deleted + valueBoolean: false diff --git a/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml b/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml new file mode 100644 index 0000000..3147543 --- /dev/null +++ b/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml @@ -0,0 +1,39 @@ +databaseChangeLog: + - changeSet: + id: create-users-roles-table + author: julia + changes: + - createTable: + tableName: users_roles + columns: + - column: + name: user_id + type: BIGINT + constraints: + nullable: false + - column: + name: role_id + type: BIGINT + constraints: + nullable: false + + - addPrimaryKey: + tableName: users_roles + columnNames: user_id, role_id + constraintName: pk_users_roles + + - addForeignKeyConstraint: + baseTableName: users_roles + baseColumnNames: user_id + constraintName: fk_users_roles_user + referencedTableName: users + referencedColumnNames: id + onDelete: CASCADE + + - addForeignKeyConstraint: + baseTableName: users_roles + baseColumnNames: role_id + constraintName: fk_users_roles_role + referencedTableName: roles + referencedColumnNames: id + onDelete: CASCADE diff --git a/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml b/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml new file mode 100644 index 0000000..44fe76d --- /dev/null +++ b/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml @@ -0,0 +1,14 @@ +databaseChangeLog: + - changeSet: + id: insert-users-roles + author: julia + changes: + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueComputed: "(SELECT id FROM users WHERE email = 'admin@gmail.com')" + - column: + name: role_id + valueComputed: "(SELECT id FROM roles WHERE role = 'ROLE_ADMIN')" diff --git a/src/main/resources/db/changelog/changes/01-create-platforms-table.yaml b/src/main/resources/db/changelog/changes/07-create-platforms-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/01-create-platforms-table.yaml rename to src/main/resources/db/changelog/changes/07-create-platforms-table.yaml diff --git a/src/main/resources/db/changelog/changes/02-insert-default-platforms.yaml b/src/main/resources/db/changelog/changes/08-insert-default-platforms.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/02-insert-default-platforms.yaml rename to src/main/resources/db/changelog/changes/08-insert-default-platforms.yaml diff --git a/src/main/resources/db/changelog/changes/03-create-genres-table.yaml b/src/main/resources/db/changelog/changes/09-create-genres-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/03-create-genres-table.yaml rename to src/main/resources/db/changelog/changes/09-create-genres-table.yaml diff --git a/src/main/resources/db/changelog/changes/04-insert-default-genres.yaml b/src/main/resources/db/changelog/changes/10-insert-default-genres.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/04-insert-default-genres.yaml rename to src/main/resources/db/changelog/changes/10-insert-default-genres.yaml diff --git a/src/main/resources/db/changelog/changes/05-create-games-table.yaml b/src/main/resources/db/changelog/changes/11-create-games-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/05-create-games-table.yaml rename to src/main/resources/db/changelog/changes/11-create-games-table.yaml diff --git a/src/main/resources/db/changelog/changes/06-create-games-platforms-table.yaml b/src/main/resources/db/changelog/changes/12-create-games-platforms-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/06-create-games-platforms-table.yaml rename to src/main/resources/db/changelog/changes/12-create-games-platforms-table.yaml diff --git a/src/main/resources/db/changelog/changes/07-create-games-genres-table.yaml b/src/main/resources/db/changelog/changes/13-create-games-genres-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/07-create-games-genres-table.yaml rename to src/main/resources/db/changelog/changes/13-create-games-genres-table.yaml diff --git a/src/main/resources/db/changelog/changes/09-create-user-game-table.yaml b/src/main/resources/db/changelog/changes/14-create-user-game-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/09-create-user-game-table.yaml rename to src/main/resources/db/changelog/changes/14-create-user-game-table.yaml diff --git a/src/main/resources/db/changelog/changes/10-create-comments-table.yaml b/src/main/resources/db/changelog/changes/15-create-comments-table.yaml similarity index 100% rename from src/main/resources/db/changelog/changes/10-create-comments-table.yaml rename to src/main/resources/db/changelog/changes/15-create-comments-table.yaml diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index f885d62..0763141 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -1,21 +1,31 @@ databaseChangeLog: - include: - file: classpath:/db/changelog/changes/01-create-platforms-table.yaml + file: classpath:/db/changelog/changes/01-create-roles-table.yaml - include: - file: classpath:/db/changelog/changes/02-insert-default-platforms.yaml + file: classpath:/db/changelog/changes/02-insert-into-roles-table.yaml - include: - file: classpath:/db/changelog/changes/03-create-genres-table.yaml + file: classpath:/db/changelog/changes/03-create-users-table.yaml - include: - file: classpath:/db/changelog/changes/04-insert-default-genres.yaml + file: classpath:/db/changelog/changes/04-insert-into-users-table.yaml - include: - file: classpath:/db/changelog/changes/05-create-games-table.yaml + file: classpath:/db/changelog/changes/05-create-users-roles-table.yaml - include: - file: classpath:/db/changelog/changes/06-create-games-platforms-table.yaml + file: classpath:/db/changelog/changes/06-insert-into-users-roles-table.yaml - include: - file: classpath:/db/changelog/changes/07-create-games-genres-table.yaml + file: classpath:/db/changelog/changes/07-create-platforms-table.yaml - include: - file: classpath:/db/changelog/changes/08-create-users-table.yaml + file: classpath:/db/changelog/changes/08-insert-default-platforms.yaml - include: - file: classpath:/db/changelog/changes/09-create-user-game-table.yaml + file: classpath:/db/changelog/changes/09-create-genres-table.yaml - include: - file: classpath:/db/changelog/changes/10-create-comments-table.yaml + file: classpath:/db/changelog/changes/10-insert-default-genres.yaml + - include: + file: classpath:/db/changelog/changes/11-create-games-table.yaml + - include: + file: classpath:/db/changelog/changes/12-create-games-platforms-table.yaml + - include: + file: classpath:/db/changelog/changes/13-create-games-genres-table.yaml + - include: + file: classpath:/db/changelog/changes/14-create-user-game-table.yaml + - include: + file: classpath:/db/changelog/changes/15-create-comments-table.yaml From c7a1f6f0e855325070248e1fa0fa1ec93d873c81 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:38:36 +0200 Subject: [PATCH 19/41] allowed any authenticated user se userinfo (#26) * allowed any authenticated user se userinfo * refactored readme * ran mvn clean package --- README.md | 4 ++-- .../dto/internal/user/UserResponseDto.java | 1 - .../backend/service/user/UserServiceImpl.java | 20 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 106c82a..fd7e961 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ _ Description: Authenticates a user and returns JWT token. ## UserController ### getUserInfo -- Description: Returns user information by user ID. Access is restricted based on the authenticated user. +- Description: Returns user information by user ID. Any authenticated user can see other user's profile info. - URL: http://localhost:8080/api/users/{id} - Method: GET - Authentication: Required @@ -217,7 +217,7 @@ _ Description: Authenticates a user and returns JWT token. - Authentication: Required ### getUserGamesByStatus -- Description: Returns a paginated list of the authenticated user’s games filtered by status. +- Description: Returns a paginated list of any user’s games filtered by status. - URL: http://localhost:8080/api/user-games - Method: GET - Authentication: Required diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java index 573b7e1..82c086e 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java @@ -3,7 +3,6 @@ public record UserResponseDto( Long id, String profileName, - String email, String about, String location ) { diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index 797f42a..c412aa8 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -4,7 +4,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; -import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.exception.InvalidInputException; import com.videogamescatalogue.backend.exception.RegistrationException; import com.videogamescatalogue.backend.mapper.user.UserMapper; @@ -39,8 +39,15 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { @Override public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { - checkUserCanAccess(userId, authenticatedUser); - return userMapper.toDto(authenticatedUser); + if (authenticatedUser.getId().equals(userId)) { + return userMapper.toDto(authenticatedUser); + } + User user = userRepository.findById(userId).orElseThrow( + () -> new EntityNotFoundException( + "There is no user by id: " + userId + ) + ); + return userMapper.toDto(user); } @Override @@ -85,13 +92,6 @@ private void validateCurrentPassword( } } - private void checkUserCanAccess(Long userId, User authenticatedUser) { - if (!userId.equals(authenticatedUser.getId())) { - throw new AccessNotAllowedException("User with id: " + authenticatedUser.getId() - + " is not allowed to access info of user with id: " + userId); - } - } - private void checkUserAlreadyExists(String email) { if (userRepository.existsByEmail(email)) { throw new RegistrationException("Can't register user. User with email: " From b2c465ca2117b9d862078b45c7f30f81936cad8a Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Wed, 24 Dec 2025 11:54:33 +0200 Subject: [PATCH 20/41] configured setting user role during registration (#27) --- README.md | 1 + .../backend/controller/AdminGameController.java | 4 ++++ .../backend/repository/RoleRepository.java | 9 +++++++++ .../backend/service/user/UserServiceImpl.java | 14 ++++++++++++++ 4 files changed, 28 insertions(+) create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/RoleRepository.java diff --git a/README.md b/README.md index fd7e961..2fadf2b 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ _ Description: Authenticates a user and returns JWT token. ## AdminGameController Provides administrative endpoints for managing game data fetched from an external API. These endpoints are intended only for administrators and are protected by role-based security. +Important! To run manual update of the database using AdminGameController endpoints, you need to log in as ADMIN. ### fetchBestGamesManually - Description: Fetches the current list of best games from the external API and saves only games that do not already exist in the database. diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java index ac86d26..ab0e9b3 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/AdminGameController.java @@ -8,11 +8,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Log4j2 @Tag(name = "Admin", description = "Admin management of games") @RequiredArgsConstructor @RequestMapping("/admin") @@ -32,6 +34,7 @@ public class AdminGameController { ) @PostMapping("/fetch-best-games") public void fetchBestGamesManually() { + log.info("Admin called fetch best games manually"); gameService.fetchBestGames(); } @@ -46,6 +49,7 @@ public void fetchBestGamesManually() { ) @PostMapping("/fetch-update-best-games") public void fetchAndUpdateAllGamesManually() { + log.info("Admin called fetch and update best games manually"); gameService.fetchAndUpdateBestGames(); } } diff --git a/src/main/java/com/videogamescatalogue/backend/repository/RoleRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/RoleRepository.java new file mode 100644 index 0000000..d2327ef --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/RoleRepository.java @@ -0,0 +1,9 @@ +package com.videogamescatalogue.backend.repository; + +import com.videogamescatalogue.backend.model.Role; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RoleRepository extends JpaRepository { + Optional findByRole(Role.RoleName role); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index c412aa8..f10c514 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -8,8 +8,11 @@ import com.videogamescatalogue.backend.exception.InvalidInputException; import com.videogamescatalogue.backend.exception.RegistrationException; import com.videogamescatalogue.backend.mapper.user.UserMapper; +import com.videogamescatalogue.backend.model.Role; import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.repository.RoleRepository; import com.videogamescatalogue.backend.repository.UserRepository; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.crypto.password.PasswordEncoder; @@ -22,6 +25,16 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; private final PasswordEncoder passwordEncoder; + private final RoleRepository roleRepository; + private Role roleUser; + + @PostConstruct + private void init() { + roleUser = roleRepository.findByRole(Role.RoleName.ROLE_USER) + .orElseThrow(() -> new EntityNotFoundException( + "Can't find role by name: " + Role.RoleName.ROLE_USER.name()) + ); + } @Override public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { @@ -29,6 +42,7 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { User user = userMapper.toModel(requestDto); user.setPassword(passwordEncoder.encode(requestDto.password())); + user.getRoles().add(roleUser); User savedUser = userRepository.save(user); From b591c1266ba231bd7e3b7b91842a9a50e9956881 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:19:55 +0200 Subject: [PATCH 21/41] created service tests (#28) * created service tests * removed unused import --- .../backend/service/game/GameService.java | 2 - .../backend/service/game/GameServiceImpl.java | 5 - .../comment/CommentServiceImplTest.java | 250 ++++++++++++++++ .../service/game/GameServiceImplTest.java | 271 ++++++++++++++++++ .../service/user/UserServiceImplTest.java | 261 +++++++++++++++++ .../usergame/UserGameServiceImplTest.java | 142 +++++++++ 6 files changed, 924 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java create mode 100644 src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java create mode 100644 src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java create mode 100644 src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java index a4174f6..ad2e5ab 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameService.java @@ -13,8 +13,6 @@ public interface GameService { void fetchAndUpdateBestGames(); - void updateDbGames(); - Page getAllGamesFromDb(Pageable pageable); GameWithStatusDto getByApiId(Long id, User user); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index ad8a0f4..71a536b 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -76,11 +76,6 @@ public void fetchAndUpdateBestGames() { log.info("Saved and updated games"); } - @Override - public void updateDbGames() { - - } - @Override public Page getAllGamesFromDb(Pageable pageable) { return gameRepository.findAll(pageable) diff --git a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java new file mode 100644 index 0000000..9ef4ac4 --- /dev/null +++ b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java @@ -0,0 +1,250 @@ +package com.videogamescatalogue.backend.service.comment; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.videogamescatalogue.backend.dto.internal.comment.CommentDto; +import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; +import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto; +import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.mapper.comment.CommentMapper; +import com.videogamescatalogue.backend.model.Comment; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Genre; +import com.videogamescatalogue.backend.model.Platform; +import com.videogamescatalogue.backend.model.Role; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.repository.CommentRepository; +import com.videogamescatalogue.backend.repository.GameRepository; +import com.videogamescatalogue.backend.service.RawgApiClient; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +class CommentServiceImplTest { + @Mock + private CommentMapper commentMapper; + @Mock + private GameRepository gameRepository; + @Mock + private RawgApiClient apiClient; + @Mock + private CommentRepository commentRepository; + @InjectMocks + private CommentServiceImpl commentService; + private Game game; + private CreateCommentRequestDto createCommentRequestDto; + private Role role; + private User user; + private Comment comment; + private CommentDto commentDto; + private Pageable pageable; + private Page commentPage; + private Page commentDtoPage; + private UpdateCommentRequestDto updateCommentRequestDto; + private CommentDto commentDtoUpdated; + + @BeforeEach + void setUp() { + Platform platformModel = new Platform(); + platformModel.setId(10L); + platformModel.setGeneralName(Platform.GeneralName.PC); + Genre genreModel = new Genre(); + genreModel.setId(20L); + genreModel.setName(Genre.Name.ACTION); + game = new Game(); + game.setApiId(1L); + game.setName("Game"); + game.setYear(2025); + game.setBackgroundImage("link"); + game.setPlatforms(Set.of(platformModel)); + game.setGenres(Set.of(genreModel)); + game.setApiRating(BigDecimal.valueOf(4.75)); + game.setDescription("description"); + + createCommentRequestDto = new CreateCommentRequestDto( + "comment text", 5 + ); + + role = new Role(); + role.setId(1L); + role.setRole(Role.RoleName.ROLE_USER); + + user = new User(); + user.setId(10L); + user.setProfileName("profileName"); + user.setPassword("$2a$10$iHLvIfNAFWubZLSUjY4kCeB9.gMurj6ePKehoCcHnn0dWEZBvnY9i"); + user.setEmail("test@gmail.com"); + user.setAbout("about"); + user.setLocation("location"); + user.getRoles().add(role); + + comment = new Comment(); + comment.setText("comment text"); + comment.setRating(5); + comment.setUser(user); + + commentDto = new CommentDto( + 1L, 1L, 10L, "comment text", + LocalDateTime.now(), 5 + ); + + pageable = PageRequest.of(0, 30); + + commentPage = new PageImpl<>(List.of(comment)); + + commentDtoPage = new PageImpl<>(List.of(commentDto)); + + updateCommentRequestDto = new UpdateCommentRequestDto( + "comment text updated", null + ); + + commentDtoUpdated = new CommentDto( + 2L, 1L, 10L, "comment text updated", + LocalDateTime.now(), 5 + ); + } + + @Test + void create_validRequestAndGameInDb_returnCommentDto() { + Long apiId = game.getApiId(); + when(commentMapper.toModel(createCommentRequestDto)) + .thenReturn(comment); + when(gameRepository.findByApiId(apiId)).thenReturn(Optional.of(game)); + when(commentRepository.save(any())).thenAnswer( + invocation -> invocation.getArguments()[0]); + when(commentMapper.toDto(comment)).thenReturn(commentDto); + + CommentDto actual = commentService.create( + apiId, createCommentRequestDto, user + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Comment.class); + verify(commentRepository).save(captor.capture()); + Comment captured = captor.getValue(); + + assertNotNull(actual); + assertEquals(commentDto, actual); + assertNotNull(captured.getGame()); + assertNotNull(captured.getUser()); + assertNotNull(captured.getLocalDateTime()); + + verify(gameRepository).findByApiId(apiId); + verifyNoInteractions(apiClient); + } + + @Test + void getCommentsForGame_validRequest_returnPageCommentDto() { + Long apiId = game.getApiId(); + + when(commentRepository.findAllByGameApiId(apiId, pageable)) + .thenReturn(commentPage); + when(commentMapper.toDto(comment)).thenReturn(commentDto); + + Page actual = commentService.getCommentsForGame(apiId, pageable); + + assertNotNull(actual); + assertEquals(commentDtoPage, actual); + } + + @Test + void getUserComments_validRequest_returnPageCommentDto() { + Long userId = user.getId(); + + when(commentRepository.findAllByUserId(userId, pageable)) + .thenReturn(commentPage); + when(commentMapper.toDto(comment)).thenReturn(commentDto); + + Page actual = commentService.getUserComments(userId, pageable); + + assertNotNull(actual); + assertEquals(commentDtoPage, actual); + } + + @Test + void update_userIsCreator_update() { + Long commentId = comment.getId(); + Long userId = user.getId(); + when(commentRepository.findById(commentId)) + .thenReturn(Optional.of(comment)); + when(commentRepository.save(comment)) + .thenAnswer(invocation -> invocation.getArguments()[0]); + when(commentMapper.toDto(any())) + .thenReturn(commentDtoUpdated); + + CommentDto actual = commentService.update( + commentId, updateCommentRequestDto, userId + ); + + assertDoesNotThrow(() -> commentService.update( + commentId, updateCommentRequestDto, userId + )); + assertNotNull(actual); + assertEquals(commentDtoUpdated, actual); + } + + @Test + void update_userIsNotCreator_throwException() { + Long commentId = comment.getId(); + Long userId = 100L; + when(commentRepository.findById(commentId)) + .thenReturn(Optional.of(comment)); + + assertThrows(AccessNotAllowedException.class, + () -> commentService.update( + commentId, updateCommentRequestDto, userId + )); + + verify(commentRepository).findById(commentId); + verifyNoMoreInteractions(commentRepository); + verifyNoInteractions(commentMapper); + } + + @Test + void delete_userIsCreator_delete() { + Long commentId = comment.getId(); + Long userId = user.getId(); + + when(commentRepository.existsByIdAndUserId(commentId, userId)) + .thenReturn(true); + doNothing().when(commentRepository).deleteById(commentId); + + assertDoesNotThrow(() -> commentService.delete(commentId, userId)); + } + + @Test + void delete_userIsNotCreator_throwException() { + Long commentId = comment.getId(); + Long userId = user.getId(); + + when(commentRepository.existsByIdAndUserId(commentId, userId)) + .thenReturn(false); + + assertThrows( + AccessNotAllowedException.class, + () -> commentService.delete(commentId, userId) + ); + } +} diff --git a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java new file mode 100644 index 0000000..850368b --- /dev/null +++ b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java @@ -0,0 +1,271 @@ +package com.videogamescatalogue.backend.service.game; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.videogamescatalogue.backend.dto.external.ApiPlatformWrapper; +import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; +import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; +import com.videogamescatalogue.backend.dto.external.ApiResponseGenreDto; +import com.videogamescatalogue.backend.dto.external.ApiResponsePlatformDto; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; +import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; +import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; +import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Genre; +import com.videogamescatalogue.backend.model.Platform; +import com.videogamescatalogue.backend.repository.GameRepository; +import com.videogamescatalogue.backend.service.RawgApiClient; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +class GameServiceImplTest { + @Mock + private RawgApiClient apiClient; + @Mock + private GameMapper gameMapper; + @Mock + private GameRepository gameRepository; + @InjectMocks + private GameServiceImpl gameService; + private List apiResponse; + private ApiResponseGameDto apiResponseGameDto; + private List gameModelList; + private List responseGamesIds; + private List zeroGamesToSave; + private Pageable pageable; + private List oneGameToSave; + private Page oneGamePage; + private Game gameModel; + private GameDto gameDto; + private Page oneGameDtoPage; + private GameWithStatusDto gameWithDescrAndNullStatusDto; + private ApiResponseFullGameDto apiResponseFullGameDto; + private Game gameModelWithDescription; + + @BeforeEach + void setUp() { + ApiPlatformWrapper apiPlatformWrapper = new ApiPlatformWrapper( + new ApiResponsePlatformDto("PC") + ); + ApiResponseGenreDto apiResponseGenreDto = new ApiResponseGenreDto("Action"); + apiResponseGameDto = new ApiResponseGameDto( + 1L, "Game", + "2025-12-12", "link", + List.of(apiPlatformWrapper), + List.of(apiResponseGenreDto), + BigDecimal.valueOf(4.75)); + apiResponse = List.of(apiResponseGameDto); + + Platform platformModel = new Platform(); + platformModel.setId(10L); + platformModel.setGeneralName(Platform.GeneralName.PC); + Genre genreModel = new Genre(); + genreModel.setId(20L); + genreModel.setName(Genre.Name.ACTION); + + gameModel = new Game(); + gameModel.setApiId(1L); + gameModel.setName("Game"); + gameModel.setYear(2025); + gameModel.setBackgroundImage("link"); + gameModel.setPlatforms(Set.of(platformModel)); + gameModel.setGenres(Set.of(genreModel)); + gameModel.setApiRating(BigDecimal.valueOf(4.75)); + gameModel.setDescription(null); + gameModelList = List.of(gameModel); + responseGamesIds = List.of(1L); + zeroGamesToSave = List.of(); + pageable = PageRequest.of(0, 30); + oneGameToSave = List.of(gameModel); + oneGamePage = new PageImpl<>(gameModelList); + + PlatformDto platformDto = new PlatformDto("PC"); + GenreDto genreDto = new GenreDto("Action"); + gameDto = new GameDto( + 1L, "Game", 2025, "link", + Set.of(platformDto), Set.of(genreDto), + BigDecimal.valueOf(4.75), null + ); + oneGameDtoPage = new PageImpl<>(List.of(gameDto)); + gameWithDescrAndNullStatusDto = new GameWithStatusDto( + 1L, "Game", 2025, "link", + Set.of(platformDto), Set.of(genreDto), + BigDecimal.valueOf(4.75), + "description", null + ); + + apiResponseFullGameDto = new ApiResponseFullGameDto( + 1L, "Game", "description", + "2025-12-12", "link", + List.of(apiPlatformWrapper), + List.of(apiResponseGenreDto), + BigDecimal.valueOf(4.75) + ); + + gameModelWithDescription = new Game(); + gameModelWithDescription.setApiId(1L); + gameModelWithDescription.setName("Game"); + gameModelWithDescription.setYear(2025); + gameModelWithDescription.setBackgroundImage("link"); + gameModelWithDescription.setPlatforms(Set.of(platformModel)); + gameModelWithDescription.setGenres(Set.of(genreModel)); + gameModelWithDescription.setApiRating(BigDecimal.valueOf(4.75)); + gameModelWithDescription.setDescription("description"); + } + + @Test + void fetchBestGames_NoNewGames_SavesZeroGames() { + when(apiClient.getBestGames()) + .thenReturn(apiResponse); + when(gameMapper.toModelList(apiResponse)) + .thenReturn(gameModelList); + when(gameRepository.findAllByApiIdIn(responseGamesIds)) + .thenReturn(gameModelList); + when(gameRepository.saveAll(zeroGamesToSave)) + .thenReturn(null); + + gameService.fetchBestGames(); + + verify(apiClient).getBestGames(); + verify(gameMapper).toModelList(apiResponse); + verify(gameRepository).findAllByApiIdIn(responseGamesIds); + verify(gameRepository).saveAll(zeroGamesToSave); + verifyNoMoreInteractions( + apiClient, gameMapper, gameRepository + ); + } + + @Test + void fetchAndUpdateBestGames_validRequest_SavesAndUpdates() { + when(apiClient.getBestGames()) + .thenReturn(apiResponse); + when(gameMapper.toModelList(apiResponse)) + .thenReturn(gameModelList); + when(gameRepository.findAllByApiIdIn(responseGamesIds)) + .thenReturn(gameModelList); + when(gameRepository.saveAll(oneGameToSave)) + .thenReturn(null); + + gameService.fetchAndUpdateBestGames(); + + verify(apiClient).getBestGames(); + verify(gameMapper).toModelList(apiResponse); + verify(gameRepository).findAllByApiIdIn(responseGamesIds); + verify(gameRepository).saveAll(oneGameToSave); + verifyNoMoreInteractions(apiClient, gameMapper, gameRepository); + } + + @Test + void getAllGamesFromDb_validRequest_returnsPageGameDtos() { + when(gameRepository.findAll(pageable)) + .thenReturn(oneGamePage); + when(gameMapper.toDto(gameModel)).thenReturn(gameDto); + + Page actual = gameService.getAllGamesFromDb(pageable); + + assertNotNull(actual); + assertEquals(oneGameDtoPage, actual); + + verify(gameRepository).findAll(pageable); + verify(gameMapper).toDto(gameModel); + verifyNoMoreInteractions(gameMapper, gameRepository); + } + + @Test + void getByApiId_gameWithDescrInDbNoLoggedUser_returnsGameWithStatusDto() { + Long apiId = 1L; + gameModel.setDescription("description"); + + when(gameRepository.findByApiId(apiId)) + .thenReturn(Optional.of(gameModel)); + when(gameMapper.toDtoWithStatus(gameModel, null)) + .thenReturn(gameWithDescrAndNullStatusDto); + + GameWithStatusDto actual = gameService.getByApiId(apiId, null); + + assertNotNull(actual); + assertEquals(gameWithDescrAndNullStatusDto, actual); + assertNull(actual.status()); + + verify(gameRepository).findByApiId(apiId); + verify(gameMapper).toDtoWithStatus(gameModel, null); + verifyNoMoreInteractions(gameMapper, gameRepository); + verifyNoInteractions(apiClient); + } + + @Test + void getByApiId_gameWithoutDescrInDbNoLoggedUser_returnsGameWithStatusDto() { + Long apiId = 1L; + + when(gameRepository.findByApiId(apiId)) + .thenReturn(Optional.of(gameModel)); + when(apiClient.getGameById(apiId)) + .thenReturn(apiResponseFullGameDto); + when(gameRepository.save(gameModel)) + .thenReturn(gameModel); + + when(gameMapper.toDtoWithStatus(gameModel, null)) + .thenReturn(gameWithDescrAndNullStatusDto); + + GameWithStatusDto actual = gameService.getByApiId(apiId, null); + + assertNotNull(actual); + assertEquals(gameWithDescrAndNullStatusDto, actual); + assertNull(actual.status()); + + verify(gameRepository).findByApiId(apiId); + verify(gameMapper).toDtoWithStatus(gameModel, null); + verify(apiClient).getGameById(apiId); + verifyNoMoreInteractions(gameMapper, gameRepository, apiClient); + } + + @Test + void getAllGamesFromApi_validRequest_returnPageGameDto() { + when(apiClient.getAllGames(pageable)) + .thenReturn(new PageImpl<>(apiResponse)); + when(gameMapper.toModel(apiResponseGameDto)) + .thenReturn(gameModel); + when(gameMapper.toDto(gameModel)).thenReturn(gameDto); + + Page actual = gameService.getAllGamesFromApi(pageable); + + assertNotNull(actual); + assertEquals(new PageImpl<>( + List.of(gameDto)), actual + ); + + verify(apiClient).getAllGames(pageable); + verifyNoInteractions(gameRepository); + } + + @Test + void search_validRequest_returnPageGameDto() { + verifyNoInteractions(apiClient); + } + + @Test + void apiSearch() { + verifyNoInteractions(gameRepository); + } +} diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java new file mode 100644 index 0000000..1971fa7 --- /dev/null +++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java @@ -0,0 +1,261 @@ +package com.videogamescatalogue.backend.service.user; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.exception.InvalidInputException; +import com.videogamescatalogue.backend.exception.RegistrationException; +import com.videogamescatalogue.backend.mapper.user.UserMapper; +import com.videogamescatalogue.backend.model.Role; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.repository.UserRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class UserServiceImplTest { + @Mock + private UserRepository userRepository; + @Mock + private UserMapper userMapper; + @Mock + private PasswordEncoder passwordEncoder; + @InjectMocks + private UserServiceImpl userService; + private String email = "test@gmail.com"; + private UserRegistrationRequestDto registrationRequestDto; + private User user; + private UserResponseDto responseDtoUser; + private Role role; + private UserResponseDto responseDtoBob; + private User userBob; + private UpdateUserRequestDto updateUserRequestDto; + private ChangePasswordRequestDto changePasswordRequestDto; + private ChangePasswordRequestDto changePasswordRequestDtoNoMatch; + + @BeforeEach + void setUp() { + registrationRequestDto = new UserRegistrationRequestDto( + "profileName", "password5454764", + "password5454764", "test@gmail.com", + "about", "location" + ); + role = new Role(); + role.setId(1L); + role.setRole(Role.RoleName.ROLE_USER); + + user = new User(); + user.setId(10L); + user.setProfileName("profileName"); + user.setPassword( + "$2a$10$iHLvIfNAFWubZLSUjY4kCeB9.gMurj6ePKehoCcHnn0dWEZBvnY9i" + ); + user.setEmail("test@gmail.com"); + user.setAbout("about"); + user.setLocation("location"); + user.getRoles().add(role); + + responseDtoUser = new UserResponseDto( + 10L, "profileName", + "about", "location" + ); + + userBob = new User(); + userBob.setId(9L); + userBob.setProfileName("profileNameBob"); + userBob.setPassword("hfgrhgthehethetheheheihd"); + userBob.setEmail("testBob@gmail.com"); + userBob.setAbout("aboutBob"); + userBob.setLocation("locationBob"); + userBob.getRoles().add(role); + + responseDtoBob = new UserResponseDto( + 9L, "profileNameBob", + "aboutBob", "locationBob" + ); + + updateUserRequestDto = new UpdateUserRequestDto( + "updated profileName", null, + null, null + ); + + changePasswordRequestDto = new ChangePasswordRequestDto( + "jfhgf747837", + "newpassword36445", + "newpassword36445" + ); + changePasswordRequestDtoNoMatch = new ChangePasswordRequestDto( + "password", + "newpassword36445", + "newpassword364" + ); + } + + @Test + void registerUser_userExists_throwException() { + when(userRepository.existsByEmail(email)) + .thenReturn(true); + assertThrows(RegistrationException.class, + () -> userService.registerUser( + registrationRequestDto + )); + + } + + @Test + void registerUser_validRequest_returnUserDto() { + String encodedPassword = user.getPassword(); + + when(userRepository.existsByEmail(email)) + .thenReturn(false); + when(userMapper.toModel(registrationRequestDto)) + .thenReturn(user); + when(passwordEncoder.encode( + registrationRequestDto.password())) + .thenReturn(encodedPassword); + when(userRepository.save(user)) + .thenReturn(user); + when(userMapper.toDto(user)).thenReturn(responseDtoBob); + + UserResponseDto actual = userService.registerUser( + registrationRequestDto + ); + + assertEquals(responseDtoBob, actual); + + verify(passwordEncoder).encode(registrationRequestDto.password()); + verify(userRepository).existsByEmail(email); + } + + @Test + void getUserInfo_usersMatch_returnAuthenticatedUserInfo() { + when(userMapper.toDto(user)) + .thenReturn(responseDtoBob); + + UserResponseDto actual = userService.getUserInfo(10L, user); + + assertEquals(responseDtoBob, actual); + + verifyNoInteractions(userRepository); + } + + @Test + void getUserInfo_usersDoNotMatch_returnOtherUserInfo() { + when(userRepository.findById(9L)) + .thenReturn(Optional.of(userBob)); + when(userMapper.toDto(userBob)) + .thenReturn(responseDtoBob); + + UserResponseDto actual = userService.getUserInfo(9L, user); + + assertEquals(responseDtoBob, actual); + assertNotEquals(responseDtoUser, actual); + + verify(userRepository).findById(9L); + verify(userMapper).toDto(userBob); + } + + @Test + void updateUserInfo_validRequest_returnUserResponseDto() { + when(userMapper.updateProfileInfo( + user, updateUserRequestDto + )) + .thenReturn(user); + when(userRepository.save(user)) + .thenReturn(user); + when(userMapper.toDto(user)).thenReturn(responseDtoBob); + + UserResponseDto actual = userService.updateUserInfo( + updateUserRequestDto, user + ); + + assertEquals(responseDtoBob, actual); + + verify(userRepository).save(user); + } + + @Test + void changePassword_currentPasswordInvalid_throwException() { + when(passwordEncoder.matches( + changePasswordRequestDto.currentPassword(), + user.getPassword() + )).thenReturn(false); + assertThrows(InvalidInputException.class, + () -> userService.changePassword( + changePasswordRequestDto, user + )); + + verify(passwordEncoder).matches( + changePasswordRequestDto.currentPassword(), + user.getPassword() + ); + verifyNoMoreInteractions(passwordEncoder); + verifyNoInteractions(userRepository, userMapper); + } + + @Test + void changePassword_passwordsDoNotMatch_throwException() { + when(passwordEncoder.matches( + changePasswordRequestDtoNoMatch.currentPassword(), + user.getPassword() + )).thenReturn(true); + assertThrows(InvalidInputException.class, + () -> userService.changePassword( + changePasswordRequestDtoNoMatch, user + )); + + verify(passwordEncoder).matches( + changePasswordRequestDtoNoMatch.currentPassword(), + user.getPassword() + ); + verifyNoMoreInteractions(passwordEncoder); + verifyNoInteractions(userRepository, userMapper); + } + + @Test + void changePassword_validRequest_returnUserResponseDto() { + String oldPassword = user.getPassword(); + when(passwordEncoder.matches( + changePasswordRequestDto.currentPassword(), + oldPassword + )).thenReturn(true); + when(passwordEncoder.encode( + changePasswordRequestDto.newPassword() + )) + .thenReturn("hfgahfghrgrlgnlrg/lng/lahdhtoi"); + when(userRepository.save(user)).thenReturn(user); + when(userMapper.toDto(user)).thenReturn(responseDtoBob); + + UserResponseDto actual = userService.changePassword( + changePasswordRequestDto, user + ); + + assertNotNull(actual); + assertEquals(responseDtoBob, actual); + + verify(passwordEncoder).matches( + changePasswordRequestDto.currentPassword(), + oldPassword + ); + verify(passwordEncoder).encode( + changePasswordRequestDto.newPassword() + ); + verify(userRepository).save(user); + } +} diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java new file mode 100644 index 0000000..2e23196 --- /dev/null +++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java @@ -0,0 +1,142 @@ +package com.videogamescatalogue.backend.service.usergame; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; +import com.videogamescatalogue.backend.mapper.usergame.UserGameMapper; +import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Role; +import com.videogamescatalogue.backend.model.User; +import com.videogamescatalogue.backend.model.UserGame; +import com.videogamescatalogue.backend.repository.GameRepository; +import com.videogamescatalogue.backend.repository.UserGameRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserGameServiceImplTest { + @Mock + private GameRepository gameRepository; + @Mock + private UserGameRepository userGameRepository; + @Mock + private UserGameMapper userGameMapper; + @InjectMocks + private UserGameServiceImpl userGameService; + private CreateUserGameDto createUserGameDto; + private Role role; + private User user; + private UserGame userGame; + private UserGameDto userGameDto; + + @BeforeEach + void setUp() { + createUserGameDto = new CreateUserGameDto( + 1L, UserGame.GameStatus.BACKLOG + ); + + role = new Role(); + role.setId(1L); + role.setRole(Role.RoleName.ROLE_USER); + + user = new User(); + user.setId(10L); + user.setProfileName("profileName"); + user.setPassword("$2a$10$iHLvIfNAFWubZLSUjY4kCeB9.gMurj6ePKehoCcHnn0dWEZBvnY9i"); + user.setEmail("test@gmail.com"); + user.setAbout("about"); + user.setLocation("location"); + user.getRoles().add(role); + + userGame = new UserGame(); + userGame.setUser(user); + userGame.setGame(new Game()); + userGame.setStatus(UserGame.GameStatus.BACKLOG); + + userGameDto = new UserGameDto( + 1L, 10L, 1L, 100L, + UserGame.GameStatus.BACKLOG.getValue() + ); + } + + @Test + void createOrUpdate_alreadyExists_update() { + when(userGameRepository.findByUserIdAndGameApiId( + user.getId(), + createUserGameDto.apiId() + )).thenReturn(Optional.of(userGame)); + when(userGameRepository.save(userGame)) + .thenReturn(userGame); + when(userGameMapper.toDto(userGame)) + .thenReturn(userGameDto); + + UserGameDto actual = userGameService.createOrUpdate( + createUserGameDto, user + ); + + assertNotNull(actual); + assertEquals(userGameDto, actual); + + verify(userGameRepository).findByUserIdAndGameApiId( + user.getId(), + createUserGameDto.apiId() + ); + verifyNoInteractions(gameRepository); + } + + @Test + void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() { + when(userGameRepository.findByUserIdAndGameApiId( + user.getId(), + createUserGameDto.apiId() + )).thenReturn(Optional.empty()); + when(gameRepository.findByApiId(createUserGameDto.apiId())) + .thenReturn(Optional.of(new Game())); + when(userGameRepository.save( + any(UserGame.class) + )) + .thenAnswer(invocation -> invocation.getArguments()[0]); + when(userGameMapper.toDto( + any(UserGame.class)) + ).thenReturn(userGameDto); + + UserGameDto actual = userGameService.createOrUpdate( + createUserGameDto, user + ); + + assertNotNull(actual); + assertEquals(userGameDto, actual); + + verify(userGameRepository).findByUserIdAndGameApiId( + user.getId(), + createUserGameDto.apiId() + ); + verify(gameRepository).findByApiId(createUserGameDto.apiId()); + } + + @Test + void delete_validRequest_delete() { + Long userGameId = userGame.getId(); + when(userGameRepository.findById(userGameId)) + .thenReturn(Optional.of(userGame)); + + assertDoesNotThrow(() -> userGameService.delete( + userGameId, user + )); + + verify(userGameRepository).findById(userGameId); + verify(userGameRepository).deleteById(userGameId); + } +} From e2eb0ec0aea55f99ce445e8de933e41346876d9b Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 27 Dec 2025 22:21:04 +0200 Subject: [PATCH 22/41] made method return gameDto (#29) --- .../dto/internal/usergame/UserGameDto.java | 5 ++-- .../backend/mapper/usergame/GameProvider.java | 19 +++++++++++++++ .../mapper/usergame/UserGameMapper.java | 5 ++-- .../usergame/UserGameServiceImplTest.java | 23 ++++++++++++++++++- 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/usergame/GameProvider.java diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java index d70a8bc..0264300 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameDto.java @@ -1,10 +1,11 @@ package com.videogamescatalogue.backend.dto.internal.usergame; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; + public record UserGameDto( Long id, Long userId, - Long gameId, - Long gameApiId, + GameDto gameDto, String status ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/GameProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/GameProvider.java new file mode 100644 index 0000000..bcc0b92 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/GameProvider.java @@ -0,0 +1,19 @@ +package com.videogamescatalogue.backend.mapper.usergame; + +import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Game; +import lombok.RequiredArgsConstructor; +import org.mapstruct.Named; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GameProvider { + private final GameMapper gameMapper; + + @Named("toGameDto") + public GameDto toGameDto(Game game) { + return gameMapper.toDto(game); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java index 62bc9e6..4032949 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java @@ -6,11 +6,10 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(config = MapperConfig.class) +@Mapper(config = MapperConfig.class, uses = GameProvider.class) public interface UserGameMapper { @Mapping(source = "user.id", target = "userId") - @Mapping(source = "game.id", target = "gameId") - @Mapping(source = "game.apiId", target = "gameApiId") + @Mapping(source = "game", target = "gameDto", qualifiedByName = "toGameDto") UserGameDto toDto(UserGame userGame); } diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java index 2e23196..adf6300 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java @@ -8,6 +8,9 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; +import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; +import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; import com.videogamescatalogue.backend.mapper.usergame.UserGameMapper; @@ -17,7 +20,9 @@ import com.videogamescatalogue.backend.model.UserGame; import com.videogamescatalogue.backend.repository.GameRepository; import com.videogamescatalogue.backend.repository.UserGameRepository; +import java.math.BigDecimal; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,6 +45,9 @@ class UserGameServiceImplTest { private User user; private UserGame userGame; private UserGameDto userGameDto; + private GameDto gameDto; + private PlatformDto platformDto; + private GenreDto genreDto; @BeforeEach void setUp() { @@ -65,8 +73,21 @@ void setUp() { userGame.setGame(new Game()); userGame.setStatus(UserGame.GameStatus.BACKLOG); + platformDto = new PlatformDto("PC"); + + genreDto = new GenreDto("Action"); + + gameDto = new GameDto( + 1234L, "Game name", + 2016, "link", + Set.of(platformDto), + Set.of(genreDto), + BigDecimal.valueOf(4.8), + "description" + ); + userGameDto = new UserGameDto( - 1L, 10L, 1L, 100L, + 1L, 10L, gameDto, UserGame.GameStatus.BACKLOG.getValue() ); } From 13695a424024b6456a5d596b6654eb29f20613e9 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 28 Dec 2025 12:52:39 +0200 Subject: [PATCH 23/41] configured cors (#30) --- .../com/videogamescatalogue/backend/config/CorsConfig.java | 5 +++-- .../videogamescatalogue/backend/config/SecurityConfig.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java b/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java index ef756a6..818f0ae 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java @@ -15,8 +15,9 @@ public WebMvcConfigurer corsConfigurer() { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") - .allowedMethods("*") - .allowedHeaders("*"); + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("Authorization", "Content-Type") + .allowCredentials(false); } }; } diff --git a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java index c4c624f..4f3e2eb 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -35,7 +36,7 @@ public PasswordEncoder getPasswordEncoder() { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http - .cors(AbstractHttpConfigurer::disable) + .cors(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( auth -> auth From f0bf62cde57d3f1b2d3b0d906fed66d49bb494ae Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 28 Dec 2025 19:30:52 +0200 Subject: [PATCH 24/41] added preconditions to yamls (#31) --- .../changes/01-create-roles-table.yaml | 5 + .../changes/02-insert-into-roles-table.yaml | 16 +- .../changes/03-create-users-table.yaml | 8 +- .../changes/04-insert-into-users-table.yaml | 5 + .../changes/05-create-users-roles-table.yaml | 5 + .../06-insert-into-users-roles-table.yaml | 5 + .../changes/07-create-platforms-table.yaml | 7 +- .../changes/08-insert-default-platforms.yaml | 99 +++++++++- .../changes/09-create-genres-table.yaml | 7 +- .../changes/10-insert-default-genres.yaml | 180 +++++++++++++++++- .../changes/11-create-games-table.yaml | 7 +- .../12-create-games-platforms-table.yaml | 7 +- .../changes/13-create-games-genres-table.yaml | 7 +- .../changes/14-create-user-game-table.yaml | 7 +- .../changes/15-create-comments-table.yaml | 7 +- 15 files changed, 359 insertions(+), 13 deletions(-) diff --git a/src/main/resources/db/changelog/changes/01-create-roles-table.yaml b/src/main/resources/db/changelog/changes/01-create-roles-table.yaml index a1dbcce..9dbda68 100644 --- a/src/main/resources/db/changelog/changes/01-create-roles-table.yaml +++ b/src/main/resources/db/changelog/changes/01-create-roles-table.yaml @@ -2,6 +2,11 @@ databaseChangeLog: - changeSet: id: create-roles-table author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: roles changes: - createTable: tableName: roles diff --git a/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml b/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml index 0d1a4d7..91d8f37 100644 --- a/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml +++ b/src/main/resources/db/changelog/changes/02-insert-into-roles-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: - id: insert-roles + id: insert-roles-admin author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM roles WHERE role='ROLE_ADMIN' changes: - insert: tableName: roles @@ -10,6 +15,15 @@ databaseChangeLog: name: role value: ROLE_ADMIN + - changeSet: + id: insert-roles-user + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM roles WHERE role='ROLE_USER' + changes: - insert: tableName: roles columns: diff --git a/src/main/resources/db/changelog/changes/03-create-users-table.yaml b/src/main/resources/db/changelog/changes/03-create-users-table.yaml index 99ae4fb..8154474 100644 --- a/src/main/resources/db/changelog/changes/03-create-users-table.yaml +++ b/src/main/resources/db/changelog/changes/03-create-users-table.yaml @@ -1,10 +1,16 @@ databaseChangeLog: - changeSet: id: create-users-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: users changes: - createTable: tableName: users + ifNotExists: true columns: - column: name: id diff --git a/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml b/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml index b2a33a9..f6e7299 100644 --- a/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml +++ b/src/main/resources/db/changelog/changes/04-insert-into-users-table.yaml @@ -2,6 +2,11 @@ databaseChangeLog: - changeSet: id: insert-users author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM users WHERE email='admin@gmail.com' changes: - insert: tableName: users diff --git a/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml b/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml index 3147543..5b4b3a9 100644 --- a/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml +++ b/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml @@ -2,6 +2,11 @@ databaseChangeLog: - changeSet: id: create-users-roles-table author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: users_roles changes: - createTable: tableName: users_roles diff --git a/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml b/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml index 44fe76d..eb4d6e1 100644 --- a/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml +++ b/src/main/resources/db/changelog/changes/06-insert-into-users-roles-table.yaml @@ -2,6 +2,11 @@ databaseChangeLog: - changeSet: id: insert-users-roles author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM users_roles WHERE user_id='1' changes: - insert: tableName: users_roles diff --git a/src/main/resources/db/changelog/changes/07-create-platforms-table.yaml b/src/main/resources/db/changelog/changes/07-create-platforms-table.yaml index c46138b..ba3727d 100644 --- a/src/main/resources/db/changelog/changes/07-create-platforms-table.yaml +++ b/src/main/resources/db/changelog/changes/07-create-platforms-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-platforms-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: platforms changes: - createTable: tableName: platforms diff --git a/src/main/resources/db/changelog/changes/08-insert-default-platforms.yaml b/src/main/resources/db/changelog/changes/08-insert-default-platforms.yaml index eaa8931..99fe152 100644 --- a/src/main/resources/db/changelog/changes/08-insert-default-platforms.yaml +++ b/src/main/resources/db/changelog/changes/08-insert-default-platforms.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: - id: insert-default-platforms - author: Yuliia + id: insert-default-platforms-1 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='PC' changes: - insert: tableName: platforms @@ -10,6 +15,15 @@ databaseChangeLog: name: general_name value: "PC" + - changeSet: + id: insert-default-platforms-2 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='PLAYSTATION' + changes: - insert: tableName: platforms columns: @@ -17,6 +31,15 @@ databaseChangeLog: name: general_name value: "PLAYSTATION" + - changeSet: + id: insert-default-platforms-3 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='XBOX' + changes: - insert: tableName: platforms columns: @@ -24,6 +47,15 @@ databaseChangeLog: name: general_name value: "XBOX" + - changeSet: + id: insert-default-platforms-4 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='NINTENDO_SWITCH' + changes: - insert: tableName: platforms columns: @@ -31,6 +63,15 @@ databaseChangeLog: name: general_name value: "NINTENDO_SWITCH" + - changeSet: + id: insert-default-platforms-5 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='MOBILE' + changes: - insert: tableName: platforms columns: @@ -38,6 +79,15 @@ databaseChangeLog: name: general_name value: "MOBILE" + - changeSet: + id: insert-default-platforms-6 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='MAC' + changes: - insert: tableName: platforms columns: @@ -45,6 +95,15 @@ databaseChangeLog: name: general_name value: "MAC" + - changeSet: + id: insert-default-platforms-7 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='LINUX' + changes: - insert: tableName: platforms columns: @@ -52,6 +111,15 @@ databaseChangeLog: name: general_name value: "LINUX" + - changeSet: + id: insert-default-platforms-8 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='SEGA' + changes: - insert: tableName: platforms columns: @@ -59,6 +127,15 @@ databaseChangeLog: name: general_name value: "SEGA" + - changeSet: + id: insert-default-platforms-9 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='ATARI' + changes: - insert: tableName: platforms columns: @@ -66,6 +143,15 @@ databaseChangeLog: name: general_name value: "ATARI" + - changeSet: + id: insert-default-platforms-10 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='CLASSIC_CONSOLE' + changes: - insert: tableName: platforms columns: @@ -73,6 +159,15 @@ databaseChangeLog: name: general_name value: "CLASSIC_CONSOLE" + - changeSet: + id: insert-default-platforms-11 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM platforms WHERE general_name='UNKNOWN' + changes: - insert: tableName: platforms columns: diff --git a/src/main/resources/db/changelog/changes/09-create-genres-table.yaml b/src/main/resources/db/changelog/changes/09-create-genres-table.yaml index 435bd4f..84b452d 100644 --- a/src/main/resources/db/changelog/changes/09-create-genres-table.yaml +++ b/src/main/resources/db/changelog/changes/09-create-genres-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-genres-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: genres changes: - createTable: tableName: genres diff --git a/src/main/resources/db/changelog/changes/10-insert-default-genres.yaml b/src/main/resources/db/changelog/changes/10-insert-default-genres.yaml index 03fefc0..633e4e7 100644 --- a/src/main/resources/db/changelog/changes/10-insert-default-genres.yaml +++ b/src/main/resources/db/changelog/changes/10-insert-default-genres.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: - id: insert-default-genres - author: Yuliia + id: insert-default-genres-1 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='ACTION' changes: - insert: tableName: genres @@ -10,6 +15,15 @@ databaseChangeLog: name: name value: "ACTION" + - changeSet: + id: insert-default-genres-2 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='INDIE' + changes: - insert: tableName: genres columns: @@ -17,6 +31,15 @@ databaseChangeLog: name: name value: "INDIE" + - changeSet: + id: insert-default-genres-3 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='ADVENTURE' + changes: - insert: tableName: genres columns: @@ -24,6 +47,15 @@ databaseChangeLog: name: name value: "ADVENTURE" + - changeSet: + id: insert-default-genres-4 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='RPG' + changes: - insert: tableName: genres columns: @@ -31,6 +63,15 @@ databaseChangeLog: name: name value: "RPG" + - changeSet: + id: insert-default-genres-5 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='STRATEGY' + changes: - insert: tableName: genres columns: @@ -38,6 +79,15 @@ databaseChangeLog: name: name value: "STRATEGY" + - changeSet: + id: insert-default-genres-6 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='SHOOTER' + changes: - insert: tableName: genres columns: @@ -45,6 +95,15 @@ databaseChangeLog: name: name value: "SHOOTER" + - changeSet: + id: insert-default-genres-7 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='CASUAL' + changes: - insert: tableName: genres columns: @@ -52,6 +111,15 @@ databaseChangeLog: name: name value: "CASUAL" + - changeSet: + id: insert-default-genres-8 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='SIMULATION' + changes: - insert: tableName: genres columns: @@ -59,6 +127,15 @@ databaseChangeLog: name: name value: "SIMULATION" + - changeSet: + id: insert-default-genres-9 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='PUZZLE' + changes: - insert: tableName: genres columns: @@ -66,6 +143,15 @@ databaseChangeLog: name: name value: "PUZZLE" + - changeSet: + id: insert-default-genres-10 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='ARCADE' + changes: - insert: tableName: genres columns: @@ -73,6 +159,15 @@ databaseChangeLog: name: name value: "ARCADE" + - changeSet: + id: insert-default-genres-11 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='PLATFORMER' + changes: - insert: tableName: genres columns: @@ -80,6 +175,15 @@ databaseChangeLog: name: name value: "PLATFORMER" + - changeSet: + id: insert-default-genres-12 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='MASS_MULTIPLAYER' + changes: - insert: tableName: genres columns: @@ -87,6 +191,15 @@ databaseChangeLog: name: name value: "MASS_MULTIPLAYER" + - changeSet: + id: insert-default-genres-13 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='RACING' + changes: - insert: tableName: genres columns: @@ -94,6 +207,15 @@ databaseChangeLog: name: name value: "RACING" + - changeSet: + id: insert-default-genres-14 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='SPORTS' + changes: - insert: tableName: genres columns: @@ -101,6 +223,15 @@ databaseChangeLog: name: name value: "SPORTS" + - changeSet: + id: insert-default-genres-15 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='FIGHTING' + changes: - insert: tableName: genres columns: @@ -108,6 +239,15 @@ databaseChangeLog: name: name value: "FIGHTING" + - changeSet: + id: insert-default-genres-16 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='FAMILY' + changes: - insert: tableName: genres columns: @@ -115,6 +255,15 @@ databaseChangeLog: name: name value: "FAMILY" + - changeSet: + id: insert-default-genres-17 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='BOARD' + changes: - insert: tableName: genres columns: @@ -122,6 +271,15 @@ databaseChangeLog: name: name value: "BOARD" + - changeSet: + id: insert-default-genres-18 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='CARD' + changes: - insert: tableName: genres columns: @@ -129,6 +287,15 @@ databaseChangeLog: name: name value: "CARD" + - changeSet: + id: insert-default-genres-19 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='EDUCATIONAL' + changes: - insert: tableName: genres columns: @@ -136,6 +303,15 @@ databaseChangeLog: name: name value: "EDUCATIONAL" + - changeSet: + id: insert-default-genres-20 + author: julia + preConditions: + - onFail: MARK_RAN + - sqlCheck: + expectedResult: 0 + sql: SELECT COUNT(*) FROM genres WHERE name='UNKNOWN' + changes: - insert: tableName: genres columns: diff --git a/src/main/resources/db/changelog/changes/11-create-games-table.yaml b/src/main/resources/db/changelog/changes/11-create-games-table.yaml index c1634a9..ea7f840 100644 --- a/src/main/resources/db/changelog/changes/11-create-games-table.yaml +++ b/src/main/resources/db/changelog/changes/11-create-games-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-games-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: games changes: - createTable: tableName: games diff --git a/src/main/resources/db/changelog/changes/12-create-games-platforms-table.yaml b/src/main/resources/db/changelog/changes/12-create-games-platforms-table.yaml index f93662e..1583e72 100644 --- a/src/main/resources/db/changelog/changes/12-create-games-platforms-table.yaml +++ b/src/main/resources/db/changelog/changes/12-create-games-platforms-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-games-platforms-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: games_platforms changes: - createTable: tableName: games_platforms diff --git a/src/main/resources/db/changelog/changes/13-create-games-genres-table.yaml b/src/main/resources/db/changelog/changes/13-create-games-genres-table.yaml index 8c1cc20..67313ac 100644 --- a/src/main/resources/db/changelog/changes/13-create-games-genres-table.yaml +++ b/src/main/resources/db/changelog/changes/13-create-games-genres-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-games-genres-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: games_genres changes: - createTable: tableName: games_genres diff --git a/src/main/resources/db/changelog/changes/14-create-user-game-table.yaml b/src/main/resources/db/changelog/changes/14-create-user-game-table.yaml index bb19ca5..5415957 100644 --- a/src/main/resources/db/changelog/changes/14-create-user-game-table.yaml +++ b/src/main/resources/db/changelog/changes/14-create-user-game-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-user-games-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: user_games changes: - createTable: tableName: user_games diff --git a/src/main/resources/db/changelog/changes/15-create-comments-table.yaml b/src/main/resources/db/changelog/changes/15-create-comments-table.yaml index e62f289..a813b24 100644 --- a/src/main/resources/db/changelog/changes/15-create-comments-table.yaml +++ b/src/main/resources/db/changelog/changes/15-create-comments-table.yaml @@ -1,7 +1,12 @@ databaseChangeLog: - changeSet: id: create-comments-table - author: Yuliia + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: comments changes: - createTable: tableName: comments From 03d2282b8cae4f18a1d12636c40f7dc98e65513e Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:37:45 +0200 Subject: [PATCH 25/41] added patch method to cors (#32) --- .../java/com/videogamescatalogue/backend/config/CorsConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java b/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java index 818f0ae..a1b13e4 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/CorsConfig.java @@ -15,7 +15,7 @@ public WebMvcConfigurer corsConfigurer() { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") .allowedHeaders("Authorization", "Content-Type") .allowCredentials(false); } From e674437caf511665315172dfc5d7c10a719c22e0 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Mon, 29 Dec 2025 19:00:07 +0200 Subject: [PATCH 26/41] added response code (#33) --- .../backend/controller/UserGameController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java index fcfc227..c4b8699 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java @@ -14,6 +14,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @Tag(name = "User Games", description = "Manage games in the authenticated user's library") @@ -91,6 +93,7 @@ public UserGameDto createOrUpdate( content = @Content ) @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) public void delete( @PathVariable Long id, @AuthenticationPrincipal User user From c039b0c451d8434400335f901df0013186b528fa Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:42:30 +0200 Subject: [PATCH 27/41] retun user info depending on param, return user id when login (#34) --- README.md | 8 +++++--- .../backend/controller/UserController.java | 20 +++++++++++++------ .../internal/user/UserLoginResponseDto.java | 5 ++++- .../security/AuthenticationServiceImpl.java | 5 ++++- .../backend/service/user/UserService.java | 4 +++- .../backend/service/user/UserServiceImpl.java | 7 ++++++- .../service/user/UserServiceImplTest.java | 4 ++-- 7 files changed, 38 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2fadf2b..83d9582 100644 --- a/README.md +++ b/README.md @@ -186,9 +186,11 @@ _ Description: Authenticates a user and returns JWT token. ## UserController ### getUserInfo -- Description: Returns user information by user ID. Any authenticated user can see other user's profile info. -- URL: http://localhost:8080/api/users/{id} -- Method: GET +- Description: Returns user information. If id is provided, returns info by id. + If id is not provided, returns info about authenticated user. Any authenticated user can see other user's profile info. +- URL: http://localhost:8080/api/users/info +- Method: GET +- @RequestParam(required = false) Long id - Authentication: Required ### updateUserInfo diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java index 17ab850..8edb8a4 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java @@ -15,9 +15,9 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "Users", description = "Operations related to user profiles and accounts") @@ -28,8 +28,13 @@ public class UserController { private final UserService userService; @Operation( - summary = "Get user information by ID", - description = " Returns user profile information. Requires authentication" + summary = "Get user information", + description = """ + Returns user profile information. + If id is provided, returns info by id. + If id is not provided, returns info about authenticated user. + Requires authentication + """ ) @ApiResponse( responseCode = "200", @@ -48,12 +53,15 @@ public class UserController { description = "User not found", content = @Content ) - @GetMapping("/{id}") + @GetMapping("/info") public UserResponseDto getUserInfo( - @PathVariable Long id, + @RequestParam(required = false) Long id, @AuthenticationPrincipal User user ) { - return userService.getUserInfo(id, user); + if (id == null) { + return userService.getAuthenticatedUserInfo(user); + } + return userService.getUserInfoById(id, user); } @Operation( diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java index 30a904c..d927bbf 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserLoginResponseDto.java @@ -1,4 +1,7 @@ package com.videogamescatalogue.backend.dto.internal.user; -public record UserLoginResponseDto(String token) { +public record UserLoginResponseDto( + String token, + Long userId +) { } diff --git a/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java index 8e6a5f4..ff5fb26 100644 --- a/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/security/AuthenticationServiceImpl.java @@ -2,6 +2,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; +import com.videogamescatalogue.backend.model.User; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -21,6 +22,8 @@ public UserLoginResponseDto authenticate(UserLoginRequestDto requestDto) { requestDto.email(), requestDto.password()) ); String token = jwtUtil.generateToken(authentication.getName()); - return new UserLoginResponseDto(token); + User user = (User) authentication.getPrincipal(); + Long userId = user.getId(); + return new UserLoginResponseDto(token, userId); } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java index 73c69ac..f53525c 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java @@ -11,7 +11,9 @@ UserResponseDto registerUser( UserRegistrationRequestDto requestDto ); - UserResponseDto getUserInfo(Long userId, User authenticatedUser); + UserResponseDto getUserInfoById(Long userId, User authenticatedUser); + + UserResponseDto getAuthenticatedUserInfo(User authenticatedUser); UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser); diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index f10c514..9b64dbc 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -52,7 +52,7 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { } @Override - public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { + public UserResponseDto getUserInfoById(Long userId, User authenticatedUser) { if (authenticatedUser.getId().equals(userId)) { return userMapper.toDto(authenticatedUser); } @@ -64,6 +64,11 @@ public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { return userMapper.toDto(user); } + @Override + public UserResponseDto getAuthenticatedUserInfo(User authenticatedUser) { + return userMapper.toDto(authenticatedUser); + } + @Override public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser) { User updatedUser = userMapper.updateProfileInfo(authenticatedUser, requestDto); diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java index 1971fa7..aa447e3 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java @@ -148,7 +148,7 @@ void getUserInfo_usersMatch_returnAuthenticatedUserInfo() { when(userMapper.toDto(user)) .thenReturn(responseDtoBob); - UserResponseDto actual = userService.getUserInfo(10L, user); + UserResponseDto actual = userService.getUserInfoById(10L, user); assertEquals(responseDtoBob, actual); @@ -162,7 +162,7 @@ void getUserInfo_usersDoNotMatch_returnOtherUserInfo() { when(userMapper.toDto(userBob)) .thenReturn(responseDtoBob); - UserResponseDto actual = userService.getUserInfo(9L, user); + UserResponseDto actual = userService.getUserInfoById(9L, user); assertEquals(responseDtoBob, actual); assertNotEquals(responseDtoUser, actual); From 42527259c35bdfe4c1558b41be08df1fd4505d46 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:59:53 +0200 Subject: [PATCH 28/41] updated readme, removed unused (#35) --- README.md | 70 ++++++++++++++----- ...VideogamescatalogueBackendApplication.java | 2 +- .../backend/exception/ApiException.java | 4 -- .../backend/exception/MappingException.java | 3 - 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 83d9582..c1eb05c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ # VideoGames Catalogue Backend +VideoGames Catalogue Backend is a Spring Boot application that provides a REST API for fetching, managing, and commenting on video games. +It integrates with an external game API to populate the database automatically and supports role-based access for administrators and users. +This backend is designed to support a frontend application or mobile app for video game enthusiasts. + +## Table of Contents +- [Key Features](#key-features) +- [Running the backend locally](#running-the-backend-locally) +- [Swagger documentation](#swagger-documentation) +- [API Endpoint Details](#api-endpoint-details) +- [Automatic Game Fetch on Application Startup](#automatic-game-fetch-on-application-startup) +- [Scheduled Daily Game Fetch](#scheduled-daily-game-fetch) +- [Postman](#postman) + +## Key Features +- JWT-based authentication and authorization +- User profile management +- Commenting for games +- Integration with an external games API +- Admin endpoints for managing game data +- Automatic and scheduled game fetching ## Running the backend locally 1. Before running the backend, make sure the following tools are installed: @@ -54,7 +74,7 @@ https://git-scm.com/ cd backend git checkout dev ``` - + 3. Build and run the project using Maven: ```bash mvn spring-boot:run @@ -63,7 +83,7 @@ https://git-scm.com/ To test endpoints use Swagger documentation by link (when running the app locally, instructions below): http://localhost:8080/api/swagger-ui/index.html ### How to Use the API Documentation (Swagger UI) -Important! To run manual update of the database using AdminGameController endpoints, you need to log in as ADMIN. +Important! To run manual update of the database using AdminGameController endpoints, you need to log in as ADMIN. This API uses JWT authentication. To access protected endpoints, you must register, log in, and authorise Swagger with your token. @@ -73,8 +93,8 @@ To access protected endpoints, you must register, log in, and authorise Swagger - Click POST /auth/register - Click “Try it out” - Fill in the request body -- Click Execute -✅ If registration is successful, the user is created. +- Click Execute + ✅ If registration is successful, the user is created. 2️⃣ Log In and Get JWT Token - In the Authentication section @@ -82,21 +102,21 @@ To access protected endpoints, you must register, log in, and authorise Swagger - Click “Try it out” - Enter your credentials - Click Execute -📌 The response will contain a JWT token, for example: -{ -"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." -} -👉 Copy this token + 📌 The response will contain a JWT token, for example: + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + 👉 Copy this token 3️⃣ Authorise Swagger with JWT Token - At the top-right corner of Swagger UI, click the 🔒 Authorize button - In the popup window: - Paste your token in this format: - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -⚠️ Important: The word Bearer and a space must be included + ⚠️ Important: The word Bearer and a space must be included - Click Authorize - Click Close -✅ Swagger is now authorised + ✅ Swagger is now authorised 4️⃣ Call Protected Endpoints Once authorised, you can access endpoints that require authentication. @@ -107,19 +127,19 @@ If you refresh the page or your token expires: - Click 🔒 Authorize again - Paste a new token -## API Endpoint Details. +## API Endpoint Details. ## GameController ### getAllGamesFromDb - Description: Returns a page of games from DB. -- URL: `http://localhost:8080/api/games/local` +- URL: http://localhost:8080/api/games/local - Method: GET - Authentication: Not required ### getByApiId - Description: Returns a game from db by its apiId if the game is in db. Fetches the game from API if there is no game by the provided apiId. -- URL: `http://localhost:8080/api/games/{gameApiId}` +- URL: http://localhost:8080/api/games/{gameApiId} - Method: GET - Authentication: Not required @@ -189,7 +209,7 @@ _ Description: Authenticates a user and returns JWT token. - Description: Returns user information. If id is provided, returns info by id. If id is not provided, returns info about authenticated user. Any authenticated user can see other user's profile info. - URL: http://localhost:8080/api/users/info -- Method: GET +- Method: GET - @RequestParam(required = false) Long id - Authentication: Required @@ -242,7 +262,7 @@ Important! To run manual update of the database using AdminGameController endpoi ## Automatic Game Fetch on Application Startup The application is configured to automatically fetch best games from the external API when the backend starts. -# How It Works +### How It Works - The main Spring Boot application class implements CommandLineRunner - On startup, Spring executes the run() method - This triggers an automatic call to gameService @@ -250,8 +270,20 @@ The application is configured to automatically fetch best games from the externa ## Scheduled Daily Game Fetch The application includes a scheduled task that automatically fetches best games once per day. -- Time: Every day at 06:00 AM -- Time zone: Europe/Kyiv -- Trigger mechanism: Spring’s @Scheduled with a cron expression +- At specified time +- At specified time zone - The scheduler runs automatically without any manual intervention - Only new games are saved + +## Postman +A Postman collection is available to simplify testing the API. +👉 [Open Online Bookstore Postman Collection](https://web.postman.co/workspace/49ed7a22-2d52-45ef-8ca1-c68f46105379/collection/40367151-a6927aa5-a3dc-41f6-b78a-440c659f52d5?action=share&source=copy-link&creator=40367151) + +How to use this Postman collection: +1. Click the link above. +2. Import the collection into your Postman app. +3. Run requests against http://localhost:8080/api/ +4. Authenticate by registering and/or logging in to get a JWT token. +5. Go to the Authorization tab. +6. Choose Bearer Token as the Auth Type. +7. Paste the JWT token you received into the token field to access all protected endpoints. diff --git a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java index 0e5ef6d..f1b9ada 100644 --- a/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java +++ b/src/main/java/com/videogamescatalogue/backend/VideogamescatalogueBackendApplication.java @@ -20,7 +20,7 @@ public static void main(String[] args) { } @Override - public void run(String... args) throws Exception { + public void run(String... args) { gameService.fetchBestGames(); } } diff --git a/src/main/java/com/videogamescatalogue/backend/exception/ApiException.java b/src/main/java/com/videogamescatalogue/backend/exception/ApiException.java index 7e89ab7..f6e78a6 100644 --- a/src/main/java/com/videogamescatalogue/backend/exception/ApiException.java +++ b/src/main/java/com/videogamescatalogue/backend/exception/ApiException.java @@ -1,10 +1,6 @@ package com.videogamescatalogue.backend.exception; public class ApiException extends RuntimeException { - public ApiException(String message) { - super(message); - } - public ApiException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/com/videogamescatalogue/backend/exception/MappingException.java b/src/main/java/com/videogamescatalogue/backend/exception/MappingException.java index 50ca4ef..02e4cbc 100644 --- a/src/main/java/com/videogamescatalogue/backend/exception/MappingException.java +++ b/src/main/java/com/videogamescatalogue/backend/exception/MappingException.java @@ -1,7 +1,4 @@ package com.videogamescatalogue.backend.exception; public class MappingException extends RuntimeException { - public MappingException(String message) { - super(message); - } } From efb0d61f498454167645b120a87ad8d540bc8a0d Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 4 Jan 2026 13:52:03 +0200 Subject: [PATCH 29/41] added info about userGames and changed max value of rating to 10 (#37) --- .../comment/CreateCommentRequestDto.java | 2 +- .../dto/internal/user/UserResponseDto.java | 6 ++++- .../internal/usergame/UserGameStatusDto.java | 7 ++++++ .../backend/mapper/user/UserGameProvider.java | 23 +++++++++++++++++++ .../backend/mapper/user/UserMapper.java | 3 ++- .../mapper/usergame/UserGameMapper.java | 14 +++++++++++ .../repository/UserGameRepository.java | 3 +++ 7 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameStatusDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/user/UserGameProvider.java diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java index 629448f..90aea12 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CreateCommentRequestDto.java @@ -10,7 +10,7 @@ public record CreateCommentRequestDto( String text, @NotNull @Min(0) - @Max(5) + @Max(10) Integer rating ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java index 82c086e..4c4aa64 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserResponseDto.java @@ -1,9 +1,13 @@ package com.videogamescatalogue.backend.dto.internal.user; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; +import java.util.List; + public record UserResponseDto( Long id, String profileName, String about, - String location + String location, + List userGames ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameStatusDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameStatusDto.java new file mode 100644 index 0000000..940a6bf --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/usergame/UserGameStatusDto.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.dto.internal.usergame; + +public record UserGameStatusDto( + Long apiId, + String status +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserGameProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserGameProvider.java new file mode 100644 index 0000000..2ea7968 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserGameProvider.java @@ -0,0 +1,23 @@ +package com.videogamescatalogue.backend.mapper.user; + +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; +import com.videogamescatalogue.backend.mapper.usergame.UserGameMapper; +import com.videogamescatalogue.backend.model.UserGame; +import com.videogamescatalogue.backend.repository.UserGameRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.mapstruct.Named; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UserGameProvider { + private final UserGameRepository userGameRepository; + private final UserGameMapper userGameMapper; + + @Named("getStatusDtoList") + public List getStatusDtoList(Long userId) { + List userGames = userGameRepository.findAllByUserId(userId); + return userGameMapper.toStatusDtoList(userGames); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java index bbe94dc..60a75e0 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java @@ -9,11 +9,12 @@ import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; -@Mapper(config = MapperConfig.class) +@Mapper(config = MapperConfig.class, uses = UserGameProvider.class) public interface UserMapper { @Mapping(target = "password", ignore = true) User toModel(UserRegistrationRequestDto requestDto); + @Mapping(source = "id", target = "userGames", qualifiedByName = "getStatusDtoList") UserResponseDto toDto(User user); User updateProfileInfo(@MappingTarget User user, UpdateUserRequestDto requestDto); diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java index 4032949..49a68e1 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java @@ -1,10 +1,15 @@ package com.videogamescatalogue.backend.mapper.usergame; import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; +import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.model.UserGame; +import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; @Mapper(config = MapperConfig.class, uses = GameProvider.class) public interface UserGameMapper { @@ -12,4 +17,13 @@ public interface UserGameMapper { @Mapping(source = "game", target = "gameDto", qualifiedByName = "toGameDto") UserGameDto toDto(UserGame userGame); + List toStatusDtoList(List userGames); + + @Mapping(source = "game", target = "apiId", qualifiedByName = "toApiId") + UserGameStatusDto toStatusDto(UserGame userGame); + + @Named("toApiId") + default Long toApiId(Game game) { + return game.getApiId(); + } } diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java index 2e83633..cf93e15 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java @@ -1,6 +1,7 @@ package com.videogamescatalogue.backend.repository; import com.videogamescatalogue.backend.model.UserGame; +import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,6 +10,8 @@ public interface UserGameRepository extends JpaRepository { Optional findByUserIdAndGameApiId(Long userId, Long apiId); + List findAllByUserId(Long userId); + Page findByUserIdAndStatus( Long userId, UserGame.GameStatus status, Pageable pageable ); From 3a4d6066837c93ef60ed3c9c2e1d91cddc0a662b Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 4 Jan 2026 18:04:52 +0200 Subject: [PATCH 30/41] added ci file (#39) * added ci file * run mvn clean package --- .github/workflows/ci.yml | 17 +++++++++++++++++ .../backend/mapper/usergame/UserGameMapper.java | 1 - .../game/GameSpecificationBuilder.java | 2 +- .../service/user/UserServiceImplTest.java | 11 +++++++++-- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..30920b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: Java CI +on: + - push + - pull_request +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn --batch-mode --update-snapshots verify diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java index 49a68e1..6c9a081 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/usergame/UserGameMapper.java @@ -1,7 +1,6 @@ package com.videogamescatalogue.backend.mapper.usergame; import com.videogamescatalogue.backend.config.MapperConfig; -import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; import com.videogamescatalogue.backend.model.Game; diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java index ad5ee03..5e5a4df 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java @@ -20,7 +20,7 @@ public Specification build(GameSearchParameters searchParameters) { if (searchParameters == null) { throw new IllegalArgumentException("Search Parameters cannot be null"); } - Specification specification = Specification.where(null); + Specification specification = Specification.where((Specification) null); specification = getSpecificationForParam( searchParameters.name(), Game.SpecificationKey.NAME.getValue(), specification diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java index aa447e3..82bafe1 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java @@ -13,12 +13,14 @@ import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; import com.videogamescatalogue.backend.exception.InvalidInputException; import com.videogamescatalogue.backend.exception.RegistrationException; import com.videogamescatalogue.backend.mapper.user.UserMapper; import com.videogamescatalogue.backend.model.Role; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.repository.UserRepository; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -71,9 +73,13 @@ void setUp() { user.setLocation("location"); user.getRoles().add(role); + UserGameStatusDto userGameStatusDto = new UserGameStatusDto( + 5463L, "COMPLETED" + ); responseDtoUser = new UserResponseDto( 10L, "profileName", - "about", "location" + "about", "location", + List.of(userGameStatusDto) ); userBob = new User(); @@ -87,7 +93,8 @@ void setUp() { responseDtoBob = new UserResponseDto( 9L, "profileNameBob", - "aboutBob", "locationBob" + "aboutBob", "locationBob", + List.of(userGameStatusDto) ); updateUserRequestDto = new UpdateUserRequestDto( From 6ab2dfd994423388cbbe4daed8ebe052a842efa7 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:05:43 +0200 Subject: [PATCH 31/41] added profileName and gameName to commentDto (#40) --- pom.xml | 33 ++++++++++++++++++- .../dto/internal/comment/CommentDto.java | 2 ++ .../backend/mapper/comment/CommentMapper.java | 15 ++++++++- .../game/GameSpecificationBuilder.java | 2 +- .../comment/CommentServiceImplTest.java | 6 ++-- 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 7584c00..c3ac23b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0 + 3.3.3 com.videogamescatalogue @@ -140,6 +140,37 @@ src + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 17 + 17 + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruct.binding.version} + + + + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java index 8a04bd2..589f8d4 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java @@ -5,7 +5,9 @@ public record CommentDto( Long id, Long gameApiId, + String gameName, Long userId, + String profileName, String text, LocalDateTime localDateTime, Integer rating diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java index 2504425..4b34909 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java @@ -4,6 +4,7 @@ import com.videogamescatalogue.backend.dto.internal.comment.CommentDto; import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; import com.videogamescatalogue.backend.model.Comment; +import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.model.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -13,11 +14,23 @@ public interface CommentMapper { Comment toModel(CreateCommentRequestDto requestDto); - @Mapping(source = "user", target = "userId", qualifiedByName = "toUserId") + @Mapping(target = "gameName", source = "game", qualifiedByName = "toGameName") + @Mapping(target = "userId", source = "user", qualifiedByName = "toUserId") + @Mapping(target = "profileName", source = "user", qualifiedByName = "toProfileName") CommentDto toDto(Comment comment); @Named("toUserId") default Long toUserId(User user) { return user.getId(); } + + @Named("toGameName") + default String toGameName(Game game) { + return game.getName(); + } + + @Named("toProfileName") + default String toProfileName(User user) { + return user.getProfileName(); + } } diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java index 5e5a4df..ad5ee03 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java @@ -20,7 +20,7 @@ public Specification build(GameSearchParameters searchParameters) { if (searchParameters == null) { throw new IllegalArgumentException("Search Parameters cannot be null"); } - Specification specification = Specification.where((Specification) null); + Specification specification = Specification.where(null); specification = getSpecificationForParam( searchParameters.name(), Game.SpecificationKey.NAME.getValue(), specification diff --git a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java index 9ef4ac4..4b7a9c0 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java @@ -107,7 +107,8 @@ void setUp() { comment.setUser(user); commentDto = new CommentDto( - 1L, 1L, 10L, "comment text", + 1L, 1L, "GameName", 10L, + "user", "comment text", LocalDateTime.now(), 5 ); @@ -122,7 +123,8 @@ void setUp() { ); commentDtoUpdated = new CommentDto( - 2L, 1L, 10L, "comment text updated", + 2L, 1L, "GameName", 10L, + "user", "comment text updated", LocalDateTime.now(), 5 ); } From 91eb2758ef930eb4b0eaea83e837788f2f9a2891 Mon Sep 17 00:00:00 2001 From: Alina Bondarenko Date: Mon, 5 Jan 2026 19:01:22 +0200 Subject: [PATCH 32/41] webhook after ci (#44) --- .github/workflows/backend-webhook.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend-webhook.yml b/.github/workflows/backend-webhook.yml index 94a3b79..8a9870a 100644 --- a/.github/workflows/backend-webhook.yml +++ b/.github/workflows/backend-webhook.yml @@ -1,13 +1,17 @@ name: Trigger Build on Main Repo on: - push: + workflow_run: + workflows: ["Java CI"] + types: + - completed branches: - main jobs: notify: runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Trigger build in main repository run: | @@ -15,9 +19,9 @@ jobs: -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.PAT_TOKEN }}" \ https://api.github.com/repos/Bondaliname/my-project-infrastructure/dispatches \ - -d '{"event_type":"backend-updated","client_payload":{"ref":"${{ github.ref }}","sha":"${{ github.sha }}"}}' - + -d '{"event_type":"backend-updated","client_payload":{"ref":"refs/heads/${{ github.event.workflow_run.head_branch }}","sha":"${{ github.event.workflow_run.head_sha }}"}}' + - name: Summary run: | - echo "Commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "Branch: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "Commit: ${{ github.event.workflow_run.head_sha }}" >> $GITHUB_STEP_SUMMARY + echo "Branch: ${{ github.event.workflow_run.head_branch }}" >> $GITHUB_STEP_SUMMARY From ac19eac31c324fa5acfbda934e31169ab92d376b Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:03:10 +0200 Subject: [PATCH 33/41] Update public endpoints (#46) * made getUserInfo public * made getUserComments public * made getByStatus userGame public * ran mvn clean package --- README.md | 10 +++--- .../backend/config/SecurityConfig.java | 14 +++++--- .../backend/controller/CommentController.java | 9 +++-- .../backend/controller/UserController.java | 7 ++-- .../controller/UserGameController.java | 5 +-- .../AuthenticationRequiredException.java | 7 ++++ .../CustomGlobalExceptionHandler.java | 9 +++++ .../service/comment/CommentService.java | 2 +- .../service/comment/CommentServiceImpl.java | 15 +++++++- .../backend/service/user/UserService.java | 4 +-- .../backend/service/user/UserServiceImpl.java | 35 +++++++++++-------- .../service/usergame/UserGameService.java | 1 + .../service/usergame/UserGameServiceImpl.java | 16 +++++++++ .../comment/CommentServiceImplTest.java | 2 +- .../service/user/UserServiceImplTest.java | 33 ++++++++++++++--- 15 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java diff --git a/README.md b/README.md index c1eb05c..ec81aaa 100644 --- a/README.md +++ b/README.md @@ -187,10 +187,10 @@ _ Description: Authenticates a user and returns JWT token. - Authentication: Not required ### getUserComments -- Description: Returns a paginated list of comments created by the authenticated user. +- Description: Returns a paginated list of comments created by the authenticated user or a user by id. - URL: http://localhost:8080/api/comments - Method: GET -- Authentication: Required +- Authentication: Not required ### updateComment - Description: Updates an existing comment owned by the authenticated user. @@ -207,11 +207,11 @@ _ Description: Authenticates a user and returns JWT token. ## UserController ### getUserInfo - Description: Returns user information. If id is provided, returns info by id. - If id is not provided, returns info about authenticated user. Any authenticated user can see other user's profile info. + If id is not provided, returns info about authenticated user. Any user can see other user's profile info. - URL: http://localhost:8080/api/users/info - Method: GET - @RequestParam(required = false) Long id -- Authentication: Required +- Authentication: not required ### updateUserInfo - Description: Updates the authenticated user’s profile information (profileName, email, about, location). @@ -242,7 +242,7 @@ _ Description: Authenticates a user and returns JWT token. - Description: Returns a paginated list of any user’s games filtered by status. - URL: http://localhost:8080/api/user-games - Method: GET -- Authentication: Required +- Authentication: Not required ## AdminGameController Provides administrative endpoints for managing game data fetched from an external API. These endpoints are intended only for administrators and are protected by role-based security. diff --git a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java index 4f3e2eb..d728510 100644 --- a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java +++ b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; @@ -23,8 +24,13 @@ @RequiredArgsConstructor @Configuration public class SecurityConfig { - public static final String[] PUBLIC_ENDPOINTS = {"/auth/**", "/games/**", "/error", - "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health/**"}; + public static final String[] PUBLIC_ENDPOINTS = { + "/auth/**", "/games/**", "/users/info/**", "/error", + "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health/**" + }; + public static final String[] GET_PUBLIC_ENDPOINTS = { + "/user-games/**", "/comments/**" + }; private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -40,8 +46,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( auth -> auth - .requestMatchers(PUBLIC_ENDPOINTS) - .permitAll() + .requestMatchers(PUBLIC_ENDPOINTS).permitAll() + .requestMatchers(HttpMethod.GET, GET_PUBLIC_ENDPOINTS).permitAll() .anyRequest() .authenticated() ) diff --git a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java index f26275c..42e5aed 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java @@ -24,6 +24,7 @@ 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.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -81,8 +82,9 @@ public Page getCommentsForGame( } @Operation( - summary = "Get authenticated user's comments", - description = "Returns paginated comments created by the authenticated user" + summary = "Get usercomments", + description = "Returns paginated comments created by " + + "the authenticated user or a user by id" ) @ApiResponse( responseCode = "200", @@ -96,11 +98,12 @@ public Page getCommentsForGame( ) @GetMapping("/comments") public Page getUserComments( + @RequestParam(required = false) Long id, @AuthenticationPrincipal User user, @PageableDefault(size = DEFAULT_PAGE_SIZE) Pageable pageable ) { - return commentService.getUserComments(user.getId(), pageable); + return commentService.getUserComments(user, id, pageable); } @Operation( diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java index 8edb8a4..d5dcda8 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java @@ -33,7 +33,7 @@ public class UserController { Returns user profile information. If id is provided, returns info by id. If id is not provided, returns info about authenticated user. - Requires authentication + Does not require authentication """ ) @ApiResponse( @@ -58,10 +58,7 @@ public UserResponseDto getUserInfo( @RequestParam(required = false) Long id, @AuthenticationPrincipal User user ) { - if (id == null) { - return userService.getAuthenticatedUserInfo(user); - } - return userService.getUserInfoById(id, user); + return userService.getUserInfo(id, user); } @Operation( diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java index c4b8699..402ac6b 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java @@ -123,12 +123,13 @@ public void delete( @GetMapping public Page getByStatus( @RequestParam UserGame.GameStatus status, - @AuthenticationPrincipal User user, + @RequestParam(required = false) Long userId, + @AuthenticationPrincipal User authenticatedUser, @PageableDefault(size = DEFAULT_PAGE_SIZE) Pageable pageable ) { return userGameService.getByStatus( - status, user.getId(), pageable + status, userId, authenticatedUser, pageable ); } } diff --git a/src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java b/src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java new file mode 100644 index 0000000..fa6e9d9 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.exception; + +public class AuthenticationRequiredException extends RuntimeException { + public AuthenticationRequiredException(String message) { + super(message); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java index 980ea33..48de145 100644 --- a/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java +++ b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java @@ -146,6 +146,15 @@ protected ResponseEntity handleSpecificationProviderNotFoundException( HttpStatus.NOT_FOUND); } + @ExceptionHandler(AuthenticationRequiredException.class) + protected ResponseEntity handleAuthenticationRequiredException( + AuthenticationRequiredException ex + ) { + log.info("Authentication Required error", ex); + return new ResponseEntity<>(getBody(List.of(ex.getMessage())), + HttpStatus.UNAUTHORIZED); + } + private String getErrorMessage(ObjectError error) { if (error instanceof FieldError fieldError) { return fieldError.getField() + ": " + error.getDefaultMessage(); diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java index 6b0e156..e70fb77 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java @@ -16,7 +16,7 @@ CommentDto create( Page getCommentsForGame(Long gameApiId, Pageable pageable); - Page getUserComments(Long userId, Pageable pageable); + Page getUserComments(User authenticatedUser, Long userId, Pageable pageable); CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId); diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java index 45949fd..8c15d2d 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java @@ -5,6 +5,7 @@ import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto; import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto; import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.mapper.comment.CommentMapper; import com.videogamescatalogue.backend.mapper.game.GameMapper; @@ -48,7 +49,19 @@ public Page getCommentsForGame(Long gameApiId, Pageable pageable) { } @Override - public Page getUserComments(Long userId, Pageable pageable) { + public Page getUserComments( + User authenticatedUser, Long userId, Pageable pageable + ) { + if (authenticatedUser == null && userId == null) { + throw new AuthenticationRequiredException("Authentication is required"); + } + if (userId == null) { + return findCommentsByUserId(authenticatedUser.getId(), pageable); + } + return findCommentsByUserId(userId, pageable); + } + + private Page findCommentsByUserId(Long userId, Pageable pageable) { Page userComments = commentRepository.findAllByUserId(userId, pageable); return userComments.map(commentMapper::toDto); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java index f53525c..73c69ac 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java @@ -11,9 +11,7 @@ UserResponseDto registerUser( UserRegistrationRequestDto requestDto ); - UserResponseDto getUserInfoById(Long userId, User authenticatedUser); - - UserResponseDto getAuthenticatedUserInfo(User authenticatedUser); + UserResponseDto getUserInfo(Long userId, User authenticatedUser); UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser); diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index 9b64dbc..5de302c 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -4,6 +4,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.exception.InvalidInputException; import com.videogamescatalogue.backend.exception.RegistrationException; @@ -52,21 +53,14 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { } @Override - public UserResponseDto getUserInfoById(Long userId, User authenticatedUser) { - if (authenticatedUser.getId().equals(userId)) { - return userMapper.toDto(authenticatedUser); + public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { + if (authenticatedUser == null && userId == null) { + throw new AuthenticationRequiredException("Authentication is required"); } - User user = userRepository.findById(userId).orElseThrow( - () -> new EntityNotFoundException( - "There is no user by id: " + userId - ) - ); - return userMapper.toDto(user); - } - - @Override - public UserResponseDto getAuthenticatedUserInfo(User authenticatedUser) { - return userMapper.toDto(authenticatedUser); + if (userId == null) { + return getAuthenticatedUserInfo(authenticatedUser); + } + return getUserInfoById(userId); } @Override @@ -117,4 +111,17 @@ private void checkUserAlreadyExists(String email) { + email + " is already registered."); } } + + private UserResponseDto getUserInfoById(Long userId) { + User user = userRepository.findById(userId).orElseThrow( + () -> new EntityNotFoundException( + "There is no user by id: " + userId + ) + ); + return userMapper.toDto(user); + } + + private UserResponseDto getAuthenticatedUserInfo(User authenticatedUser) { + return userMapper.toDto(authenticatedUser); + } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java index f5636fa..8f3844b 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java @@ -15,6 +15,7 @@ public interface UserGameService { Page getByStatus( UserGame.GameStatus status, Long userId, + User authenticatedUser, Pageable pageable ); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java index ae75c5d..1c0c19c 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java @@ -4,6 +4,7 @@ import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; import com.videogamescatalogue.backend.exception.AccessNotAllowedException; +import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.mapper.game.GameMapper; import com.videogamescatalogue.backend.mapper.usergame.UserGameMapper; @@ -61,7 +62,22 @@ public void delete(Long id, User user) { public Page getByStatus( UserGame.GameStatus status, Long userId, + User authenticatedUser, Pageable pageable) { + if (authenticatedUser == null && userId == null) { + throw new AuthenticationRequiredException("Authentication is required"); + } + if (userId == null) { + return findByUserIdAndStatus( + authenticatedUser.getId(), status, pageable + ); + } + return findByUserIdAndStatus(userId,status,pageable); + } + + private Page findByUserIdAndStatus( + Long userId, UserGame.GameStatus status, Pageable pageable + ) { Page userGames = userGameRepository.findByUserIdAndStatus( userId, status, pageable ); diff --git a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java index 4b7a9c0..966b0b0 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java @@ -179,7 +179,7 @@ void getUserComments_validRequest_returnPageCommentDto() { .thenReturn(commentPage); when(commentMapper.toDto(comment)).thenReturn(commentDto); - Page actual = commentService.getUserComments(userId, pageable); + Page actual = commentService.getUserComments(user, userId, pageable); assertNotNull(actual); assertEquals(commentDtoPage, actual); diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java index 82bafe1..8d7ef53 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java @@ -14,6 +14,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; +import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.InvalidInputException; import com.videogamescatalogue.backend.exception.RegistrationException; import com.videogamescatalogue.backend.mapper.user.UserMapper; @@ -151,15 +152,39 @@ void registerUser_validRequest_returnUserDto() { } @Test - void getUserInfo_usersMatch_returnAuthenticatedUserInfo() { + void getUserInfo_onlyAuthUser_returnAuthenticatedUserInfo() { when(userMapper.toDto(user)) + .thenReturn(responseDtoUser); + + UserResponseDto actual = userService.getUserInfo(null, user); + + assertEquals(responseDtoUser, actual); + + verifyNoInteractions(userRepository); + } + + @Test + void getUserInfo_onlyUserId_returnUserInfoById() { + when(userRepository.findById(9L)) + .thenReturn(Optional.ofNullable(userBob)); + when(userMapper.toDto(userBob)) .thenReturn(responseDtoBob); - UserResponseDto actual = userService.getUserInfoById(10L, user); + UserResponseDto actual = userService.getUserInfo(9L, null); assertEquals(responseDtoBob, actual); - verifyNoInteractions(userRepository); + verify(userRepository).findById(9L); + } + + @Test + void getUserInfo_thUserNoId_throwException() { + assertThrows( + AuthenticationRequiredException.class, + () -> userService.getUserInfo(null, null) + ); + + verifyNoInteractions(userRepository, userMapper); } @Test @@ -169,7 +194,7 @@ void getUserInfo_usersDoNotMatch_returnOtherUserInfo() { when(userMapper.toDto(userBob)) .thenReturn(responseDtoBob); - UserResponseDto actual = userService.getUserInfoById(9L, user); + UserResponseDto actual = userService.getUserInfo(9L, user); assertEquals(responseDtoBob, actual); assertNotEquals(responseDtoUser, actual); From de8e901bc17846de16c4633d3cb4a1c32b9351a4 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:06:52 +0200 Subject: [PATCH 34/41] refactored delete method (#48) --- README.md | 2 +- .../controller/UserGameController.java | 6 ++--- .../repository/UserGameRepository.java | 2 +- .../backend/service/game/GameServiceImpl.java | 2 +- .../service/usergame/UserGameService.java | 2 +- .../service/usergame/UserGameServiceImpl.java | 25 ++++++++----------- src/main/resources/application.properties | 2 ++ .../usergame/UserGameServiceImplTest.java | 24 +++++++++++------- 8 files changed, 35 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ec81aaa..a9a0ddb 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ _ Description: Authenticates a user and returns JWT token. ### deleteUserGame - Description: Removes a game from the authenticated user’s list. -- URL: http://localhost:8080/api/user-games/{id} +- URL: http://localhost:8080/api/user-games/{apiId} - Method: DELETE - Authentication: Required diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java index 402ac6b..ca3f434 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java @@ -92,13 +92,13 @@ public UserGameDto createOrUpdate( description = "User game not found", content = @Content ) - @DeleteMapping("/{id}") + @DeleteMapping("/{apiId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete( - @PathVariable Long id, + @PathVariable Long apiId, @AuthenticationPrincipal User user ) { - userGameService.delete(id, user); + userGameService.delete(apiId, user); } @Operation( diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java index cf93e15..1e281cf 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface UserGameRepository extends JpaRepository { - Optional findByUserIdAndGameApiId(Long userId, Long apiId); + Optional findByUser_IdAndGame_ApiId(Long userId, Long apiId); List findAllByUserId(Long userId); diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 71a536b..8671744 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -140,7 +140,7 @@ private UserGame.GameStatus getGameStatus(Long apiId, User user) { return null; } - Optional userGameOptional = userGameRepository.findByUserIdAndGameApiId( + Optional userGameOptional = userGameRepository.findByUser_IdAndGame_ApiId( user.getId(), apiId ); return userGameOptional.map(UserGame::getStatus) diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java index 8f3844b..23e9f01 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java @@ -10,7 +10,7 @@ public interface UserGameService { UserGameDto createOrUpdate(CreateUserGameDto createDto, User user); - void delete(Long id, User user); + void delete(Long apiId, User user); Page getByStatus( UserGame.GameStatus status, diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java index 1c0c19c..e498194 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java @@ -3,7 +3,6 @@ import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto; -import com.videogamescatalogue.backend.exception.AccessNotAllowedException; import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.mapper.game.GameMapper; @@ -31,7 +30,7 @@ public class UserGameServiceImpl implements UserGameService { @Override public UserGameDto createOrUpdate(CreateUserGameDto createDto, User user) { - Optional userGameOptional = userGameRepository.findByUserIdAndGameApiId( + Optional userGameOptional = userGameRepository.findByUser_IdAndGame_ApiId( user.getId(), createDto.apiId() ); @@ -53,9 +52,9 @@ public UserGameDto createOrUpdate(CreateUserGameDto createDto, User user) { } @Override - public void delete(Long id, User user) { - checkBelongsToUser(id, user.getId()); - userGameRepository.deleteById(id); + public void delete(Long apiId, User user) { + UserGame userGame = getUserGame(user.getId(), apiId); + userGameRepository.delete(userGame); } @Override @@ -84,17 +83,15 @@ private Page findByUserIdAndStatus( return userGames.map(userGameMapper::toDto); } - private void checkBelongsToUser(Long id, Long userId) { - Optional userGameOptional = userGameRepository.findById(id); + private UserGame getUserGame(Long userId, Long apiId) { + Optional userGameOptional = userGameRepository.findByUser_IdAndGame_ApiId( + userId, apiId + ); if (userGameOptional.isEmpty()) { - throw new EntityNotFoundException("There is no userGame by id: " + id); - } - - Long userGameUserId = userGameOptional.get().getUser().getId(); - if (!userGameUserId.equals(userId)) { - throw new AccessNotAllowedException("User with id: " - + userId + "is not allowed to access userGame with id: " + id); + throw new EntityNotFoundException("There is no userGame by apiId: " + apiId + + " for user with id: " + userId); } + return userGameOptional.get(); } private UserGameDto addGameAndSave(UserGame userGame, Game game) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ca164fd..c870717 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,6 +9,8 @@ spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml +spring.jpa.show-sql=true + spring.jpa.hibernate.ddl-auto=none server.servlet.context-path=/api diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java index adf6300..441d9d7 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java @@ -68,9 +68,11 @@ void setUp() { user.setLocation("location"); user.getRoles().add(role); + Game game = new Game(); + game.setApiId(100L); userGame = new UserGame(); userGame.setUser(user); - userGame.setGame(new Game()); + userGame.setGame(game); userGame.setStatus(UserGame.GameStatus.BACKLOG); platformDto = new PlatformDto("PC"); @@ -94,7 +96,7 @@ void setUp() { @Test void createOrUpdate_alreadyExists_update() { - when(userGameRepository.findByUserIdAndGameApiId( + when(userGameRepository.findByUser_IdAndGame_ApiId( user.getId(), createUserGameDto.apiId() )).thenReturn(Optional.of(userGame)); @@ -110,7 +112,7 @@ void createOrUpdate_alreadyExists_update() { assertNotNull(actual); assertEquals(userGameDto, actual); - verify(userGameRepository).findByUserIdAndGameApiId( + verify(userGameRepository).findByUser_IdAndGame_ApiId( user.getId(), createUserGameDto.apiId() ); @@ -119,7 +121,7 @@ void createOrUpdate_alreadyExists_update() { @Test void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() { - when(userGameRepository.findByUserIdAndGameApiId( + when(userGameRepository.findByUser_IdAndGame_ApiId( user.getId(), createUserGameDto.apiId() )).thenReturn(Optional.empty()); @@ -140,7 +142,7 @@ void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() { assertNotNull(actual); assertEquals(userGameDto, actual); - verify(userGameRepository).findByUserIdAndGameApiId( + verify(userGameRepository).findByUser_IdAndGame_ApiId( user.getId(), createUserGameDto.apiId() ); @@ -150,14 +152,18 @@ void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() { @Test void delete_validRequest_delete() { Long userGameId = userGame.getId(); - when(userGameRepository.findById(userGameId)) + when(userGameRepository.findByUser_IdAndGame_ApiId( + user.getId(), userGame.getGame().getApiId() + )) .thenReturn(Optional.of(userGame)); assertDoesNotThrow(() -> userGameService.delete( - userGameId, user + userGame.getGame().getApiId(), user )); - verify(userGameRepository).findById(userGameId); - verify(userGameRepository).deleteById(userGameId); + verify(userGameRepository).findByUser_IdAndGame_ApiId( + user.getId(), userGame.getGame().getApiId() + ); + verify(userGameRepository).delete(userGame); } } From 724f9c3b53738af7a038f5d7bd754a1cd257ffa1 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:46:39 +0200 Subject: [PATCH 35/41] made filtering games by several years (#49) --- .../backend/dto/internal/GameSearchParameters.java | 2 +- .../com/videogamescatalogue/backend/model/Game.java | 2 +- .../repository/game/GameSpecificationBuilder.java | 4 ++-- .../game/spec/GamePlatformsSpecificationProvider.java | 4 ++-- .../game/spec/GameYearSpecificationProvider.java | 10 ++++++---- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java index ad56359..c85104d 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java @@ -4,7 +4,7 @@ public record GameSearchParameters( String name, - Integer year, + List years, List platforms, List genres ) { diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index e4eaa2e..449fda0 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -67,7 +67,7 @@ public class Game { @Getter public enum SpecificationKey { NAME("name"), - YEAR("year"), + YEARS("years"), PLATFORMS("platforms"), GENRES("genres"); diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java index ad5ee03..4113159 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java @@ -26,8 +26,8 @@ public Specification build(GameSearchParameters searchParameters) { Game.SpecificationKey.NAME.getValue(), specification ); specification = getSpecificationForParam( - searchParameters.year(), - Game.SpecificationKey.YEAR.getValue(), specification + searchParameters.years(), + Game.SpecificationKey.YEARS.getValue(), specification ); specification = getSpecificationForParam( searchParameters.platforms(), diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java index c72ceae..e61c41f 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java @@ -39,9 +39,9 @@ public Predicate toPredicate( .map(Platform.GeneralName::valueOf) .toList(); - Join platromJoin = root.join(KEY); + Join platformJoin = root.join(KEY); - return platromJoin.get("generalName").in(names); + return platformJoin.get("generalName").in(names); } }; } diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java index 7ed36b2..6936fbb 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java @@ -6,13 +6,14 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import java.util.List; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; @Component -public class GameYearSpecificationProvider implements SpecificationProvider { +public class GameYearSpecificationProvider implements SpecificationProvider> { - public static final String KEY = Game.SpecificationKey.YEAR.getValue(); + public static final String KEY = Game.SpecificationKey.YEARS.getValue(); @Override public String getKey() { @@ -20,7 +21,7 @@ public String getKey() { } @Override - public Specification getSpecification(Integer param) { + public Specification getSpecification(List years) { return new Specification() { @Override public Predicate toPredicate( @@ -28,7 +29,8 @@ public Predicate toPredicate( CriteriaQuery query, CriteriaBuilder criteriaBuilder ) { - return criteriaBuilder.equal(root.get(KEY), param); + + return root.get("year").in(years); } }; } From e211d8ddd4fb428864881cc5a94bc5f97d6f39d6 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:27:58 +0200 Subject: [PATCH 36/41] allowed rating to be up to 10 (#51) --- .../backend/dto/internal/comment/UpdateCommentRequestDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java index 5c5d3a9..26db09c 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java @@ -8,7 +8,7 @@ public record UpdateCommentRequestDto( @Size(max = 2000, message = "Comment must be less than 2000 digits") String text, @Min(0) - @Max(5) + @Max(10) Integer rating ) { } From 05ebf887a6172303cf6765b100dd38f11a76ad2d Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:10:33 +0200 Subject: [PATCH 37/41] added regex to validate (#53) --- .../internal/user/UserRegistrationRequestDto.java | 15 ++++++++++++--- .../backend/repository/UserRepository.java | 2 ++ .../backend/service/user/UserServiceImpl.java | 12 ++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java index 3c4b168..40a07a7 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java @@ -3,6 +3,7 @@ import com.videogamescatalogue.backend.validation.FieldMatch; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @FieldMatch(first = "password", @@ -10,15 +11,23 @@ message = "Password and repeat password must match") public record UserRegistrationRequestDto( @NotBlank(message = "Username is required. Please provide username.") + @Pattern(regexp = "^[A-Za-z0-9_]{3,20}$", + message = "Profile name may contain only letters, digits, and underscore " + + "and be between 3 and 20 digits") String profileName, @NotBlank(message = "Password is required. Please provide your password.") - @Size(min = 8, max = 25, message = "Password must be between 8 and 25 digits") + @Pattern(regexp = "^(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?])" + + "[A-Za-z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?]{8,30}$", + message = "Password must include uppercase, digit, and special character" + + "and be between 8 and 30 digits") String password, @NotBlank(message = "Please, repeat your password.") - @Size(min = 8, max = 25, message = "Repeat password must be between 8 and 25 digits") + @Size(min = 8, max = 30, message = "Repeat password must be between 8 and 30 digits") String repeatPassword, - @Email(message = "Invalid format of email.") + @Email(message = "Invalid format of email.", + regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9]+\\.com$") @NotBlank(message = "Email is required. Please provide your email.") + @Size(min = 14, max = 72, message = "Email must be between 14 and 72 digits") String email, @Size(max = 5000, message = "About user info must be less than 5000 digits") String about, diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java index e20ce98..4611492 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java @@ -8,6 +8,8 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); + boolean existsByProfileNameIgnoreCase(String profileName); + @EntityGraph(attributePaths = "roles") Optional findByEmail(String email); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index 5de302c..ae29973 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -39,7 +39,8 @@ private void init() { @Override public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { - checkUserAlreadyExists(requestDto.email()); + checkUserAlreadyExistsByEmail(requestDto.email()); + checkProfileNameAlreadyExists(requestDto.profileName()); User user = userMapper.toModel(requestDto); user.setPassword(passwordEncoder.encode(requestDto.password())); @@ -105,13 +106,20 @@ private void validateCurrentPassword( } } - private void checkUserAlreadyExists(String email) { + private void checkUserAlreadyExistsByEmail(String email) { if (userRepository.existsByEmail(email)) { throw new RegistrationException("Can't register user. User with email: " + email + " is already registered."); } } + private void checkProfileNameAlreadyExists(String profileName) { + if (userRepository.existsByProfileNameIgnoreCase(profileName)) { + throw new RegistrationException("Can't register user with profileName " + + profileName + ". This profileName is already in use."); + } + } + private UserResponseDto getUserInfoById(Long userId) { User user = userRepository.findById(userId).orElseThrow( () -> new EntityNotFoundException( From 0788d61cfd5b9e822e0d3bb35938881e9b3c77cd Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:10:51 +0200 Subject: [PATCH 38/41] Add developer info to game (#55) * created developer entiry, configured liquibase * added logic to get developers info if missing * refactored mapping * added developer info --- .../dto/external/ApiResponseDeveloperDto.java | 7 +++ .../dto/external/ApiResponseFullGameDto.java | 2 + .../dto/internal/developer/DeveloperDto.java | 8 ++++ .../backend/dto/internal/game/GameDto.java | 2 + .../dto/internal/game/GameWithStatusDto.java | 2 + .../mapper/developer/DeveloperMapper.java | 24 ++++++++++ .../mapper/developer/DeveloperProvider.java | 48 +++++++++++++++++++ .../backend/mapper/game/GameMapper.java | 13 ++++- .../backend/model/Developer.java | 26 ++++++++++ .../backend/model/Game.java | 9 ++++ .../repository/DeveloperRepository.java | 12 +++++ .../backend/service/game/GameServiceImpl.java | 25 ++++++++-- .../changes/16-create-developers-table.yaml | 33 +++++++++++++ .../17-create-games-developers-table.yaml | 43 +++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 4 ++ .../service/game/GameServiceImplTest.java | 21 ++++++++ .../usergame/UserGameServiceImplTest.java | 9 +++- 17 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java create mode 100644 src/main/java/com/videogamescatalogue/backend/model/Developer.java create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java create mode 100644 src/main/resources/db/changelog/changes/16-create-developers-table.yaml create mode 100644 src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java new file mode 100644 index 0000000..1b6ec8b --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.dto.external; + +public record ApiResponseDeveloperDto( + Long id, + String name +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java index 53741cf..6ed5f6f 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java @@ -15,6 +15,8 @@ public record ApiResponseFullGameDto( List genres, + List developers, + BigDecimal rating ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java new file mode 100644 index 0000000..64a1ee1 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java @@ -0,0 +1,8 @@ +package com.videogamescatalogue.backend.dto.internal.developer; + +public record DeveloperDto( + Long id, + Long apiId, + String name +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java index 69f8089..1008165 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java @@ -1,5 +1,6 @@ package com.videogamescatalogue.backend.dto.internal.game; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import java.math.BigDecimal; @@ -12,6 +13,7 @@ public record GameDto( String backgroundImage, Set platforms, Set genres, + Set developers, BigDecimal apiRating, String description ) { diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java index 1df968d..bfa39fc 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java @@ -1,5 +1,6 @@ package com.videogamescatalogue.backend.dto.internal.game; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.model.UserGame; @@ -13,6 +14,7 @@ public record GameWithStatusDto( String backgroundImage, Set platforms, Set genres, + Set developers, BigDecimal apiRating, String description, UserGame.GameStatus status diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java new file mode 100644 index 0000000..28f0e20 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java @@ -0,0 +1,24 @@ +package com.videogamescatalogue.backend.mapper.developer; + +import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; +import com.videogamescatalogue.backend.model.Developer; +import java.util.List; +import java.util.Set; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface DeveloperMapper { + @Mapping(source = "id", target = "apiId") + @Mapping(target = "id", ignore = true) + Developer toModel(ApiResponseDeveloperDto apiResponseDeveloperDto); + + Set toModelSet(List developers); + + Set toDtoSet(Set developers); + + DeveloperDto toDto(Developer developer); + +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java new file mode 100644 index 0000000..9897f7b --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java @@ -0,0 +1,48 @@ +package com.videogamescatalogue.backend.mapper.developer; + +import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; +import com.videogamescatalogue.backend.model.Developer; +import com.videogamescatalogue.backend.repository.DeveloperRepository; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.mapstruct.Named; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class DeveloperProvider { + private final DeveloperMapper developerMapper; + private final DeveloperRepository developerRepository; + + @Named("toDevelopersSet") + public Set toDevelopersSet(List developers) { + List developerApiIds = developers.stream() + .map(ApiResponseDeveloperDto::id) + .toList(); + + List existingDevelopers = developerRepository.findAllByApiIdIn(developerApiIds); + + Map existingDevelopersMap = existingDevelopers.stream() + .collect(Collectors.toMap( + Developer::getApiId, + d -> d + )); + + Set developersSet = developers.stream() + .map(d -> existingDevelopersMap.getOrDefault( + d.id(), + developerMapper.toModel(d))) + .collect(Collectors.toSet()); + + return developersSet; + } + + @Named("toDeveloperDtosSet") + public Set toDeveloperDtosSet(Set developers) { + return developerMapper.toDtoSet(developers); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java index 3458fe7..513dc15 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java @@ -6,6 +6,7 @@ import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.exception.ParsingException; +import com.videogamescatalogue.backend.mapper.developer.DeveloperProvider; import com.videogamescatalogue.backend.mapper.genre.GenreProvider; import com.videogamescatalogue.backend.mapper.platform.PlatformProvider; import com.videogamescatalogue.backend.model.Game; @@ -17,7 +18,11 @@ import org.mapstruct.Mapping; import org.mapstruct.Named; -@Mapper(config = MapperConfig.class, uses = {PlatformProvider.class, GenreProvider.class}) +@Mapper(config = MapperConfig.class, uses = { + PlatformProvider.class, + GenreProvider.class, + DeveloperProvider.class +}) public interface GameMapper { List toModelList(List games); @@ -34,14 +39,20 @@ public interface GameMapper { @Mapping(source = "released", target = "year", qualifiedByName = "toYear") @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformsSet") @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenresSet") + @Mapping(source = "developers", target = "developers", qualifiedByName = "toDevelopersSet") @Mapping(source = "rating", target = "apiRating") Game toModel(ApiResponseFullGameDto apiResponseGameDto); @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformDtosSet") @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenreDtosSet") + @Mapping(source = "developers", target = "developers", qualifiedByName = "toDeveloperDtosSet") GameDto toDto(Game game); @Mapping(target = "status", source = "status") + @Mapping( + source = "game.developers", target = "developers", + qualifiedByName = "toDeveloperDtosSet" + ) GameWithStatusDto toDtoWithStatus(Game game, UserGame.GameStatus status); @Named("toYear") diff --git a/src/main/java/com/videogamescatalogue/backend/model/Developer.java b/src/main/java/com/videogamescatalogue/backend/model/Developer.java new file mode 100644 index 0000000..8af6844 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/model/Developer.java @@ -0,0 +1,26 @@ +package com.videogamescatalogue.backend.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "developers") +@Getter +@Setter +public class Developer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private Long apiId; + + @Column(nullable = false) + private String name; +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 449fda0..1fd14e8 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -57,6 +57,15 @@ public class Game { ) private Set genres = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "games_developers", + joinColumns = @JoinColumn(name = "game_id"), + inverseJoinColumns = @JoinColumn(name = "developer_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"game_id", "developer_id"}) + ) + private Set developers = new HashSet<>(); + private BigDecimal apiRating; private String description; diff --git a/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java new file mode 100644 index 0000000..fbb8a77 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java @@ -0,0 +1,12 @@ +package com.videogamescatalogue.backend.repository; + +import com.videogamescatalogue.backend.model.Developer; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DeveloperRepository extends JpaRepository { + Optional findByApiId(Long apiId); + + List findAllByApiIdIn(List apiIds); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 8671744..8e4f841 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -5,18 +5,23 @@ import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; +import com.videogamescatalogue.backend.mapper.developer.DeveloperMapper; import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Developer; import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.model.UserGame; +import com.videogamescatalogue.backend.repository.DeveloperRepository; import com.videogamescatalogue.backend.repository.GameRepository; import com.videogamescatalogue.backend.repository.SpecificationBuilder; import com.videogamescatalogue.backend.repository.UserGameRepository; import com.videogamescatalogue.backend.service.RawgApiClient; +import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -32,9 +37,11 @@ public class GameServiceImpl implements GameService { private final RawgApiClient apiClient; private final GameMapper gameMapper; + private final DeveloperMapper developerMapper; private final GameRepository gameRepository; private final SpecificationBuilder specificationBuilder; private final UserGameRepository userGameRepository; + private final DeveloperRepository developerRepository; @Override public void fetchBestGames() { @@ -82,6 +89,7 @@ public Page getAllGamesFromDb(Pageable pageable) { .map(gameMapper::toDto); } + @Transactional @Override public GameWithStatusDto getByApiId(Long apiId, User user) { Game game = findOrUpdate(apiId); @@ -154,15 +162,26 @@ private Game findOrUpdate(Long apiId) { } Game game = gameOptional.get(); if (game.getDescription() == null) { - return updateGameDescription(apiId, game); + updateGameDescription(apiId, game); + } + if (game.getDevelopers().isEmpty()) { + updateGameDevelopers(apiId, game); } return game; } - private Game updateGameDescription(Long apiId, Game game) { + private void updateGameDescription(Long apiId, Game game) { ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); game.setDescription(apiGame.description()); - return gameRepository.save(game); + gameRepository.save(game); + } + + private void updateGameDevelopers(Long apiId, Game game) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + Set developers = developerMapper.toModelSet(apiGame.developers()); + developerRepository.saveAll(developers); + game.setDevelopers(developers); + gameRepository.save(game); } private Game findFromApi(Long apiId) { diff --git a/src/main/resources/db/changelog/changes/16-create-developers-table.yaml b/src/main/resources/db/changelog/changes/16-create-developers-table.yaml new file mode 100644 index 0000000..2cb4f89 --- /dev/null +++ b/src/main/resources/db/changelog/changes/16-create-developers-table.yaml @@ -0,0 +1,33 @@ +databaseChangeLog: + - changeSet: + id: create-developers-table + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: developers + changes: + - createTable: + tableName: developers + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: api_id + type: BIGINT + constraints: + nullable: false + unique: true + + - column: + name: name + type: VARCHAR(200) + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml b/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml new file mode 100644 index 0000000..bc69400 --- /dev/null +++ b/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml @@ -0,0 +1,43 @@ +databaseChangeLog: + - changeSet: + id: create-games-developers-table + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: games_developers + changes: + - createTable: + tableName: games_developers + columns: + - column: + name: game_id + type: BIGINT + constraints: + nullable: false + + - column: + name: developer_id + type: BIGINT + constraints: + nullable: false + + - addPrimaryKey: + tableName: games_developers + columnNames: game_id, developer_id + constraintName: pk_games_developers + + - addForeignKeyConstraint: + baseTableName: games_developers + baseColumnNames: game_id + referencedTableName: games + referencedColumnNames: id + constraintName: fk_games_developers_game + + - addForeignKeyConstraint: + baseTableName: games_developers + baseColumnNames: developer_id + referencedTableName: developers + referencedColumnNames: id + constraintName: fk_games_developers_developer diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 0763141..6baf17b 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -29,3 +29,7 @@ databaseChangeLog: file: classpath:/db/changelog/changes/14-create-user-game-table.yaml - include: file: classpath:/db/changelog/changes/15-create-comments-table.yaml + - include: + file: classpath:/db/changelog/changes/16-create-developers-table.yaml + - include: + file: classpath:/db/changelog/changes/17-create-games-developers-table.yaml diff --git a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java index 850368b..7f4e014 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java @@ -9,15 +9,18 @@ import static org.mockito.Mockito.when; import com.videogamescatalogue.backend.dto.external.ApiPlatformWrapper; +import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto; import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGenreDto; import com.videogamescatalogue.backend.dto.external.ApiResponsePlatformDto; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Developer; import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.model.Genre; import com.videogamescatalogue.backend.model.Platform; @@ -84,6 +87,11 @@ void setUp() { genreModel.setId(20L); genreModel.setName(Genre.Name.ACTION); + Developer developer = new Developer(); + developer.setId(30L); + developer.setApiId(457389L); + developer.setName("Developer"); + gameModel = new Game(); gameModel.setApiId(1L); gameModel.setName("Game"); @@ -93,6 +101,8 @@ void setUp() { gameModel.setGenres(Set.of(genreModel)); gameModel.setApiRating(BigDecimal.valueOf(4.75)); gameModel.setDescription(null); + gameModel.setDevelopers(Set.of(developer)); + gameModelList = List.of(gameModel); responseGamesIds = List.of(1L); zeroGamesToSave = List.of(); @@ -102,24 +112,35 @@ void setUp() { PlatformDto platformDto = new PlatformDto("PC"); GenreDto genreDto = new GenreDto("Action"); + DeveloperDto developerDto = new DeveloperDto( + 30L, 457389L, "Developer" + ); gameDto = new GameDto( 1L, "Game", 2025, "link", Set.of(platformDto), Set.of(genreDto), + Set.of(developerDto), BigDecimal.valueOf(4.75), null ); oneGameDtoPage = new PageImpl<>(List.of(gameDto)); gameWithDescrAndNullStatusDto = new GameWithStatusDto( 1L, "Game", 2025, "link", Set.of(platformDto), Set.of(genreDto), + Set.of(developerDto), BigDecimal.valueOf(4.75), "description", null ); + ApiResponseDeveloperDto apiResponseDeveloperDto = + new ApiResponseDeveloperDto( + 457389L, "Developer" + ); + apiResponseFullGameDto = new ApiResponseFullGameDto( 1L, "Game", "description", "2025-12-12", "link", List.of(apiPlatformWrapper), List.of(apiResponseGenreDto), + List.of(apiResponseDeveloperDto), BigDecimal.valueOf(4.75) ); diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java index 441d9d7..25000c1 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; @@ -45,9 +46,10 @@ class UserGameServiceImplTest { private User user; private UserGame userGame; private UserGameDto userGameDto; - private GameDto gameDto; private PlatformDto platformDto; private GenreDto genreDto; + private DeveloperDto developerDto; + private GameDto gameDto; @BeforeEach void setUp() { @@ -79,11 +81,16 @@ void setUp() { genreDto = new GenreDto("Action"); + developerDto = new DeveloperDto( + 30L, 23456L, "Developer" + ); + gameDto = new GameDto( 1234L, "Game name", 2016, "link", Set.of(platformDto), Set.of(genreDto), + Set.of(developerDto), BigDecimal.valueOf(4.8), "description" ); From d87a8f39202a6748c2bd9ea3d5909f86c04f8ae9 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:47:07 +0200 Subject: [PATCH 39/41] removed email from update method (#56) --- .../backend/dto/internal/user/UpdateUserRequestDto.java | 3 --- .../backend/service/user/UserServiceImplTest.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java index 90bdc95..29c0db2 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java @@ -1,12 +1,9 @@ package com.videogamescatalogue.backend.dto.internal.user; -import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Size; public record UpdateUserRequestDto( String profileName, - @Email(message = "Invalid format of email.") - String email, @Size(max = 5000, message = "About user info must be less than 5000 digits") String about, @Size(max = 50, message = "Location info must be less than 50 digits") diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java index 8d7ef53..d5064f0 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java @@ -100,7 +100,7 @@ void setUp() { updateUserRequestDto = new UpdateUserRequestDto( "updated profileName", null, - null, null + null ); changePasswordRequestDto = new ChangePasswordRequestDto( From 93715e93ccb97420f1ab0db8a31870a1260d3713 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:44:55 +0200 Subject: [PATCH 40/41] return token after registartion (#57) --- .../controller/AuthenticationController.java | 3 ++- .../user/UserRegistrationResponseDto.java | 10 +++++++++ .../backend/mapper/user/UserMapper.java | 4 ++++ .../backend/service/user/UserService.java | 3 ++- .../backend/service/user/UserServiceImpl.java | 16 ++++++++++++-- .../service/user/UserServiceImplTest.java | 21 ++++++++++++++++--- 6 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java index f4f7c3f..b619977 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java @@ -3,6 +3,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.security.AuthenticationService; import com.videogamescatalogue.backend.service.user.UserService; @@ -45,7 +46,7 @@ public class AuthenticationController { } ) @PostMapping("/registration") - UserResponseDto registerUser( + UserRegistrationResponseDto registerUser( @RequestBody @Valid UserRegistrationRequestDto requestDto ) { return userService.registerUser(requestDto); diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java new file mode 100644 index 0000000..4e04a2c --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java @@ -0,0 +1,10 @@ +package com.videogamescatalogue.backend.dto.internal.user; + +public record UserRegistrationResponseDto( + Long id, + String profileName, + String about, + String location, + String token +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java index 60a75e0..e93d4ee 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java @@ -3,6 +3,7 @@ import com.videogamescatalogue.backend.config.MapperConfig; import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.model.User; import org.mapstruct.Mapper; @@ -17,5 +18,8 @@ public interface UserMapper { @Mapping(source = "id", target = "userGames", qualifiedByName = "getStatusDtoList") UserResponseDto toDto(User user); + @Mapping(source = "token", target = "token") + UserRegistrationResponseDto toRegistrationResponseDto(User user, String token); + User updateProfileInfo(@MappingTarget User user, UpdateUserRequestDto requestDto); } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java index 73c69ac..5212084 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java @@ -3,11 +3,12 @@ import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.model.User; public interface UserService { - UserResponseDto registerUser( + UserRegistrationResponseDto registerUser( UserRegistrationRequestDto requestDto ); diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index ae29973..cea8ec2 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -2,7 +2,10 @@ import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; @@ -13,7 +16,9 @@ import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.repository.RoleRepository; import com.videogamescatalogue.backend.repository.UserRepository; +import com.videogamescatalogue.backend.security.AuthenticationService; import jakarta.annotation.PostConstruct; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.crypto.password.PasswordEncoder; @@ -27,6 +32,7 @@ public class UserServiceImpl implements UserService { private final UserMapper userMapper; private final PasswordEncoder passwordEncoder; private final RoleRepository roleRepository; + private final AuthenticationService authenticationService; private Role roleUser; @PostConstruct @@ -37,8 +43,9 @@ private void init() { ); } + @Transactional @Override - public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { + public UserRegistrationResponseDto registerUser(UserRegistrationRequestDto requestDto) { checkUserAlreadyExistsByEmail(requestDto.email()); checkProfileNameAlreadyExists(requestDto.profileName()); @@ -50,7 +57,12 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) { log.info("User registered successfully, id={}",savedUser.getId()); - return userMapper.toDto(savedUser); + UserLoginResponseDto authResponseDto = authenticationService.authenticate( + new UserLoginRequestDto( + requestDto.email(), requestDto.password() + )); + + return userMapper.toRegistrationResponseDto(savedUser, authResponseDto.token()); } @Override diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java index d5064f0..d689459 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java @@ -11,7 +11,10 @@ import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; +import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto; import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; @@ -21,6 +24,7 @@ import com.videogamescatalogue.backend.model.Role; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.repository.UserRepository; +import com.videogamescatalogue.backend.security.AuthenticationService; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -39,6 +43,8 @@ class UserServiceImplTest { private UserMapper userMapper; @Mock private PasswordEncoder passwordEncoder; + @Mock + private AuthenticationService authenticationService; @InjectMocks private UserServiceImpl userService; private String email = "test@gmail.com"; @@ -128,6 +134,11 @@ void registerUser_userExists_throwException() { @Test void registerUser_validRequest_returnUserDto() { + final UserRegistrationResponseDto responseDto = new UserRegistrationResponseDto( + 1L, "profileName", "about", + "location", "token" + ); + String encodedPassword = user.getPassword(); when(userRepository.existsByEmail(email)) @@ -139,13 +150,17 @@ void registerUser_validRequest_returnUserDto() { .thenReturn(encodedPassword); when(userRepository.save(user)) .thenReturn(user); - when(userMapper.toDto(user)).thenReturn(responseDtoBob); + when(authenticationService.authenticate(new UserLoginRequestDto( + registrationRequestDto.email(), registrationRequestDto.password() + ))).thenReturn(new UserLoginResponseDto("token", 10L)); + when(userMapper.toRegistrationResponseDto(user, "token")) + .thenReturn(responseDto); - UserResponseDto actual = userService.registerUser( + UserRegistrationResponseDto actual = userService.registerUser( registrationRequestDto ); - assertEquals(responseDtoBob, actual); + assertEquals(responseDto, actual); verify(passwordEncoder).encode(registrationRequestDto.password()); verify(userRepository).existsByEmail(email); From dc703c88ff69c8db4dcbf0a1f75bc609bf5d32a2 Mon Sep 17 00:00:00 2001 From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:17:21 +0200 Subject: [PATCH 41/41] corrected duplicate developers save (#59) * corrected duplicate developers save * ran mvn clean package --- .../backend/controller/GameController.java | 32 --------- .../backend/model/Game.java | 4 ++ .../backend/repository/GameRepository.java | 7 ++ .../backend/security/JwtUtil.java | 14 ++-- .../backend/service/RawgApiClient.java | 13 ++-- .../service/comment/CommentServiceImpl.java | 12 ++-- .../backend/service/game/GameServiceImpl.java | 67 +++++++++++++------ .../backend/service/user/UserServiceImpl.java | 18 ++++- 8 files changed, 96 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index ef62b38..bcbcd65 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -3,8 +3,6 @@ import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; -import com.videogamescatalogue.backend.model.Genre; -import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.game.GameService; import io.swagger.v3.oas.annotations.Operation; @@ -12,9 +10,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.Arrays; import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -130,7 +126,6 @@ public Page search( @PageableDefault(size = DEFAULT_PAGE_SIZE) Pageable pageable ) { - validateSearchParams(searchParameters); return gameService.search(searchParameters, pageable); } @@ -149,31 +144,4 @@ public Page apiSearch( ) { return gameService.apiSearch(searchParams); } - - private void validateSearchParams(GameSearchParameters searchParameters) { - if (searchParameters.platforms() != null) { - try { - searchParameters.platforms() - .forEach( - p -> Platform.GeneralName.valueOf(p.toUpperCase()) - ); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "Invalid platforms provided. Valid platforms: " - + Arrays.stream(Platform.GeneralName.values()) - .map(Enum::toString) - .collect(Collectors.joining(", ")), e); - } - } - if (searchParameters.genres() != null) { - try { - searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase())); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid genres provided. Valid genres: " - + Arrays.stream(Genre.Name.values()) - .map(Enum::toString) - .collect(Collectors.joining(", ")), e); - } - } - } } diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 1fd14e8..010f878 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -13,6 +13,7 @@ import java.math.BigDecimal; import java.util.HashSet; import java.util.Set; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.SQLDelete; @@ -39,6 +40,7 @@ public class Game { private String backgroundImage; + @EqualsAndHashCode.Exclude @ManyToMany @JoinTable( name = "games_platforms", @@ -48,6 +50,7 @@ public class Game { ) private Set platforms = new HashSet<>(); + @EqualsAndHashCode.Exclude @ManyToMany @JoinTable( name = "games_genres", @@ -57,6 +60,7 @@ public class Game { ) private Set genres = new HashSet<>(); + @EqualsAndHashCode.Exclude @ManyToMany @JoinTable( name = "games_developers", diff --git a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java index cf88999..c11a615 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java @@ -3,11 +3,18 @@ import com.videogamescatalogue.backend.model.Game; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface GameRepository extends JpaRepository, JpaSpecificationExecutor { + @EntityGraph(attributePaths = { + "platforms", "genres", "developers" + }) Optional findByApiId(Long apiId); + @EntityGraph(attributePaths = { + "platforms", "genres", "developers" + }) List findAllByApiIdIn(List apiIds); } diff --git a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java index 87d254f..a51e9e5 100644 --- a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java +++ b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java @@ -41,13 +41,6 @@ public boolean isValidToken(String token) { } } - private Jws getAllClaims(String token) { - return Jwts.parserBuilder() - .setSigningKey(secret) - .build() - .parseClaimsJws(token); - } - public String getUsername(String token) { return getSpecificClaim(token, Claims::getSubject); } @@ -56,4 +49,11 @@ private T getSpecificClaim(String token, Function claimsResolver) Jws allClaims = getAllClaims(token); return claimsResolver.apply(allClaims.getBody()); } + + private Jws getAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(secret) + .build() + .parseClaimsJws(token); + } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index d22dacb..d47504e 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -45,6 +45,9 @@ public class RawgApiClient { private static final String DATES_BETWEEN_URL_PART = "&dates=" + LocalDate.now() + "%2C" + LocalDate.now().minusMonths(12); private static final String METACRITIC_URL_PART = "metacritic=80%2C100"; + private static final int NUMBER_OF_DOWNLOAD_PORTIONS = 11; + private static final String REQUEST_HEADER_NAME = "User-Agent"; + private static final String REQUEST_HEADER_VALUE = "VideoGamesCatalogue"; @Value("${rawg.key}") private String apiKey; @@ -56,7 +59,7 @@ public List getBestGames() { ArrayList result = new ArrayList<>(); - for (int i = 1; i < 11; i++) { + for (int i = 1; i < NUMBER_OF_DOWNLOAD_PORTIONS; i++) { log.info("Create request for page {}", i); String url = BASE_URL + GAME_URL_PART @@ -70,7 +73,7 @@ public List getBestGames() { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); @@ -95,7 +98,7 @@ public Page getAllGames(Pageable pageable) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); @@ -112,7 +115,7 @@ public ApiResponseFullGameDto getGameById(Long id) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseFullGameDto game = getIndividualGame(httpRequest); @@ -129,7 +132,7 @@ public Page search(Map searchParams) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url.toString())) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java index 8c15d2d..841e9f1 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -31,6 +32,7 @@ public class CommentServiceImpl implements CommentService { private final GameMapper gameMapper; private final CommentRepository commentRepository; + @Transactional @Override public CommentDto create(Long gameApiId, CreateCommentRequestDto requestDto, User user) { Comment comment = commentMapper.toModel(requestDto); @@ -61,11 +63,6 @@ public Page getUserComments( return findCommentsByUserId(userId, pageable); } - private Page findCommentsByUserId(Long userId, Pageable pageable) { - Page userComments = commentRepository.findAllByUserId(userId, pageable); - return userComments.map(commentMapper::toDto); - } - @Override public CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId) { Comment comment = commentRepository.findById(commentId) @@ -94,6 +91,11 @@ public void delete(Long commentId, Long userId) { commentRepository.deleteById(commentId); } + private Page findCommentsByUserId(Long userId, Pageable pageable) { + Page userComments = commentRepository.findAllByUserId(userId, pageable); + return userComments.map(commentMapper::toDto); + } + private void existsByIdAndUserId(Long commentId, Long userId) { if (!commentRepository.existsByIdAndUserId(commentId, userId)) { throw new AccessNotAllowedException("User with id: " + userId diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 8e4f841..8aa5eb3 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -6,9 +6,12 @@ import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.mapper.developer.DeveloperMapper; +import com.videogamescatalogue.backend.mapper.developer.DeveloperProvider; import com.videogamescatalogue.backend.mapper.game.GameMapper; import com.videogamescatalogue.backend.model.Developer; import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Genre; +import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.model.UserGame; import com.videogamescatalogue.backend.repository.DeveloperRepository; @@ -18,6 +21,7 @@ import com.videogamescatalogue.backend.service.RawgApiClient; import jakarta.transaction.Transactional; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -42,6 +46,7 @@ public class GameServiceImpl implements GameService { private final SpecificationBuilder specificationBuilder; private final UserGameRepository userGameRepository; private final DeveloperRepository developerRepository; + private final DeveloperProvider developerProvider; @Override public void fetchBestGames() { @@ -108,6 +113,7 @@ public Page getAllGamesFromApi(Pageable pageable) { @Override public Page search(GameSearchParameters searchParameters, Pageable pageable) { + validateSearchParams(searchParameters); Specification specification = specificationBuilder.build(searchParameters); return gameRepository.findAll(specification, pageable) @@ -157,35 +163,56 @@ private UserGame.GameStatus getGameStatus(Long apiId, User user) { private Game findOrUpdate(Long apiId) { Optional gameOptional = gameRepository.findByApiId(apiId); + if (gameOptional.isPresent() + && gameOptional.get().getDescription() != null + && !gameOptional.get().getDevelopers().isEmpty()) { + return gameOptional.get(); + } + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); if (gameOptional.isEmpty()) { - return findFromApi(apiId); + return gameMapper.toModel(apiGame); } Game game = gameOptional.get(); + updateGameInfo(game, apiGame); + return game; + } + + private void updateGameInfo(Game game, ApiResponseFullGameDto apiGame) { if (game.getDescription() == null) { - updateGameDescription(apiId, game); + game.setDescription(apiGame.description()); } if (game.getDevelopers().isEmpty()) { - updateGameDevelopers(apiId, game); + Set developersSet = developerProvider.toDevelopersSet(apiGame.developers()); + developerRepository.saveAll(developersSet); + game.setDevelopers(developersSet); } - return game; - } - - private void updateGameDescription(Long apiId, Game game) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - game.setDescription(apiGame.description()); gameRepository.save(game); } - private void updateGameDevelopers(Long apiId, Game game) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - Set developers = developerMapper.toModelSet(apiGame.developers()); - developerRepository.saveAll(developers); - game.setDevelopers(developers); - gameRepository.save(game); - } - - private Game findFromApi(Long apiId) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - return gameMapper.toModel(apiGame); + private void validateSearchParams(GameSearchParameters searchParameters) { + if (searchParameters.platforms() != null) { + try { + searchParameters.platforms() + .forEach( + p -> Platform.GeneralName.valueOf(p.toUpperCase()) + ); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid platforms provided. Valid platforms: " + + Arrays.stream(Platform.GeneralName.values()) + .map(Enum::toString) + .collect(Collectors.joining(", ")), e); + } + } + if (searchParameters.genres() != null) { + try { + searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase())); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid genres provided. Valid genres: " + + Arrays.stream(Genre.Name.values()) + .map(Enum::toString) + .collect(Collectors.joining(", ")), e); + } + } } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index cea8ec2..eb7a181 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -7,6 +7,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.exception.AccessNotAllowedException; import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.exception.InvalidInputException; @@ -78,6 +79,14 @@ public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { @Override public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser) { + if (authenticatedUser == null) { + throw new AccessNotAllowedException( + "You are not allowed to modify this user info. Please log in." + ); + } + if (requestDto.profileName() != null) { + checkProfileNameAlreadyExists(requestDto.profileName()); + } User updatedUser = userMapper.updateProfileInfo(authenticatedUser, requestDto); User savedUser = userRepository.save(updatedUser); @@ -90,6 +99,11 @@ public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User auth public UserResponseDto changePassword( ChangePasswordRequestDto requestDto, User authenticatedUser ) { + if (authenticatedUser == null) { + throw new AccessNotAllowedException( + "You are not allowed to modify this user info. Please log in." + ); + } validateCurrentPassword(requestDto, authenticatedUser); checkPasswordsMatch(requestDto); @@ -127,8 +141,8 @@ private void checkUserAlreadyExistsByEmail(String email) { private void checkProfileNameAlreadyExists(String profileName) { if (userRepository.existsByProfileNameIgnoreCase(profileName)) { - throw new RegistrationException("Can't register user with profileName " - + profileName + ". This profileName is already in use."); + throw new RegistrationException("Profile Name " + + profileName + " is already in use."); } }