feat(settings): store a favorite platform

This commit is contained in:
lobo committed 2026-09-29 10:20:54 +02:00
1 parent 315e22c220
commit ea4ae089ff
14 files changed
+103 -3

No files matched your search

+1
View File
@@ -95,6 +95,7 @@ To Be Played is split into three repositories:
| `PUT` | `/api/users/settings/appearance` | access token |
| `PUT` | `/api/users/settings/profile-style` (`{accent, corners, bannerAppid, backgroundAppid}`; `204`) | access token |
| `PUT` | `/api/users/settings/dashboard-tiles` (`{tiles: [{id, size}]}` in order, at most 20; `{tiles: null}` resets; `204`) | access token |
| `PUT` | `/api/users/settings/favorite-platform` (`{platform}`, free text up to 32, e.g. `PS5`; `null` clears it; `204`) | access token |
| `PUT` | `/api/users/settings/profile-tiles` (`{tiles: [{id, size}]}`: `SUMMARY`, `STATUS`, `PLAYING`, `FINISHED`, `STATS_<GROUP>`; `204`) | access token |
| `GET` | `/api/public/profiles/{displayName}` | public |
| `GET` | `/api/stats/public` | public |
@@ -3,6 +3,7 @@ package auth_service.modules.user.api;
import auth_service.shared.security.UserPrincipal;
import auth_service.modules.user.api.dto.request.AppearanceRequest;
import auth_service.modules.user.api.dto.request.DashboardTilesRequest;
import auth_service.modules.user.api.dto.request.FavoritePlatformRequest;
import auth_service.modules.user.api.dto.request.ProfileTilesRequest;
import auth_service.modules.user.api.dto.request.ProfileStyleRequest;
import auth_service.modules.user.api.dto.request.UserSettingsRequest;
@@ -62,6 +63,13 @@ public class UserController {
return ResponseEntity.noContent().build();
}
@PutMapping(value = "/settings/favorite-platform", version = "1.0")
public ResponseEntity<Void> updateFavoritePlatform(@AuthenticationPrincipal UserPrincipal principal,
@Valid @RequestBody FavoritePlatformRequest request) {
this.userService.updateFavoritePlatform(principal.getIdAsLong(), request.platform());
return ResponseEntity.noContent().build();
}
@PutMapping(value = "/settings/profile-style", version = "1.0")
public ResponseEntity<Void> updateProfileStyle(@AuthenticationPrincipal UserPrincipal principal,
@Valid @RequestBody ProfileStyleRequest request) {
@@ -0,0 +1,10 @@
package auth_service.modules.user.api.dto.request;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
/**
* @param platform free text like Core's game platform: the frontend offers the list; null clears it
*/
public record FavoritePlatformRequest(@Size(max = 32) @Pattern(regexp = ".*\\S.*") String platform) {
}
@@ -7,5 +7,6 @@ import java.util.List;
@Builder
public record SettingsResponse(long id, boolean useDarkTheme, String language, boolean publicProfile,
AppearanceResponse appearance, ProfileStyleResponse profileStyle,
List<TileResponse> dashboardTiles, List<TileResponse> profileTiles) {
List<TileResponse> dashboardTiles, List<TileResponse> profileTiles,
String favoritePlatform) {
}
@@ -47,6 +47,7 @@ public abstract class ApiMapper {
.dashboardTiles(user.getSettings().getDashboardTiles() == null ? null
: toApi(user.getSettings().getDashboardTiles()))
.profileTiles(toApi(user.getSettings().getProfileTiles()))
.favoritePlatform(user.getSettings().getFavoritePlatform())
.build())
.build();
}
@@ -19,6 +19,7 @@ public class UserSettings {
private List<Tile> dashboardTiles;
@Builder.Default
private List<Tile> profileTiles = DEFAULT_PROFILE_TILES;
private String favoritePlatform;
/** Also the column default of user_settings.profile_tiles. */
public static final List<Tile> DEFAULT_PROFILE_TILES = List.of(new Tile("SUMMARY", "M"), new Tile("STATUS", "M"),
@@ -38,6 +38,8 @@ public interface UserRepository {
void updateProfileTiles(long userId, List<Tile> tiles);
void updateFavoritePlatform(long userId, String platform);
void updateProfileStyle(long userId, ProfileStyle style);
Optional<SteamIdHolder> findSteamIdHolder(long steamId);
@@ -110,6 +110,12 @@ public class UserRepositoryImpl implements UserRepository {
settingsOf(userId).setProfileTiles(Tile.storedAll(tiles));
}
@Transactional
@Override
public void updateFavoritePlatform(long userId, String platform) {
settingsOf(userId).setFavoritePlatform(platform);
}
@Transactional
@Override
public void updateProfileStyle(long userId, ProfileStyle style) {
@@ -71,6 +71,9 @@ public class UserSettingsEntity {
@Column(name = "dashboard_tiles")
private List<String> dashboardTiles;
@Column(name = "favorite_platform")
private String favoritePlatform;
@JdbcTypeCode(SqlTypes.ARRAY)
@Column(name = "profile_tiles", nullable = false)
private List<String> profileTiles = Tile.storedAll(UserSettings.DEFAULT_PROFILE_TILES);
@@ -37,6 +37,7 @@ public abstract class EntityMapper {
.dashboardTiles(entity.getSettings().getDashboardTiles() == null ? null
: entity.getSettings().getDashboardTiles().stream().map(Tile::parse).toList())
.profileTiles(entity.getSettings().getProfileTiles().stream().map(Tile::parse).toList())
.favoritePlatform(entity.getSettings().getFavoritePlatform())
.build();
} else {
// Fallback default settings for users without an entry in the database
@@ -112,6 +112,13 @@ public class UserService {
userRepository.updateProfileTiles(userId, Tile.distinct(tiles));
}
@RequiresRole(Role.USER)
@Transactional
@CacheEvict(value = "usersByEmail", allEntries = true)
public void updateFavoritePlatform(long userId, String platform) {
userRepository.updateFavoritePlatform(userId, platform == null ? null : platform.strip());
}
@RequiresRole(Role.USER)
@Transactional
@CacheEvict(value = "usersByEmail", allEntries = true)
@@ -0,0 +1,3 @@
-- The platform preselected when a game is added by hand or imported (Core item 23), an IGDB abbreviation such as
-- 'PS5'. NULL: none.
ALTER TABLE user_settings ADD COLUMN favorite_platform VARCHAR(32);
@@ -36,6 +36,7 @@ import java.util.List;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@@ -355,7 +356,7 @@ class UserControllerTest extends AbstractWebMvcTest {
String email = "test@example.com";
UserSettings settings = UserSettings.builder().language("en").theme("dark")
.dashboardTiles(List.of(new Tile("STATS_TREND", "L")))
.profileTiles(List.of(new Tile("PLAYING", "S"))).build();
.profileTiles(List.of(new Tile("PLAYING", "S"))).favoritePlatform("PS5").build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
);
@@ -366,7 +367,8 @@ class UserControllerTest extends AbstractWebMvcTest {
.andExpect(jsonPath("$.settings.dashboardTiles[0].id").value("STATS_TREND"))
.andExpect(jsonPath("$.settings.dashboardTiles[0].size").value("L"))
.andExpect(jsonPath("$.settings.profileTiles[0].id").value("PLAYING"))
.andExpect(jsonPath("$.settings.profileTiles[0].size").value("S"));
.andExpect(jsonPath("$.settings.profileTiles[0].size").value("S"))
.andExpect(jsonPath("$.settings.favoritePlatform").value("PS5"));
}
@Test
@@ -410,6 +412,38 @@ class UserControllerTest extends AbstractWebMvcTest {
verify(userService).updateDashboardTiles(1L, null);
}
@Test
void updateFavoritePlatform_StoresOrClearsIt() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/favorite-platform")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"platform\":\"PS5\"}"))
.andExpect(status().isNoContent());
mockMvc.perform(put("/api/users/settings/favorite-platform")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"platform\":null}"))
.andExpect(status().isNoContent());
verify(userService).updateFavoritePlatform(1L, "PS5");
verify(userService).updateFavoritePlatform(1L, null);
}
@Test
void updateFavoritePlatform_BlankOrTooLong_Is400() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
for (String platform : List.of(" ", "x".repeat(33))) {
mockMvc.perform(put("/api/users/settings/favorite-platform")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"platform\":\"" + platform + "\"}"))
.andExpect(status().isBadRequest());
}
verify(userService, never()).updateFavoritePlatform(anyLong(), any());
}
@Test
void updateProfileTiles_StoresTheTiles() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
@@ -330,6 +330,28 @@ class UserRepositoryTest extends AbstractPersistenceTest {
assertEquals(profile, saved.getProfileTiles());
}
@Test
void updateFavoritePlatform_StoresAndClears() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()
.email("favorite@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
assertNull(userRepository.findByEmail("favorite@example.com").orElseThrow().getSettings().getFavoritePlatform());
userRepository.updateFavoritePlatform(userEntity.getId(), "Switch");
entityManager.flush();
entityManager.clear();
assertEquals("Switch", userRepository.findByEmail("favorite@example.com").orElseThrow().getSettings()
.getFavoritePlatform());
userRepository.updateFavoritePlatform(userEntity.getId(), null);
entityManager.flush();
entityManager.clear();
assertNull(userRepository.findByEmail("favorite@example.com").orElseThrow().getSettings().getFavoritePlatform());
}
@Test
void updateDashboardTiles_NullResetsToTheDefault() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()