diff --git a/Elsa.sln b/Elsa.sln index 669b46998..290d05449 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -245,6 +245,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "drop-ins", "drop-ins", "{6A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleDropIn", "src\samples\drop-ins\SampleDropIn\SampleDropIn.csproj", "{E0567846-AC0E-4E09-96A1-EFED4B570A01}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.SasTokens", "src\modules\Elsa.SasTokens\Elsa.SasTokens.csproj", "{3095C95A-F1F9-487A-A983-1B100A33E4C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -611,6 +613,10 @@ Global {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Release|Any CPU.Build.0 = Release|Any CPU + {3095C95A-F1F9-487A-A983-1B100A33E4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3095C95A-F1F9-487A-A983-1B100A33E4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3095C95A-F1F9-487A-A983-1B100A33E4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3095C95A-F1F9-487A-A983-1B100A33E4C7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} @@ -721,5 +727,6 @@ Global {4E7F15D1-729E-4DA7-992A-C3A2C6054B37} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} {6AB6F2F7-CAC1-45C8-9D87-EEE56BC68601} = {155227F0-A33B-40AA-A4B4-06F813EB921B} {E0567846-AC0E-4E09-96A1-EFED4B570A01} = {6AB6F2F7-CAC1-45C8-9D87-EEE56BC68601} + {3095C95A-F1F9-487A-A983-1B100A33E4C7} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} EndGlobalSection EndGlobal diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index 80f5f04d2..f2dcadea2 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -9,12 +9,15 @@ using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.EntityFrameworkCore.Modules.Runtime; using Elsa.Extensions; using Elsa.Http.Handlers; +using Elsa.Http.Options; using Elsa.MongoDb.Extensions; using Elsa.MongoDb.Modules.Identity; using Elsa.MongoDb.Modules.Management; using Elsa.MongoDb.Modules.Runtime; using Elsa.WorkflowServer.Web; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Options; using Proto.Persistence.Sqlite; using Proto.Persistence.SqlServer; @@ -141,7 +144,12 @@ services .UseRealTimeWorkflows() .UseJavaScript(js => js.JintOptions = options => options.AllowClrAccess = true) .UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default) - .UseHttp(http => http.HttpEndpointAuthorizationHandler = sp => sp.GetRequiredService()) + .UseHttp(http => + { + http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options); + http.HttpEndpointAuthorizationHandler = sp => sp.GetRequiredService(); + }) + .UseSasTokens(sas => sas.DataProtectionProvider = sp => DataProtectionProvider.Create("Elsa Workflows")) .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)); // Initialize drop-ins. @@ -173,7 +181,8 @@ app.UseAuthentication(); app.UseAuthorization(); // Elsa API endpoints for designer. -app.UseWorkflowsApi(); +var routePrefix = app.Services.GetRequiredService>().Value.ApiRoutePrefix; +app.UseWorkflowsApi(routePrefix); // Captures unhandled exceptions and returns a JSON response. app.UseJsonSerializationErrorHandler(); diff --git a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json index 992ac324f..27fa35940 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json +++ b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json @@ -23,6 +23,17 @@ "Port": 2525, "DefaultSender": "noreply@crmservices.com" }, + "Http": { + "BaseUrl": "https://localhost:5001", + "BasePath": "/workflows", + "ApiRoutePrefix": "elsa/api", + "AvailableContentTypes": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ] + }, "Identity": { "Tokens": { "SigningKey": "secret-signing-key", diff --git a/src/common/Elsa.Api.Common/Abstractions/Endpoints.cs b/src/common/Elsa.Api.Common/Abstractions/Endpoints.cs index 806de8138..051aa209f 100644 --- a/src/common/Elsa.Api.Common/Abstractions/Endpoints.cs +++ b/src/common/Elsa.Api.Common/Abstractions/Endpoints.cs @@ -2,6 +2,17 @@ using FastEndpoints; namespace Elsa.Abstractions; +public abstract class ElsaEndpointWithoutRequest : EndpointWithoutRequest +{ + protected void ConfigurePermissions(params string[] permissions) + { + if (!EndpointSecurityOptions.SecurityIsEnabled) + AllowAnonymous(); + else + Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray()); + } +} + public abstract class ElsaEndpointWithoutRequest : EndpointWithoutRequest where TResponse : notnull { protected void ConfigurePermissions(params string[] permissions) diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj b/src/modules/Elsa.Http/Elsa.Http.csproj index 7a879f69d..92e943ed9 100644 --- a/src/modules/Elsa.Http/Elsa.Http.csproj +++ b/src/modules/Elsa.Http/Elsa.Http.csproj @@ -24,6 +24,7 @@ + diff --git a/src/modules/Elsa.Http/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Http/Extensions/ExpressionExecutionContextExtensions.cs new file mode 100644 index 000000000..c86313fe1 --- /dev/null +++ b/src/modules/Elsa.Http/Extensions/ExpressionExecutionContextExtensions.cs @@ -0,0 +1,51 @@ +using Elsa.Expressions.Models; +using Elsa.Http.Contracts; +using Elsa.Http.Models; +using Elsa.Http.Options; +using Elsa.SasTokens.Contracts; +using Microsoft.Extensions.Options; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +internal static class ExpressionExecutionContextExtensions +{ + public static string GenerateEventTriggerUrl(this ExpressionExecutionContext context, string eventName, TimeSpan lifetime) + { + var token = context.GenerateEventTriggerTokenInternal(eventName, lifetime); + return context.GenerateEventTriggerUrlInternal(token); + } + + public static string GenerateEventTriggerUrl(this ExpressionExecutionContext context, string eventName, DateTimeOffset expiresAt) + { + var token = context.GenerateEventTriggerTokenInternal(eventName, expiresAt: expiresAt); + return context.GenerateEventTriggerUrlInternal(token); + } + + public static string GenerateEventTriggerUrl(this ExpressionExecutionContext context, string eventName) + { + var token = context.GenerateEventTriggerTokenInternal(eventName); + return context.GenerateEventTriggerUrlInternal(token); + } + + private static string GenerateEventTriggerUrlInternal(this ExpressionExecutionContext context, string token) + { + var options = context.GetRequiredService>().Value; + var url = $"{options.ApiRoutePrefix}/events/trigger?t={token}"; + var absoluteUrlProvider = context.GetRequiredService(); + return absoluteUrlProvider.ToAbsoluteUrl(url).ToString(); + } + + private static string GenerateEventTriggerTokenInternal(this ExpressionExecutionContext context, string eventName, TimeSpan? lifetime = default, DateTimeOffset? expiresAt = default) + { + var workflowInstanceId = context.GetWorkflowExecutionContext().Id; + var payload = new EventTokenPayload(eventName, workflowInstanceId); + var tokenService = context.GetRequiredService(); + + return lifetime != null + ? tokenService.CreateToken(payload, lifetime.Value) + : expiresAt != null + ? tokenService.CreateToken(payload, expiresAt.Value) + : tokenService.CreateToken(payload); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Extensions/ActivityContextExtensions.cs b/src/modules/Elsa.Http/Extensions/HttpActivityExecutionContextExtensions.cs similarity index 93% rename from src/modules/Elsa.Http/Extensions/ActivityContextExtensions.cs rename to src/modules/Elsa.Http/Extensions/HttpActivityExecutionContextExtensions.cs index a29a3660b..550435665 100644 --- a/src/modules/Elsa.Http/Extensions/ActivityContextExtensions.cs +++ b/src/modules/Elsa.Http/Extensions/HttpActivityExecutionContextExtensions.cs @@ -1,11 +1,13 @@ using Elsa.Http.Contracts; +using Elsa.Http.Models; +using Elsa.SasTokens.Contracts; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Models; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; -internal static class ActivityContextExtensions +internal static class HttpActivityExecutionContextExtensions { public static async Task ParseContentAsync(this ActivityExecutionContext context, Stream content, string contentType, Type? returnType, CancellationToken cancellationToken) { diff --git a/src/modules/Elsa.Http/Models/EventTokenPayload.cs b/src/modules/Elsa.Http/Models/EventTokenPayload.cs new file mode 100644 index 000000000..c35deb837 --- /dev/null +++ b/src/modules/Elsa.Http/Models/EventTokenPayload.cs @@ -0,0 +1,8 @@ +namespace Elsa.Http.Models; + +/// +/// Represents the payload of an event, serialized as a secured token. +/// +/// The name of the event. +/// The ID of the workflow instance to trigger with the event. +public record EventTokenPayload(string EventName, string WorkflowInstanceId); \ No newline at end of file diff --git a/src/modules/Elsa.Http/Options/HttpActivityOptions.cs b/src/modules/Elsa.Http/Options/HttpActivityOptions.cs index 2894059dc..4a5ee4ae1 100644 --- a/src/modules/Elsa.Http/Options/HttpActivityOptions.cs +++ b/src/modules/Elsa.Http/Options/HttpActivityOptions.cs @@ -17,10 +17,15 @@ public class HttpActivityOptions /// public Uri BaseUrl { get; set; } = default!; + /// + /// The prefix used for API routes. + /// + public string ApiRoutePrefix { get; set; } = "elsa/api"; + /// /// A configurable set of available content types available from the activity. /// - public ISet AvailableContentTypes { get; set; } = new HashSet + public ISet AvailableContentTypes { get; set; } = new SortedSet { "application/json", "application/xml", diff --git a/src/modules/Elsa.Http/Scripting/JavaScript/HttpJavaScriptHandler.cs b/src/modules/Elsa.Http/Scripting/JavaScript/HttpJavaScriptHandler.cs index 46125aebd..16bb014d3 100644 --- a/src/modules/Elsa.Http/Scripting/JavaScript/HttpJavaScriptHandler.cs +++ b/src/modules/Elsa.Http/Scripting/JavaScript/HttpJavaScriptHandler.cs @@ -1,6 +1,7 @@ using Elsa.Extensions; using Elsa.Http.Models; using Elsa.JavaScript.Notifications; +using Elsa.JavaScript.TypeDefinitions.Builders; using Elsa.JavaScript.TypeDefinitions.Contracts; using Elsa.JavaScript.TypeDefinitions.Models; using Elsa.Mediator.Contracts; @@ -12,7 +13,7 @@ namespace Elsa.Http.Scripting.JavaScript; /// Configures the JavaScript engine with additional .NET types that can be instantiated. /// [PublicAPI] -public class HttpJavaScriptHandler : INotificationHandler, ITypeDefinitionProvider +public class HttpJavaScriptHandler : INotificationHandler, ITypeDefinitionProvider, IFunctionDefinitionProvider { private readonly ITypeDescriber _typeDescriber; @@ -23,26 +24,59 @@ public class HttpJavaScriptHandler : INotificationHandler, { _typeDescriber = typeDescriber; } - + /// - public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) + Task INotificationHandler.HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) { var engine = notification.Engine; engine.RegisterType(); engine.RegisterType(); + + var activityExecutionContext = notification.Context; + + engine.SetValue("createEventTriggerUrl", (Func)((eventName, lifetimeOrExpiryDate) => + { + return lifetimeOrExpiryDate switch + { + TimeSpan lifetime => activityExecutionContext.GenerateEventTriggerUrl(eventName, lifetime), + DateTimeOffset expiryDate => activityExecutionContext.GenerateEventTriggerUrl(eventName, expiryDate), + _ => activityExecutionContext.GenerateEventTriggerUrl(eventName) + }; + })); return Task.CompletedTask; } /// - public ValueTask> GetTypeDefinitionsAsync(TypeDefinitionContext context) + ValueTask> ITypeDefinitionProvider.GetTypeDefinitionsAsync(TypeDefinitionContext context) { var definitions = GetTypeDefinitions(context); return new(definitions); } + /// + ValueTask> IFunctionDefinitionProvider.GetFunctionDefinitionsAsync(TypeDefinitionContext context) + { + var definitions = GetFunctionDefinitions(context); + return new(definitions); + } + private IEnumerable GetTypeDefinitions(TypeDefinitionContext context) { yield return _typeDescriber.DescribeType(typeof(HttpRequestHeaders)); yield return _typeDescriber.DescribeType(typeof(Downloadable)); } + + private IEnumerable GetFunctionDefinitions(TypeDefinitionContext context) + { + yield return CreateFunctionDefinition(function => function.Name("createEventTriggerUrl").ReturnType("string").Parameter("eventName", "string")); + yield return CreateFunctionDefinition(function => function.Name("createEventTriggerUrl").ReturnType("string").Parameter("eventName", "string").Parameter("lifetime", "TimeSpan")); + yield return CreateFunctionDefinition(function => function.Name("createEventTriggerUrl").ReturnType("string").Parameter("eventName", "string").Parameter("expiresAt", "DateTimeOffset")); + } + + private FunctionDefinition CreateFunctionDefinition(Action setup) + { + var builder = new FunctionDefinitionBuilder(); + setup(builder); + return builder.BuildFunctionDefinition(); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Services/DefaultAbsoluteUrlProvider.cs b/src/modules/Elsa.Http/Services/DefaultAbsoluteUrlProvider.cs index 39a7f4c96..ddc8c9568 100644 --- a/src/modules/Elsa.Http/Services/DefaultAbsoluteUrlProvider.cs +++ b/src/modules/Elsa.Http/Services/DefaultAbsoluteUrlProvider.cs @@ -4,11 +4,17 @@ using Microsoft.Extensions.Options; namespace Elsa.Http.Services; +/// public class DefaultAbsoluteUrlProvider : IAbsoluteUrlProvider { private readonly IOptions _options; + + /// + /// Initializes a new instance of the class. + /// public DefaultAbsoluteUrlProvider(IOptions options) => _options = options; + /// public Uri ToAbsoluteUrl(string relativePath) { var baseUrl = _options.Value.BaseUrl; diff --git a/src/modules/Elsa.SasTokens/Contracts/DataProtectorTokenService.cs b/src/modules/Elsa.SasTokens/Contracts/DataProtectorTokenService.cs new file mode 100644 index 000000000..df79897c5 --- /dev/null +++ b/src/modules/Elsa.SasTokens/Contracts/DataProtectorTokenService.cs @@ -0,0 +1,66 @@ +using System.Text.Json; +using Microsoft.AspNetCore.DataProtection; + +namespace Elsa.SasTokens.Contracts; + +/// +/// A service that can create and decrypt SAS (Shared Access Signature) tokens using the service. +/// +public class DataProtectorTokenService : ITokenService +{ + private readonly IDataProtector _dataProtector; + + /// + /// Initializes a new instance of the class. + /// + public DataProtectorTokenService(IDataProtectionProvider dataProtector) + { + _dataProtector = dataProtector.CreateProtector("Elsa Tokens"); + } + + /// + public string CreateToken(T payload, TimeSpan lifetime) + { + var json = JsonSerializer.Serialize(payload); + return _dataProtector.ToTimeLimitedDataProtector().Protect(json, lifetime); + } + + /// + public string CreateToken(T payload, DateTimeOffset expiresAt) + { + var json = JsonSerializer.Serialize(payload); + return _dataProtector.ToTimeLimitedDataProtector().Protect(json, expiresAt); + } + + /// + public string CreateToken(T payload) + { + var json = JsonSerializer.Serialize(payload); + return _dataProtector.Protect(json); + } + + /// + public bool TryDecryptToken(string token, out T payload) + { + payload = default!; + + try + { + payload = DecryptToken(token); + return true; + } + catch + { + // ignored. + } + + return false; + } + + /// + public T DecryptToken(string token) + { + var json = _dataProtector.Unprotect(token); + return JsonSerializer.Deserialize(json)!; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.SasTokens/Contracts/ITokenService.cs b/src/modules/Elsa.SasTokens/Contracts/ITokenService.cs new file mode 100644 index 000000000..8d489d555 --- /dev/null +++ b/src/modules/Elsa.SasTokens/Contracts/ITokenService.cs @@ -0,0 +1,32 @@ +namespace Elsa.SasTokens.Contracts; + +/// +/// A service that can create and decrypt SAS (Shared Access Signature) tokens. +/// +public interface ITokenService +{ + /// + /// Creates a SAS (Shared Access Signature) token containing the specified data. + /// + string CreateToken(T payload, TimeSpan lifetime); + + /// + /// Creates a SAS (Shared Access Signature) token containing the specified data. + /// + string CreateToken(T payload, DateTimeOffset expiresAt); + + /// + /// Creates a SAS (Shared Access Signature) token containing the specified data. + /// + string CreateToken(T payload); + + /// + /// Decrypts the specified SAS token. + /// + T DecryptToken(string token); + + /// + /// Decrypts the specified SAS token. + /// + bool TryDecryptToken(string token, out T payload); +} \ No newline at end of file diff --git a/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj b/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj new file mode 100644 index 000000000..40b59d99b --- /dev/null +++ b/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj @@ -0,0 +1,22 @@ + + + + + + + net6.0;net7.0 + + Provides services to geenrate SAS tokens. + + elsa module security sas tokens + + + + + + + + + + + diff --git a/src/modules/Elsa.SasTokens/Extensions/ModuleExtensions.cs b/src/modules/Elsa.SasTokens/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..9c42b9c45 --- /dev/null +++ b/src/modules/Elsa.SasTokens/Extensions/ModuleExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Features.Services; +using Elsa.SasTokens.Features; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Provides extensions to install the feature. +/// +public static class ModuleExtensions +{ + /// + /// Install the feature. + /// + public static IModule UseSasTokens(this IModule module, Action? configure = default) + { + module.Configure(configure); + return module; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.SasTokens/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.SasTokens/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..c468b5db1 --- /dev/null +++ b/src/modules/Elsa.SasTokens/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using Elsa.SasTokens.Contracts; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.SasTokens.Extensions; + +/// +/// Contains extension methods for the interface. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds the SAS tokens module to the service collection. + /// + public static IServiceCollection AddSasTokens(this IServiceCollection services, Func dataProtectionProvider) + { + services.AddSingleton(sp => + { + var protectionProvider = dataProtectionProvider(sp); + return new DataProtectorTokenService(protectionProvider); + }); + return services; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.SasTokens/Features/SasTokensFeature.cs b/src/modules/Elsa.SasTokens/Features/SasTokensFeature.cs new file mode 100644 index 000000000..b4b3ec0b0 --- /dev/null +++ b/src/modules/Elsa.SasTokens/Features/SasTokensFeature.cs @@ -0,0 +1,28 @@ +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Elsa.SasTokens.Extensions; +using Microsoft.AspNetCore.DataProtection; + +namespace Elsa.SasTokens.Features; + +/// +/// Adds the SAS tokens feature to the workflow runtime. +/// +public class SasTokensFeature : FeatureBase +{ + /// + public SasTokensFeature(IModule module) : base(module) + { + } + + /// + /// Gets or sets the data protection provider used to create the used to encrypt and decrypt the SAS tokens. + /// + public Func DataProtectionProvider { get; set; } = _ => Microsoft.AspNetCore.DataProtection.DataProtectionProvider.Create("Elsa Workflows"); + + /// + public override void Apply() + { + Services.AddSasTokens(DataProtectionProvider); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.SasTokens/FodyWeavers.xml b/src/modules/Elsa.SasTokens/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.SasTokens/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Events/Trigger/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerAuthenticated/Endpoint.cs similarity index 94% rename from src/modules/Elsa.Workflows.Api/Endpoints/Events/Trigger/Endpoint.cs rename to src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerAuthenticated/Endpoint.cs index 1d6cddf4a..fb082732f 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/Events/Trigger/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerAuthenticated/Endpoint.cs @@ -1,10 +1,11 @@ using Elsa.Abstractions; using Elsa.Http.Contracts; +using Elsa.Workflows.Api.Endpoints.Events.Trigger; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Runtime.Contracts; using JetBrains.Annotations; -namespace Elsa.Workflows.Api.Endpoints.Events.Trigger; +namespace Elsa.Workflows.Api.Endpoints.Events.TriggerAuthenticated; /// /// Triggers all workflows that are waiting for the specified event. diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Events/Trigger/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerAuthenticated/Models.cs similarity index 100% rename from src/modules/Elsa.Workflows.Api/Endpoints/Events/Trigger/Models.cs rename to src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerAuthenticated/Models.cs diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerPublic/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerPublic/Endpoint.cs new file mode 100644 index 000000000..34cc440e5 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerPublic/Endpoint.cs @@ -0,0 +1,56 @@ +using Elsa.Abstractions; +using Elsa.Http.Contracts; +using Elsa.Http.Models; +using Elsa.SasTokens.Contracts; +using Elsa.Workflows.Runtime.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Workflows.Api.Endpoints.Events.TriggerPublic; + +/// +/// Resumes a workflow instance blocked by a specified event encoded in the provided SAS token. +/// +[PublicAPI] +internal class Trigger : ElsaEndpointWithoutRequest +{ + private readonly ITokenService _tokenService; + private readonly IEventPublisher _eventPublisher; + private readonly IHttpBookmarkProcessor _httpBookmarkProcessor; + + /// + public Trigger(ITokenService tokenService, IEventPublisher eventPublisher, IHttpBookmarkProcessor httpBookmarkProcessor) + { + _tokenService = tokenService; + _eventPublisher = eventPublisher; + _httpBookmarkProcessor = httpBookmarkProcessor; + } + + /// + public override void Configure() + { + Get("/events/trigger"); + AllowAnonymous(); + } + + /// + public override async Task HandleAsync(CancellationToken cancellationToken) + { + var token = Query("t")!; + + if (!_tokenService.TryDecryptToken(token, out var payload)) + { + AddError("Invalid token."); + await SendErrorsAsync(cancellation: cancellationToken); + return; + } + + var eventName = payload.EventName; + var workflowInstanceId = payload.WorkflowInstanceId; + var results = await _eventPublisher.PublishAsync(eventName, workflowInstanceId: workflowInstanceId, cancellationToken: cancellationToken); + + await _httpBookmarkProcessor.ProcessBookmarks(results, cancellationToken: cancellationToken); + + if (!HttpContext.Response.HasStarted) + await SendOkAsync(cancellationToken); + } +} \ No newline at end of file