one time password

This commit is contained in:
jojo aquino 2025-04-05 14:48:04 +01:00
parent 1d29b6ef4d
commit 77c665f64e
14 changed files with 300 additions and 114 deletions

View File

@ -0,0 +1,9 @@
namespace EnotaryoPH.Data.Constants
{
public enum DocumentSigningStatus
{
New = 0,
ReadyForSigning = 10,
Signed = 20
}
}

View File

@ -22,6 +22,9 @@
TransactionApproved = 41,
TransactionRejected = 42,
OTPSent = 45,
OTPVerified = 46,
VideoConferenceStarted = 50,
VideoRecordingStarted = 51,
VideoRecordingStopped = 52,

View File

@ -8,6 +8,15 @@ namespace EnotaryoPH.Data.Entities
[Column("CreatedOn")]
public DateTime? CreatedOn { get; set; }
[Column("Device")]
public string? Device { get; set; }
[Column("IPAddress")]
public string? IPAddress { get; set; }
[Column("Latitude")]
public decimal? Latitude { get; set; }
[Column("LawyerVideoConferenceParticipant_UID")]
public Guid? LawyerVideoConferenceParticipant_UID { get; set; }
@ -20,12 +29,21 @@ namespace EnotaryoPH.Data.Entities
[Column("LawyerVideoConferenceScheduleID")]
public int LawyerVideoConferenceScheduleID { get; set; }
[Column("Longitude")]
public decimal? Longitude { get; set; }
[Column("MeetingRoomTokenID")]
public string? MeetingRoomTokenID { get; set; }
[Column("MeetingRoomUserID")]
public string? MeetingRoomUserID { get; set; }
[Column("OTPEntered")]
public string? OTPEntered { get; set; }
[Column("OTPHash")]
public string? OTPHash { get; set; }
[ForeignKey("ParticipantID")]
public User Participant { get; set; }

View File

@ -0,0 +1,8 @@
namespace EnotaryoPH.Web.Common.Jobs.Models
{
public class OTPEmailModel
{
public Dictionary<Guid, string> ParticipantOTP { get; set; }
public Guid Transaction_UID { get; set; }
}
}

View File

@ -0,0 +1,75 @@
using Coravel.Invocable;
using Coravel.Mailer.Mail.Interfaces;
using EnotaryoPH.Data;
using EnotaryoPH.Web.Common.Jobs.Models;
using EnotaryoPH.Web.Common.Models;
using EnotaryoPH.Web.Mailables;
namespace EnotaryoPH.Web.Common.Jobs
{
public class OneTimePasswordInvocable : IInvocable, IInvocableWithPayload<OTPEmailModel>
{
private readonly IMailer _mailer;
private readonly NotaryoDBContext _notaryoDBContext;
private readonly Settings _settings;
private readonly IPasswordService _passwordService;
private readonly IEventService _eventService;
public OneTimePasswordInvocable(IMailer mailer, NotaryoDBContext notaryoDBContext, Settings settings, IPasswordService passwordService, IEventService eventService)
{
_mailer = mailer;
_notaryoDBContext = notaryoDBContext;
_settings = settings;
_passwordService = passwordService;
_eventService = eventService;
}
public OTPEmailModel Payload { get; set; }
public async Task Invoke()
{
var schedule = _notaryoDBContext.LawyerVideoConferenceSchedules
.AsNoTracking()
.Include(sched => sched.Transaction)
.ThenInclude(transaction => transaction.TransactionDocument)
.Include(sched => sched.Transaction)
.ThenInclude(transaction => transaction.Lawyer)
.ThenInclude(lawyer => lawyer.User)
.Include(sched => sched.LawyerVideoConferenceParticipants)
.ThenInclude(participant => participant.Participant)
.FirstOrDefault(sched => sched.Transaction.Transaction_UID == Payload.Transaction_UID);
if (schedule == null)
{
return;
}
var participantDic = schedule.LawyerVideoConferenceParticipants.ToDictionary(p => p.LawyerVideoConferenceParticipant_UID.GetValueOrDefault(), p => p);
foreach (var otp in Payload.ParticipantOTP)
{
if (!participantDic.ContainsKey(otp.Key))
{
continue;
}
var participant = participantDic[otp.Key];
var emailViewModel = new OneTimePasswordViewModel
{
DocumentType = schedule.Transaction.TransactionDocument.DocumentType,
Email = participant.Participant.Email,
OTP = otp.Value,
ParticipantName = participant.Participant.Fullname,
LawyerName = schedule.Transaction.Lawyer.User.Fullname,
MeetingRoomURL = $"{_settings.BaseUrl}/Participant/VideoCall/Room/{schedule.Transaction.Transaction_UID}",
};
await _mailer.SendAsync(new OneTimePasswordMailable(emailViewModel));
await _eventService.LogAsync(NotaryoEvent.OTPSent, Payload.Transaction_UID, new
{
emailViewModel.Email,
emailViewModel.ParticipantName,
emailViewModel.MeetingRoomURL
});
}
}
}
}

View File

@ -1,10 +1,14 @@
using Azure;
using System.Security.Cryptography;
using Azure;
using Azure.Communication;
using Azure.Communication.CallAutomation;
using Azure.Communication.Identity;
using Azure.Communication.Rooms;
using Coravel.Queuing.Interfaces;
using EnotaryoPH.Data;
using EnotaryoPH.Data.Entities;
using EnotaryoPH.Web.Common.Jobs;
using EnotaryoPH.Web.Common.Jobs.Models;
namespace EnotaryoPH.Web.Common.Services
{
@ -17,9 +21,11 @@ namespace EnotaryoPH.Web.Common.Services
private readonly IConfiguration _configuration;
private readonly NotaryoDBContext _dbContext;
private readonly IEventService _eventService;
private readonly IPasswordService _passwordService;
private readonly IQueue _queue;
private readonly RoomsClient _roomsClient;
public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService)
public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService, IPasswordService passwordService, IQueue queue)
{
_dbContext = dbContext;
_communicationIdentityClient = communicationIdentityClient;
@ -27,6 +33,8 @@ namespace EnotaryoPH.Web.Common.Services
_callAutomationClient = callAutomationClient;
_configuration = configuration;
_eventService = eventService;
_passwordService = passwordService;
_queue = queue;
}
public async Task ApproveTransactionAsync(Guid transaction_UID)
@ -128,6 +136,7 @@ namespace EnotaryoPH.Web.Common.Services
if (schedule.Status == nameof(VideoConferenceStatus.New))
{
await CreateMeetingRoomAsync(schedule);
await CreateAndEmailOTPAsync(schedule);
_dbContext.UpdateOrCreate(schedule);
_dbContext.SaveChanges();
}
@ -154,6 +163,23 @@ namespace EnotaryoPH.Web.Common.Services
}
}
private async Task CreateAndEmailOTPAsync(LawyerVideoConferenceSchedule schedule)
{
var participantOTP = new Dictionary<Guid, string>();
const string validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
foreach (var participant in schedule.LawyerVideoConferenceParticipants)
{
var otp = RandomNumberGenerator.GetString(validChars, 5);
participant.OTPHash = _passwordService.HashPassword(otp);
participantOTP.Add(participant.LawyerVideoConferenceParticipant_UID.GetValueOrDefault(), otp);
}
_queue.QueueInvocableWithPayload<OneTimePasswordInvocable, OTPEmailModel>(new OTPEmailModel
{
ParticipantOTP = participantOTP,
Transaction_UID = schedule.Transaction.Transaction_UID.GetValueOrDefault()
});
}
private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule)
{
var roomParticipants = new List<RoomParticipant>();
@ -181,7 +207,9 @@ namespace EnotaryoPH.Web.Common.Services
.Include(t => t.TransactionSignatories)
.Single(t => t.Transaction_UID == transaction_UID);
var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
var schedule = _dbContext.LawyerVideoConferenceSchedules
.Include(sched => sched.Transaction)
.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
if (schedule != null)
{
return schedule;
@ -200,7 +228,7 @@ namespace EnotaryoPH.Web.Common.Services
CreatedOn = DateTime.UtcNow,
Status = nameof(VideoConferenceStatus.New),
LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
ParticipantID = signatory.UserID,
ParticipantID = signatory.UserID
});
participants.Add(new LawyerVideoConferenceParticipant
{
@ -212,6 +240,7 @@ namespace EnotaryoPH.Web.Common.Services
schedule.MeetingDate = DateTime.UtcNow;
schedule.LawyerVideoConferenceParticipants = participants.ToList();
schedule.Transaction = transactionEntity;
return schedule;
}

View File

@ -0,0 +1,16 @@
using Coravel.Mailer.Mail;
namespace EnotaryoPH.Web.Mailables
{
public class OneTimePasswordMailable : Mailable<OneTimePasswordViewModel>
{
private readonly OneTimePasswordViewModel _model;
public OneTimePasswordMailable(OneTimePasswordViewModel model) => _model = model;
public override void Build() => this
.To(_model.Email)
.From("noreply@enotaryoph.com")
.View("~/Views/Mail/OneTimePassword.cshtml", _model);
}
}

View File

@ -0,0 +1,12 @@
namespace EnotaryoPH.Web.Mailables
{
public class OneTimePasswordViewModel
{
public string DocumentType { get; set; }
public string Email { get; set; }
public string LawyerName { get; set; }
public string MeetingRoomURL { get; set; }
public string OTP { get; set; }
public string ParticipantName { get; set; }
}
}

View File

@ -14,7 +14,6 @@
<div class="sidemenu">
<div class="align-items-center sidemenu__menuitem"><a class="d-flex flex-grow-1 justify-content-center align-items-center justify-content-md-start p-1 text-decoration-none" href="#available-transactions"><i class="far fa-check-circle fs-4 text-success sidemenu__menuitem__icon" style="padding: 5px;"></i><span class="d-none d-md-inline-block ms-1 sidemenu__menuitem__text">Available</span></a></div>
<div class="align-items-center sidemenu__menuitem"><a class="d-flex flex-grow-1 justify-content-center align-items-center justify-content-md-start p-1 text-decoration-none" href="#incomplete-docs"><i class="far fa-clock fs-4 text-warning sidemenu__menuitem__icon" style="padding: 5px;"></i><span class="d-none d-md-inline-block ms-1 sidemenu__menuitem__text">My Jobs</span></a></div>
<div class="align-items-center sidemenu__menuitem"><a class="d-flex flex-grow-1 justify-content-center align-items-center justify-content-md-start p-1 text-decoration-none" href="#identification-docs"><i class="far fa-address-card fs-4 text-dark sidemenu__menuitem__icon" style="padding: 5px;"></i><span class="d-none d-md-inline-block ms-1 sidemenu__menuitem__text">Identification Docs</span></a></div>
</div>
</div>
<div class="col g-0 mx-2">
@ -27,7 +26,6 @@
<tr>
<th>Type</th>
<th>Date</th>
<th>Status</th>
<th>Link</th>
</tr>
</thead>
@ -37,8 +35,7 @@
<tr>
<td>@transaction.Type</td>
<td>@transaction.Date.ToString("g")</td>
<td>@transaction.Status</td>
<td><a href="@transaction.Link">Link</a></td>
<td><a href="@transaction.Link">View</a></td>
</tr>
}
</tbody>
@ -67,7 +64,7 @@
<td>@transaction.Type</td>
<td>@transaction.Date.ToShortDateString()</td>
<td>@transaction.Status</td>
<td><a href="@transaction.Link">Link</a></td>
<td><a href="@transaction.Link">View</a></td>
</tr>
}
</tbody>
@ -78,4 +75,10 @@
</div>
</div>
</div>
</section>
</section>
@section Scripts {
<script src="~/lib/fontawesome-free-6.7.1-web/js/all.min.js"></script>
}

View File

@ -51,6 +51,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
return Redirect("/");
}
DocumentType = _Transaction.TransactionDocument.DocumentType;
var schedule_UID = await _videoConferenceService.StartAsync(Transaction_UID);
CommunicationUserToken = currentUser.Role == nameof(UserType.Notary)
@ -70,7 +72,58 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
return Page();
}
private LawyerVideoConferenceParticipant GetParticipant(User currentUser) => _Transaction.Schedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID);
public IActionResult OnGetDocument()
{
var document = _dbContext.TransactionDocuments
.Include(doc => doc.Transaction)
.FirstOrDefault(doc => doc.Transaction.Transaction_UID == Transaction_UID);
return new FileContentResult(document.File, "application/pdf");
}
public IActionResult OnGetIdentificationDocument(string meetingRoomUserID)
{
var participant = _dbContext.LawyerVideoConferenceParticipants
.AsNoTracking()
.FirstOrDefault(participant => participant.MeetingRoomUserID == meetingRoomUserID);
if (participant == null)
{
return NotFound();
}
var identificationDocumentID = _dbContext.TransactionSelfies.AsNoTracking()
.Where(selfie => selfie.UserID == participant.ParticipantID && selfie.Transaction.Transaction_UID == Transaction_UID)
.Select(selfie => selfie.IdentificationDocumentID).FirstOrDefault();
if (identificationDocumentID == 0)
{
return NotFound();
}
var identificationDocument = _dbContext.IdentificationDocuments.FirstOrDefault(id => id.IdentificationDocumentID == identificationDocumentID);
if (identificationDocument == null)
{
return NotFound();
}
return new FileContentResult(identificationDocument.File, "image/jpeg");
}
public IActionResult OnGetSelfieImage(string meetingRoomUserID)
{
var participant = _dbContext.LawyerVideoConferenceParticipants
.AsNoTracking()
.FirstOrDefault(participant => participant.MeetingRoomUserID == meetingRoomUserID);
if (participant == null)
{
return NotFound();
}
var transactionSelfie = _dbContext.TransactionSelfies.FirstOrDefault(selfie => selfie.UserID == participant.ParticipantID && selfie.Transaction.Transaction_UID == Transaction_UID);
if (transactionSelfie == null)
{
return NotFound();
}
return new FileContentResult(transactionSelfie.File, "image/jpeg");
}
public async Task<JsonResult> OnPostApproveAsync()
{
@ -85,6 +138,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
return new JsonResult(true);
}
private LawyerVideoConferenceParticipant GetParticipant(User currentUser) => _Transaction.Schedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID);
private void LoadTransaction() => _Transaction = _dbContext.Transactions
.Include(t => t.TransactionSignatories)
.Include(t => t.Lawyer)
@ -92,6 +147,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
.Include(t => t.Principal)
.Include(t => t.Schedule)
.ThenInclude(sch => sch.LawyerVideoConferenceParticipants)
.Include(t => t.TransactionDocument)
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
public string CommunicationRoomId { get; private set; }
@ -102,7 +158,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
public string DisplayName { get; private set; }
public string ParticipantType { get; set; }
public string DocumentType { get; set; }
public List<RoomParticipantViewModel> Participants
{
@ -134,52 +190,9 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
}
}
public string ParticipantType { get; set; }
[BindProperty(SupportsGet = true)] public string ServerCallID { get; set; }
[BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; }
public IActionResult OnGetSelfieImage(string meetingRoomUserID)
{
var participant = _dbContext.LawyerVideoConferenceParticipants
.AsNoTracking()
.FirstOrDefault(participant => participant.MeetingRoomUserID == meetingRoomUserID);
if (participant == null)
{
return NotFound();
}
var transactionSelfie = _dbContext.TransactionSelfies.FirstOrDefault(selfie => selfie.UserID == participant.ParticipantID && selfie.Transaction.Transaction_UID == Transaction_UID);
if (transactionSelfie == null)
{
return NotFound();
}
return new FileContentResult(transactionSelfie.File, "image/jpeg");
}
public IActionResult OnGetIdentificationDocument(string meetingRoomUserID)
{
var participant = _dbContext.LawyerVideoConferenceParticipants
.AsNoTracking()
.FirstOrDefault(participant => participant.MeetingRoomUserID == meetingRoomUserID);
if (participant == null)
{
return NotFound();
}
var identificationDocumentID = _dbContext.TransactionSelfies.AsNoTracking()
.Where(selfie => selfie.UserID == participant.ParticipantID && selfie.Transaction.Transaction_UID == Transaction_UID)
.Select(selfie => selfie.IdentificationDocumentID).FirstOrDefault();
if (identificationDocumentID == 0)
{
return NotFound();
}
var identificationDocument = _dbContext.IdentificationDocuments.FirstOrDefault(id => id.IdentificationDocumentID == identificationDocumentID);
if (identificationDocument == null)
{
return NotFound();
}
return new FileContentResult(identificationDocument.File, "image/jpeg");
}
}
}

View File

@ -6,21 +6,22 @@
control_communicationUserId = document.getElementById("CommunicationUserId"),
control_communicationUserToken = document.getElementById("CommunicationUserToken"),
control_displayName = document.getElementById("DisplayName"),
control_participants = document.getElementById("Participants"),
control_reject = document.getElementById("Reject"),
control_templateSidePane = document.getElementById("TemplateSidePane"),
control_templateParticipantItem = document.getElementById("TemplateParticipantItem"),
control_draggableModal = document.getElementById("DraggableModal"),
control_identificationImage = document.getElementById("IdentificationImage"),
control_otpForm = document.getElementById("OtpForm"),
control_participantListGroup = document.getElementById("ParticipantListGroup"),
control_participants = document.getElementById("Participants"),
control_participantType = document.getElementById("ParticipantType"),
control_pdfViewer = document.getElementById("PdfViewer"),
control_reject = document.getElementById("Reject"),
control_rightSidebarModal = document.getElementById("RightSidebarModal"),
/*control_videoGrid = document.getElementById("VideoGrid"),*/
control_selfieImage = document.getElementById("SelfieImage"),
control_serverCallIID = document.getElementById("ServerCallID"),
control_signatoryName = document.getElementById("SignatoryName"),
control_templateParticipantItem = document.getElementById("TemplateParticipantItem"),
control_templateSidePane = document.getElementById("TemplateSidePane"),
control_videoGridContainer = document.getElementById("videoGrid-container"),
control_viewDocument = document.getElementById("ViewDocument"),
control_serverCallIID = document.getElementById("ServerCallID"),
control_participantType = document.getElementById("ParticipantType"),
control_participantListGroup = document.getElementById("ParticipantListGroup"),
control_selfieImage = document.getElementById("SelfieImage"),
control_identificationImage = document.getElementById("IdentificationImage"),
control_signatoryName = document.getElementById("SignatoryName"),
x = 1;
let participants = JSON.parse(control_participants.value);
@ -40,7 +41,21 @@
tooltipContent: 'View the document.'
},
onItemClick: function () {
alert('Document Modal goes here.');
const modal = bootstrap.Modal.getOrCreateInstance(control_pdfViewer);
modal.show();
}
};
},
function (args) {
return {
placement: 'primary',
strings: {
label: 'OTP',
tooltipContent: 'Enter your OTP'
},
onItemClick: function () {
const modal = bootstrap.Modal.getOrCreateInstance(control_otpForm);
modal.show();
}
};
},
@ -180,56 +195,16 @@
}
function approveTransaction(e) {
jfa.communication.videocall.stopCall(true);
let url = jfa.utilities.routing.getCurrentURLWithHandler("Approve");
jfa.utilities.request.post(url, {})
.then(resp => {
if (resp.ok === true) {
debugger;
jfa.communication.videocall.stopCall(true);
}
})
.catch(err => console.error(err));
}
//function _updateGrid() {
// control_videoGrid.innerHTML = '';
// participants.forEach((participant, index) => {
// const col = document.createElement('div');
// col.className = 'participant-col';
// const tmpl = control_templateVideo.cloneNode(true).content;
// const vidcontainer = tmpl.querySelector(".video-container")
// if (vidcontainer) {
// vidcontainer.id = participant.Id;
// vidcontainer.classList.add(participant.Id);
// vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container');
// }
// let participantName = tmpl.querySelector(".participant-name")
// if (participantName) {
// participantName.textContent = participant.DisplayName;
// }
// col.appendChild(tmpl);
// control_videoGrid.appendChild(col);
// });
// // Dynamically adjust grid columns based on participant count
// const count = participants.length;
// const cols = count <= 2 ? 'col-12 col-sm-12 offset-md-1 col-md-10 offset-lg-0 col-lg-6' :
// count <= 4 ? 'col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6' :
// count <= 8 ? 'col-6 col-md-6 col-lg-4 col-xl-4' :
// count <= 9 ? 'col-4' :
// 'col-6 col-sm-4 col-lg-3';
// document.querySelectorAll('.participant-col').forEach(el => {
// el.className = `participant-col ${cols}`;
// });
// const fluid = count <= 2 ? 'container-fluid' :
// count <= 8 ? 'container-xxl' :
// 'container-xl'
// control_videoGridContainer.className = `${fluid} py-3`;
//}
function _createParticipantListItems() {
control_participantListGroup.innerHTML = '';
participants.forEach(participant => {
@ -250,7 +225,6 @@
control_participantListGroup.addEventListener('click', function (event) {
let target = event.target?.closest('.list-group-item');
if (target) {
const participant = participants.find(p => p.UID == target.dataset.participantUid);
if (!participant) {
return;
@ -260,7 +234,6 @@
let selfieUrl = jfa.utilities.routing.getCurrentURLWithHandler("SelfieImage");
selfieUrl.searchParams.append("meetingRoomUserID", participant.RoomUserID);
let identificationUrl = jfa.utilities.routing.getCurrentURLWithHandler("IdentificationDocument");
identificationUrl.searchParams.append("meetingRoomUserID", participant.RoomUserID);
control_selfieImage.src = selfieUrl;
@ -304,9 +277,7 @@
async function _init() {
_bindEvents();
//_updateGrid();
await _initVideoCall();
debugger;
_createParticipantListItems();
}

View File

@ -32,7 +32,6 @@ namespace EnotaryoPH.Web.Pages.Principal.TransactionStatus
Title = transaction.TransactionDocument.DocumentType;
StartedOn = transaction.CreatedOn.GetValueOrDefault();
StatusDescription = "Not all signatories have signed up to enotaryo and completed the onboarding process. This transaction cannot proceed until this has been resolved.";
Signatories = transaction.TransactionSignatories.ConvertAll(s => new SignatoryViewModel
{
@ -41,6 +40,10 @@ namespace EnotaryoPH.Web.Pages.Principal.TransactionStatus
Status = s.Status
});
StatusDescription = Signatories.Count > 1
? "Not all signatories have signed up to enotaryo and completed the onboarding process. This transaction cannot proceed until this has been resolved."
: "Please wait while our Notary Public team reviews your document.";
return Page();
}

View File

@ -64,6 +64,7 @@ namespace EnotaryoPH.Web
builder.Services.AddScheduler();
builder.Services.AddMailer(config);
builder.Services.AddTransient<SignatoryInvitationInvocable>();
builder.Services.AddTransient<OneTimePasswordInvocable>();
builder.Services.AddTransient<CheckRecordingAvailabilityInvocable>();
builder.Services.AddTransient<IVideoConferenceService, VideoConferenceService>();
builder.Services.AddTransient<IEventService, EventService>();

View File

@ -0,0 +1,25 @@
@using EnotaryoPH.Web.Mailables
@model OneTimePasswordViewModel
@{
ViewBag.Heading = "One Time Password";
ViewBag.Preview = "Your OTP";
}
<p>
Hello @Model.ParticipantName,
This is your one time password:
<blockquote>
@Model.OTP
</blockquote>
Please do not share this code with anyone other than the Notary Public, Atty. @Model.LawyerName during the Video Conference.
You can join the e-Notaryo video conference by clicking this button: @await Component.InvokeAsync("EmailLinkButton", new { text = "Video Conference", url = Model.MeetingRoomURL })
</p>
@section links
{
<a href="@Model.MeetingRoomURL">Meeting Room Page</a>
}