AuthenticatedUserService.java

package com.albertsilva.dev.asjcatalog.security.auth;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;

import com.albertsilva.dev.asjcatalog.domain.user.User;
import com.albertsilva.dev.asjcatalog.repository.UserRepository;
import com.albertsilva.dev.asjcatalog.service.exception.AuthenticatedUserNotFoundException;

/**
 * Resolve o <b>usuário autenticado</b> da requisição a partir do JWT.
 *
 * <p>
 * <b>Fonte da identidade:</b> o {@code Authentication} do
 * {@link SecurityContextHolder}, cujo principal deve ser um {@link Jwt}. O
 * usuário é identificado pelo claim customizado <b>{@code userId}</b> (não pelo
 * e-mail, pelo claim {@code username}, por {@code sub} nem por {@code jti}) e
 * carregado com {@code UserRepository.findById}. No código atual, este é o
 * único ponto da aplicação que transforma o JWT em um {@code User}.
 * </p>
 *
 * <p>
 * <b>Usos:</b> {@code AccountService} (perfil e senha do próprio usuário),
 * {@code UniqueEmailForAuthenticatedUserValidator} e a expressão
 * {@code @authenticatedUserService.isCurrentUser(#id)} de
 * {@code UserController.findById}.
 * </p>
 *
 * <p>
 * <b>Não faz:</b> não verifica se o usuário está ativo (uma conta desativada
 * com token ainda válido é resolvida normalmente); não usa transação própria;
 * cada chamada faz uma consulta ao banco.
 * </p>
 *
 * <pre>
 * SecurityContext → Authentication.getPrincipal() (Jwt)
 *   → claim "userId" → UserRepository.findById → User
 * </pre>
 */
@Service
public class AuthenticatedUserService {

  private final UserRepository userRepository;

  /**
   * @param userRepository repositório usado para carregar o usuário pelo id do JWT
   */
  public AuthenticatedUserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  /**
   * Devolve o usuário do JWT da requisição corrente.
   *
   * <p>
   * Lança {@link AuthenticatedUserNotFoundException} (com chave de mensagem) em
   * quatro situações: sem {@code Authentication} ou com principal que não seja
   * {@link Jwt} ({@code error.auth.invalid.principal}); claim {@code userId}
   * ausente ou menor ou igual a zero ({@code error.auth.userId.claim.notFound});
   * usuário inexistente ({@code error.auth.user.notFound}). O claim é lido como
   * {@code Long}.
   * </p>
   *
   * @return usuário persistido correspondente ao {@code userId} do token
   * @throws AuthenticatedUserNotFoundException nas situações acima
   */
  public User getAuthenticatedUser() {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
      throw new AuthenticatedUserNotFoundException("error.auth.invalid.principal");
    }

    Long userId = jwt.getClaim("userId");

    if (userId == null || userId <= 0) {
      throw new AuthenticatedUserNotFoundException("error.auth.userId.claim.notFound");
    }

    return userRepository.findById(userId)
        .orElseThrow(() -> new AuthenticatedUserNotFoundException("error.auth.user.notFound"));
  }

  /**
   * Indica se {@code userId} é o id do usuário do JWT. Chama
   * {@link #getAuthenticatedUser()}, portanto lança a mesma exceção quando não há
   * identidade válida.
   *
   * @param userId id a comparar (um valor {@code null} resulta em
   *               {@code false})
   * @return {@code true} se for o usuário autenticado
   */
  public boolean isCurrentUser(Long userId) {
    return getAuthenticatedUser().getId().equals(userId);
  }
}