diff --git a/Blocktrust.CredentialWorkflow.Core.Tests/Workflow/ExecuteWorkflow/ExecuteWorkflowHandlerTests.cs b/Blocktrust.CredentialWorkflow.Core.Tests/Workflow/ExecuteWorkflow/ExecuteWorkflowHandlerTests.cs new file mode 100644 index 0000000..5dc38e1 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core.Tests/Workflow/ExecuteWorkflow/ExecuteWorkflowHandlerTests.cs @@ -0,0 +1,151 @@ +using FluentAssertions; +using Xunit; + +namespace Blocktrust.CredentialWorkflow.Core.Tests.Workflow.ExecuteWorkflow +{ + public class ExecuteWorkflowHandlerTests + { + private readonly ExecuteWorkflowHandler _handler; + + public ExecuteWorkflowHandlerTests() + { + // Since we're only testing ProcessEmailTemplate which doesn't use _mediator, + // we can pass null as it won't be used in our tests + _handler = new ExecuteWorkflowHandler(null!); + } + + [Fact] + public void ProcessEmailTemplate_WithNullTemplate_ShouldReturnEmptyString() + { + // Arrange + string? template = null; + var parameters = new Dictionary + { + { "name", "John" } + }; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public void ProcessEmailTemplate_WithEmptyTemplate_ShouldReturnEmptyString() + { + // Arrange + var template = string.Empty; + var parameters = new Dictionary + { + { "name", "John" } + }; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public void ProcessEmailTemplate_WithNullParameters_ShouldReturnOriginalTemplate() + { + // Arrange + var template = "Hello [name], welcome!"; + Dictionary? parameters = null; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().Be(template); + } + + [Fact] + public void ProcessEmailTemplate_WithEmptyParameters_ShouldReturnOriginalTemplate() + { + // Arrange + var template = "Hello [name], welcome!"; + var parameters = new Dictionary(); + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().Be(template); + } + + [Fact] + public void ProcessEmailTemplate_WithSingleParameter_ShouldReplaceCorrectly() + { + // Arrange + var template = "Hello [name], welcome!"; + var parameters = new Dictionary + { + { "name", "John" } + }; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().Be("Hello John, welcome!"); + } + + [Fact] + public void ProcessEmailTemplate_WithMultipleParameters_ShouldReplaceAll() + { + // Arrange + var template = "Hello [name], welcome to [company]!"; + var parameters = new Dictionary + { + { "name", "John" }, + { "company", "Acme Corp" } + }; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().Be("Hello John, welcome to Acme Corp!"); + } + + [Fact] + public void ProcessEmailTemplate_WithParameterHavingNullValue_ShouldReplaceWithEmptyString() + { + // Arrange + var template = "Hello [name], welcome!"; + var parameters = new Dictionary + { + { "name", null! } + }; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().Be("Hello , welcome!"); + } + + [Fact] + public void ProcessEmailTemplate_WithUnmatchedParameters_ShouldNotReplace() + { + // Arrange + var template = "Hello [name], welcome to [company]!"; + var parameters = new Dictionary + { + { "name", "John" }, + { "location", "New York" } // This parameter doesn't exist in template + }; + + // Act + var result = _handler.ProcessEmailTemplate(template, parameters); + + // Assert + result.Should().Be("Hello John, welcome to [company]!"); + } + + + } +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Blocktrust.CredentialWorkflow.Core.csproj b/Blocktrust.CredentialWorkflow.Core/Blocktrust.CredentialWorkflow.Core.csproj index 19f61b6..6d3539f 100644 --- a/Blocktrust.CredentialWorkflow.Core/Blocktrust.CredentialWorkflow.Core.csproj +++ b/Blocktrust.CredentialWorkflow.Core/Blocktrust.CredentialWorkflow.Core.csproj @@ -33,6 +33,7 @@ + @@ -58,7 +59,10 @@ + + + diff --git a/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/ExecuteWorkflow/ExecuteWorkflowHandler.cs b/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/ExecuteWorkflow/ExecuteWorkflowHandler.cs index ff366fe..6e6848c 100644 --- a/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/ExecuteWorkflow/ExecuteWorkflowHandler.cs +++ b/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/ExecuteWorkflow/ExecuteWorkflowHandler.cs @@ -26,7 +26,6 @@ using Action = Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Actions.Action; using ExecutionContext = Blocktrust.CredentialWorkflow.Core.Domain.Common.ExecutionContext; -namespace Blocktrust.CredentialWorkflow.Core.Commands.Workflow.ExecuteWorkflow; using System.Text; using Blocktrust.DIDComm.Message.Attachments; @@ -37,6 +36,7 @@ namespace Blocktrust.CredentialWorkflow.Core.Commands.Workflow.ExecuteWorkflow; using Mediator.Common; using Mediator.Common.Models.CredentialOffer; using Workflow = Domain.Workflow.Workflow; +namespace Blocktrust.CredentialWorkflow.Core.Commands.Workflow.ExecuteWorkflow; public class ExecuteWorkflowHandler : IRequestHandler> { @@ -82,9 +82,6 @@ public async Task> Handle(ExecuteWorkflowRequest request, Cancellat while (true) { - // Find the next action: - // - If previousActionId is null, we look for an action whose runAfter references the triggerId with EFlowStatus.Succeeded - // - If previousActionId is set, we look for an action whose runAfter references that previousActionId with EFlowStatus.Succeeded var nextActionKvp = FindNextAction( workflow.ProcessFlow.Actions, triggerId, @@ -93,11 +90,9 @@ public async Task> Handle(ExecuteWorkflowRequest request, Cancellat if (nextActionKvp is null) { - // No further action found => we’re done. Break out of the loop and finalize with success. break; } - // Process the action var actionId = nextActionKvp.Value.Key; var action = nextActionKvp.Value.Value; @@ -117,11 +112,8 @@ public async Task> Handle(ExecuteWorkflowRequest request, Cancellat ); if (result.IsFailed || result.Value.Equals(false)) { - // Already finished with failure inside the method return result; } - - // If we got here, we succeeded for this action break; } case EActionType.VerifyW3CCredential: @@ -137,10 +129,25 @@ public async Task> Handle(ExecuteWorkflowRequest request, Cancellat ); if (result.IsFailed || result.Value.Equals(false)) { - // Already finished with failure inside the method return result; } - + break; + } + case EActionType.Email: + { + var result = await ProcessEmailAction( + action, + actionOutcome, + workflowOutcomeId, + executionContext, + actionOutcomes, + workflow, + cancellationToken + ); + if (result.IsFailed || result.Value.Equals(false)) + { + return result; + } break; } case EActionType.DIDComm: @@ -156,10 +163,8 @@ public async Task> Handle(ExecuteWorkflowRequest request, Cancellat ); if (result.IsFailed || result.Value.Equals(false)) { - // Already finished with failure inside the method return result; } - break; } default: @@ -168,19 +173,209 @@ public async Task> Handle(ExecuteWorkflowRequest request, Cancellat } } - // Add success outcome for this action actionOutcomes.Add(actionOutcome); - - // Move forward to the next iteration - // Mark the "previous action" as this one (so next look-up references this action's ID) previousActionId = actionId; } - // If we exited the loop normally, finalize with success: return await FinishActionsWithSuccess(workflowOutcomeId, actionOutcomes, cancellationToken); } - private async Task> ProcessDIDCommAction(Action action, ActionOutcome actionOutcome, Guid workflowOutcomeId, ExecutionContext executionContext, List actionOutcomes, Workflow workflow, CancellationToken cancellationToken) + private KeyValuePair? FindNextAction( + Dictionary actions, + Guid triggerId, + Guid? previousActionId + ) + { + var predecessorId = previousActionId ?? triggerId; + + var nextAction = actions + .SingleOrDefault(x => x.Value.RunAfter.Count == 1 + && x.Value.RunAfter.Single() == predecessorId); + + if (nextAction.Key == default && nextAction.Value == null) + { + return null; + } + + return nextAction; + } + + private async Task> ProcessIssueW3CCredentialAction( + Action action, + ActionOutcome actionOutcome, + Guid workflowOutcomeId, + ExecutionContext executionContext, + List actionOutcomes, + Workflow workflow, + CancellationToken cancellationToken + ) + { + var input = (IssueW3cCredential)action.Input; + + var subjectDid = await GetParameterFromExecutionContext(input.SubjectDid, executionContext, workflow, actionOutcomes, EActionType.IssueW3CCredential); + if (subjectDid == null) + { + var errorMessage = "The subject DID is not provided in the execution context parameters."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var issuerDid = await GetParameterFromExecutionContext(input.IssuerDid, executionContext, workflow, actionOutcomes, EActionType.IssueW3CCredential); + if (issuerDid == null) + { + var errorMessage = "The issuer DID is not provided."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var createW3CCredentialRequest = new CreateW3cCredentialRequest( + issuerDid: issuerDid, + subjectDid: subjectDid, + additionalSubjectData: GetClaimsFromExecutionContext(input.Claims, executionContext), + validFrom: null, + expirationDate: null + ); + + var createW3CCredentialResult = await _mediator.Send(createW3CCredentialRequest, cancellationToken); + if (createW3CCredentialResult.IsFailed) + { + var errorMessage = "The W3C credential could not be created."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var issuignKeyResult = await _mediator.Send(new GetPrivateIssuingKeyByDidRequest(issuerDid), cancellationToken); + if (issuignKeyResult.IsFailed) + { + var errorMessage = "The private key for the issuer DID could not be found."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + byte[] privatekeyResult; + try + { + privatekeyResult = Base64Url.Decode(issuignKeyResult.Value); + } + catch (Exception e) + { + var errorMessage = "The private key for the issuer DID could not be parsed: " + e.Message; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var signedCredentialRequest = new SignW3cCredentialRequest( + credential: createW3CCredentialResult.Value, + issuerDid: issuerDid, + privateKey: privatekeyResult + ); + + var signedCredentialResult = await _mediator.Send(signedCredentialRequest, cancellationToken); + if (signedCredentialResult.IsFailed) + { + var errorMessage = signedCredentialResult.Errors.FirstOrDefault()?.Message + ?? "The credential could not be signed."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var successString = signedCredentialResult.Value; + actionOutcome.FinishOutcomeWithSuccess(successString); + + return Result.Ok(true); + } + + private async Task> ProcessVerifyW3CCredentialAction( + Action action, + ActionOutcome actionOutcome, + Guid workflowOutcomeId, + ExecutionContext executionContext, + List actionOutcomes, + Workflow workflow, + CancellationToken cancellationToken + ) + { + var input = (VerifyW3cCredential)action.Input; + + var credentialStr = await GetParameterFromExecutionContext(input.CredentialReference, executionContext, workflow, actionOutcomes, EActionType.VerifyW3CCredential); + if (string.IsNullOrWhiteSpace(credentialStr)) + { + var errorMessage = "No credential found in the execution context to verify."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var verifyRequest = new VerifyW3CCredentialRequest(credentialStr, input.CheckSignature, input.CheckExpiry, input.CheckRevocationStatus, input.CheckSchema, input.CheckTrustRegistry); + var verifyResult = await _mediator.Send(verifyRequest, cancellationToken); + if (verifyResult.IsFailed) + { + var errorMessage = verifyResult.Errors.FirstOrDefault()?.Message + ?? "Verification failed."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var verificationResult = verifyResult.Value; + var successString = $"Credential verified. IsValid={verificationResult.IsValid}"; + actionOutcome.FinishOutcomeWithSuccess(successString); + + return Result.Ok(true); + } + + private async Task> ProcessEmailAction( + Action action, + ActionOutcome actionOutcome, + Guid workflowOutcomeId, + ExecutionContext executionContext, + List actionOutcomes, + Workflow workflow, + CancellationToken cancellationToken + ) + { + var input = (EmailAction)action.Input; + + var toEmail = await GetParameterFromExecutionContext(input.To, executionContext, workflow, actionOutcomes); + if (string.IsNullOrWhiteSpace(toEmail)) + { + var errorMessage = "The recipient email address is not provided in the execution context parameters."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + var parameters = new Dictionary(); + foreach (var param in input.Parameters) + { + var value = await GetParameterFromExecutionContext(param.Value, executionContext, workflow, actionOutcomes); + if (value != null) + { + parameters[param.Key] = value; + } + } + + try + { + var subject = ProcessEmailTemplate(input.Subject, parameters); +var body = ProcessEmailTemplate(input.Body, parameters); + + var sendEmailRequest = new SendEmailActionRequest(toEmail, subject, body); + var sendResult = await _mediator.Send(sendEmailRequest, cancellationToken); + + if (sendResult.IsFailed) + { + var errorMessage = sendResult.Errors.FirstOrDefault()?.Message ?? "Failed to send email."; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + + actionOutcome.FinishOutcomeWithSuccess("Email sent successfully"); + return Result.Ok(true); + } + catch (Exception ex) + { + var errorMessage = $"Error sending email: {ex.Message}"; + return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + } + } + + private async Task> ProcessDIDCommAction( + Action action, + ActionOutcome actionOutcome, + Guid workflowOutcomeId, + ExecutionContext executionContext, + List actionOutcomes, + Workflow workflow, + CancellationToken cancellationToken + ) { var input = (DIDCommAction)action.Input; @@ -266,7 +461,6 @@ private async Task> ProcessDIDCommAction(Action action, ActionOutco } else if (input.Type == EDIDCommType.CredentialIssuance) { - // Get the credential as string from context var credentialStr = await GetParameterFromExecutionContext(input.CredentialReference, executionContext, workflow, actionOutcomes, EActionType.DIDComm); if (string.IsNullOrWhiteSpace(credentialStr)) { @@ -309,198 +503,36 @@ private async Task> ProcessDIDCommAction(Action action, ActionOutco return Result.Ok(true); } - - private static Message BuildIssuingMessage(PeerDid localPeerDid, PeerDid prismPeerDid, string messageId, string signedCredential) + private string ProcessEmailTemplate(string template, Dictionary parameters) { - var unixTimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (string.IsNullOrEmpty(template)) + return string.Empty; + + if (parameters == null || !parameters.Any()) + return template; - var body = new Dictionary + var processedTemplate = template; + foreach (var param in parameters) { - { "goal_code", GoalCodes.PrismCredentialOffer }, - { "comment", null! }, - { "formats", new List() } - }; - var attachment = new AttachmentBuilder(Guid.NewGuid().ToString(), new Base64(signedCredential)) - .Build(); - var responseMessage = new MessageBuilder( - id: Guid.NewGuid().ToString(), - type: ProtocolConstants.IssueCredential2Issue, - body: body - ) - .thid(messageId) - .from(localPeerDid.Value) - .to(new List() { prismPeerDid.Value }) - .attachments(new List() + if (!string.IsNullOrEmpty(param.Key)) { - attachment - }) - .createdTime(unixTimeStamp) - .expiresTime(unixTimeStamp + 1000) - .build(); - return responseMessage; - } - - /// - /// Finds the first action that references either: - /// - the triggerId (if previousActionId == null) - /// - the previousActionId (otherwise) - /// in its RunAfter dictionary with EFlowStatus == Succeeded. - /// Returns null if no matching action is found. - /// - private KeyValuePair? FindNextAction( - Dictionary actions, - Guid triggerId, - Guid? previousActionId - ) - { - // The ID we must match in the RunAfter dictionary - var predecessorId = previousActionId ?? triggerId; - - // SingleOrDefault to find a unique action that references predecessorId with Succeeded - var nextAction = actions - .SingleOrDefault(x => x.Value.RunAfter.Count == 1 - && x.Value.RunAfter.Single() == predecessorId); - - // If Key == default(Guid), it means SingleOrDefault found nothing - if (nextAction.Key == default && nextAction.Value == null) - { - // No action found - return null; - } - - return nextAction; - } - - /// - /// Processes the IssueW3CCredential action and updates the given actionOutcome upon success/failure. - /// Returns a Result indicating if the action was successful or not. If unsuccessful, it already finishes the workflow with failure. - /// - private async Task> ProcessIssueW3CCredentialAction( - Action action, - ActionOutcome actionOutcome, - Guid workflowOutcomeId, - ExecutionContext executionContext, - List actionOutcomes, - Workflow workflow, - CancellationToken cancellationToken - ) - { - var input = (IssueW3cCredential)action.Input; - - var subjectDid = await GetParameterFromExecutionContext(input.SubjectDid, executionContext, workflow, actionOutcomes, EActionType.IssueW3CCredential); - if (subjectDid == null) - { - var errorMessage = "The subject DID is not provided in the execution context parameters."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - var issuerDid = await GetParameterFromExecutionContext(input.IssuerDid, executionContext, workflow, actionOutcomes, EActionType.IssueW3CCredential); - if (issuerDid == null) - { - var errorMessage = "The issuer DID is not provided."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - // 1) Create W3C credential - var createW3CCredentialRequest = new CreateW3cCredentialRequest( - issuerDid: issuerDid, - subjectDid: subjectDid, - additionalSubjectData: GetClaimsFromExecutionContext(input.Claims, executionContext), - validFrom: null, - expirationDate: null - ); - - var createW3CCredentialResult = await _mediator.Send(createW3CCredentialRequest, cancellationToken); - if (createW3CCredentialResult.IsFailed) - { - var errorMessage = "The W3C credential could not be created."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - // 2) Retrieve private key - var issuignKeyResult = await _mediator.Send(new GetPrivateIssuingKeyByDidRequest(issuerDid), cancellationToken); - if (issuignKeyResult.IsFailed) - { - var errorMessage = "The private key for the issuer DID could not be found."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - byte[] privatekeyResult; - try - { - privatekeyResult = Base64Url.Decode(issuignKeyResult.Value); - } - catch (Exception e) - { - var errorMessage = "The private key for the issuer DID could not be parsed: " + e.Message; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - // 3) Sign W3C credential - var signedCredentialRequest = new SignW3cCredentialRequest( - credential: createW3CCredentialResult.Value, - issuerDid: issuerDid, - privateKey: privatekeyResult - ); - - var signedCredentialResult = await _mediator.Send(signedCredentialRequest, cancellationToken); - if (signedCredentialResult.IsFailed) - { - var errorMessage = signedCredentialResult.Errors.FirstOrDefault()?.Message - ?? "The credential could not be signed."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - var successString = signedCredentialResult.Value; - actionOutcome.FinishOutcomeWithSuccess(successString); - - return Result.Ok(true); - } - - /// - /// Processes the VerifyW3CCredential action logic: - /// - Retrieve the credential from execution context - /// - Send VerifyW3CCredentialRequest - /// - Record success/failure in the actionOutcome - /// - private async Task> ProcessVerifyW3CCredentialAction( - Action action, - ActionOutcome actionOutcome, - Guid workflowOutcomeId, - ExecutionContext executionContext, - List actionOutcomes, - Workflow workflow, - CancellationToken cancellationToken - ) - { - var input = (VerifyW3cCredential)action.Input; - - // Get the credential as string from context - var credentialStr = await GetParameterFromExecutionContext(input.CredentialReference, executionContext, workflow, actionOutcomes, EActionType.VerifyW3CCredential); - if (string.IsNullOrWhiteSpace(credentialStr)) - { - var errorMessage = "No credential found in the execution context to verify."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); - } - - // Send the verification request - var verifyRequest = new VerifyW3CCredentialRequest(credentialStr, input.CheckSignature, input.CheckExpiry, input.CheckRevocationStatus, input.CheckSchema, input.CheckTrustRegistry); - var verifyResult = await _mediator.Send(verifyRequest, cancellationToken); - if (verifyResult.IsFailed) - { - var errorMessage = verifyResult.Errors.FirstOrDefault()?.Message - ?? "Verification failed."; - return await FinishActionsWithFailure(workflowOutcomeId, actionOutcome, errorMessage, actionOutcomes, cancellationToken); + var paramValue = param.Value ?? string.Empty; + var key = param.Key.Trim(); + processedTemplate = Regex.Replace( + processedTemplate, + $"\\[{key}\\]", + paramValue, + RegexOptions.IgnoreCase); + } } - var verificationResult = verifyResult.Value; - var successString = $"Credential verified. IsValid={verificationResult.IsValid}"; - actionOutcome.FinishOutcomeWithSuccess(successString); - - return Result.Ok(true); + return processedTemplate; } - private async Task> FinishActionsWithSuccess(Guid workflowOutcomeId, List actionOutcomes, CancellationToken cancellationToken) + private async Task> FinishActionsWithSuccess( + Guid workflowOutcomeId, + List actionOutcomes, + CancellationToken cancellationToken) { var workflowUpdateResult = await _mediator.Send( new UpdateWorkflowOutcomeRequest( @@ -545,7 +577,7 @@ CancellationToken cancellationToken return Result.Ok(false); } - private ExecutionContext BuildExecutionContext(Workflow workflow, string? executionContextString) + private ExecutionContext BuildExecutionContext(Domain.Workflow.Workflow workflow, string? executionContextString) { var trigger = workflow?.ProcessFlow?.Triggers.FirstOrDefault().Value; if (trigger is null) @@ -561,110 +593,145 @@ private ExecutionContext BuildExecutionContext(Workflow workflow, string? execut return new ExecutionContext(workflow!.TenantId); } - private async Task GetParameterFromExecutionContext(ParameterReference parameterReference, ExecutionContext executionContext, Workflow workflow, List actionOutcomes, EActionType? actionType) + private static Message BuildIssuingMessage(PeerDid localPeerDid, PeerDid prismPeerDid, string messageId, string signedCredential) { - if (parameterReference.Source == ParameterSource.TriggerInput) - { - if (executionContext.InputContext is null) - { - return null; - } + var unixTimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var exists = executionContext.InputContext.TryGetValue(parameterReference.Path.ToLowerInvariant(), out var value); - if (!exists) + var body = new Dictionary + { + { "goal_code", GoalCodes.PrismCredentialOffer }, + { "comment", null! }, + { "formats", new List() } + }; + var attachment = new AttachmentBuilder(Guid.NewGuid().ToString(), new Base64(signedCredential)) + .Build(); + var responseMessage = new MessageBuilder( + id: Guid.NewGuid().ToString(), + type: ProtocolConstants.IssueCredential2Issue, + body: body + ) + .thid(messageId) + .from(localPeerDid.Value) + .to(new List() { prismPeerDid.Value }) + .attachments(new List() { - return null; - } + attachment + }) + .createdTime(unixTimeStamp) + .expiresTime(unixTimeStamp + 1000) + .build(); + return responseMessage; + } - return value; - } - else if (parameterReference.Source == ParameterSource.AppSettings) + private async Task GetParameterFromExecutionContext( + ParameterReference parameterReference, + ExecutionContext executionContext, + Workflow workflow, + List actionOutcomes, + EActionType? actionType = null) + { + switch (parameterReference.Source) { - if (actionType == EActionType.IssueW3CCredential) - { - var issuingKeys = await _mediator.Send(new GetIssuingKeysRequest(executionContext.TenantId)); - if (issuingKeys.IsFailed || issuingKeys.Value is null || issuingKeys.Value.Count == 0) + case ParameterSource.Static: + return parameterReference.Path; + + case ParameterSource.TriggerInput: + if (executionContext.InputContext is null) { return null; } - if (parameterReference.Path.Equals("DefaultIssuerDid", StringComparison.CurrentCultureIgnoreCase)) + var exists = executionContext.InputContext.TryGetValue(parameterReference.Path.ToLowerInvariant(), out var value); + if (!exists) { - return issuingKeys.Value.First().Did; + return null; } - foreach (var issuingKey in issuingKeys.Value) + return value; + + case ParameterSource.AppSettings: + if (actionType == EActionType.IssueW3CCredential) { - var isParseable = Guid.TryParse(parameterReference.Path, out var keyId); - if (!isParseable) + var issuingKeys = await _mediator.Send(new GetIssuingKeysRequest(executionContext.TenantId)); + if (issuingKeys.IsFailed || issuingKeys.Value is null || issuingKeys.Value.Count == 0) { return null; } - if (issuingKey.IssuingKeyId.Equals(keyId)) + if (parameterReference.Path.Equals("DefaultIssuerDid", StringComparison.CurrentCultureIgnoreCase)) { - return issuingKey.Did; + return issuingKeys.Value.First().Did; } - } - } - else if (actionType == EActionType.DIDComm) - { - var peerDids = await _mediator.Send(new GetPeerDIDsRequest(executionContext.TenantId)); - if (peerDids.IsFailed || peerDids.Value is null || peerDids.Value.Count == 0) - { - return null; - } - if (parameterReference.Path.Equals("DefaultSenderDid", StringComparison.CurrentCultureIgnoreCase)) - { - return peerDids.Value.First().PeerDID; + foreach (var issuingKey in issuingKeys.Value) + { + var isParseable = Guid.TryParse(parameterReference.Path, out var keyId); + if (!isParseable) + { + return null; + } + +if (issuingKey.IssuingKeyId.Equals(keyId)) + { + return issuingKey.Did; + } + } } - - foreach (var peerDid in peerDids.Value) + else if (actionType == EActionType.DIDComm) { - var isParseable = Guid.TryParse(parameterReference.Path, out var keyId); - if (!isParseable) + var peerDids = await _mediator.Send(new GetPeerDIDsRequest(executionContext.TenantId)); + if (peerDids.IsFailed || peerDids.Value is null || peerDids.Value.Count == 0) { return null; } - if (peerDid.PeerDIDEntityId.Equals(keyId)) + if (parameterReference.Path.Equals("DefaultSenderDid", StringComparison.CurrentCultureIgnoreCase)) { - return peerDid.PeerDID; + return peerDids.Value.First().PeerDID; + } + + foreach (var peerDid in peerDids.Value) + { + var isParseable = Guid.TryParse(parameterReference.Path, out var keyId); + if (!isParseable) + { + return null; + } + + if (peerDid.PeerDIDEntityId.Equals(keyId)) + { + return peerDid.PeerDID; + } } } - } - else - { return null; - } - } - else if (parameterReference.Source == ParameterSource.ActionOutcome) - { - var actionId = parameterReference.ActionId; - var referencedActionExists = workflow.ProcessFlow!.Actions.Any(p => p.Key.Equals(actionId)); - if (!referencedActionExists) - { - return null; - } - var referencedAction = workflow.ProcessFlow.Actions.FirstOrDefault(p => p.Key.Equals(actionId)); - switch (referencedAction.Value.Type) - { - case EActionType.IssueW3CCredential: - var actionOutcome = actionOutcomes.FirstOrDefault(p => p.ActionId.Equals(actionId)); - if (actionOutcome is null) - { + case ParameterSource.ActionOutcome: + var actionId = parameterReference.ActionId; + var referencedActionExists = workflow.ProcessFlow!.Actions.Any(p => p.Key.Equals(actionId)); + if (!referencedActionExists) + { + return null; + } + + var referencedAction = workflow.ProcessFlow.Actions.FirstOrDefault(p => p.Key.Equals(actionId)); + switch (referencedAction.Value.Type) + { + case EActionType.IssueW3CCredential: + var actionOutcome = actionOutcomes.FirstOrDefault(p => p.ActionId.Equals(actionId)); + if (actionOutcome is null) + { + return null; + } + + return actionOutcome.OutcomeJson; + default: return null; - } + } - var credential = actionOutcome.OutcomeJson; - return credential; - break; - } + default: + return null; } - - return null; } private Dictionary? GetClaimsFromExecutionContext(Dictionary inputClaims, ExecutionContext executionContext) diff --git a/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/SendEmailAction/SendEmailActionHandler.cs b/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/SendEmailAction/SendEmailActionHandler.cs new file mode 100644 index 0000000..c2f35d0 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/SendEmailAction/SendEmailActionHandler.cs @@ -0,0 +1,32 @@ +using Blocktrust.CredentialWorkflow.Core.Services; +using FluentResults; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Blocktrust.CredentialWorkflow.Core.Commands.Workflow.SendEmailAction; + +public class SendEmailActionHandler : IRequestHandler +{ + private readonly IEmailService _emailService; + private readonly ILogger _logger; + + public SendEmailActionHandler(IEmailService emailService, ILogger logger) + { + _emailService = emailService; + _logger = logger; + } + + public async Task Handle(SendEmailActionRequest request, CancellationToken cancellationToken) + { + try + { + await _emailService.SendEmailAsync(request.ToEmail, request.Subject, request.Body); + return Result.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending email"); + return Result.Fail(ex.Message); + } + } +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/SendEmailAction/SendEmailActionRequest.cs b/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/SendEmailAction/SendEmailActionRequest.cs new file mode 100644 index 0000000..e0689c0 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Commands/Workflow/SendEmailAction/SendEmailActionRequest.cs @@ -0,0 +1,18 @@ +using FluentResults; +using MediatR; + +namespace Blocktrust.CredentialWorkflow.Core.Commands.Workflow.SendEmailAction; + +public class SendEmailActionRequest : IRequest +{ + public string ToEmail { get; } + public string Subject { get; } + public string Body { get; } + + public SendEmailActionRequest(string toEmail, string subject, string body) + { + ToEmail = toEmail; + Subject = subject; + Body = body; + } +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/EmailAction.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/EmailAction.cs index 6bc99de..4a2d1e0 100644 --- a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/EmailAction.cs +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/EmailAction.cs @@ -1,7 +1,7 @@ -namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Actions.Outgoing; - using System.Text.Json.Serialization; -using Common; +using Blocktrust.CredentialWorkflow.Core.Domain.Common; + +namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Actions.Outgoing; public class EmailAction : ActionInput { diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/OutgoingActions.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/OutgoingActions.cs new file mode 100644 index 0000000..dacecfe --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Actions/Outgoing/OutgoingActions.cs @@ -0,0 +1,52 @@ +// using System.Text.Json.Serialization; +// using Blocktrust.CredentialWorkflow.Core.Domain.Common; +// +// namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Actions.Outgoing; +// +// public class DIDCommAction : ActionInput +// { +// [JsonPropertyName("type")] +// public EDIDCommType Type { get; set; } +// +// [JsonPropertyName("peerDid")] +// public ParameterReference PeerDid { get; set; } = new(); +// +// [JsonPropertyName("message")] +// public Dictionary MessageContent { get; set; } = new(); +// } +// +// public enum EDIDCommType +// { +// TrustPing, +// Message +// } +// +// public class HttpAction : ActionInput +// { +// [JsonPropertyName("method")] +// public string Method { get; set; } = "POST"; +// +// [JsonPropertyName("endpoint")] +// public ParameterReference Endpoint { get; set; } = new(); +// +// [JsonPropertyName("headers")] +// public Dictionary Headers { get; set; } = new(); +// +// [JsonPropertyName("body")] +// public Dictionary Body { get; set; } = new(); +// } + +// public class EmailAction : ActionInput +// { +// [JsonPropertyName("to")] +// public ParameterReference To { get; set; } = new(); +// +// [JsonPropertyName("subject")] +// public ParameterReference Subject { get; set; } = new(); +// +// [JsonPropertyName("body")] +// public ParameterReference Body { get; set; } = new(); +// +// [JsonPropertyName("attachments")] +// public List Attachments { get; set; } = new(); +// } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Services/EmailService.cs b/Blocktrust.CredentialWorkflow.Core/Services/EmailService.cs new file mode 100644 index 0000000..f6276f8 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Services/EmailService.cs @@ -0,0 +1,81 @@ +using System.Text.RegularExpressions; +using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Actions.Outgoing; +using Blocktrust.CredentialWorkflow.Core.Settings; +using Microsoft.Extensions.Options; +using SendGrid; +using SendGrid.Helpers.Mail; +using Microsoft.Extensions.Logging; + +namespace Blocktrust.CredentialWorkflow.Core.Services; + +public interface IEmailService +{ + Task SendEmailAsync(string toEmail, string subject, string message, List attachments = null); + string ProcessTemplate(string template, Dictionary parameters); +} + +public class EmailService : IEmailService +{ + private readonly ILogger _logger; + private readonly EmailSettings _emailSettings; + private readonly Regex _parameterRegex = new(@"{{(\w+)}}"); + + public EmailService(IOptions emailSettings, ILogger logger) + { + _emailSettings = emailSettings.Value; + _logger = logger; + } + + public async Task SendEmailAsync(string toEmail, string subject, string message, List attachments = null) + { + if (string.IsNullOrEmpty(_emailSettings.SendGridKey)) + { + throw new Exception("SendGrid API key is not configured"); + } + + var client = new SendGridClient(_emailSettings.SendGridKey); + var msg = new SendGridMessage + { + From = new EmailAddress(_emailSettings.SendGridFromEmail, _emailSettings.DefaultFromName), + Subject = subject, + PlainTextContent = message, + HtmlContent = message + }; + + msg.AddTo(new EmailAddress(toEmail)); + + if (attachments != null && attachments.Any()) + { + foreach (var attachment in attachments) + { + var bytes = Convert.FromBase64String(attachment.Content); + msg.AddAttachment(attachment.FileName, Convert.ToBase64String(bytes)); + } + } + + var response = await client.SendEmailAsync(msg); + _logger.LogInformation(response.IsSuccessStatusCode + ? $"Email to {toEmail} queued successfully!" + : $"Failed to send email to {toEmail}"); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to send email: {response.StatusCode}"); + } + } + + public string ProcessTemplate(string template, Dictionary parameters) + { + return _parameterRegex.Replace(template, match => + { + var paramName = match.Groups[1].Value; + return parameters.TryGetValue(paramName, out var value) ? value : match.Value; + }); + } +} + +public class AttachmentInfo +{ + public string FileName { get; set; } + public string Content { get; set; } // Base64 encoded content +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Settings/EmailSettings.cs b/Blocktrust.CredentialWorkflow.Core/Settings/EmailSettings.cs new file mode 100644 index 0000000..292074b --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Settings/EmailSettings.cs @@ -0,0 +1,8 @@ +namespace Blocktrust.CredentialWorkflow.Core.Settings; + +public class EmailSettings +{ + public string SendGridKey { get; set; } = string.Empty; + public string SendGridFromEmail { get; set; } = string.Empty; + public string DefaultFromName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/Actions/Outgoing/EmailActionComponent.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/Actions/Outgoing/EmailActionComponent.razor index e3ba6e0..fbcc3c6 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/Actions/Outgoing/EmailActionComponent.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/Actions/Outgoing/EmailActionComponent.razor @@ -15,7 +15,6 @@ - @if (ActionInput.To.Source == ParameterSource.Static) { + } @@ -36,89 +46,73 @@
-
- - - @if (ActionInput.Subject.Source == ParameterSource.Static) - { - - } - else - { - - } -
+
-
- - - @if (ActionInput.Body.Source == ParameterSource.Static) - { - - } - else - { - - } -
+
- +
- +
- - @foreach (var attachment in ActionInput.Attachments) +
+ Use these parameters in subject and body by typing [parameterName] +
+ @foreach (var param in ActionInput.Parameters) {
- + - - - + @if (param.Value.Source == ParameterSource.Static) + { + + } + else + { + + }
@@ -137,18 +131,30 @@ await OnChange.InvokeAsync(); } - private async Task AddAttachment() + private async Task AddParameter() { - ActionInput.Attachments.Add(new ParameterReference + var paramName = $"param{ActionInput.Parameters.Count + 1}"; + ActionInput.Parameters[paramName] = new ParameterReference { Source = ParameterSource.Static - }); + }; await OnChange.InvokeAsync(); } - private async Task RemoveAttachment(ParameterReference attachment) + private async Task UpdateParameterKey(string oldKey, string newKey) + { + if (oldKey != newKey && !string.IsNullOrWhiteSpace(newKey) && !ActionInput.Parameters.ContainsKey(newKey)) + { + var value = ActionInput.Parameters[oldKey]; + ActionInput.Parameters.Remove(oldKey); + ActionInput.Parameters[newKey] = value; + await OnChange.InvokeAsync(); + } + } + + private async Task RemoveParameter(string key) { - ActionInput.Attachments.Remove(attachment); + ActionInput.Parameters.Remove(key); await OnChange.InvokeAsync(); } } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor index e83c912..da0fa75 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor @@ -127,7 +127,6 @@ OnChange="OnChange" TriggerParameters="TriggerParameters"/> } - break; case EActionType.VerifyW3CCredential: diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor index af5c4f1..bd77249 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor @@ -233,8 +233,9 @@ { Id = Guid.NewGuid(), To = new ParameterReference { Source = ParameterSource.Static }, - Subject = new ParameterReference { Source = ParameterSource.Static }, - Body = new ParameterReference { Source = ParameterSource.Static } + Subject = string.Empty, + Body = string.Empty, + Parameters = new Dictionary() }, EActionType.IssueW3CCredential => new IssueW3cCredential { diff --git a/Blocktrust.CredentialWorkflow.Web/Controllers/WorkflowController.cs b/Blocktrust.CredentialWorkflow.Web/Controllers/WorkflowController.cs index 80d78aa..505e626 100644 --- a/Blocktrust.CredentialWorkflow.Web/Controllers/WorkflowController.cs +++ b/Blocktrust.CredentialWorkflow.Web/Controllers/WorkflowController.cs @@ -1,115 +1,117 @@ -namespace Blocktrust.CredentialWorkflow.Web.Controllers +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Blocktrust.CredentialWorkflow.Core.Commands.Workflow.GetWorkflowById; +using Blocktrust.CredentialWorkflow.Core.Commands.WorkflowOutcome.CreateWorkflowOutcome; +using Blocktrust.CredentialWorkflow.Core.Domain.Common; +using Blocktrust.CredentialWorkflow.Core.Domain.Enums; +using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers; +using Blocktrust.CredentialWorkflow.Core.Services; +using Core.Commands.WorkflowOutcome.CreateWorkflowOutcome; +using Core.Domain.ProcessFlow.Triggers; +using Core.Services; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Blocktrust.CredentialWorkflow.Web.Controllers; + +[ApiController] +[Route("api/workflow/{workflowGuidId:guid}")] +[AllowAnonymous] +public class WorkflowController : ControllerBase { - using System.IO; - using System.Text; - using System.Threading.Tasks; - using Blocktrust.CredentialWorkflow.Core.Commands.Workflow.GetWorkflowById; - using Blocktrust.CredentialWorkflow.Core.Domain.Common; - using Core.Domain.ProcessFlow.Triggers; - using Core.Services; - using MediatR; - using Microsoft.AspNetCore.Mvc; - using System; - using System.Linq; - using System.Text.Json; - using Core.Commands.WorkflowOutcome.CreateWorkflowOutcome; - using Core.Domain.Enums; - using Microsoft.AspNetCore.Authorization; - - [ApiController] - [Route("api/workflow/{workflowGuidId:guid}")] - [AllowAnonymous] - public class WorkflowController : ControllerBase + private readonly IMediator _mediator; + private readonly ITriggerValidationService _triggerValidationService; + private readonly IWorkflowQueue _workflowQueue; + + public WorkflowController(IMediator mediator, ITriggerValidationService triggerValidationService, IWorkflowQueue workflowQueue) + { + _mediator = mediator; + _triggerValidationService = triggerValidationService; + _workflowQueue = workflowQueue; + } + + [HttpGet] + public Task ExecuteWorkflowGet([FromRoute] Guid workflowGuidId) + { + return ExecuteWorkflowInternalAsync(HttpRequestMethod.GET, workflowGuidId); + } + + [HttpPost] + public Task ExecuteWorkflowPost([FromRoute] Guid workflowGuidId) { - private readonly IMediator _mediator; - private readonly ITriggerValidationService _triggerValidationService; - private readonly IWorkflowQueue _workflowQueue; + return ExecuteWorkflowInternalAsync(HttpRequestMethod.POST, workflowGuidId); + } + + private async Task ExecuteWorkflowInternalAsync(HttpRequestMethod httpMethod, Guid workflowGuidId) + { + // Create and populate a new instance of SimplifiedHttpContext + var simplifiedHttpContext = new SimplifiedHttpContext + { + Method = httpMethod + }; + + // Populate query parameters + foreach (var queryParam in Request.Query) + { + simplifiedHttpContext.QueryParameters[queryParam.Key] = queryParam.Value.ToString(); + } + + // Read the entire request body (if any) + using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) + { + simplifiedHttpContext.Body = await reader.ReadToEndAsync(); + } + + // Retrieve the workflow details via MediatR + var getWorkflowRequest = new GetWorkflowByIdRequest(workflowGuidId); + var getWorkflowResult = await _mediator.Send(getWorkflowRequest); + if (getWorkflowResult.IsFailed) + { + return BadRequest(getWorkflowResult.Errors); + } - public WorkflowController(IMediator mediator, ITriggerValidationService triggerValidationService, IWorkflowQueue workflowQueue) + if (getWorkflowResult.Value.WorkflowState == EWorkflowState.Inactive) { - _mediator = mediator; - _triggerValidationService = triggerValidationService; - _workflowQueue = workflowQueue; + return BadRequest("The workflow is inactive"); } - [HttpGet] - public Task ExecuteWorkflowGet([FromRoute] Guid workflowGuidId) + // Check if the workflow has triggers + var processFlow = getWorkflowResult.Value.ProcessFlow; + if (processFlow is null || !processFlow.Triggers.Any()) { - return ExecuteWorkflowInternalAsync(HttpRequestMethod.GET, workflowGuidId); + return BadRequest("The workflow does not have a trigger"); } - [HttpPost] - public Task ExecuteWorkflowPost([FromRoute] Guid workflowGuidId) + // Ensure that the trigger is an HTTP request trigger + var trigger = processFlow.Triggers.First().Value; + if (trigger.Type != ETriggerType.HttpRequest) { - return ExecuteWorkflowInternalAsync(HttpRequestMethod.POST, workflowGuidId); + return BadRequest("The workflow trigger is not an HTTP request trigger"); } - private async Task ExecuteWorkflowInternalAsync(HttpRequestMethod httpMethod, Guid workflowGuidId) + // Validate the HTTP trigger using the simplified context + var httpRequestTrigger = (TriggerInputHttpRequest)trigger.Input; + var validationResult = _triggerValidationService.Validate(httpRequestTrigger, simplifiedHttpContext); + if (validationResult.IsFailed) { - // Create and populate a new instance of SimplifiedHttpContext - var simplifiedHttpContext = new SimplifiedHttpContext - { - Method = httpMethod - }; - - // Populate query parameters - foreach (var queryParam in Request.Query) - { - simplifiedHttpContext.QueryParameters[queryParam.Key] = queryParam.Value.ToString(); - } - - // Read the entire request body (if any) - using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) - { - simplifiedHttpContext.Body = await reader.ReadToEndAsync(); - } - - // Retrieve the workflow details via MediatR - var getWorkflowRequest = new GetWorkflowByIdRequest(workflowGuidId); - var getWorkflowResult = await _mediator.Send(getWorkflowRequest); - if (getWorkflowResult.IsFailed) - { - return BadRequest(getWorkflowResult.Errors); - } - - if (getWorkflowResult.Value.WorkflowState == EWorkflowState.Inactive) - { - return BadRequest("The workflow is inactive"); - } - - // Check if the workflow has triggers - var processFlow = getWorkflowResult.Value.ProcessFlow; - if (processFlow is null || !processFlow.Triggers.Any()) - { - return BadRequest("The workflow does not have a trigger"); - } - - // Ensure that the trigger is an HTTP request trigger - var trigger = processFlow.Triggers.First().Value; - if (trigger.Type != ETriggerType.HttpRequest) - { - return BadRequest("The workflow trigger is not an HTTP request trigger"); - } - - // Validate the HTTP trigger using the simplified context - var httpRequestTrigger = (TriggerInputHttpRequest)trigger.Input; - var validationResult = _triggerValidationService.Validate(httpRequestTrigger, simplifiedHttpContext); - if (validationResult.IsFailed) - { - return BadRequest(validationResult.Errors.First().Message); - } - - var executionContext = simplifiedHttpContext.ToJson(); - - var outcomeResult = await _mediator.Send(new CreateWorkflowOutcomeRequest(getWorkflowResult.Value.WorkflowId, executionContext)); - if (outcomeResult.IsFailed) - { - return BadRequest($"Failed to create outcome for workflow {getWorkflowResult.Value.WorkflowId}"); - } - - await _workflowQueue.EnqueueAsync(outcomeResult.Value); - - return Ok("Workflow retrieved successfully"); + return BadRequest(validationResult.Errors.First().Message); } + + var executionContext = simplifiedHttpContext.ToJson(); + + var outcomeResult = await _mediator.Send(new CreateWorkflowOutcomeRequest(getWorkflowResult.Value.WorkflowId, executionContext)); + if (outcomeResult.IsFailed) + { + return BadRequest($"Failed to create outcome for workflow {getWorkflowResult.Value.WorkflowId}"); + } + + await _workflowQueue.EnqueueAsync(outcomeResult.Value); + + return Ok("Workflow retrieved successfully"); } -} +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Program.cs b/Blocktrust.CredentialWorkflow.Web/Program.cs index 5d32f14..509ab65 100644 --- a/Blocktrust.CredentialWorkflow.Web/Program.cs +++ b/Blocktrust.CredentialWorkflow.Web/Program.cs @@ -59,11 +59,15 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Configure strongly typed settings builder.Services.Configure( builder.Configuration.GetSection("AppSettings")); builder.Services.Configure( builder.Configuration.GetSection("CredentialSettings")); +builder.Services.Configure( + builder.Configuration.GetSection("EmailSettings")); // Configure Authentication and Authorization builder.Services.AddAuthentication(options => diff --git a/Blocktrust.CredentialWorkflow.Web/appsettings.json b/Blocktrust.CredentialWorkflow.Web/appsettings.json index 8caafc1..c566037 100644 --- a/Blocktrust.CredentialWorkflow.Web/appsettings.json +++ b/Blocktrust.CredentialWorkflow.Web/appsettings.json @@ -8,12 +8,17 @@ }, "CredentialSettings": { "DefaultIssuerDid": "did:prism:your_default_issuer", - "SigningKeyId": "your_signing_key_id", + "SigningKeyId": "signing_key_id", "DefaultClaimTemplates": { "type": "VerifiableCredential", "version": "1.0" } }, + "EmailSettings": { + "SendGridKey": "sendgrid-api-key", + "SendGridFromEmail": "verified-sender@domain.com", + "DefaultFromName": "Credential Workflow Platform" + }, "Logging": { "LogLevel": { "Default": "Information",