using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Auth.ServerIntegration.Tenancy; 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") .RequireAuthorization(PlatformPolicies.ContextRead) .RequireTenant(); legacy.MapGet("/context/regions", async Task( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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); }