resolved workflow issues
This commit is contained in:
19
pom.xml
19
pom.xml
@@ -99,13 +99,17 @@
|
|||||||
<version>20231013</version> <!-- Use the latest version available -->
|
<version>20231013</version> <!-- Use the latest version available -->
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>joda-time</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>joda-time</artifactId>
|
||||||
<scope>runtime</scope>
|
<version>2.12.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>jackson-datatype-joda</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -143,7 +147,10 @@
|
|||||||
<artifactId>nflow-rest-api-spring-web</artifactId>
|
<artifactId>nflow-rest-api-spring-web</artifactId>
|
||||||
<version>10.0.1</version>
|
<version>10.0.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
21
src/main/java/zw/qantra/tm/configs/JacksonConfig.java
Normal file
21
src/main/java/zw/qantra/tm/configs/JacksonConfig.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package zw.qantra.tm.configs;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
objectMapper.registerModule(new JodaModule());
|
||||||
|
return objectMapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,9 +29,9 @@ public class SecurityConfig {
|
|||||||
http
|
http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/explorer/**").permitAll()
|
.requestMatchers("/explorer/**").permitAll()
|
||||||
.requestMatchers("/nflow/**").permitAll()
|
.requestMatchers("/nflow/**").permitAll()
|
||||||
.requestMatchers("/auth/**").permitAll()
|
.requestMatchers("/auth/**").permitAll()
|
||||||
.requestMatchers("/public/**").permitAll()
|
.requestMatchers("/public/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.controllers;
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
|
import io.nflow.engine.service.WorkflowInstanceService;
|
||||||
|
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -8,6 +10,12 @@ import zw.qantra.tm.domain.dtos.AuthResponse;
|
|||||||
import zw.qantra.tm.domain.dtos.LoginRequest;
|
import zw.qantra.tm.domain.dtos.LoginRequest;
|
||||||
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
||||||
import zw.qantra.tm.domain.services.UserService;
|
import zw.qantra.tm.domain.services.UserService;
|
||||||
|
import zw.qantra.tm.domain.workflows.RegistrationWorkflow;
|
||||||
|
import zw.qantra.tm.domain.workflows.WorkflowUtils;
|
||||||
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
@@ -16,11 +24,26 @@ import zw.qantra.tm.domain.services.UserService;
|
|||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final WorkflowInstanceService workflowService;
|
||||||
|
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||||
|
private final WorkflowUtils workflowUtils;
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
public ResponseEntity<Object> register(@Valid @RequestBody RegisterRequest request)
|
||||||
AuthResponse response = userService.register(request);
|
throws ExecutionException, InterruptedException {
|
||||||
return ResponseEntity.ok(response);
|
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||||
|
.setType(RegistrationWorkflow.TYPE)
|
||||||
|
.setExternalId(request.getUsername() + "-" + System.currentTimeMillis())
|
||||||
|
.putStateVariable("metadata", request)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
long workflowId = workflowService.insertWorkflowInstance(instance);
|
||||||
|
|
||||||
|
CompletableFuture<Object> future = workflowUtils
|
||||||
|
.waitForState(workflowId, "sendOtp", 30000, 1000);
|
||||||
|
future.get();
|
||||||
|
return ResponseEntity.ok(Utils.toJson(future));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package zw.qantra.tm.domain.dtos;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
public class AuthResponse {
|
public class AuthResponse {
|
||||||
private String token;
|
private String token;
|
||||||
private String type = "Bearer";
|
private String type = "Bearer";
|
||||||
|
|||||||
17
src/main/java/zw/qantra/tm/domain/dtos/PasswordRequest.java
Normal file
17
src/main/java/zw/qantra/tm/domain/dtos/PasswordRequest.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PasswordRequest {
|
||||||
|
@NotBlank(message = "Username is required")
|
||||||
|
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank(message = "Password is required")
|
||||||
|
@Size(min = 6, message = "Password must be at least 6 characters")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -11,8 +11,6 @@ public class RegisterRequest {
|
|||||||
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@NotBlank(message = "Password is required")
|
|
||||||
@Size(min = 6, message = "Password must be at least 6 characters")
|
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@NotBlank(message = "Email is required")
|
@NotBlank(message = "Email is required")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
@@ -31,10 +32,12 @@ public class BaseEntity {
|
|||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(name = "created_at", nullable = true, updatable = false)
|
@Column(name = "created_at", nullable = true, updatable = false)
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
|
||||||
private LocalDateTime createdAt; // Or Date, Timestamp
|
private LocalDateTime createdAt; // Or Date, Timestamp
|
||||||
|
|
||||||
@UpdateTimestamp
|
@UpdateTimestamp
|
||||||
@Column(name = "updated_at", nullable = true)
|
@Column(name = "updated_at", nullable = true)
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
|
||||||
private LocalDateTime updatedAt; // Or Date, Timestamp
|
private LocalDateTime updatedAt; // Or Date, Timestamp
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package zw.qantra.tm.domain.workflows;
|
||||||
|
|
||||||
|
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
||||||
|
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.services.UserService;
|
||||||
|
import io.nflow.engine.workflow.curated.State;
|
||||||
|
import zw.qantra.tm.domain.dtos.AuthResponse;
|
||||||
|
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RegistrationWorkflow extends WorkflowDefinition {
|
||||||
|
|
||||||
|
public static final String TYPE = "registrationWorkflow";
|
||||||
|
|
||||||
|
public static final State BEGIN = new State("begin", WorkflowStateType.start);
|
||||||
|
public static final State REGISTER = new State("register", WorkflowStateType.normal);
|
||||||
|
public static final State SEND_OTP = new State("sendOtp", WorkflowStateType.manual);
|
||||||
|
public static final State VERIFY_OTP = new State("verifyOtp", WorkflowStateType.normal);
|
||||||
|
public static final State RESEND_OTP = new State("resendOtp", WorkflowStateType.manual);
|
||||||
|
public static final State SET_PIN = new State("setPin", WorkflowStateType.normal);
|
||||||
|
public static final State UPDATE_ERP = new State("updateErp", WorkflowStateType.normal);
|
||||||
|
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
||||||
|
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
@Autowired
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
public RegistrationWorkflow() {
|
||||||
|
super(TYPE, BEGIN, FAILED);
|
||||||
|
|
||||||
|
permit(BEGIN, REGISTER);
|
||||||
|
permit(REGISTER, SEND_OTP);
|
||||||
|
permit(SEND_OTP, VERIFY_OTP, RESEND_OTP);
|
||||||
|
permit(RESEND_OTP, VERIFY_OTP, FAILED);
|
||||||
|
permit(VERIFY_OTP, SET_PIN, FAILED);
|
||||||
|
permit(SET_PIN, UPDATE_ERP);
|
||||||
|
permit(UPDATE_ERP, DONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction begin(StateExecution execution) {
|
||||||
|
return NextAction.moveToState(REGISTER, "Registration started");
|
||||||
|
}
|
||||||
|
|
||||||
|
// register validates most of the initial registration details
|
||||||
|
public NextAction register(StateExecution execution) {
|
||||||
|
|
||||||
|
RegisterRequest payload = execution.getVariable("metadata", RegisterRequest.class);
|
||||||
|
|
||||||
|
if(payload == null)
|
||||||
|
return NextAction.moveToState(FAILED, "No payload provided");
|
||||||
|
|
||||||
|
// Validate the payload using javax validation annotations
|
||||||
|
Set<ConstraintViolation<RegisterRequest>> violations = validator.validate(payload);
|
||||||
|
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
StringBuilder errorMessage = new StringBuilder("Validation failed: ");
|
||||||
|
for (ConstraintViolation<RegisterRequest> violation : violations) {
|
||||||
|
errorMessage.append(violation.getMessage()).append("; ");
|
||||||
|
}
|
||||||
|
return NextAction.moveToState(FAILED, errorMessage.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction verifyOtp(StateExecution execution) {
|
||||||
|
return NextAction.stopInState(SET_PIN, "OTP verified successfully for: " );
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction setPin(StateExecution execution) {
|
||||||
|
RegisterRequest response = execution.getVariable("response", RegisterRequest.class);
|
||||||
|
return NextAction.stopInState(UPDATE_ERP, "PIN set successfully for: " + response.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction updateErp(StateExecution execution) {
|
||||||
|
RegisterRequest response = execution.getVariable("response", RegisterRequest.class);
|
||||||
|
return NextAction.stopInState(DONE, "ERP updated successfully for: " + response.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package zw.qantra.tm.domain.workflows;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import io.nflow.engine.service.WorkflowInstanceInclude;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import io.nflow.engine.service.WorkflowInstanceService;
|
||||||
|
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
|
||||||
|
public class WorkflowUtils {
|
||||||
|
private final WorkflowInstanceService workflowInstanceService;
|
||||||
|
|
||||||
|
public CompletableFuture<Object> waitForState(long workflowId, String state, int timeout, int pollingInterval) {
|
||||||
|
CompletableFuture<Object> future = new CompletableFuture<>();
|
||||||
|
|
||||||
|
while(!future.isDone()){ // check if the future is done
|
||||||
|
WorkflowInstance instance = workflowInstanceService.getWorkflowInstance(
|
||||||
|
workflowId, EnumSet.of(WorkflowInstanceInclude.ACTIONS), 1L);
|
||||||
|
try {
|
||||||
|
if(instance.state.equalsIgnoreCase(state)){
|
||||||
|
future.complete(instance.getStateVariable("metadata"));
|
||||||
|
}
|
||||||
|
Thread.sleep(pollingInterval);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
timeout -= pollingInterval;
|
||||||
|
if(timeout <= 0){
|
||||||
|
future.complete(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -43,3 +43,7 @@ erpnext.tax.account=VAT - QPAY
|
|||||||
erpnext.tax.rate=15.0
|
erpnext.tax.rate=15.0
|
||||||
|
|
||||||
logging.level.root=WARN
|
logging.level.root=WARN
|
||||||
|
nflow.db.postgresql.driver=org.postgresql.Driver
|
||||||
|
nflow.db.postgresql.url=jdbc:postgresql://173.212.247.232:5532/qpay
|
||||||
|
nflow.db.postgresql.user=postgres
|
||||||
|
nflow.db.postgresql.password=zdDZMzq6F4B4L1IUl
|
||||||
|
|||||||
Reference in New Issue
Block a user