crysisprophet1234 / Bookstore-Poletto

Poletto Bookstore, venda e aluguel de livros.
https://polettobookstore.netlify.app/
0 stars 0 forks source link

Arquitetando a verificação de e-mail no backend #41

Open crysisprophet1234 opened 11 months ago

crysisprophet1234 commented 11 months ago

Com as novas entidades, a entity USER terá a propriedade AccountStatus que determinará se o usuário já verificou ou não sua conta por e-mail, no momento da criação ele é considerando UNVERIFIED e assim ficará até que alcance a rota verify-account.

Processo padrão de criação de conta:

Rota: /auth/v3/register

Dados recebidos:

{
    "email": "user@mail.com",
    "password": "abc1234"
}
@Override
    @Transactional
    public UserDto register(UserDto dto) {

        User entity = UserMapperV3.INSTANCE.userDtoToUser(dto);

        if (authRepository.existsByEmail(dto.getEmail())) {
            throw new AlreadyExistingAccountException(dto.getEmail());
        }

        entity.getRoles().add(roleRepository.getReferenceById(1L));

        entity.setAccountStatus(AccountStatus.UNVERIFIED); -> CONTA INICIA COMO UNVERIFIED

        entity.setUserStatus(UserStatus.ACTIVE);

        entity.setPassword(passwordEncoder.encode(dto.getPassword()));

        entity = authRepository.save(entity);

        logger.info("Resource USER saved: " + entity.toString());

        return userDtoWithLinks(UserMapperV3.INSTANCE.userToUserDto(entity));

    }

Após isso o frontend deve chamar as rotas de criação de pessoa atrelando ao usuário recém criado.

Para verificar a conta, primeiro deve ser enviado o e-mail para o endereço que o novo usuário cadastrou, isso deve acontecer automáticamente (chamado pelo frontend) após criação do usuário e depois disso o usuário deverá solicitar caso precise um novo -email.

Rota: /v3/users/{userId}/send-verification-email

@Override
    @Transactional
    public void sendAccountVerificationEmail(Long userId) {

        User userEntity = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("Resource USER not found, ID: " + userId));

        VerificationToken verificationToken = verificationTokenRepository.findLastValidTokenByUserId(userEntity.getId())
                .orElseGet(() -> createVerificationToken(userEntity));

        String verifyAccountURL = ServletUriComponentsBuilder.fromCurrentContextPath()
                .replacePath("api/v3/users/verify-account?token=" + verificationToken.getToken())
                .build()
                .toUriString();

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");

        Map<String, Object> model = new HashMap<>();
        model.put("username", "Mock username");
        model.put("verification_link", verifyAccountURL);
        model.put("expiration_date", formatter.format(verificationToken.getExpiresAt()));

        emailService.sendEmailFromTemplate(
            userEntity.getEmail(),
            "Verificação de conta Poletto Bookstore",
            "account-verification-template",
            model
        );

        logger.info("Account verification e-mail sent to {}, USER with ID {}", userEntity.getEmail(), userEntity.getId());

    }

Nesse momento é renderizado a html account-verification-template com os dados do usuário e depos enviado para o e-mail que o usuário cadastrou contendo um link que leverá para a página no frontend responsável pela " confirmação " da verificação de conta, que conterá um link para a próxima rota na API que de fato efetiva a verificação da conta:

Rota: /v3/users/verify-account?verificationToken={UUID token}

@Override
@Transactional
public void verifyAccount(UUID verificationToken) {

    VerificationToken verificationTokenEntity = verificationTokenRepository.findByToken(verificationToken)
            .orElseThrow(() -> new InvalidTokenException());

    if (verificationTokenEntity.getExpiresAt().isBefore(LocalDateTime.now())) {
        throw new InvalidTokenException(verificationTokenEntity.getExpiresAt());
    }

    User userEntity = userRepository.getReferenceById(verificationTokenEntity.getUser().getId());

    userEntity.setAccountStatus(AccountStatus.VERIFIED);

    logger.info("Resource USER with id {} updated account status to VERIFIED", userEntity.getId());

}

Entidade VerificationToken

@Entity
@Table(name = "verification_token")
public class VerificationToken implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private UUID token;

    @Column(nullable = false)
    private LocalDateTime createdAt;

    @Column(nullable = false)
    private LocalDateTime expiresAt;

    @ManyToOne
    @JoinColumn(name = "user_id", updatable = false)
    private User user;

A resposta de ambos metódos de solicitação do e-mail e verificação apenas respondem 204 indicando o sucesso ou não, é responsabilidade do client chamar as rotas para não gerar encadeamento desnecessário no backend.

Essa issue foi criada para atrelar as commits relacionadas a essa implementação além das melhorias que podem ser realizadas como:

  1. Maior restrição do conteúdo passado para a classe EmailService, como uma Enum de templates html e objetos específicos relacionados a cada Enum como o conteúdo a ser preenchido no html.
  2. Implementar restrição na verificação e solicitação de e-mail, validar token e verificar se o próprio usuário solicitando o e-mail, impedindo outros usuário de solicitar.
  3. Atualmente os método de envio do e-mial e verificação não interrompe a execução caso o usuário em questão já esteja verificado, isso poderia mudar considerando que a continuidade da execução impactar no servidor visto que são criadas novos objetos no banco e e-mails enviados pelo client da gmail.
  4. Considerar a possibilidade de "limpar' os registros de token para um usuário depois que ele estiver autenticado, considerando que não mais úteis em contexto algum e isso reduziria o volume de dados no banco.
crysisprophet1234 commented 10 months ago

baf7e840c63863c854e8fe055d15c28a3a49f272 definição da controladora dos métodos de envio de e-mail de verificação e recebimento de token para verificar conta de usuário.

crysisprophet1234 commented 10 months ago

e128b65 definição da entidade VerificationToken

crysisprophet1234 commented 10 months ago

e5b89bc definição da nova entidade User com propriedade accountStatus indicando se usuário possui e-mail verificado ou não

crysisprophet1234 commented 10 months ago

d232442 definição da classe de serviço com métodos de envio de e-mail de verificação, criação de token, verificação de conta por token