Files
2026-04-25 18:05:57 -04:00

107 lines
3.4 KiB
C#

using ControlPlane.Api.Services;
using ControlPlane.Core.Messages;
using ControlPlane.Core.Models;
using ControlPlane.Core.Services;
using MassTransit;
using System.Text.Json;
namespace ControlPlane.Api.Endpoints;
public static class ProvisioningEndpoints
{
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
public static IEndpointRouteBuilder MapProvisioningEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/provision").WithTags("Provisioning");
group.MapPost("/", QueueProvisioningJob);
group.MapGet("/{id:guid}", GetJobStatus);
group.MapGet("/{id:guid}/stream", StreamJobEvents);
app.MapGet("/api/tenants", GetTenants).WithTags("Tenants");
return app;
}
private static async Task<IResult> QueueProvisioningJob(
ProvisioningRequest request,
Dictionary<Guid, ProvisioningJob> jobs,
IPublishEndpoint bus)
{
var job = new ProvisioningJob
{
ClientName = request.ClientName,
StateCode = request.StateCode.ToUpperInvariant(),
Subdomain = request.Subdomain,
AdminEmail = request.AdminEmail,
SiteCode = request.SiteCode,
Environment = request.Environment,
Tier = request.Tier,
Status = ProvisioningStatus.Pending
};
jobs[job.Id] = job;
await bus.Publish(new ProvisionClientCommand
{
JobId = job.Id,
ClientName = job.ClientName,
StateCode = job.StateCode,
Subdomain = job.Subdomain,
AdminEmail = job.AdminEmail,
SiteCode = job.SiteCode,
Environment = job.Environment,
Tier = job.Tier
});
return Results.Accepted($"/api/provision/{job.Id}", new { job.Id, job.Status });
}
private static IResult GetJobStatus(Guid id, Dictionary<Guid, ProvisioningJob> jobs) =>
jobs.TryGetValue(id, out var job) ? Results.Ok(job) : Results.NotFound();
private static IResult GetTenants(TenantRegistryService registry) =>
Results.Ok(registry.GetAll());
private static async Task StreamJobEvents(
Guid id,
SseEventBus bus,
Dictionary<Guid, ProvisioningJob> jobs,
HttpContext ctx,
CancellationToken cancellationToken)
{
if (!jobs.ContainsKey(id))
{
ctx.Response.StatusCode = 404;
return;
}
ctx.Response.Headers.ContentType = "text/event-stream";
ctx.Response.Headers.CacheControl = "no-cache";
ctx.Response.Headers.Connection = "keep-alive";
var channel = bus.Subscribe(id);
try
{
await foreach (var evt in channel.Reader.ReadAllAsync(cancellationToken))
{
var json = JsonSerializer.Serialize(evt, JsonOpts);
await ctx.Response.WriteAsync($"data: {json}\n\n", cancellationToken);
await ctx.Response.Body.FlushAsync(cancellationToken);
if (evt.Type is "job_complete" or "job_failed") break;
}
}
catch (OperationCanceledException)
{
// Client disconnected (e.g. browser refresh) — not an error.
}
finally
{
bus.Unsubscribe(id, channel);
}
}
}