Added lots of new stuff

- extended the jwt and put userid into it
- completed update settings
- added settings also in me call
This commit is contained in:
lobo committed 2026-04-11 12:07:27 +02:00
1 parent 6e194b9c9a
commit 03d5a011ae
23 files changed
+271 -30

No files matched your search

@@ -0,0 +1,10 @@
package auth_service.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
// This class enables Spring's @Async annotation functionality globally
}
@@ -45,10 +45,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
try { try {
final String userEmail = jwtService.extractEmail(jwt); final String userEmail = jwtService.extractEmail(jwt);
final String userId = jwtService.extractUserId(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (userEmail != null && userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserPrincipal principal = new UserPrincipal(userId, userEmail);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userEmail, principal,
null, null,
Collections.emptyList() Collections.emptyList()
); );
@@ -28,9 +28,10 @@ public class JwtService {
* *
* @return A signed JWT string * @return A signed JWT string
*/ */
public String generateToken(String email) { public String generateToken(String userId, String email) {
return Jwts.builder() return Jwts.builder()
.subject(email) .subject(email)
.claim("userId", userId)
.issuedAt(new Date(System.currentTimeMillis())) .issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS)) .expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS))
.signWith(getSigningKey()) .signWith(getSigningKey())
@@ -46,6 +47,15 @@ public class JwtService {
.getSubject(); .getSubject();
} }
public String extractUserId(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.get("userId", String.class);
}
public boolean isTokenValid(String token) { public boolean isTokenValid(String token) {
try { try {
Jwts.parser() Jwts.parser()
@@ -0,0 +1,18 @@
package auth_service.configuration.security;
import java.security.Principal;
public record UserPrincipal(
String id,
String email
) implements Principal {
@Override
public String getName() {
return id;
}
public long getIdAsLong() {
return Long.parseLong(id);
}
}
@@ -36,8 +36,7 @@ public class LoginController {
var user = ApiMapper.toModel(request); var user = ApiMapper.toModel(request);
var authenticatedUser = loginService.login(user); var authenticatedUser = loginService.login(user);
var token = jwtService.generateToken(authenticatedUser.getEmail()); var token = jwtService.generateToken(authenticatedUser.getId() + "", authenticatedUser.getEmail());
return ResponseEntity.ok(new LoginResponse("Login successful", token)); return ResponseEntity.ok(new LoginResponse("Login successful", token));
} }
} }
@@ -1,16 +1,15 @@
package auth_service.domain.user.controller; package auth_service.domain.user.controller;
import auth_service.configuration.security.UserPrincipal;
import auth_service.domain.user.controller.dto.request.UserSettingsRequest;
import auth_service.domain.user.controller.dto.response.UserProfileResponse; import auth_service.domain.user.controller.dto.response.UserProfileResponse;
import auth_service.domain.user.controller.mapper.ApiMapper; import auth_service.domain.user.controller.mapper.ApiMapper;
import auth_service.domain.user.service.UserService; import auth_service.domain.user.service.UserService;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
/** /**
* Controller handling user profile operations for the dashboard. * Controller handling user profile operations for the dashboard.
@@ -28,11 +27,19 @@ public class UserController {
*/ */
@GetMapping(value = "/me", version = "1.0", produces = "application/json") @GetMapping(value = "/me", version = "1.0", produces = "application/json")
@Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests") @Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests")
public ResponseEntity<UserProfileResponse> getCurrentUserV1(Principal principal) { public ResponseEntity<UserProfileResponse> getCurrentUserV1(@AuthenticationPrincipal UserPrincipal principal) {
// this is the email from the login, if a user is not authenticated, it will not even come to this point. // this is the email from the login, if a user is not authenticated, it will not even come to this point.
var authenticatedEmail = principal.getName(); var authenticatedEmail = principal.email();
var profileResponse = this.userService.getUserByEmail(authenticatedEmail); var profileResponse = this.userService.getUserByEmail(authenticatedEmail);
return ResponseEntity.ok(ApiMapper.toApi(profileResponse)); return ResponseEntity.ok(ApiMapper.toApi(profileResponse));
} }
@PutMapping(value = "/settings", version = "1.0", produces = "application/json")
@Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests")
public ResponseEntity<Void> updateUserSettings(@AuthenticationPrincipal UserPrincipal principal,
@RequestBody UserSettingsRequest request) {
this.userService.updateUserSettings(principal.getIdAsLong(), request.steamId(), ApiMapper.toDomain(request));
return ResponseEntity.ok().build();
}
} }
@@ -0,0 +1,4 @@
package auth_service.domain.user.controller.dto.request;
public record UserSettingsRequest(boolean useDarkTheme, String language, long steamId) {
}
@@ -0,0 +1,7 @@
package auth_service.domain.user.controller.dto.response;
import lombok.Builder;
@Builder
public record SettingsResponse(long id, boolean useDarkTheme, String language) {
}
@@ -5,5 +5,5 @@ import lombok.Builder;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Builder @Builder
public record UserProfileResponse(long id, String email, String role, LocalDateTime lastLogin, LocalDateTime createdDate, long steamId) { public record UserProfileResponse(long id, String email, String role, LocalDateTime lastLogin, LocalDateTime createdDate, long steamId, SettingsResponse settings) {
} }
@@ -1,8 +1,11 @@
package auth_service.domain.user.controller.mapper; package auth_service.domain.user.controller.mapper;
import auth_service.domain.user.controller.dto.request.UserSettingsRequest;
import auth_service.domain.user.controller.dto.response.SettingsResponse;
import auth_service.domain.user.controller.dto.response.UserProfileResponse; import auth_service.domain.user.controller.dto.response.UserProfileResponse;
import auth_service.domain.user.model.User; import auth_service.domain.user.model.User;
import auth_service.domain.user.model.UserSettings;
public abstract class ApiMapper { public abstract class ApiMapper {
private ApiMapper() { private ApiMapper() {
@@ -15,6 +18,20 @@ public abstract class ApiMapper {
.createdDate(user.getCreatedDate()) .createdDate(user.getCreatedDate())
.lastLogin(user.getLastLogin()) .lastLogin(user.getLastLogin())
.steamId(user.getSteamId()) .steamId(user.getSteamId())
.role("USER").build(); .role("USER")
.settings(SettingsResponse.builder()
.id(user.getId())
.useDarkTheme(user.getSettings().getTheme().equals("dark"))
.language(user.getSettings().getLanguage())
.build())
.build();
} }
public static UserSettings toDomain(UserSettingsRequest request) {
return UserSettings.builder()
.theme(request.useDarkTheme() ? "dark" : "light")
.language(request.language())
.build();
}
} }
@@ -14,4 +14,6 @@ public class User {
private LocalDateTime lastLogin; private LocalDateTime lastLogin;
private LocalDateTime createdDate; private LocalDateTime createdDate;
private long steamId; private long steamId;
private UserSettings settings;
} }
@@ -0,0 +1,11 @@
package auth_service.domain.user.model;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class UserSettings {
private String language;
private String theme;
}
@@ -2,6 +2,9 @@ package auth_service.domain.user.persistence;
import auth_service.domain.user.model.User; import auth_service.domain.user.model.User;
import auth_service.domain.user.model.UserSettings;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import java.util.Optional; import java.util.Optional;
@@ -16,4 +19,10 @@ public interface UserRepository {
* @return An Optional containing the user if found * @return An Optional containing the user if found
*/ */
Optional<User> findByEmail(String email); Optional<User> findByEmail(String email);
@NotNull
UserSettings findSettingsByUserId(String userId);
@Transactional
boolean updateSettings(long userId, long steamId, UserSettings settings);
} }
@@ -1,14 +1,19 @@
package auth_service.domain.user.persistence; package auth_service.domain.user.persistence;
import auth_service.domain.user.model.User; import auth_service.domain.user.model.User;
import auth_service.domain.user.model.UserSettings;
import auth_service.domain.user.persistence.entities.UserEntity; import auth_service.domain.user.persistence.entities.UserEntity;
import auth_service.domain.user.persistence.entities.UserSettingsEntity;
import auth_service.domain.user.persistence.mapper.EntityMapper; import auth_service.domain.user.persistence.mapper.EntityMapper;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
@Repository @Repository
public class UserRepositoryImpl implements UserRepository { public class UserRepositoryImpl implements UserRepository {
@@ -21,7 +26,11 @@ public class UserRepositoryImpl implements UserRepository {
var userEntityCriteriaQuery = criteriaBuilder.createQuery(UserEntity.class); var userEntityCriteriaQuery = criteriaBuilder.createQuery(UserEntity.class);
var userEntity = userEntityCriteriaQuery.from(UserEntity.class); var userEntity = userEntityCriteriaQuery.from(UserEntity.class);
// Build the query: SELECT u FROM UserEntity u WHERE u.email = :email // Fetch the settings eagerly using a LEFT JOIN to prevent N+1 queries.
// A LEFT JOIN ensures the user is returned even if no settings exist yet.
userEntity.fetch("settings", jakarta.persistence.criteria.JoinType.LEFT);
// Build the query: SELECT u FROM UserEntity u LEFT JOIN FETCH u.settings WHERE u.email = :email
userEntityCriteriaQuery.select(userEntity).where(criteriaBuilder.equal(userEntity.get("email"), email)); userEntityCriteriaQuery.select(userEntity).where(criteriaBuilder.equal(userEntity.get("email"), email));
TypedQuery<UserEntity> typedQuery = entityManager.createQuery(userEntityCriteriaQuery); TypedQuery<UserEntity> typedQuery = entityManager.createQuery(userEntityCriteriaQuery);
@@ -30,4 +39,37 @@ public class UserRepositoryImpl implements UserRepository {
.findFirst() .findFirst()
.map(EntityMapper::mapToModel); .map(EntityMapper::mapToModel);
} }
@Override
public @NotNull UserSettings findSettingsByUserId(String userId) {
UserSettingsEntity entity = entityManager.find(UserSettingsEntity.class, UUID.fromString(userId));
if (entity == null) {
// Return defaults if none exist yet
return UserSettings.builder()
.language("")
.theme("light")
.build();
}
return UserSettings.builder()
.language(entity.getLanguage())
.theme(entity.getTheme())
.build();
}
@Transactional
@Override
public boolean updateSettings(long userId, long steamId, UserSettings settings) {
var entity = EntityMapper.toPersistence(userId, settings);
// merge() acts as an upsert: it updates if the ID exists, or inserts if it doesn't.
entityManager.merge(entity);
var userEntity = entityManager.find(UserEntity.class, userId);
if (userEntity != null && userEntity.getSteamId() != steamId) {
userEntity.setSteamId(steamId);
return true;
}
return false;
}
} }
@@ -38,4 +38,8 @@ public class UserEntity {
@Column(name = "last_login_date") @Column(name = "last_login_date")
private LocalDateTime lastLoginDate; private LocalDateTime lastLoginDate;
@OneToOne(cascade = jakarta.persistence.CascadeType.ALL, fetch = jakarta.persistence.FetchType.LAZY)
@jakarta.persistence.PrimaryKeyJoinColumn
private UserSettingsEntity settings;
} }
@@ -0,0 +1,26 @@
package auth_service.domain.user.persistence.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "user_settings")
@Getter
@Setter
public class UserSettingsEntity {
// No @GeneratedValue! The ID is strictly the user_id from the users table.
@Id
@Column(name = "user_id")
private long userId;
@Column(nullable = false)
private String language;
@Column(nullable = false)
private String theme;
}
@@ -1,7 +1,9 @@
package auth_service.domain.user.persistence.mapper; package auth_service.domain.user.persistence.mapper;
import auth_service.domain.user.model.User; import auth_service.domain.user.model.User;
import auth_service.domain.user.model.UserSettings;
import auth_service.domain.user.persistence.entities.UserEntity; import auth_service.domain.user.persistence.entities.UserEntity;
import auth_service.domain.user.persistence.entities.UserSettingsEntity;
public abstract class EntityMapper { public abstract class EntityMapper {
@@ -9,17 +11,42 @@ public abstract class EntityMapper {
} }
public static User mapToModel(UserEntity entity) { public static User mapToModel(UserEntity entity) {
User model = User.builder().build(); if (entity == null) {
return null;
if (entity != null) {
model.setId(entity.getId());
model.setEmail(entity.getEmail());
model.setEncryptedPassword(entity.getPassword());
model.setLastLogin(entity.getLastLoginDate());
model.setCreatedDate(entity.getRegistrationDate());
model.setSteamId(entity.getSteamId());
} }
return model; // 1. Map the settings if they exist, otherwise use defaults
UserSettings settingsModel;
if (entity.getSettings() != null) {
settingsModel = UserSettings.builder()
.language(entity.getSettings().getLanguage())
.theme(entity.getSettings().getTheme())
.build();
} else {
// Fallback default settings for users without an entry in the database
settingsModel = UserSettings.builder()
.language("en")
.theme("system")
.build();
}
// 2. Map the main user fields and attach the settings
return User.builder()
.id(entity.getId())
.email(entity.getEmail())
.encryptedPassword(entity.getPassword())
.lastLogin(entity.getLastLoginDate())
.createdDate(entity.getRegistrationDate())
.steamId(entity.getSteamId())
.settings(settingsModel)
.build();
}
public static UserSettingsEntity toPersistence(long userId, UserSettings settings) {
UserSettingsEntity entity = new UserSettingsEntity();
entity.setUserId(userId);
entity.setLanguage(settings.getLanguage());
entity.setTheme(settings.getTheme());
return entity;
} }
} }
@@ -2,11 +2,16 @@ package auth_service.domain.user.service;
import auth_service.domain.user.exception.UserNotFoundException; import auth_service.domain.user.exception.UserNotFoundException;
import auth_service.domain.user.model.User; import auth_service.domain.user.model.User;
import auth_service.domain.user.model.UserSettings;
import auth_service.domain.user.persistence.UserRepository; import auth_service.domain.user.persistence.UserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserService { public class UserService {
@@ -23,4 +28,14 @@ public class UserService {
public User getUserByEmail(String email) { public User getUserByEmail(String email) {
return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException("Could not find user with email " + email)); return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException("Could not find user with email " + email));
} }
@Async
@Transactional
public void updateUserSettings(long userId, long steamId, UserSettings userSettings) {
var updated = userRepository.updateSettings(userId, steamId, userSettings);
if (updated) {
log.info("Steam id has changed!");
}
log.info("User settings has been updated!");
}
} }
@@ -15,6 +15,9 @@ spring:
baseline-on-migrate: true baseline-on-migrate: true
# Explicitly state where to look for scripts # Explicitly state where to look for scripts
locations: classpath:db/migration locations: classpath:db/migration
threads:
virtual:
enabled: true
mvc: mvc:
apiversion: apiversion:
use: use:
@@ -1,6 +1,6 @@
-- Create the users table -- Create the users table
ALTER TABLE users ALTER TABLE users
ADD COLUMN steam_id BIGINT, ADD COLUMN steam_id BIGINT DEFAULT 0,
ADD COLUMN registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ADD COLUMN registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN last_login_date TIMESTAMP; ADD COLUMN last_login_date TIMESTAMP;
@@ -0,0 +1,10 @@
CREATE TABLE user_settings
(
user_id BIGSERIAL PRIMARY KEY REFERENCES users (id) ON DELETE CASCADE,
language VARCHAR(10) NOT NULL DEFAULT '',
theme VARCHAR(20) NOT NULL DEFAULT 'light'
);
INSERT INTO user_settings (user_id, language, theme)
SELECT id, '', 'light'
FROM users;
@@ -29,10 +29,11 @@ class JwtServiceTest {
@Test @Test
void generateToken_Success() { void generateToken_Success() {
// Arrange // Arrange
var userId = "0815";
var email = "test@example.com"; var email = "test@example.com";
// Act // Act
var token = jwtService.generateToken(email); var token = jwtService.generateToken(userId, email);
// Assert // Assert
assertNotNull(token); assertNotNull(token);
@@ -42,8 +43,9 @@ class JwtServiceTest {
@Test @Test
void extractEmail_Success() { void extractEmail_Success() {
// Arrange // Arrange
var userId = "0815";
var email = "test@example.com"; var email = "test@example.com";
var token = jwtService.generateToken(email); var token = jwtService.generateToken(userId, email);
// Act // Act
var extractedEmail = jwtService.extractEmail(token); var extractedEmail = jwtService.extractEmail(token);
@@ -52,11 +54,26 @@ class JwtServiceTest {
assertEquals(email, extractedEmail); assertEquals(email, extractedEmail);
} }
@Test
void extractUserId_Success() {
// Arrange
var userId = "0815";
var email = "test@example.com";
var token = jwtService.generateToken(userId, email);
// Act
var extractedUserId = jwtService.extractUserId(token);
// Assert
assertEquals(userId, extractedUserId);
}
@Test @Test
void isTokenValid_Success() { void isTokenValid_Success() {
// Arrange // Arrange
var userId = "0815";
var email = "test@example.com"; var email = "test@example.com";
var token = jwtService.generateToken(email); var token = jwtService.generateToken(userId, email);
// Act // Act
var isValid = jwtService.isTokenValid(token); var isValid = jwtService.isTokenValid(token);
@@ -51,7 +51,7 @@ class LoginControllerTest {
var request = new LoginRequest("test@example.com", "password"); var request = new LoginRequest("test@example.com", "password");
var authenticatedUser = User.builder().email("test@example.com").build(); var authenticatedUser = User.builder().email("test@example.com").build();
when(loginService.login(any(User.class))).thenReturn(authenticatedUser); when(loginService.login(any(User.class))).thenReturn(authenticatedUser);
when(jwtService.generateToken("test@example.com")).thenReturn("mock-token"); when(jwtService.generateToken("0815", "test@example.com")).thenReturn("mock-token");
// Act & Assert // Act & Assert
mockMvc.perform(post("/api/auth/login") mockMvc.perform(post("/api/auth/login")