using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.JobEngine.Core.Domain; using StellaOps.JobEngine.Infrastructure.Repositories; using StellaOps.JobEngine.WebService.Contracts; using StellaOps.JobEngine.WebService.Services; using static StellaOps.Localization.T; namespace StellaOps.JobEngine.WebService.Endpoints; /// /// REST API endpoints for audit log operations. /// public static class AuditEndpoints { /// /// Maps audit endpoints to the route builder. /// public static RouteGroupBuilder MapAuditEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/jobengine/audit") .WithTags("Orchestrator Audit") .RequireAuthorization(JobEnginePolicies.Read) .RequireTenant(); // List and get operations group.MapGet(string.Empty, ListAuditEntries) .WithName("Orchestrator_ListAuditEntries") .WithDescription(_t("orchestrator.audit.list_description")); group.MapGet("{entryId:guid}", GetAuditEntry) .WithName("Orchestrator_GetAuditEntry") .WithDescription(_t("orchestrator.audit.get_description")); group.MapGet("resource/{resourceType}/{resourceId:guid}", GetResourceHistory) .WithName("Orchestrator_GetResourceHistory") .WithDescription(_t("orchestrator.audit.get_resource_history_description")); group.MapGet("latest", GetLatestEntry) .WithName("Orchestrator_GetLatestAuditEntry") .WithDescription(_t("orchestrator.audit.get_latest_description")); group.MapGet("sequence/{startSeq:long}/{endSeq:long}", GetBySequenceRange) .WithName("Orchestrator_GetAuditBySequence") .WithDescription(_t("orchestrator.audit.get_by_sequence_description")); // Summary and verification group.MapGet("summary", GetAuditSummary) .WithName("Orchestrator_GetAuditSummary") .WithDescription(_t("orchestrator.audit.summary_description")); group.MapGet("verify", VerifyAuditChain) .WithName("Orchestrator_VerifyAuditChain") .WithDescription(_t("orchestrator.audit.verify_description")); return group; } private static async Task ListAuditEntries( HttpContext context, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, [FromQuery] string? eventType = null, [FromQuery] string? resourceType = null, [FromQuery] Guid? resourceId = null, [FromQuery] string? actorId = null, [FromQuery] DateTimeOffset? startTime = null, [FromQuery] DateTimeOffset? endTime = null, [FromQuery] int? limit = null, [FromQuery] string? cursor = null, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var effectiveLimit = EndpointHelpers.GetLimit(limit); var offset = EndpointHelpers.ParseCursorOffset(cursor); AuditEventType? parsedEventType = null; if (!string.IsNullOrEmpty(eventType) && Enum.TryParse(eventType, true, out var et)) { parsedEventType = et; } var entries = await repository.ListAsync( tenantId, parsedEventType, resourceType, resourceId, actorId, startTime, endTime, effectiveLimit, offset, cancellationToken).ConfigureAwait(false); var responses = entries.Select(AuditEntryResponse.FromDomain).ToList(); var nextCursor = EndpointHelpers.CreateNextCursor(offset, effectiveLimit, responses.Count); return Results.Ok(new AuditEntryListResponse(responses, nextCursor)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetAuditEntry( HttpContext context, [FromRoute] Guid entryId, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var entry = await repository.GetByIdAsync(tenantId, entryId, cancellationToken).ConfigureAwait(false); if (entry is null) { return Results.NotFound(); } return Results.Ok(AuditEntryResponse.FromDomain(entry)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetResourceHistory( HttpContext context, [FromRoute] string resourceType, [FromRoute] Guid resourceId, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, [FromQuery] int? limit = null, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var effectiveLimit = EndpointHelpers.GetLimit(limit); var entries = await repository.GetByResourceAsync( tenantId, resourceType, resourceId, effectiveLimit, cancellationToken).ConfigureAwait(false); var responses = entries.Select(AuditEntryResponse.FromDomain).ToList(); return Results.Ok(new AuditEntryListResponse(responses, null)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetLatestEntry( HttpContext context, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var entry = await repository.GetLatestAsync(tenantId, cancellationToken).ConfigureAwait(false); if (entry is null) { return Results.NotFound(); } return Results.Ok(AuditEntryResponse.FromDomain(entry)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetBySequenceRange( HttpContext context, [FromRoute] long startSeq, [FromRoute] long endSeq, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); if (startSeq < 1 || endSeq < startSeq) { return Results.BadRequest(new { error = _t("orchestrator.audit.error.invalid_sequence_range") }); } var entries = await repository.GetBySequenceRangeAsync( tenantId, startSeq, endSeq, cancellationToken).ConfigureAwait(false); var responses = entries.Select(AuditEntryResponse.FromDomain).ToList(); return Results.Ok(new AuditEntryListResponse(responses, null)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetAuditSummary( HttpContext context, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, [FromQuery] DateTimeOffset? since = null, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var summary = await repository.GetSummaryAsync(tenantId, since, cancellationToken).ConfigureAwait(false); return Results.Ok(AuditSummaryResponse.FromDomain(summary)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task VerifyAuditChain( HttpContext context, [FromServices] TenantResolver tenantResolver, [FromServices] IAuditRepository repository, [FromQuery] long? startSeq = null, [FromQuery] long? endSeq = null, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var result = await repository.VerifyChainAsync(tenantId, startSeq, endSeq, cancellationToken).ConfigureAwait(false); Infrastructure.JobEngineMetrics.AuditChainVerified(tenantId, result.IsValid); return Results.Ok(ChainVerificationResponse.FromDomain(result)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } }