improvements on nflow workflows
This commit is contained in:
@@ -16,6 +16,7 @@ public class JacksonConfig {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.registerModule(new JodaModule());
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
package zw.qantra.tm.domain.controllers;
|
||||
|
||||
import io.nflow.engine.service.WorkflowInstanceInclude;
|
||||
import io.nflow.engine.service.WorkflowInstanceService;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.joda.time.DateTime;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import zw.qantra.tm.domain.dtos.AuthResponse;
|
||||
import zw.qantra.tm.domain.dtos.LoginRequest;
|
||||
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
||||
import zw.qantra.tm.domain.dtos.VerifyRequest;
|
||||
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.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@@ -21,10 +28,10 @@ import java.util.concurrent.ExecutionException;
|
||||
@RequestMapping("/auth")
|
||||
@RequiredArgsConstructor
|
||||
@CrossOrigin(origins = "*")
|
||||
public class AuthController {
|
||||
public class OnboardingController {
|
||||
|
||||
private final UserService userService;
|
||||
private final WorkflowInstanceService workflowService;
|
||||
private final WorkflowInstanceService workflowInstanceService;
|
||||
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||
private final WorkflowUtils workflowUtils;
|
||||
|
||||
@@ -34,15 +41,43 @@ public class AuthController {
|
||||
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||
.setType(RegistrationWorkflow.TYPE)
|
||||
.setExternalId(request.getUsername() + "-" + System.currentTimeMillis())
|
||||
.putStateVariable("metadata", request)
|
||||
.putStateVariable("body", request)
|
||||
.build();
|
||||
|
||||
long workflowId = workflowService.insertWorkflowInstance(instance);
|
||||
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
||||
|
||||
CompletableFuture<Object> future = workflowUtils
|
||||
.waitForState(workflowId, "sendOtp", 30000, 1000);
|
||||
Object result = future.get();
|
||||
return ResponseEntity.ok(Utils.toJson(result));
|
||||
Object response = future.get();
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ResponseEntity<Object> verify(@Valid @RequestBody VerifyRequest request)
|
||||
throws ExecutionException, InterruptedException {
|
||||
// Create an update to move the workflow to the next state
|
||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||
.setId(request.getWorkflowId())
|
||||
.setState("verifyOtp") // The state you want to transition to
|
||||
.setNextActivation(DateTime.now()) // Schedule immediate execution
|
||||
.setStateVariables(new HashMap<>(){{ put("body", Utils.toJson(request)); }})
|
||||
.setStateText("Resumed from manual state")
|
||||
.build();
|
||||
|
||||
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
|
||||
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
|
||||
.setExecutionEnd(DateTime.now())
|
||||
.build();
|
||||
|
||||
// Apply the update
|
||||
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||
|
||||
CompletableFuture<Object> future = workflowUtils
|
||||
.waitForState(request.getWorkflowId(), "verifyOtp", 30000, 1000);
|
||||
Object response = future.get();
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
18
src/main/java/zw/qantra/tm/domain/dtos/StateResponse.java
Normal file
18
src/main/java/zw/qantra/tm/domain/dtos/StateResponse.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class StateResponse {
|
||||
private String state;
|
||||
private Object body;
|
||||
private String message;
|
||||
private String workflowId;
|
||||
private String externalId;
|
||||
}
|
||||
12
src/main/java/zw/qantra/tm/domain/dtos/VerifyRequest.java
Normal file
12
src/main/java/zw/qantra/tm/domain/dtos/VerifyRequest.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VerifyRequest {
|
||||
private String username;
|
||||
private String otpCode;
|
||||
private String otpType;
|
||||
private Long workflowId;
|
||||
private String externalId;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package zw.qantra.tm.domain.repositories;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.Otp;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -10,5 +11,6 @@ import java.util.Optional;
|
||||
public interface OtpRepository extends JpaRepository<Otp, Long> {
|
||||
Optional<Otp> findByUsernameAndOtpType(String username, String otpType);
|
||||
Optional<Otp> findByUsernameAndOtpTypeAndOtp(String username, String otpType, String otp);
|
||||
Optional<Otp> findByUsernameAndOtpTypeAndOtpAndOtpStatus(String username, String otpType, String otp, Status status);
|
||||
void deleteByUsernameAndOtpType(String username, String otpType);
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
@@ -21,9 +20,6 @@ public class OtpService {
|
||||
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()
|
||||
@@ -37,7 +33,8 @@ public class OtpService {
|
||||
}
|
||||
|
||||
public boolean verifyOtp(String username, String otpCode, String otpType) {
|
||||
Optional<Otp> otpOpt = otpRepository.findByUsernameAndOtpTypeAndOtp(username, otpType, otpCode);
|
||||
Optional<Otp> otpOpt = otpRepository.findByUsernameAndOtpTypeAndOtpAndOtpStatus(
|
||||
username, otpType, otpCode, Status.ACTIVE);
|
||||
|
||||
if (otpOpt.isEmpty()) {
|
||||
return false;
|
||||
|
||||
@@ -3,14 +3,15 @@ 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.beans.factory.annotation.Value;
|
||||
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.dtos.VerifyRequest;
|
||||
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;
|
||||
@@ -19,24 +20,16 @@ import zw.qantra.tm.domain.dtos.RegisterRequest;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validator;
|
||||
import zw.qantra.tm.rest.RestService;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class RegistrationWorkflow extends WorkflowDefinition {
|
||||
Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class);
|
||||
|
||||
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
|
||||
@@ -44,16 +37,35 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
||||
@Autowired
|
||||
private OtpService otpService;
|
||||
@Autowired
|
||||
private EmailService emailService;
|
||||
private RestService restService;
|
||||
|
||||
@Value("${infobip.url}")
|
||||
private String infobipUrl;
|
||||
@Value("${infobip.apikey}")
|
||||
private String infobipApiKey;
|
||||
|
||||
public static final String TYPE = "registrationWorkflow";
|
||||
|
||||
public static final State BEGIN = new State("begin", WorkflowStateType.start);
|
||||
public static final State REGISTER = new State("register");
|
||||
public static final State SEND_OTP = new State("sendOtp", WorkflowStateType.manual);
|
||||
public static final State VERIFY_OTP = new State("verifyOtp");
|
||||
public static final State OTP_SUCCESS = new State("otpSuccess", WorkflowStateType.manual);
|
||||
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);
|
||||
|
||||
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(SEND_OTP, VERIFY_OTP);
|
||||
permit(VERIFY_OTP, OTP_SUCCESS, FAILED);
|
||||
permit(RESEND_OTP, OTP_SUCCESS, FAILED);
|
||||
permit(OTP_SUCCESS, SET_PIN, FAILED);
|
||||
permit(SET_PIN, UPDATE_ERP);
|
||||
permit(UPDATE_ERP, DONE);
|
||||
}
|
||||
@@ -65,7 +77,7 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
||||
// register validates most of the initial registration details
|
||||
public NextAction register(StateExecution execution) {
|
||||
|
||||
RegisterRequest payload = execution.getVariable("metadata", RegisterRequest.class);
|
||||
RegisterRequest payload = execution.getVariable("body", RegisterRequest.class);
|
||||
|
||||
if(payload == null)
|
||||
return NextAction.moveToState(FAILED, "No payload provided");
|
||||
@@ -82,65 +94,71 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
||||
}
|
||||
|
||||
// todo: add this to the appropriate action
|
||||
execution.setVariable("metadata", payload);
|
||||
execution.setVariable("body", payload);
|
||||
|
||||
return NextAction.moveToState(SEND_OTP, "Initial registration success for: " + payload.getUsername());
|
||||
}
|
||||
|
||||
public void sendOtp(StateExecution execution) {
|
||||
try {
|
||||
RegisterRequest registerRequest = execution.getVariable("metadata", RegisterRequest.class);
|
||||
RegisterRequest registerRequest = execution.getVariable("body", 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);
|
||||
}
|
||||
String string = getSMSString(registerRequest.getPhone(), otp.getOtp());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Authorization", "App " + infobipApiKey);
|
||||
headers.add("Content-Type", "application/json");
|
||||
headers.add("Accept", "application/json");
|
||||
// LinkedHashMap response = restService.postAs(infobipUrl, string, headers, LinkedHashMap.class);
|
||||
|
||||
log.info("otp generated successfully: {}", otp.getOtp());
|
||||
} catch (Exception e) {
|
||||
log.error("Error sending OTP: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public NextAction verifyOtp(StateExecution execution) {
|
||||
return NextAction.stopInState(SET_PIN, "OTP verified successfully for: " );
|
||||
VerifyRequest payload = execution.getVariable("body", VerifyRequest.class);
|
||||
boolean otpValid = otpService.verifyOtp(payload.getUsername(), payload.getOtpCode(), payload.getOtpType());
|
||||
|
||||
if(!otpValid) {
|
||||
return NextAction.moveToState(FAILED, "Invalid OTP");
|
||||
}
|
||||
|
||||
return NextAction.moveToState(OTP_SUCCESS, "OTP verified successfully");
|
||||
}
|
||||
|
||||
public void otpSuccess(StateExecution execution) {
|
||||
log.info("OTP verified successfully");
|
||||
}
|
||||
|
||||
public NextAction setPin(StateExecution execution) {
|
||||
RegisterRequest response = execution.getVariable("response", RegisterRequest.class);
|
||||
RegisterRequest response = execution.getVariable("body", 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);
|
||||
RegisterRequest response = execution.getVariable("body", RegisterRequest.class);
|
||||
return NextAction.stopInState(DONE, "ERP updated successfully for: " + response.getUsername());
|
||||
}
|
||||
|
||||
|
||||
String getSMSString(String to, String otp) {
|
||||
return String.format("{\n" +
|
||||
" \"messages\": [\n" +
|
||||
" {\n" +
|
||||
" \"destinations\": [\n" +
|
||||
" {\n" +
|
||||
" \"to\": \"%s\"\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"from\": \"447491163443\",\n" +
|
||||
" \"text\": \"Your verification code is: %s\"\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}", to, otp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package zw.qantra.tm.domain.workflows;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import io.nflow.engine.service.WorkflowInstanceInclude;
|
||||
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;
|
||||
import zw.qantra.tm.domain.dtos.StateResponse;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import static io.nflow.engine.service.WorkflowInstanceInclude.*;
|
||||
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
||||
public class WorkflowUtils {
|
||||
private final WorkflowInstanceService workflowInstanceService;
|
||||
|
||||
@@ -22,22 +26,35 @@ public class WorkflowUtils {
|
||||
|
||||
while(!future.isDone()){ // check if the future is done
|
||||
WorkflowInstance instance = workflowInstanceService.getWorkflowInstance(
|
||||
workflowId, EnumSet.of(WorkflowInstanceInclude.ACTIONS), 1L);
|
||||
workflowId,
|
||||
EnumSet.of(ACTIONS, ACTION_STATE_VARIABLES, CURRENT_STATE_VARIABLES),
|
||||
1L);
|
||||
try {
|
||||
if(instance.state.equalsIgnoreCase(state)){
|
||||
future.complete(instance.getStateVariable("metadata"));
|
||||
}
|
||||
Thread.sleep(pollingInterval);
|
||||
if(instance.state.equalsIgnoreCase(state) &&
|
||||
instance.status.equals(WorkflowInstance.WorkflowInstanceStatus.manual)){
|
||||
future.complete(getStateResponse(instance, "success"));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
timeout -= pollingInterval;
|
||||
if(timeout <= 0){
|
||||
future.complete(instance);
|
||||
future.complete(getStateResponse(instance, "Request timed out"));
|
||||
}
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public StateResponse getStateResponse(WorkflowInstance instance, String message){
|
||||
return StateResponse.builder()
|
||||
.state(instance.state)
|
||||
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
|
||||
.message(instance.getStateVariable("message"))
|
||||
.workflowId(instance.id.toString())
|
||||
.externalId(instance.externalId)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,13 +49,6 @@ 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
|
||||
infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced
|
||||
infobip.apikey=0ede918b7c1110465a0518021521638c-ebc4fdb7-547d-413b-bbca-781949ac6597
|
||||
|
||||
|
||||
Reference in New Issue
Block a user