ui progressing
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class ContextEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapContextEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var context = app.MapGroup("/api/v2/context")
|
||||
.WithTags("Platform Context");
|
||||
|
||||
context.MapGet("/regions", async Task<IResult>(
|
||||
HttpContext httpContext,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformContextService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(httpContext, resolver, out _, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var regions = await service.GetRegionsAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(regions);
|
||||
})
|
||||
.WithName("GetPlatformContextRegions")
|
||||
.WithSummary("List global regions for context selection")
|
||||
.RequireAuthorization(PlatformPolicies.ContextRead);
|
||||
|
||||
context.MapGet("/environments", async Task<IResult>(
|
||||
HttpContext httpContext,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformContextService service,
|
||||
[FromQuery(Name = "regions")] string? regions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(httpContext, resolver, out _, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var regionFilter = ParseCsv(regions);
|
||||
var environments = await service.GetEnvironmentsAsync(regionFilter, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(environments);
|
||||
})
|
||||
.WithName("GetPlatformContextEnvironments")
|
||||
.WithSummary("List global environments with optional region filter")
|
||||
.RequireAuthorization(PlatformPolicies.ContextRead);
|
||||
|
||||
context.MapGet("/preferences", async Task<IResult>(
|
||||
HttpContext httpContext,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformContextService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(httpContext, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var preferences = await service.GetPreferencesAsync(requestContext!, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(preferences);
|
||||
})
|
||||
.WithName("GetPlatformContextPreferences")
|
||||
.WithSummary("Get persisted context preferences for the current user")
|
||||
.RequireAuthorization(PlatformPolicies.ContextRead);
|
||||
|
||||
context.MapPut("/preferences", async Task<IResult>(
|
||||
HttpContext httpContext,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformContextService service,
|
||||
PlatformContextPreferencesRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(httpContext, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var preferences = await service.UpsertPreferencesAsync(
|
||||
requestContext!,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(preferences);
|
||||
})
|
||||
.WithName("UpdatePlatformContextPreferences")
|
||||
.WithSummary("Update persisted context preferences for the current user")
|
||||
.RequireAuthorization(PlatformPolicies.ContextWrite);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static string[] ParseCsv(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return value
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(item => item.Trim().ToLowerInvariant())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class IntegrationReadModelEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapIntegrationReadModelEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var integrations = app.MapGroup("/api/v2/integrations")
|
||||
.WithTags("Integrations V2");
|
||||
|
||||
integrations.MapGet("/feeds", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IntegrationsReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] IntegrationListQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListFeedsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Status,
|
||||
query.SourceType,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<IntegrationFeedProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListIntegrationFeedsV2")
|
||||
.WithSummary("List advisory feed health/freshness integration projection")
|
||||
.RequireAuthorization(PlatformPolicies.IntegrationsRead);
|
||||
|
||||
integrations.MapGet("/vex-sources", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IntegrationsReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] IntegrationListQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListVexSourcesAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Status,
|
||||
query.SourceType,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<IntegrationVexSourceProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListIntegrationVexSourcesV2")
|
||||
.WithSummary("List VEX source health/freshness integration projection")
|
||||
.RequireAuthorization(PlatformPolicies.IntegrationsVexRead);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
|
||||
public sealed record IntegrationListQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Status,
|
||||
string? SourceType,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class LegacyAliasEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapLegacyAliasEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var legacy = app.MapGroup("/api/v1")
|
||||
.WithTags("Pack22 Legacy Aliases");
|
||||
|
||||
legacy.MapGet("/context/regions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformContextService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out _, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var regions = await service.GetRegionsAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(regions);
|
||||
})
|
||||
.WithName("GetPlatformContextRegionsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 context regions")
|
||||
.RequireAuthorization(PlatformPolicies.ContextRead);
|
||||
|
||||
legacy.MapGet("/releases", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] LegacyReleaseListQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListReleasesAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Type,
|
||||
query.Status,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListReleasesV1Alias")
|
||||
.WithSummary("Legacy alias for v2 releases projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
var runAliases = legacy.MapGroup("/releases/runs");
|
||||
|
||||
runAliases.MapGet(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] LegacyRunListQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListRunsAsync(
|
||||
requestContext!,
|
||||
query.Status,
|
||||
query.Lane,
|
||||
query.Environment,
|
||||
query.Region,
|
||||
query.Outcome,
|
||||
query.NeedsApproval,
|
||||
query.BlockedByDataIntegrity,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseRunProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListReleaseRunsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run list projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunDetailAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunDetailV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run detail projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/timeline", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunTimelineAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunTimelineV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run timeline projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/gate-decision", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunGateDecisionAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunGateDecisionV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run gate decision projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/approvals", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunApprovalsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunApprovalsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run approvals projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/deployments", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunDeploymentsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunDeploymentsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run deployments projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/security-inputs", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunSecurityInputsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunSecurityInputsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run security inputs projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/evidence", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunEvidenceAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunEvidenceV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run evidence projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/rollback", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunRollbackAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunRollbackV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run rollback projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/replay", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunReplayAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunReplayV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run replay projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runAliases.MapGet("/{runId:guid}/audit", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunAuditAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
|
||||
})
|
||||
.WithName("GetReleaseRunAuditV1Alias")
|
||||
.WithSummary("Legacy alias for v2 run audit projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
legacy.MapGet("/topology/regions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] LegacyTopologyRegionQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListRegionsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyRegionProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyRegionsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 topology regions projection")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
legacy.MapGet("/security/findings", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
SecurityReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] LegacySecurityFindingsQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListFindingsAsync(
|
||||
requestContext!,
|
||||
query.Pivot,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Severity,
|
||||
query.Disposition,
|
||||
query.Search,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new SecurityFindingsResponse(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset,
|
||||
page.Pivot,
|
||||
page.PivotBuckets,
|
||||
page.Facets));
|
||||
})
|
||||
.WithName("ListSecurityFindingsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 security findings projection")
|
||||
.RequireAuthorization(PlatformPolicies.SecurityRead);
|
||||
|
||||
legacy.MapGet("/integrations/feeds", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IntegrationsReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] LegacyIntegrationQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListFeedsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Status,
|
||||
query.SourceType,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<IntegrationFeedProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListIntegrationFeedsV1Alias")
|
||||
.WithSummary("Legacy alias for v2 integrations feed projection")
|
||||
.RequireAuthorization(PlatformPolicies.IntegrationsRead);
|
||||
|
||||
legacy.MapGet("/integrations/vex-sources", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IntegrationsReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] LegacyIntegrationQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListVexSourcesAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Status,
|
||||
query.SourceType,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<IntegrationVexSourceProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListIntegrationVexSourcesV1Alias")
|
||||
.WithSummary("Legacy alias for v2 integrations VEX source projection")
|
||||
.RequireAuthorization(PlatformPolicies.IntegrationsVexRead);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IResult ToRunItemResponse<TProjection>(
|
||||
PlatformRequestContext requestContext,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
TProjection? projection)
|
||||
where TProjection : class
|
||||
{
|
||||
if (projection is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "run_not_found", runId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<TProjection>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
projection));
|
||||
}
|
||||
|
||||
public sealed record LegacyReleaseListQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Type,
|
||||
string? Status,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record LegacyRunListQuery(
|
||||
string? Status,
|
||||
string? Lane,
|
||||
string? Environment,
|
||||
string? Region,
|
||||
string? Outcome,
|
||||
bool? NeedsApproval,
|
||||
bool? BlockedByDataIntegrity,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record LegacyTopologyRegionQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record LegacySecurityFindingsQuery(
|
||||
string? Pivot,
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Severity,
|
||||
string? Disposition,
|
||||
string? Search,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record LegacyIntegrationQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Status,
|
||||
string? SourceType,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class ReleaseReadModelEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapReleaseReadModelEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var releases = app.MapGroup("/api/v2/releases")
|
||||
.WithTags("Releases V2");
|
||||
|
||||
releases.MapGet(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] ReleaseListQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListReleasesAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Type,
|
||||
query.Status,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListReleasesV2")
|
||||
.WithSummary("List Pack-22 release projections")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
releases.MapGet("/{releaseId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid releaseId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var detail = await service.GetReleaseDetailAsync(
|
||||
requestContext!,
|
||||
releaseId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (detail is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "release_not_found", releaseId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<ReleaseDetailProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
detail));
|
||||
})
|
||||
.WithName("GetReleaseDetailV2")
|
||||
.WithSummary("Get Pack-22 release detail projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
releases.MapGet("/activity", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] ReleaseActivityQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListActivityAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseActivityProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListReleaseActivityV2")
|
||||
.WithSummary("List cross-release activity timeline")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
releases.MapGet("/approvals", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] ReleaseApprovalsQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListApprovalsAsync(
|
||||
requestContext!,
|
||||
query.Status,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseApprovalProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListReleaseApprovalsV2")
|
||||
.WithSummary("List cross-release approvals queue projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
var runs = releases.MapGroup("/runs");
|
||||
|
||||
runs.MapGet(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] ReleaseRunListQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListRunsAsync(
|
||||
requestContext!,
|
||||
query.Status,
|
||||
query.Lane,
|
||||
query.Environment,
|
||||
query.Region,
|
||||
query.Outcome,
|
||||
query.NeedsApproval,
|
||||
query.BlockedByDataIntegrity,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseRunProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListReleaseRunsV2")
|
||||
.WithSummary("List run-centric release projections for Pack-22 contracts")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunDetailAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunDetailProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunDetailV2")
|
||||
.WithSummary("Get canonical release run detail projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/timeline", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunTimelineAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunTimelineProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunTimelineV2")
|
||||
.WithSummary("Get release run timeline projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/gate-decision", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunGateDecisionAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunGateDecisionProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunGateDecisionV2")
|
||||
.WithSummary("Get release run gate decision projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/approvals", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunApprovalsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunApprovalsProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunApprovalsV2")
|
||||
.WithSummary("Get release run approvals checkpoints projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/deployments", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunDeploymentsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunDeploymentsProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunDeploymentsV2")
|
||||
.WithSummary("Get release run deployments projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/security-inputs", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunSecurityInputsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunSecurityInputsProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunSecurityInputsV2")
|
||||
.WithSummary("Get release run security inputs projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/evidence", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunEvidenceAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunEvidenceProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunEvidenceV2")
|
||||
.WithSummary("Get release run evidence capsule projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/rollback", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunRollbackAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunRollbackProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunRollbackV2")
|
||||
.WithSummary("Get release run rollback projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/replay", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunReplayAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunReplayProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunReplayV2")
|
||||
.WithSummary("Get release run replay projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
runs.MapGet("/{runId:guid}/audit", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
ReleaseReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
Guid runId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetRunAuditAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
||||
return item is null
|
||||
? Results.NotFound(new { error = "run_not_found", runId })
|
||||
: Results.Ok(new PlatformItemResponse<ReleaseRunAuditProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseRunAuditV2")
|
||||
.WithSummary("Get release run audit projection")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
|
||||
public sealed record ReleaseListQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Type,
|
||||
string? Status,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record ReleaseActivityQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record ReleaseApprovalsQuery(
|
||||
string? Status,
|
||||
string? Region,
|
||||
string? Environment,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record ReleaseRunListQuery(
|
||||
string? Status,
|
||||
string? Lane,
|
||||
string? Environment,
|
||||
string? Region,
|
||||
string? Outcome,
|
||||
bool? NeedsApproval,
|
||||
bool? BlockedByDataIntegrity,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class SecurityReadModelEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapSecurityReadModelEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var security = app.MapGroup("/api/v2/security")
|
||||
.WithTags("Security V2");
|
||||
|
||||
security.MapGet("/findings", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
SecurityReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] SecurityFindingsQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListFindingsAsync(
|
||||
requestContext!,
|
||||
query.Pivot,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Severity,
|
||||
query.Disposition,
|
||||
query.Search,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new SecurityFindingsResponse(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset,
|
||||
page.Pivot,
|
||||
page.PivotBuckets,
|
||||
page.Facets));
|
||||
})
|
||||
.WithName("ListSecurityFindingsV2")
|
||||
.WithSummary("List consolidated security findings with pivot/facet schema")
|
||||
.RequireAuthorization(PlatformPolicies.SecurityRead);
|
||||
|
||||
security.MapGet("/disposition", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
SecurityReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] SecurityDispositionQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListDispositionAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Status,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<SecurityDispositionProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListSecurityDispositionV2")
|
||||
.WithSummary("List consolidated security disposition projection (VEX + exceptions read-join)")
|
||||
.RequireAuthorization(PlatformPolicies.SecurityRead);
|
||||
|
||||
security.MapGet("/disposition/{findingId}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
SecurityReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
string findingId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await service.GetDispositionAsync(
|
||||
requestContext!,
|
||||
findingId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (item is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "finding_not_found", findingId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<SecurityDispositionProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetSecurityDispositionV2")
|
||||
.WithSummary("Get consolidated security disposition by finding id")
|
||||
.RequireAuthorization(PlatformPolicies.SecurityRead);
|
||||
|
||||
security.MapGet("/sbom-explorer", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
SecurityReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] SecuritySbomExplorerQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var result = await service.GetSbomExplorerAsync(
|
||||
requestContext!,
|
||||
query.Mode,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.LeftReleaseId,
|
||||
query.RightReleaseId,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new SecuritySbomExplorerResponse(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
Mode: result.Mode,
|
||||
Table: result.Table,
|
||||
GraphNodes: result.GraphNodes,
|
||||
GraphEdges: result.GraphEdges,
|
||||
Diff: result.Diff,
|
||||
TotalComponents: result.TotalComponents,
|
||||
Limit: result.Limit,
|
||||
Offset: result.Offset));
|
||||
})
|
||||
.WithName("GetSecuritySbomExplorerV2")
|
||||
.WithSummary("Get consolidated SBOM explorer projection (table/graph/diff)")
|
||||
.RequireAuthorization(PlatformPolicies.SecurityRead);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
|
||||
public sealed record SecurityFindingsQuery(
|
||||
string? Pivot,
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Severity,
|
||||
string? Disposition,
|
||||
string? Search,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record SecurityDispositionQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? Status,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
|
||||
public sealed record SecuritySbomExplorerQuery(
|
||||
string? Mode,
|
||||
string? Region,
|
||||
string? Environment,
|
||||
string? LeftReleaseId,
|
||||
string? RightReleaseId,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
public static class TopologyReadModelEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapTopologyReadModelEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var topology = app.MapGroup("/api/v2/topology")
|
||||
.WithTags("Topology V2");
|
||||
|
||||
topology.MapGet("/regions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListRegionsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyRegionProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyRegionsV2")
|
||||
.WithSummary("List topology regions")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/environments", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListEnvironmentsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyEnvironmentProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyEnvironmentsV2")
|
||||
.WithSummary("List topology environments")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/targets", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListTargetsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyTargetProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyTargetsV2")
|
||||
.WithSummary("List topology targets")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/hosts", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListHostsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyHostProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyHostsV2")
|
||||
.WithSummary("List topology hosts")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/agents", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListAgentsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyAgentProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyAgentsV2")
|
||||
.WithSummary("List topology agents")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/promotion-paths", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListPromotionPathsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyPromotionPathProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyPromotionPathsV2")
|
||||
.WithSummary("List topology promotion paths")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/workflows", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListWorkflowsAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyWorkflowProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyWorkflowsV2")
|
||||
.WithSummary("List topology workflows")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
topology.MapGet("/gate-profiles", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
TopologyReadModelService service,
|
||||
TimeProvider timeProvider,
|
||||
[AsParameters] TopologyQuery query,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var page = await service.ListGateProfilesAsync(
|
||||
requestContext!,
|
||||
query.Region,
|
||||
query.Environment,
|
||||
query.Limit,
|
||||
query.Offset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<TopologyGateProfileProjection>(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
page.Items,
|
||||
page.Total,
|
||||
page.Limit,
|
||||
page.Offset));
|
||||
})
|
||||
.WithName("ListTopologyGateProfilesV2")
|
||||
.WithSummary("List topology gate profiles")
|
||||
.RequireAuthorization(PlatformPolicies.TopologyRead);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
|
||||
public sealed record TopologyQuery(
|
||||
string? Region,
|
||||
string? Environment,
|
||||
int? Limit,
|
||||
int? Offset);
|
||||
}
|
||||
Reference in New Issue
Block a user