Add email verification functionality, including backend support, email handling, and user interface integration.

This commit is contained in:
2025-08-23 21:46:10 +02:00
parent da17cf9696
commit 641083732d
26 changed files with 878 additions and 57 deletions
@@ -0,0 +1,140 @@
package com.alttd.altitudeweb.services.mail;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
import com.alttd.altitudeweb.setup.Connection;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
public class MailVerificationService {
private final JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String fromEmail;
public java.util.List<EmailVerification> listAll(UUID userUuid) {
java.util.concurrent.CompletableFuture<java.util.List<EmailVerification>> future = new java.util.concurrent.CompletableFuture<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sql -> {
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
future.complete(mapper.findAllByUser(userUuid));
});
return future.join();
}
public EmailVerification submitEmail(UUID userUuid, String email) {
String code = generateCode();
Instant now = Instant.now();
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sql -> {
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
EmailVerification existing = mapper.findByUserAndEmail(userUuid, email);
EmailVerification toPersist;
if (existing == null) {
toPersist = new EmailVerification(UUID.randomUUID(), userUuid, email, code, false, now, null, now);
mapper.insert(toPersist);
} else {
mapper.updateCodeAndLastSent(existing.id(), code, now);
toPersist = new EmailVerification(existing.id(), userUuid, email, code, false, existing.createdAt(), null, now);
}
future.complete(toPersist);
});
EmailVerification saved = future.join();
sendVerificationEmail(saved);
return saved;
}
public Optional<EmailVerification> verifyCode(UUID userUuid, String code) {
CompletableFuture<Optional<EmailVerification>> future = new CompletableFuture<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sql -> {
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
EmailVerification found = mapper.findByUserAndCode(userUuid, code);
if (found == null) {
future.complete(Optional.empty());
return;
}
mapper.markVerified(found.id(), Instant.now());
future.complete(Optional.of(found));
});
return future.join();
}
public EmailVerification resend(UUID userUuid, String email) {
String code = generateCode();
Instant now = Instant.now();
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sql -> {
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
EmailVerification existing = mapper.findByUserAndEmail(userUuid, email);
if (existing != null) {
mapper.updateCodeAndLastSent(existing.id(), code, now);
future.complete(new EmailVerification(existing.id(), userUuid, email, code, false, existing.createdAt(), null, now));
} else {
future.complete(null);
}
});
EmailVerification updated = future.join();
if (updated != null) {
sendVerificationEmail(updated);
}
return updated;
}
public boolean delete(UUID userUuid, String email) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sql -> {
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
EmailVerification existing = mapper.findByUserAndEmail(userUuid, email);
if (existing != null) {
mapper.deleteByUserAndEmail(userUuid, email);
future.complete(true);
} else {
future.complete(false);
}
});
return future.join();
}
private void sendVerificationEmail(EmailVerification emailVerification) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(fromEmail);
helper.setTo(emailVerification.email());
helper.setSubject("Your verification code");
helper.setText("Your verification code is: " + emailVerification.verificationCode(), false);
mailSender.send(message);
} catch (MessagingException e) {
log.error("Failed to send verification email to {}", emailVerification.email(), e);
}
}
private String generateCode() {
// 6-digit numeric code
Random random = new Random();
int num = 100000 + random.nextInt(899999);
return String.valueOf(num);
}
}