progress on registration flow

This commit is contained in:
2025-09-11 00:02:20 +02:00
parent 1987c1855e
commit e13e8e1ffe
10 changed files with 238 additions and 6 deletions

View File

@@ -28,7 +28,6 @@ public class AuthController {
private final WorkflowInstanceFactory workflowInstanceFactory;
private final WorkflowUtils workflowUtils;
@PostMapping("/register")
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
throws ExecutionException, InterruptedException {
@@ -42,8 +41,8 @@ public class AuthController {
CompletableFuture<Object> future = workflowUtils
.waitForState(workflowId, "sendOtp", 30000, 1000);
future.get();
return ResponseEntity.ok(Utils.toJson(future));
Object result = future.get();
return ResponseEntity.ok(Utils.toJson(result));
}
@PostMapping("/login")

View File

@@ -0,0 +1,20 @@
package zw.qantra.tm.domain.dtos;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmailRequest {
private String to;
private List<String> cc;
private String subject;
private String body;
private boolean isHtml;
}

View File

@@ -8,5 +8,7 @@ public enum Status {
REVERSED,
AUTHORIZED,
REJECTED,
PROCESSING
PROCESSING,
COMPLETE,
ACTIVE
}

View File

@@ -0,0 +1,22 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.*;
import zw.qantra.tm.domain.enums.Status;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Otp extends BaseEntity{
private String username;
private String otp;
@Enumerated(EnumType.STRING)
private Status otpStatus;
private String otpType;
}

View File

@@ -0,0 +1,14 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.Otp;
import java.util.Optional;
@Repository
public interface OtpRepository extends JpaRepository<Otp, Long> {
Optional<Otp> findByUsernameAndOtpType(String username, String otpType);
Optional<Otp> findByUsernameAndOtpTypeAndOtp(String username, String otpType, String otp);
void deleteByUsernameAndOtpType(String username, String otpType);
}

View File

@@ -0,0 +1,46 @@
package zw.qantra.tm.domain.services;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.EmailRequest;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender mailSender;
/**
* Sends an email based on the provided EmailRequest
* @param emailRequest the email request containing recipient, subject, and body
* @return true if the email was sent successfully, false otherwise
*/
public boolean sendEmail(EmailRequest emailRequest) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(emailRequest.getTo());
if (emailRequest.getCc() != null && !emailRequest.getCc().isEmpty()) {
helper.setCc(emailRequest.getCc().toArray(new String[0]));
}
helper.setSubject(emailRequest.getSubject());
helper.setText(emailRequest.getBody(), emailRequest.isHtml());
mailSender.send(message);
log.info("Email sent successfully to {}", emailRequest.getTo());
return true;
} catch (MessagingException e) {
log.error("Failed to send email to {}: {}", emailRequest.getTo(), e.getMessage(), e);
return false;
}
}
}

View File

@@ -0,0 +1,67 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Otp;
import zw.qantra.tm.domain.repositories.OtpRepository;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class OtpService {
private static final int OTP_LENGTH = 6;
private static final String NUMBERS = "0123456789";
private static final SecureRandom random = new SecureRandom();
private final OtpRepository otpRepository;
public Otp generateOtp(String username, String otpType) {
// Delete any existing OTP for this username and type
otpRepository.deleteByUsernameAndOtpType(username, otpType);
String otpCode = generateRandomOtp();
Otp otp = Otp.builder()
.username(username)
.otp(otpCode)
.otpType(otpType)
.otpStatus(Status.ACTIVE)
.build();
return otpRepository.save(otp);
}
public boolean verifyOtp(String username, String otpCode, String otpType) {
Optional<Otp> otpOpt = otpRepository.findByUsernameAndOtpTypeAndOtp(username, otpType, otpCode);
if (otpOpt.isEmpty()) {
return false;
}
Otp otp = otpOpt.get();
// Mark OTP as used
otp.setOtpStatus(Status.COMPLETE);
otpRepository.save(otp);
return true;
}
public boolean isOtpValid(String username, String otpType) {
Optional<Otp> otpOpt = otpRepository.findByUsernameAndOtpType(username, otpType);
return otpOpt.isPresent() && otpOpt.get().getOtpStatus() == Status.ACTIVE;
}
private String generateRandomOtp() {
StringBuilder otp = new StringBuilder(OTP_LENGTH);
for (int i = 0; i < OTP_LENGTH; i++) {
otp.append(NUMBERS.charAt(random.nextInt(NUMBERS.length())));
}
return otp.toString();
}
}

View File

@@ -1,12 +1,17 @@
package zw.qantra.tm.domain.workflows;
import io.nflow.engine.workflow.definition.WorkflowStateType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import io.nflow.engine.workflow.definition.NextAction;
import io.nflow.engine.workflow.definition.StateExecution;
import io.nflow.engine.workflow.definition.WorkflowDefinition;
import zw.qantra.tm.domain.dtos.EmailRequest;
import zw.qantra.tm.domain.models.Otp;
import zw.qantra.tm.domain.services.EmailService;
import zw.qantra.tm.domain.services.OtpService;
import zw.qantra.tm.domain.services.UserService;
import io.nflow.engine.workflow.curated.State;
import zw.qantra.tm.domain.dtos.AuthResponse;
@@ -18,6 +23,7 @@ import java.util.Set;
@Component
public class RegistrationWorkflow extends WorkflowDefinition {
Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class);
public static final String TYPE = "registrationWorkflow";
@@ -35,6 +41,10 @@ public class RegistrationWorkflow extends WorkflowDefinition {
private UserService userService;
@Autowired
private Validator validator;
@Autowired
private OtpService otpService;
@Autowired
private EmailService emailService;
public RegistrationWorkflow() {
super(TYPE, BEGIN, FAILED);
@@ -72,14 +82,50 @@ public class RegistrationWorkflow extends WorkflowDefinition {
}
// todo: add this to the appropriate action
// AuthResponse response = userService.register(payload);
execution.setVariable("metadata", payload);
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
}
public void sendOtp(StateExecution execution) {
RegisterRequest response = execution.getVariable("metadata", RegisterRequest.class);
try {
RegisterRequest registerRequest = execution.getVariable("metadata", RegisterRequest.class);
String username = registerRequest.getUsername();
String email = registerRequest.getEmail();
// Generate OTP
Otp otp = otpService.generateOtp(username, "REGISTRATION");
// Create email content
String subject = "Your Verification Code";
String body = String.format(
"<html><body>" +
"<h2>Email Verification</h2>" +
"<p>Hello %s,</p>" +
"<p>Your verification code is: <strong>%s</strong></p>" +
"<p>This code will expire in 10 minutes.</p>" +
"<p>If you didn't request this code, please ignore this email.</p>" +
"</body></html>",
username, otp.getOtp()
);
// Send email
EmailRequest emailRequest = EmailRequest.builder()
.to(email)
.subject(subject)
.body(body)
.isHtml(true)
.build();
boolean emailSent = emailService.sendEmail(emailRequest);
if (!emailSent) {
log.error("Failed to send OTP email to: {}", email);
}
} catch (Exception e) {
log.error("Error sending OTP: {}", e.getMessage(), e);
}
}
public NextAction verifyOtp(StateExecution execution) {