Implemented user service

This commit is contained in:
lobo committed 2026-04-04 09:58:09 +02:00
1 parent 3ee636253b
commit afd71b82be
27 files changed
+332 -36

No files matched your search

+4
View File
@@ -34,6 +34,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Rest Caching -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
@@ -0,0 +1,37 @@
package auth_service.configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import java.util.concurrent.TimeUnit;
/**
* Global configuration to enable Spring Boot caching (backed by Caffeine).
*/
@Configuration
@EnableCaching
@Profile("!dev")
@Slf4j
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
log.info("########### Productive cache active ###########");
CaffeineCacheManager cacheManager = new CaffeineCacheManager("usersByEmail");
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterAccess(10, TimeUnit.MINUTES)
.recordStats();
}
}
@@ -0,0 +1,37 @@
package auth_service.configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import java.util.concurrent.TimeUnit;
/**
* Global configuration to enable Spring Boot caching (backed by Caffeine).
*/
@Configuration
@EnableCaching
@Profile("dev")
@Slf4j
public class DevCacheConfig {
@Bean
public CacheManager cacheManager() {
log.error("########### DEVELOPMENT CACHE ACTIVE ###########");
CaffeineCacheManager cacheManager = new CaffeineCacheManager("usersByEmail");
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterAccess(1, TimeUnit.SECONDS)
.recordStats();
}
}
@@ -1,7 +1,7 @@
package auth_service.domain.login.controller;
import auth_service.domain.login.controller.dtos.request.LoginRequest;
import auth_service.domain.login.controller.dtos.response.LoginResponse;
import auth_service.domain.login.controller.dto.request.LoginRequest;
import auth_service.domain.login.controller.dto.response.LoginResponse;
import auth_service.domain.login.controller.mapper.ApiMapper;
import auth_service.domain.login.service.LoginService;
import io.micrometer.core.annotation.Timed;
@@ -1,6 +1,6 @@
package auth_service.domain.login.controller;
import auth_service.domain.login.controller.dtos.response.ErrorResponse;
import auth_service.domain.login.controller.dto.response.ErrorResponse;
import auth_service.domain.login.exception.InvalidCredentialsException;
import auth_service.domain.login.exception.UserNotFoundException;
import lombok.NonNull;
@@ -1,4 +1,4 @@
package auth_service.domain.login.controller.dtos.request;
package auth_service.domain.login.controller.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -1,4 +1,4 @@
package auth_service.domain.login.controller.dtos.response;
package auth_service.domain.login.controller.dto.response;
public record ErrorResponse(String code, String message) {
}
@@ -1,4 +1,4 @@
package auth_service.domain.login.controller.dtos.response;
package auth_service.domain.login.controller.dto.response;
/**
* DTO representing the outgoing login response.
@@ -1,6 +1,6 @@
package auth_service.domain.login.controller.mapper;
import auth_service.domain.login.controller.dtos.request.LoginRequest;
import auth_service.domain.login.controller.dto.request.LoginRequest;
import auth_service.domain.login.model.User;
public abstract class ApiMapper {
@@ -0,0 +1,18 @@
package auth_service.domain.login.persistence;
import auth_service.domain.login.model.User;
import java.util.Optional;
/**
* Repository for database operations regarding the User entity.
*/
public interface MinimalUserRepository {
/**
* Finds a user by their email address.
* * @param email The email to search for
*
* @return An Optional containing the user if found
*/
Optional<User> findByEmail(String email);
}
@@ -1,33 +1,30 @@
package auth_service.domain.login.persistence;
import auth_service.domain.login.model.User;
import auth_service.domain.login.persistence.entities.UserEntity;
import auth_service.domain.login.persistence.entities.MinimalUserEntity;
import auth_service.domain.login.persistence.mapper.EntityMapper;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public class UserRepositoryImpl implements UserRepository {
public class MinimalUserRepositoryImpl implements MinimalUserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserEntity> query = cb.createQuery(UserEntity.class);
Root<UserEntity> user = query.from(UserEntity.class);
var criteriaBuilder = entityManager.getCriteriaBuilder();
var userEntityCriteriaQuery = criteriaBuilder.createQuery(MinimalUserEntity.class);
var userEntity = userEntityCriteriaQuery.from(MinimalUserEntity.class);
// Build the query: SELECT u FROM UserEntity u WHERE u.email = :email
query.select(user).where(cb.equal(user.get("email"), email));
userEntityCriteriaQuery.select(userEntity).where(criteriaBuilder.equal(userEntity.get("email"), email));
TypedQuery<UserEntity> typedQuery = entityManager.createQuery(query);
TypedQuery<MinimalUserEntity> typedQuery = entityManager.createQuery(userEntityCriteriaQuery);
return typedQuery.getResultStream()
.findFirst()
@@ -0,0 +1,30 @@
package auth_service.domain.login.persistence.entities;
import jakarta.persistence.*;
import lombok.*;
/**
* Represents a user entity mapped to the 'users' table in PostgreSQL.
*/
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MinimalUserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled;
}
@@ -1,14 +1,14 @@
package auth_service.domain.login.persistence.mapper;
import auth_service.domain.login.model.User;
import auth_service.domain.login.persistence.entities.UserEntity;
import auth_service.domain.login.persistence.entities.MinimalUserEntity;
public abstract class EntityMapper {
private EntityMapper() {
}
public static User mapToModel(UserEntity entity) {
public static User mapToModel(MinimalUserEntity entity) {
User model = User.builder().build();
if (entity != null) {
@@ -4,7 +4,7 @@ import auth_service.configuration.security.JwtService;
import auth_service.domain.login.exception.InvalidCredentialsException;
import auth_service.domain.login.exception.UserNotFoundException;
import auth_service.domain.login.model.User;
import auth_service.domain.login.persistence.UserRepository;
import auth_service.domain.login.persistence.MinimalUserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -14,12 +14,12 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Slf4j
public class LoginService {
private final UserRepository userRepository;
private final MinimalUserRepository minimalUserRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
public String login(User userToLogin) {
var userEntity = userRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
var userEntity = minimalUserRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
log.info("Login attempt for user {}", passwordEncoder.encode(userToLogin.getEncryptedPassword()) + " " + userEntity.getEncryptedPassword());
@@ -1,6 +1,10 @@
package auth_service.domain.user.controller;
import auth_service.domain.user.controller.dto.response.UserProfileResponse;
import auth_service.domain.user.controller.mapper.ApiMapper;
import auth_service.domain.user.service.UserService;
import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -13,8 +17,11 @@ import java.security.Principal;
*/
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* Version 1 of the 'me' endpoint, accessed via /api/users/me?v=1
* * @return The user's dashboard data
@@ -23,16 +30,9 @@ public class UserController {
@Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests")
public ResponseEntity<UserProfileResponse> getCurrentUserV1(Principal principal) {
// principal.getName() enthält die E-Mail, die der Türsteher aus dem Token gelesen hat!
String authenticatedEmail = principal.getName();
return ResponseEntity.ok(new UserProfileResponse(
authenticatedEmail,
"USER",
"Welcome to the v1 Dashboard, " + authenticatedEmail + "!"
));
}
public record UserProfileResponse(String email, String role, String dashboardMessage) {
// 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 profileResponse = this.userService.getUserByEmail(authenticatedEmail);
return ResponseEntity.ok(ApiMapper.toApi(profileResponse));
}
}
@@ -0,0 +1,42 @@
package auth_service.domain.user.controller;
import auth_service.domain.user.controller.dto.response.ErrorResponse;
import auth_service.domain.user.exception.UserNotFoundException;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Order(value = 1)
@Slf4j
public class UserExceptionController {
private static @NonNull ResponseEntity<ErrorResponse> response500(ErrorResponse body) {
log.error(body.message(), body.code());
return ResponseEntity.status(500)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.body(body);
}
private static @NonNull ResponseEntity<ErrorResponse> response400(ErrorResponse body) {
return ResponseEntity.status(400)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.body(body);
}
/*
######## CLIENT ERRORS (500-599)
*/
/*
######## CLIENT ERRORS (400-499)
*/
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> clientError(UserNotFoundException ex) {
var body = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
return response400(body);
}
}
@@ -0,0 +1,4 @@
package auth_service.domain.user.controller.dto.response;
public record ErrorResponse(String code, String message) {
}
@@ -0,0 +1,7 @@
package auth_service.domain.user.controller.dto.response;
import lombok.Builder;
@Builder
public record UserProfileResponse(long id, String email, String role) {
}
@@ -0,0 +1,17 @@
package auth_service.domain.user.controller.mapper;
import auth_service.domain.user.controller.dto.response.UserProfileResponse;
import auth_service.domain.user.model.User;
public abstract class ApiMapper {
private ApiMapper() {
}
public static UserProfileResponse toApi(User user) {
return UserProfileResponse.builder()
.id(user.getId())
.email(user.getEmail())
.role("USER").build();
}
}
@@ -0,0 +1,7 @@
package auth_service.domain.user.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,12 @@
package auth_service.domain.user.model;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class User {
private long id;
private String email;
private String encryptedPassword;
}
@@ -1,6 +1,7 @@
package auth_service.domain.login.persistence;
package auth_service.domain.user.persistence;
import auth_service.domain.login.model.User;
import auth_service.domain.user.model.User;
import java.util.Optional;
@@ -0,0 +1,33 @@
package auth_service.domain.user.persistence;
import auth_service.domain.user.model.User;
import auth_service.domain.user.persistence.entities.UserEntity;
import auth_service.domain.user.persistence.mapper.EntityMapper;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public class UserRepositoryImpl implements UserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
var criteriaBuilder = entityManager.getCriteriaBuilder();
var userEntityCriteriaQuery = criteriaBuilder.createQuery(UserEntity.class);
var userEntity = userEntityCriteriaQuery.from(UserEntity.class);
// Build the query: SELECT u FROM UserEntity u WHERE u.email = :email
userEntityCriteriaQuery.select(userEntity).where(criteriaBuilder.equal(userEntity.get("email"), email));
TypedQuery<UserEntity> typedQuery = entityManager.createQuery(userEntityCriteriaQuery);
return typedQuery.getResultStream()
.findFirst()
.map(EntityMapper::mapToModel);
}
}
@@ -1,4 +1,4 @@
package auth_service.domain.login.persistence.entities;
package auth_service.domain.user.persistence.entities;
import jakarta.persistence.*;
import lombok.*;
@@ -0,0 +1,22 @@
package auth_service.domain.user.persistence.mapper;
import auth_service.domain.user.model.User;
import auth_service.domain.user.persistence.entities.UserEntity;
public abstract class EntityMapper {
private EntityMapper() {
}
public static User mapToModel(UserEntity entity) {
User model = User.builder().build();
if (entity != null) {
model.setId(entity.getId());
model.setEmail(entity.getEmail());
model.setEncryptedPassword(entity.getPassword());
}
return model;
}
}
@@ -0,0 +1,26 @@
package auth_service.domain.user.service;
import auth_service.domain.user.exception.UserNotFoundException;
import auth_service.domain.user.model.User;
import auth_service.domain.user.persistence.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/**
* Fetches the user by email. Results are cached to ensure blazing-fast UI rendering.
*
* @param email The authenticated user's email
* @return Optional containing the User model if found
*/
@Cacheable(value = "usersByEmail", key = "#email")
public User getUserByEmail(String email) {
return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException("Could not find user with email " + email));
}
}
+2
View File
@@ -6,6 +6,8 @@ spring:
name: tbp-auth-service
profiles:
active: dev # Default profile for local startup
jpa:
open-in-view: false
flyway:
# Force Flyway to be enabled
enabled: true