UserService.java

package com.edtech.service;

import com.edtech.dto.RegisterRequestDto;
import com.edtech.model.User;
import com.edtech.model.UserRole;
import com.edtech.repository.UserRepository;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/** Documentação para UserService. */
@Service
public class UserService {

  @Value("${institutional-email.allowed-domains:unb.br}")
  private String allowedInstitutionalDomains = "unb.br";

  private final UserRepository userRepository;
  private final PasswordEncoder passwordEncoder;
  private final com.edtech.repository.VerificationTokenRepository verificationTokenRepository;
  private final EmailService emailService;
  private static final java.security.SecureRandom SECURE_RANDOM = new java.security.SecureRandom();

  /** Documentação para o método UserService. */
  public UserService(
      UserRepository userRepository,
      PasswordEncoder passwordEncoder,
      com.edtech.repository.VerificationTokenRepository verificationTokenRepository,
      EmailService emailService) {
    this.userRepository = userRepository;
    this.passwordEncoder = passwordEncoder;
    this.verificationTokenRepository = verificationTokenRepository;
    this.emailService = emailService;
  }

  /** Documentação. */
  @Transactional(readOnly = true)
  public User authenticate(String email, String password) {
    String normalizedEmail = email.trim().toLowerCase(Locale.ROOT);
    User user =
        userRepository
            .findByEmailIgnoreCase(normalizedEmail)
            .orElseThrow(() -> new InvalidCredentialsException("Credenciais inválidas."));

    if (!user.isActive()) {
      throw new AccountNotVerifiedException();
    }

    if (!passwordEncoder.matches(password, user.getPasswordHash())) {
      throw new InvalidCredentialsException("Credenciais inválidas.");
    }

    return user;
  }

  /** Documentação. */
  @Transactional
  public User register(RegisterRequestDto request) {
    String normalizedEmail = request.email().trim().toLowerCase(Locale.ROOT);

    if (!hasAllowedInstitutionalDomain(normalizedEmail)) {
      throw new InvalidInstitutionalEmailException(
          "O e-mail deve pertencer a um domínio institucional permitido.");
    }

    if (userRepository.existsByEmailIgnoreCase(normalizedEmail)) {
      throw new DuplicateEmailException("E-mail ja cadastrado.");
    }

    UserRole initialRole = request.role();

    User user =
        new User(
            request.name().trim(),
            normalizedEmail,
            passwordEncoder.encode(request.password()),
            initialRole,
            java.util.UUID.fromString("00000000-0000-0000-0000-000000000001"));

    user.setActive(false);
    userRepository.save(user);

    verificationTokenRepository.deleteByEmail(normalizedEmail);
    String code = String.format("%06d", SECURE_RANDOM.nextInt(999999));
    com.edtech.model.VerificationToken token =
        new com.edtech.model.VerificationToken(
            code, normalizedEmail, java.time.LocalDateTime.now().plusMinutes(15));
    verificationTokenRepository.save(token);

    emailService.sendVerificationEmail(normalizedEmail, code);

    return user;
  }

  /** Checks whether an email belongs to one of the configured institutional domains. */
  private boolean hasAllowedInstitutionalDomain(String email) {
    return Arrays.stream(allowedInstitutionalDomains.split(","))
        .map(domain -> domain.trim().toLowerCase(Locale.ROOT))
        .filter(domain -> !domain.isBlank())
        .anyMatch(domain -> email.endsWith("@" + domain) || email.endsWith("." + domain));
  }

  /** Documentação. */
  @Transactional
  public User verifyRegistration(String email, String code) {
    String normalizedEmail = email.trim().toLowerCase(Locale.ROOT);
    Optional<com.edtech.model.VerificationToken> tokenOpt =
        verificationTokenRepository.findByEmailAndToken(normalizedEmail, code);

    if (tokenOpt.isPresent() && !tokenOpt.get().isExpired()) {
      Optional<User> userOpt = userRepository.findByEmailIgnoreCase(normalizedEmail);
      if (userOpt.isPresent()) {
        User user = userOpt.get();
        user.setActive(true);
        userRepository.save(user);
        verificationTokenRepository.deleteByEmail(normalizedEmail);
        return user;
      }
    }
    throw new InvalidCredentialsException("Código inválido ou expirado.");
  }

  /** Generates and sends a fresh verification code for an inactive account. */
  @Transactional
  public void resendVerificationCode(String email) {
    String normalizedEmail = email.trim().toLowerCase(Locale.ROOT);
    User user =
        userRepository
            .findByEmailIgnoreCase(normalizedEmail)
            .orElseThrow(() -> new InvalidCredentialsException("Credenciais inválidas."));
    if (user.isActive()) {
      return;
    }
    verificationTokenRepository.deleteByEmail(normalizedEmail);
    String code = String.format("%06d", SECURE_RANDOM.nextInt(999999));
    verificationTokenRepository.save(
        new com.edtech.model.VerificationToken(
            code, normalizedEmail, java.time.LocalDateTime.now().plusMinutes(15)));
    emailService.sendVerificationEmail(normalizedEmail, code);
  }

  @Transactional
  public User saveUserWithoutHash(User user) {
    return userRepository.save(user);
  }
}