UniqueEmailForAuthenticatedUserValidator.java

package com.albertsilva.dev.asjcatalog.validation.user.validator;

import com.albertsilva.dev.asjcatalog.domain.user.User;
import com.albertsilva.dev.asjcatalog.repository.UserRepository;
import com.albertsilva.dev.asjcatalog.security.auth.AuthenticatedUserService;
import com.albertsilva.dev.asjcatalog.validation.user.annotation.UniqueEmailForAuthenticatedUser;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

/**
 * Validator de {@link UniqueEmailForAuthenticatedUser}: garante que o e-mail
 * novo do <b>usuário autenticado</b> não pertença a outro usuário.
 *
 * <p>
 * <b>Comportamento:</b> {@code null} ou em branco ⇒ <b>válido</b>. Caso
 * contrário: normaliza o e-mail ({@code trim()} + {@code toLowerCase()}); obtém
 * o usuário autenticado com {@code AuthenticatedUserService.getAuthenticatedUser()}
 * (claim {@code userId} do JWT no {@code SecurityContext}, seguido de
 * {@code UserRepository.findById}); consulta
 * {@code existsByEmailIgnoreCaseAndIdNot(email, idDoUsuárioAutenticado)}. Se
 * outro usuário tiver o e-mail, registra {@code {user.email.unique}}.
 * </p>
 *
 * <p>
 * <b>Dependências:</b> {@code UserRepository} e {@code AuthenticatedUserService}
 * (pacote de segurança). <b>Diferença para os demais validators de unicidade
 * de atualização:</b> o id de referência <b>não vem da URL</b>, e sim do JWT.
 * Se não houver JWT, claim ou usuário, {@code getAuthenticatedUser()} lança
 * {@code AuthenticatedUserNotFoundException}, que este validator não captura; a
 * forma como essa exceção chega ao cliente depende do Bean Validation e do
 * handler de exceções (a confirmar na camada web). O mesmo aviso de
 * normalização de {@link UniqueEmailValidator} se aplica ao valor persistido.
 * </p>
 */
public class UniqueEmailForAuthenticatedUserValidator
    implements ConstraintValidator<UniqueEmailForAuthenticatedUser, String> {

  /**
   * Repositório utilizado para consulta de usuários.
   */
  private final UserRepository repository;

  /**
   * Serviço responsável por obter o usuário autenticado.
   */
  private final AuthenticatedUserService authenticatedUserService;

  /**
   * Construtor.
   *
   * @param repository               repositório de usuários
   * @param authenticatedUserService serviço do usuário autenticado
   */
  public UniqueEmailForAuthenticatedUserValidator(UserRepository repository,
      AuthenticatedUserService authenticatedUserService) {
    this.repository = repository;
    this.authenticatedUserService = authenticatedUserService;
  }

  /**
   * Verifica se o e-mail (normalizado) está livre ou pertence ao próprio usuário
   * autenticado.
   *
   * @param value   e-mail informado (pode ser {@code null})
   * @param context contexto usado para registrar a violação personalizada
   * @return {@code true} se for nulo/em branco, se não existir ou se pertencer
   *         ao usuário autenticado; {@code false} se pertencer a outro usuário
   */
  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {

    if (value == null || value.isBlank()) {
      return true;
    }

    String normalizedEmail = value.trim().toLowerCase();

    User authenticatedUser = authenticatedUserService.getAuthenticatedUser();

    boolean emailAlreadyExists = repository.existsByEmailIgnoreCaseAndIdNot(normalizedEmail, authenticatedUser.getId());

    if (!emailAlreadyExists) {
      return true;
    }

    context.disableDefaultConstraintViolation();

    context.buildConstraintViolationWithTemplate("{user.email.unique}").addConstraintViolation();

    return false;
  }

}