elsa-core/src/apps/Elsa.ModularServer.Web/Program.cs
Sipke Schoorstra ffff359756
feat(user-tasks): add identity-neutral workflow-bound human tasks (#7955)
Adds durable, identity-neutral, workflow-bound human tasks, and reconciles the
REST surface with the approved Studio contract.

- Flat summary/detail DTOs, a global capability descriptor, and workflow context
  captured at activation.
- Scope is part of the list authorization predicate; manager decisions require
  manage:user-tasks; a denied command answers 404 so it cannot prove a task exists.
- Guest sessions are task-scoped, action-allowlisted, and revoked when the task
  closes. Invitations resolve by token hash through the repository, wait in a
  Data Protection encrypted outbox, and are rate limited per caller.
- Masked form values are disclosed only through an audited reveal command.
- Store-specific concurrency failures are translated into a single
  UserTaskRevisionConflictException, so a concurrent edit returns the documented
  revision-conflict result behind any provider instead of a 500.

EF Core (SQLite, SQL Server, PostgreSQL, MySQL, Oracle) and VNext persistence,
hosted due/reconciliation/delivery workers, docs, and 49 tests.

Note: this branch also carries two commits inherited from its branch point that
are not part of User Tasks and are squashed in here — the revert-version
allocation change from #7917 (WorkflowDefinitionPublisher.RevertVersionAsync now
allocates from the last version rather than the latest) and an NU1903 package
pin. Merged deliberately rather than rebased out.
2026-08-25 00:09:06 +02:00

148 lines
5.6 KiB
C#

using ConsoleLogStreaming.Core.Capture;
using CShells.AspNetCore.Configuration;
using CShells.AspNetCore.Extensions;
using CShells.DependencyInjection;
using Elsa.Dashboard.Api.ShellFeatures;
using Elsa.Diagnostics.ConsoleLogs.Dashboard.ShellFeatures;
using Elsa.Diagnostics.StructuredLogs.Dashboard.ShellFeatures;
using Elsa.ExternalAuthentication.OpenIdConnect.ShellFeatures;
using Elsa.ExternalAuthentication.Secrets.ShellFeatures;
using Elsa.UserTasks.Persistence.EFCore.Sqlite.ShellFeatures;
using Elsa.UserTasks.ShellFeatures;
using Elsa.ModularServer.Web;
using Elsa.ModularServer.Web.Catalog;
using Elsa.Platform.Integration.ShellFeatures;
using Elsa.ShellFeatures;
using Elsa.Workflows.Api.ShellFeatures;
using Elsa.Workflows.Management.ShellFeatures;
using Elsa.Workflows.Runtime.Dashboard.ShellFeatures;
using Elsa.Workflows.Runtime.Distributed.ShellFeatures;
using Elsa.Workflows.Runtime.ShellFeatures;
using Elsa.Workflows.ShellFeatures;
using Elsa.Workflows.Telemetry;
using Nuplane;
using Nuplane.Loading.Hosting.Builder;
using Nuplane.Sources.Directory.Configuration;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
ConsoleStreamHook.Install();
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
configuration.AddJsonFile(configuration["Elsa:PlatformIntegration:ShellOverlayPath"] ?? "platform-shell-overrides.json", optional: true, reloadOnChange: false);
var serviceVersion = typeof(Program).Assembly.GetName().Version?.ToString();
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
logging.ParseStateValues = true;
logging.SetResourceBuilder(CreateOpenTelemetryResource(builder.Environment.ApplicationName, serviceVersion));
logging.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "logs"));
});
services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(builder.Environment.ApplicationName, serviceVersion: serviceVersion))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource(WorkflowInstrumentation.ActivitySourceName)
.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "traces")))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddMeter(WorkflowInstrumentation.MeterName)
.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "metrics")));
var nuplaneConfiguration = configuration.GetSection("Nuplane");
services.AddNuplane(nuplaneConfiguration, nuplane =>
{
nuplane.AddDirectoryFeedsFromConfiguration(nuplaneConfiguration);
nuplane.AutoloadPackages(nuplaneConfiguration.GetSection("Loading"));
nuplane.OnPackagesChanged<MyPackageObserver>();
});
services.AddSingleton<NuplaneAssemblyProvider>();
builder.AddShells(shells => shells
.WithHostAssemblies()
.WithAssemblyProvider<NuplaneAssemblyProvider>()
.WithConfigurationProvider(configuration)
.WithWebRouting(options => options.EnablePathRouting = true)
.WithAuthenticationAndAuthorization()
.ConfigureAllShells(shell =>
{
shell.WithFeatures(
typeof(ElsaFeature),
typeof(WorkflowManagementFeature),
typeof(WorkflowRuntimeFeature),
typeof(WorkflowsFeature),
typeof(DistributedRuntimeFeature),
typeof(ElsaPlatformIntegrationFeature),
typeof(OpenIdConnectExternalAuthenticationFeature),
typeof(ElsaSecretsExternalAuthenticationFeature),
typeof(DashboardApiFeature),
typeof(WorkflowRuntimeDashboardFeature),
typeof(ConsoleLogsDashboardFeature),
typeof(StructuredLogsDashboardFeature),
typeof(WorkflowsApiFeature),
typeof(UserTasksFeature),
typeof(SqliteUserTasksPersistenceShellFeature));
}));
services.AddSingleton<PluginCatalog>();
services.AddHealthChecks();
services.AddAuthentication();
services.AddAuthorization();
var app = builder.Build();
app.MapHealthChecks("/");
app.MapShells();
app.UseAuthentication();
app.UseAuthorization();
app.MapSampleCatalog();
app.Run();
static ResourceBuilder CreateOpenTelemetryResource(string serviceName, string? serviceVersion)
{
return ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: serviceVersion);
}
static void ConfigureDiagnosticsOtlpExporter(OtlpExporterOptions options, IConfiguration configuration, string signal)
{
var protocol = configuration["Diagnostics:OpenTelemetry:Exporter:Protocol"];
options.Protocol = protocol?.Trim().ToLowerInvariant() switch
{
"grpc" => OtlpExportProtocol.Grpc,
"http/protobuf" or "httpprotobuf" => OtlpExportProtocol.HttpProtobuf,
_ => options.Protocol
};
var endpoint = configuration["Diagnostics:OpenTelemetry:Exporter:Endpoint"];
if (!string.IsNullOrWhiteSpace(endpoint))
options.Endpoint = new Uri(GetSignalEndpoint(endpoint, signal, options.Protocol), UriKind.Absolute);
}
static string GetSignalEndpoint(string endpoint, string signal, OtlpExportProtocol protocol)
{
if (protocol != OtlpExportProtocol.HttpProtobuf)
return endpoint;
var trimmed = endpoint.TrimEnd('/');
if (trimmed.EndsWith($"/v1/{signal}", StringComparison.OrdinalIgnoreCase))
return trimmed;
if (trimmed.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
return $"{trimmed}/{signal}";
return $"{trimmed}/v1/{signal}";
}