Skip to content
Merged

Dev #60

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f112c72
Merge pull request #7 from mategames-team/add-dockerfile
Bondaliname Dec 11, 2025
8c2e7e5
configured actuator (#8)
YuliiaNisha Dec 11, 2025
f4d4374
Improve filtering (#9)
YuliiaNisha Dec 13, 2025
389d612
Add user registration login (#11)
YuliiaNisha Dec 13, 2025
65af954
fetched description
YuliiaNisha Dec 13, 2025
4f58c5d
Merge pull request #12 from mategames-team/fetch-game-description
YuliiaNisha Dec 13, 2025
87eb0df
Merge pull request #13 from mategames-team/add-game-lists-to-user
YuliiaNisha Dec 16, 2025
55cc874
Improve fetch best games (#14)
YuliiaNisha Dec 16, 2025
e8d1baa
created user info update and get (#15)
YuliiaNisha Dec 16, 2025
151a67b
Refactor get by api id method (#16)
YuliiaNisha Dec 17, 2025
5547ad5
Create get all games from api endpoint (#17)
YuliiaNisha Dec 18, 2025
981e8ff
Configure comments (#18)
YuliiaNisha Dec 19, 2025
6a45e62
refactored comments (#19)
YuliiaNisha Dec 19, 2025
7ea45da
added status to response (#20)
YuliiaNisha Dec 19, 2025
9559498
created search in api endpoint (#21)
YuliiaNisha Dec 20, 2025
7d0e69e
Create get user comments (#22)
YuliiaNisha Dec 21, 2025
4989e6b
Update Dockerfile for dev environment
Bondaliname Dec 21, 2025
9b06e83
added swagger (#23)
YuliiaNisha Dec 21, 2025
afeb5d9
refactored security and health (#24)
YuliiaNisha Dec 22, 2025
ab2c777
Refactor db update (#25)
YuliiaNisha Dec 23, 2025
c7a1f6f
allowed any authenticated user se userinfo (#26)
YuliiaNisha Dec 23, 2025
b2c465c
configured setting user role during registration (#27)
YuliiaNisha Dec 24, 2025
b591c12
created service tests (#28)
YuliiaNisha Dec 27, 2025
e2eb0ec
made method return gameDto (#29)
YuliiaNisha Dec 27, 2025
13695a4
configured cors (#30)
YuliiaNisha Dec 28, 2025
f0bf62c
added preconditions to yamls (#31)
YuliiaNisha Dec 28, 2025
03d2282
added patch method to cors (#32)
YuliiaNisha Dec 29, 2025
e674437
added response code (#33)
YuliiaNisha Dec 29, 2025
c039b0c
retun user info depending on param, return user id when login (#34)
YuliiaNisha Dec 30, 2025
4252725
updated readme, removed unused (#35)
YuliiaNisha Jan 3, 2026
146a200
merged main to dev
YuliiaNisha Jan 4, 2026
efb0d61
added info about userGames and changed max value of rating to 10 (#37)
YuliiaNisha Jan 4, 2026
4a0365a
Merge branch 'main' into dev
YuliiaNisha Jan 4, 2026
3a4d606
added ci file (#39)
YuliiaNisha Jan 4, 2026
6ab2dfd
added profileName and gameName to commentDto (#40)
YuliiaNisha Jan 4, 2026
91eb275
webhook after ci (#44)
Bondaliname Jan 5, 2026
ac19eac
Update public endpoints (#46)
YuliiaNisha Jan 7, 2026
d37ee7d
Merge branch 'main' into dev
YuliiaNisha Jan 7, 2026
de8e901
refactored delete method (#48)
YuliiaNisha Jan 10, 2026
724f9c3
made filtering games by several years (#49)
YuliiaNisha Jan 11, 2026
690e57e
Merge branch 'main' into dev
YuliiaNisha Jan 11, 2026
e211d8d
allowed rating to be up to 10 (#51)
YuliiaNisha Jan 19, 2026
05ebf88
added regex to validate (#53)
YuliiaNisha Jan 20, 2026
0788d61
Add developer info to game (#55)
YuliiaNisha Jan 22, 2026
d87a8f3
removed email from update method (#56)
YuliiaNisha Jan 22, 2026
93715e9
return token after registartion (#57)
YuliiaNisha Jan 22, 2026
a0f4de6
Merge branch 'main' into dev
YuliiaNisha Jan 22, 2026
dc703c8
corrected duplicate developers save (#59)
YuliiaNisha Jan 28, 2026
cdfff93
Merge branch 'main' into dev
YuliiaNisha Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@
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;
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;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -130,7 +126,6 @@ public Page<GameDto> search(
@PageableDefault(size = DEFAULT_PAGE_SIZE)
Pageable pageable
) {
validateSearchParams(searchParameters);
return gameService.search(searchParameters, pageable);
}

Expand All @@ -149,31 +144,4 @@ public Page<GameDto> 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);
}
}
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/videogamescatalogue/backend/model/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,6 +40,7 @@ public class Game {

private String backgroundImage;

@EqualsAndHashCode.Exclude
@ManyToMany
@JoinTable(
name = "games_platforms",
Expand All @@ -48,6 +50,7 @@ public class Game {
)
private Set<Platform> platforms = new HashSet<>();

@EqualsAndHashCode.Exclude
@ManyToMany
@JoinTable(
name = "games_genres",
Expand All @@ -57,6 +60,7 @@ public class Game {
)
private Set<Genre> genres = new HashSet<>();

@EqualsAndHashCode.Exclude
@ManyToMany
@JoinTable(
name = "games_developers",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Game, Long>, JpaSpecificationExecutor<Game> {
@EntityGraph(attributePaths = {
"platforms", "genres", "developers"
})
Optional<Game> findByApiId(Long apiId);

@EntityGraph(attributePaths = {
"platforms", "genres", "developers"
})
List<Game> findAllByApiIdIn(List<Long> apiIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,6 @@ public boolean isValidToken(String token) {
}
}

private Jws<Claims> getAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token);
}

public String getUsername(String token) {
return getSpecificClaim(token, Claims::getSubject);
}
Expand All @@ -56,4 +49,11 @@ private <T> T getSpecificClaim(String token, Function<Claims, T> claimsResolver)
Jws<Claims> allClaims = getAllClaims(token);
return claimsResolver.apply(allClaims.getBody());
}

private Jws<Claims> getAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,7 +59,7 @@ public List<ApiResponseGameDto> getBestGames() {

ArrayList<ApiResponseGameDto> 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
Expand All @@ -70,7 +73,7 @@ public List<ApiResponseGameDto> 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);

Expand All @@ -95,7 +98,7 @@ public Page<ApiResponseGameDto> 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);

Expand All @@ -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);

Expand All @@ -129,7 +132,7 @@ public Page<ApiResponseGameDto> search(Map<String, String> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -61,11 +63,6 @@ public Page<CommentDto> getUserComments(
return findCommentsByUserId(userId, pageable);
}

private Page<CommentDto> findCommentsByUserId(Long userId, Pageable pageable) {
Page<Comment> 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)
Expand Down Expand Up @@ -94,6 +91,11 @@ public void delete(Long commentId, Long userId) {
commentRepository.deleteById(commentId);
}

private Page<CommentDto> findCommentsByUserId(Long userId, Pageable pageable) {
Page<Comment> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -42,6 +46,7 @@ public class GameServiceImpl implements GameService {
private final SpecificationBuilder<Game, GameSearchParameters> specificationBuilder;
private final UserGameRepository userGameRepository;
private final DeveloperRepository developerRepository;
private final DeveloperProvider developerProvider;

@Override
public void fetchBestGames() {
Expand Down Expand Up @@ -108,6 +113,7 @@ public Page<GameDto> getAllGamesFromApi(Pageable pageable) {

@Override
public Page<GameDto> search(GameSearchParameters searchParameters, Pageable pageable) {
validateSearchParams(searchParameters);
Specification<Game> specification = specificationBuilder.build(searchParameters);

return gameRepository.findAll(specification, pageable)
Expand Down Expand Up @@ -157,35 +163,56 @@ private UserGame.GameStatus getGameStatus(Long apiId, User user) {

private Game findOrUpdate(Long apiId) {
Optional<Game> 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<Developer> 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<Developer> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand Down Expand Up @@ -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.");
}
}

Expand Down