refactor: user and authentication service

This commit is contained in:
Rodrigo Verdiani 2026-03-18 10:45:24 -03:00
parent abde5950e8
commit 0c635f76cf
27 changed files with 242 additions and 236 deletions

View File

@ -1,7 +1,7 @@
package com.magamochi.config;
package com.magamochi.authentication.config;
import com.magamochi.security.JwtRequestFilter;
import com.magamochi.service.CustomUserDetailsService;
import com.magamochi.authentication.security.JwtRequestFilter;
import com.magamochi.user.service.CustomUserDetailsService;
import com.magamochi.util.JwtUtil;
import java.util.List;
import lombok.RequiredArgsConstructor;
@ -27,18 +27,17 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final CustomUserDetailsService customUserDetailsService;
private final JwtUtil jwtUtil;
@Bean
public JwtRequestFilter jwtRequestFilter() {
return new JwtRequestFilter(jwtUtil, userDetailsService);
return new JwtRequestFilter(jwtUtil, customUserDetailsService);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
var authProvider = new DaoAuthenticationProvider(userDetailsService);
var authProvider = new DaoAuthenticationProvider(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

View File

@ -0,0 +1,40 @@
package com.magamochi.authentication.controller;
import com.magamochi.authentication.model.dto.AuthenticationRequestDTO;
import com.magamochi.authentication.model.dto.AuthenticationResponseDTO;
import com.magamochi.authentication.model.dto.RefreshTokenRequestDTO;
import com.magamochi.authentication.service.AuthenticationService;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/authentication")
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;
@Operation(
summary = "Authenticate an user",
description = "Authenticate an user with email and password.",
tags = {"Authentication"},
operationId = "login")
@PostMapping
public DefaultResponseDTO<AuthenticationResponseDTO> login(
@RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
return DefaultResponseDTO.ok(authenticationService.authenticate(authenticationRequestDTO));
}
@Operation(
summary = "Refresh authentication token",
description = "Refresh the authentication token",
tags = {"Authentication"},
operationId = "refreshAuthToken")
@PostMapping("/refresh")
public DefaultResponseDTO<AuthenticationResponseDTO> refreshAuthToken(
@RequestBody RefreshTokenRequestDTO authenticationRequestDTO) {
return DefaultResponseDTO.ok(authenticationService.refreshAuthToken(authenticationRequestDTO));
}
}

View File

@ -1,4 +1,4 @@
package com.magamochi.model.dto;
package com.magamochi.authentication.model.dto;
import jakarta.validation.constraints.NotNull;

View File

@ -1,6 +1,6 @@
package com.magamochi.model.dto;
package com.magamochi.authentication.model.dto;
import com.magamochi.model.enumeration.UserRole;
import com.magamochi.user.model.enumeration.UserRole;
import jakarta.validation.constraints.NotNull;
public record AuthenticationResponseDTO(

View File

@ -1,4 +1,4 @@
package com.magamochi.model.dto;
package com.magamochi.authentication.model.dto;
import jakarta.validation.constraints.NotNull;

View File

@ -1,9 +1,9 @@
package com.magamochi.security;
package com.magamochi.authentication.security;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import com.magamochi.service.CustomUserDetailsService;
import com.magamochi.user.service.CustomUserDetailsService;
import com.magamochi.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;

View File

@ -0,0 +1,68 @@
package com.magamochi.authentication.service;
import com.magamochi.authentication.model.dto.AuthenticationRequestDTO;
import com.magamochi.authentication.model.dto.AuthenticationResponseDTO;
import com.magamochi.authentication.model.dto.RefreshTokenRequestDTO;
import com.magamochi.user.service.UserService;
import com.magamochi.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final UserService userService;
private final JwtUtil jwtUtil;
public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password()));
var userDetails = userDetailsService.loadUserByUsername(request.email());
var accessToken = jwtUtil.generateAccessToken(userDetails);
var refreshToken = jwtUtil.generateRefreshToken(userDetails);
var user = userService.find(userDetails.getUsername());
return new AuthenticationResponseDTO(
user.getId(),
accessToken,
refreshToken,
userDetails.getUsername(),
user.getName(),
user.getRole());
}
public AuthenticationResponseDTO refreshAuthToken(
RefreshTokenRequestDTO authenticationRequestDTO) {
var username = jwtUtil.extractUsernameFromRefreshToken(authenticationRequestDTO.refreshToken());
var userDetails = userDetailsService.loadUserByUsername(username);
if (!jwtUtil.validateRefreshToken(authenticationRequestDTO.refreshToken(), userDetails)) {
throw new BadCredentialsException("Invalid refresh token");
}
var newAccessToken = jwtUtil.generateAccessToken(userDetails);
var newRefreshToken = jwtUtil.generateRefreshToken(userDetails);
var user = userService.find(userDetails.getUsername());
return new AuthenticationResponseDTO(
user.getId(),
newAccessToken,
newRefreshToken,
userDetails.getUsername(),
user.getName(),
user.getRole());
}
}

View File

@ -11,7 +11,7 @@ import com.magamochi.common.exception.NotFoundException;
import com.magamochi.model.repository.UserFavoriteMangaRepository;
import com.magamochi.model.repository.UserMangaFollowRepository;
import com.magamochi.model.specification.MangaSpecification;
import com.magamochi.service.UserService;
import com.magamochi.user.service.UserService;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;

View File

@ -1,54 +0,0 @@
package com.magamochi.controller;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import com.magamochi.model.dto.AuthenticationRequestDTO;
import com.magamochi.model.dto.AuthenticationResponseDTO;
import com.magamochi.model.dto.RefreshTokenRequestDTO;
import com.magamochi.model.dto.RegistrationRequestDTO;
import com.magamochi.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class AuthenticationController {
private final UserService userService;
@Operation(
summary = "Authenticate user",
description = "Authenticate user with email and password.",
tags = {"Auth"},
operationId = "authenticateUser")
@PostMapping("/login")
public DefaultResponseDTO<AuthenticationResponseDTO> authenticateUser(
@RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
return DefaultResponseDTO.ok(userService.authenticate(authenticationRequestDTO));
}
@Operation(
summary = "Refresh authentication token",
description = "Refresh the authentication token",
tags = {"Auth"},
operationId = "refreshAuthToken")
@PostMapping("/refresh")
public DefaultResponseDTO<AuthenticationResponseDTO> refreshAuthToken(
@RequestBody RefreshTokenRequestDTO authenticationRequestDTO) {
return DefaultResponseDTO.ok(userService.refreshAuthToken(authenticationRequestDTO));
}
@Operation(
summary = "Register user",
description = "Register a new user.",
tags = {"Auth"},
operationId = "registerUser")
@PostMapping("/register")
public DefaultResponseDTO<Void> registerUser(
@RequestBody RegistrationRequestDTO registrationRequestDTO) {
userService.register(registrationRequestDTO);
return DefaultResponseDTO.ok().build();
}
}

View File

@ -4,8 +4,8 @@ import com.magamochi.client.NtfyClient;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import com.magamochi.image.task.ImageCleanupTask;
import com.magamochi.ingestion.task.IngestFromContentProvidersTask;
import com.magamochi.model.repository.UserRepository;
import com.magamochi.task.MangaFollowUpdateTask;
import com.magamochi.user.repository.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

View File

@ -0,0 +1,29 @@
package com.magamochi.controller;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import com.magamochi.user.model.dto.RegistrationRequestDTO;
import com.magamochi.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@Operation(
summary = "Register an user",
description = "Register a new user.",
tags = {"User"},
operationId = "registerUser")
@PostMapping("/register")
public DefaultResponseDTO<Void> registerUser(
@RequestBody RegistrationRequestDTO registrationRequestDTO) {
userService.register(registrationRequestDTO);
return DefaultResponseDTO.ok().build();
}
}

View File

@ -1,6 +1,7 @@
package com.magamochi.model.entity;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.user.model.entity.User;
import jakarta.persistence.*;
import java.time.Instant;
import lombok.*;

View File

@ -1,6 +1,7 @@
package com.magamochi.model.entity;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.user.model.entity.User;
import jakarta.persistence.*;
import lombok.*;

View File

@ -1,5 +0,0 @@
package com.magamochi.model.enumeration;
public enum UserRole {
USER
}

View File

@ -1,8 +1,8 @@
package com.magamochi.model.repository;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.model.entity.User;
import com.magamochi.model.entity.UserFavoriteManga;
import com.magamochi.user.model.entity.User;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;

View File

@ -1,8 +1,8 @@
package com.magamochi.model.repository;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.model.entity.User;
import com.magamochi.model.entity.UserMangaFollow;
import com.magamochi.user.model.entity.User;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

View File

@ -5,7 +5,7 @@ import static java.util.Objects.nonNull;
import com.magamochi.catalog.model.dto.MangaListFilterDTO;
import com.magamochi.catalog.model.entity.Author;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.model.entity.User;
import com.magamochi.user.model.entity.User;
import jakarta.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;

View File

@ -12,6 +12,7 @@ import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.model.entity.UserMangaFollow;
import com.magamochi.model.repository.*;
import com.magamochi.queue.MangaChapterDownloadProducer;
import com.magamochi.user.service.UserService;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

View File

@ -1,29 +0,0 @@
package com.magamochi.service;
import com.magamochi.ingestion.model.entity.ContentProvider;
import com.magamochi.ingestion.model.repository.ContentProviderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProviderService {
private final ContentProviderRepository contentProviderRepository;
public ContentProvider getOrCreateProvider(String providerName) {
return getOrCreateProvider(providerName, true);
}
public ContentProvider getOrCreateProvider(String providerName, Boolean supportsChapterFetch) {
return contentProviderRepository
.findByNameIgnoreCase(providerName)
.orElseGet(
() ->
contentProviderRepository.save(
ContentProvider.builder()
.name(providerName)
.active(true)
.supportsChapterFetch(supportsChapterFetch)
.build()));
}
}

View File

@ -5,6 +5,7 @@ import com.magamochi.catalog.model.repository.MangaRepository;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.model.entity.UserFavoriteManga;
import com.magamochi.model.repository.UserFavoriteMangaRepository;
import com.magamochi.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

View File

@ -1,116 +0,0 @@
package com.magamochi.service;
import static java.util.Objects.isNull;
import com.magamochi.common.exception.ConflictException;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.model.dto.AuthenticationRequestDTO;
import com.magamochi.model.dto.AuthenticationResponseDTO;
import com.magamochi.model.dto.RefreshTokenRequestDTO;
import com.magamochi.model.dto.RegistrationRequestDTO;
import com.magamochi.model.entity.User;
import com.magamochi.model.enumeration.UserRole;
import com.magamochi.model.repository.UserRepository;
import com.magamochi.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class UserService {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password()));
var userDetails = userDetailsService.loadUserByUsername(request.email());
var accessToken = jwtUtil.generateAccessToken(userDetails);
var refreshToken = jwtUtil.generateRefreshToken(userDetails);
var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
return new AuthenticationResponseDTO(
user.getId(),
accessToken,
refreshToken,
userDetails.getUsername(),
user.getName(),
user.getRole());
}
public AuthenticationResponseDTO refreshAuthToken(
RefreshTokenRequestDTO authenticationRequestDTO) {
var username = jwtUtil.extractUsernameFromRefreshToken(authenticationRequestDTO.refreshToken());
var userDetails = userDetailsService.loadUserByUsername(username);
if (!jwtUtil.validateRefreshToken(authenticationRequestDTO.refreshToken(), userDetails)) {
throw new BadCredentialsException("Invalid refresh token");
}
var newAccessToken = jwtUtil.generateAccessToken(userDetails);
var newRefreshToken = jwtUtil.generateRefreshToken(userDetails);
var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
return new AuthenticationResponseDTO(
user.getId(),
newAccessToken,
newRefreshToken,
userDetails.getUsername(),
user.getName(),
user.getRole());
}
public void register(RegistrationRequestDTO request) {
if (userRepository.existsByEmail(request.email())) {
throw new ConflictException("An user with this email already exists.");
}
userRepository.save(
User.builder()
.name(request.name())
.email(request.email())
.password(passwordEncoder.encode(request.password()))
.role(UserRole.USER)
.build());
}
public User getLoggedUserThrowIfNotFound() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
throw new NotFoundException("User not found.");
}
return findUserByEmailThrowIfNotFound(authentication.getName());
}
public User getLoggedUser() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
return null;
}
return userRepository.findByEmail(authentication.getName()).orElse(null);
}
private User findUserByEmailThrowIfNotFound(String email) {
return userRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("User not found."));
}
}

View File

@ -1,3 +1,3 @@
package com.magamochi.model.dto;
package com.magamochi.user.model.dto;
public record RegistrationRequestDTO(String name, String email, String password) {}

View File

@ -1,6 +1,7 @@
package com.magamochi.model.entity;
package com.magamochi.user.model.entity;
import com.magamochi.model.enumeration.UserRole;
import com.magamochi.model.entity.UserFavoriteManga;
import com.magamochi.user.model.enumeration.UserRole;
import jakarta.persistence.*;
import java.util.Set;
import lombok.*;

View File

@ -0,0 +1,5 @@
package com.magamochi.user.model.enumeration;
public enum UserRole {
USER
}

View File

@ -1,6 +1,6 @@
package com.magamochi.model.repository;
package com.magamochi.user.repository;
import com.magamochi.model.entity.User;
import com.magamochi.user.model.entity.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

View File

@ -1,10 +1,11 @@
package com.magamochi.service;
package com.magamochi.user.service;
import com.magamochi.model.repository.UserRepository;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.user.model.entity.User;
import com.magamochi.user.repository.UserRepository;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@ -17,15 +18,17 @@ public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
var user =
userRepository
.findByEmail(username)
.orElseThrow(
() -> new UsernameNotFoundException("User not found with email: " + username));
var user = find(username);
return new User(
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())));
}
private User find(String email) {
return userRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("User not found with email " + email + "."));
}
}

View File

@ -0,0 +1,61 @@
package com.magamochi.user.service;
import static java.util.Objects.isNull;
import com.magamochi.common.exception.ConflictException;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.user.model.dto.RegistrationRequestDTO;
import com.magamochi.user.model.entity.User;
import com.magamochi.user.model.enumeration.UserRole;
import com.magamochi.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public void register(RegistrationRequestDTO request) {
if (userRepository.existsByEmail(request.email())) {
throw new ConflictException("An user with this email already exists.");
}
userRepository.save(
User.builder()
.name(request.name())
.email(request.email())
.password(passwordEncoder.encode(request.password()))
.role(UserRole.USER)
.build());
}
public User getLoggedUserThrowIfNotFound() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
throw new NotFoundException("User not found.");
}
return find(authentication.getName());
}
public User getLoggedUser() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
return null;
}
return userRepository.findByEmail(authentication.getName()).orElse(null);
}
public User find(String email) {
return userRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("User not found with email " + email + "."));
}
}