Skip to content

Commit

Permalink
Send email with OTPCode (#76)
Browse files Browse the repository at this point in the history
* Send email with OTPCode

* Updated settings with OTP template

* Fixed tests

* Update error message
  • Loading branch information
nagarwal4 authored Mar 27, 2024
1 parent 1afcd71 commit e2277fa
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 6 deletions.
2 changes: 1 addition & 1 deletion backend/core/src/Core.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@

app.ConfigureCustomAuthenticationMiddleware();
app.ConfigureCustomExceptionMiddleware();
app.ConfigureSignatureVerificationMiddleware();

app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();

app.ConfigureSignatureVerificationMiddleware();
app.UseMiddleware<PublicKeyLinkedMiddleware>();

app.MapControllers();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private static bool SkipEndpoint(HttpContext context)
var endpoint = context.GetEndpoint();
var endpointName = endpoint?.Metadata.GetMetadata<EndpointNameMetadata>()?.EndpointName;

var excludeList = new[] { "DeviceAuthentication" };
var excludeList = new[] { "DeviceAuthentication", "SendOTPCodeEmail" };

return context.Request.Path.StartsWithSegments("/health")
|| excludeList.Contains(endpointName);
Expand Down
5 changes: 3 additions & 2 deletions backend/core/src/Core.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,13 @@
"StorageConnectionString": "",
"ContainerName": ""
},
"SendGridMailServiceOptions": {
"SendGridMailServiceOptions": {
"ApiKey": "",
"Sender": "",
"Templates": {
"WithdrawalTemplateID": "",
"FundingtemplateID": ""
"FundingtemplateID": "",
"OTPCodeTemplateID": ""
}
},
"ConnectionStrings": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using Core.Domain;
using Core.Domain.Abstractions;
using Core.Domain.Exceptions;
using Core.Domain.Repositories;
using MediatR;

namespace Core.Application.Commands.CustomerCommands
{
public class OTPCodeByEmailCommand : IRequest
{
public string CustomerCode { get; set; }

public string IP { get; set; }

public OTPCodeByEmailCommand(string customerCode, string ip)
{
CustomerCode = customerCode;
IP = ip;
}
}

public class OTPCodeByEmailCommandHandler : IRequestHandler<OTPCodeByEmailCommand>
{
private readonly ICustomerRepository _customerRepository;
private readonly ICustomerDeviceRepository _customerDeviceRepository;
private readonly ICustomerOTPGenerator _otpGenerator;
private readonly ISendGridMailService _sendGridMailService;
private readonly IUnitOfWork _unitOfWork;

public OTPCodeByEmailCommandHandler(
ICustomerRepository customerRepository,
ICustomerDeviceRepository customerDeviceRepository,
ICustomerOTPGenerator otpGenerator,
ISendGridMailService sendGridMailService,
IUnitOfWork unitOfWork)
{
_customerRepository = customerRepository;
_customerDeviceRepository = customerDeviceRepository;
_otpGenerator = otpGenerator;
_sendGridMailService = sendGridMailService;
_unitOfWork = unitOfWork;
}

public async Task Handle(OTPCodeByEmailCommand request, CancellationToken cancellationToken)
{
var customer = await _customerRepository.GetAsync(request.CustomerCode, cancellationToken);

if (customer == null)
{
throw new CustomErrorsException(DomainErrorCode.CustomerNotFoundError.ToString(), request.CustomerCode, "Customer not found.");
}

if (customer.Status != CustomerStatus.ACTIVE.ToString())
{
throw new CustomErrorsException(DomainErrorCode.CustomerNotActiveError.ToString(), request.CustomerCode, "Customer is not ACTIVE.");
}

var customerDevice = await _customerDeviceRepository.GetAsync(request.CustomerCode, cancellationToken);

// rare case of exception
if (customerDevice == null || string.IsNullOrEmpty(customerDevice.OTPKey))
{
throw new CustomErrorsException(DomainErrorCode.CustomerNotFoundError.ToString(), request.CustomerCode, "Customer device key not found.");
}

// Generate OTPCode
var otpCode = _otpGenerator.GenerateOTPCode(customerDevice.OTPKey);

// Send OTPCode to customer email
await _sendGridMailService.SendOTPCodeMailAsync(customer, otpCode);

await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ namespace Core.Domain.Abstractions
public interface ISendGridMailService
{
public Task SendMailAsync(Mail mail, Customer customer, Transaction transaction);

public Task SendOTPCodeMailAsync(Customer customer, string otpCode);
}
}
4 changes: 3 additions & 1 deletion backend/core/src/Core.Domain/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public enum DomainErrorCode
InvalidStatusError,
InvalidPropertyError,
ExistingKeyError,
SecurityCheckError
SecurityCheckError,
CustomerNotFoundError,
CustomerNotActiveError,
}

public enum PaymentRequestStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ public class MailTemplate
[JsonProperty("finishedDate")]
public string? FinishedDate { get; set; }
}

public class OTPCodeMailTemplate
{
[JsonProperty("customerFullName")]
public string? CustomerFullName { get; set; }

[JsonProperty("otpCode")]
public string? OTPCode { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,33 @@ public async Task SendMailAsync(Mail mail, Customer customer, Transaction transa
throw new CustomErrorsException("MailService", "mail", "An error occured while sending mail.");
}
}

public async Task SendOTPCodeMailAsync(Customer customer, string otpCode)
{
var from = new EmailAddress(_mailOptions.Sender);
var to = new EmailAddress(customer.Email);
var msg = new SendGridMessage();

msg.SetFrom(new EmailAddress(from.Email, from.Name));
msg.AddTo(new EmailAddress(to.Email, to.Name));

msg.SetTemplateId(_mailOptions.Templates.OTPCodeTemplateID);

// Fill in the dynamic template fields
var templateData = new OTPCodeMailTemplate()
{
CustomerFullName = customer?.GetName(),
OTPCode = otpCode
};

msg.SetTemplateData(templateData);

var response = await _sendGridClient.SendEmailAsync(msg);

if (response.StatusCode != HttpStatusCode.Accepted)
{
throw new CustomErrorsException("MailService", "mail", "An error occured while sending mail.");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ public class Templates

[Required]
public required string FundingtemplateID { get; set; }

[Required]
public required string OTPCodeTemplateID { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public string GenerateOTPCode(string otpKey)
var keyBytes = Base32Encoding.ToBytes(otpKey);

// Create a TOTP generator with the key and time step (default is 30 seconds)
var totp = new Totp(keyBytes);
var totp = new Totp(keyBytes, step: 120);

// Generate the OTP code for the current time
var otpCode = totp.ComputeTotp();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public ProcessEmailsJob(

public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Process emails job");
var mails = _mailsRepository.GetMailsAsync(MailStatus.ReadyToSend.ToString(), context.CancellationToken).Result;

if (mails != null && mails.Any())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using Asp.Versioning;
using Core.Application.Commands;
using Core.Application.Commands.CustomerCommands;
using Core.Application.Queries.CustomerQueries;
using Core.Presentation.Models;
using Core.Presentation.Models.Requests.CustomerRequests;
Expand Down Expand Up @@ -100,5 +102,18 @@ public async Task<IActionResult> DeviceAuthenticationAsync([FromBody] CreateDevi
var response = ConstructCustomResponse(result, DeviceAuthenticationResponse.FromOTPKey);
return Ok(response);
}

[HttpPost("otp/email", Name = "SendOTPCodeEmail")]
[ProducesResponseType(typeof(CustomResponse<EmptyCustomResponse>), 201)]
[ProducesResponseType(typeof(CustomErrorsResponse), 400)]
[ProducesResponseType(typeof(CustomErrorsResponse), 404)]
[ProducesResponseType(typeof(CustomErrorsResponse), 500)]
[RequiredScope("Customer.Create")]
public async Task<IActionResult> SendOTPCodeEmailAsync()
{
var command = new OTPCodeByEmailCommand(GetUserId(), GetIP());
await _sender.Send(command);
return CreatedAtRoute("SendOTPCodeEmail", null, new EmptyCustomResponse());
}
}
}

0 comments on commit e2277fa

Please sign in to comment.