progress on registration flow
This commit is contained in:
4
pom.xml
4
pom.xml
@@ -142,6 +142,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.nflow</groupId>
|
||||
<artifactId>nflow-rest-api-spring-web</artifactId>
|
||||
|
||||
@@ -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")
|
||||
|
||||
20
src/main/java/zw/qantra/tm/domain/dtos/EmailRequest.java
Normal file
20
src/main/java/zw/qantra/tm/domain/dtos/EmailRequest.java
Normal 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;
|
||||
}
|
||||
@@ -8,5 +8,7 @@ public enum Status {
|
||||
REVERSED,
|
||||
AUTHORIZED,
|
||||
REJECTED,
|
||||
PROCESSING
|
||||
PROCESSING,
|
||||
COMPLETE,
|
||||
ACTIVE
|
||||
}
|
||||
|
||||
22
src/main/java/zw/qantra/tm/domain/models/Otp.java
Normal file
22
src/main/java/zw/qantra/tm/domain/models/Otp.java
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
46
src/main/java/zw/qantra/tm/domain/services/EmailService.java
Normal file
46
src/main/java/zw/qantra/tm/domain/services/EmailService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/main/java/zw/qantra/tm/domain/services/OtpService.java
Normal file
67
src/main/java/zw/qantra/tm/domain/services/OtpService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -47,3 +47,15 @@ jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-produc
|
||||
jwt.expiration=86400000
|
||||
|
||||
logging.level.root=WARN
|
||||
|
||||
# Email Configuration
|
||||
spring.mail.host=smtp.gmail.com
|
||||
spring.mail.port=587
|
||||
spring.mail.username=vusakhoza@gmail.com
|
||||
spring.mail.password=Stikbanch@14
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
spring.mail.properties.mail.smtp.starttls.required=true
|
||||
spring.mail.properties.mail.smtp.connectiontimeout=5000
|
||||
spring.mail.properties.mail.smtp.timeout=5000
|
||||
spring.mail.properties.mail.smtp.writetimeout=5000
|
||||
|
||||
Reference in New Issue
Block a user