progress on registration flow
This commit is contained in:
4
pom.xml
4
pom.xml
@@ -142,6 +142,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-mail</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.nflow</groupId>
|
<groupId>io.nflow</groupId>
|
||||||
<artifactId>nflow-rest-api-spring-web</artifactId>
|
<artifactId>nflow-rest-api-spring-web</artifactId>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ public class AuthController {
|
|||||||
private final WorkflowInstanceFactory workflowInstanceFactory;
|
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||||
private final WorkflowUtils workflowUtils;
|
private final WorkflowUtils workflowUtils;
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
|
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
|
||||||
throws ExecutionException, InterruptedException {
|
throws ExecutionException, InterruptedException {
|
||||||
@@ -42,8 +41,8 @@ public class AuthController {
|
|||||||
|
|
||||||
CompletableFuture<Object> future = workflowUtils
|
CompletableFuture<Object> future = workflowUtils
|
||||||
.waitForState(workflowId, "sendOtp", 30000, 1000);
|
.waitForState(workflowId, "sendOtp", 30000, 1000);
|
||||||
future.get();
|
Object result = future.get();
|
||||||
return ResponseEntity.ok(Utils.toJson(future));
|
return ResponseEntity.ok(Utils.toJson(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@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,
|
REVERSED,
|
||||||
AUTHORIZED,
|
AUTHORIZED,
|
||||||
REJECTED,
|
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;
|
package zw.qantra.tm.domain.workflows;
|
||||||
|
|
||||||
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import io.nflow.engine.workflow.definition.NextAction;
|
import io.nflow.engine.workflow.definition.NextAction;
|
||||||
import io.nflow.engine.workflow.definition.StateExecution;
|
import io.nflow.engine.workflow.definition.StateExecution;
|
||||||
import io.nflow.engine.workflow.definition.WorkflowDefinition;
|
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 zw.qantra.tm.domain.services.UserService;
|
||||||
import io.nflow.engine.workflow.curated.State;
|
import io.nflow.engine.workflow.curated.State;
|
||||||
import zw.qantra.tm.domain.dtos.AuthResponse;
|
import zw.qantra.tm.domain.dtos.AuthResponse;
|
||||||
@@ -18,6 +23,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class RegistrationWorkflow extends WorkflowDefinition {
|
public class RegistrationWorkflow extends WorkflowDefinition {
|
||||||
|
Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class);
|
||||||
|
|
||||||
public static final String TYPE = "registrationWorkflow";
|
public static final String TYPE = "registrationWorkflow";
|
||||||
|
|
||||||
@@ -35,6 +41,10 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
private UserService userService;
|
private UserService userService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private Validator validator;
|
private Validator validator;
|
||||||
|
@Autowired
|
||||||
|
private OtpService otpService;
|
||||||
|
@Autowired
|
||||||
|
private EmailService emailService;
|
||||||
|
|
||||||
public RegistrationWorkflow() {
|
public RegistrationWorkflow() {
|
||||||
super(TYPE, BEGIN, FAILED);
|
super(TYPE, BEGIN, FAILED);
|
||||||
@@ -72,14 +82,50 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo: add this to the appropriate action
|
// todo: add this to the appropriate action
|
||||||
// AuthResponse response = userService.register(payload);
|
|
||||||
execution.setVariable("metadata", payload);
|
execution.setVariable("metadata", payload);
|
||||||
|
|
||||||
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
|
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendOtp(StateExecution execution) {
|
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) {
|
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
|
jwt.expiration=86400000
|
||||||
|
|
||||||
logging.level.root=WARN
|
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