using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; using StellaOps.TimelineIndexer.Core.Abstractions; using StellaOps.TimelineIndexer.Core.Models; using StellaOps.TimelineIndexer.Infrastructure.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); builder.Configuration.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true); builder.Configuration.AddEnvironmentVariables(prefix: "TIMELINE_"); builder.Services.AddTimelineIndexerPostgres(builder.Configuration); builder.Services.AddStellaOpsResourceServerAuthentication( builder.Configuration, configure: options => { options.RequiredScopes.Clear(); }); builder.Services.AddAuthorization(options => { options.AddObservabilityResourcePolicies(); options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .AddRequirements(new StellaOpsScopeRequirement(new[] { StellaOpsScopes.TimelineRead })) .Build(); options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddOpenApi(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/timeline", async ( HttpContext ctx, ITimelineQueryService service, [FromQuery] string? eventType, [FromQuery] string? correlationId, [FromQuery] string? traceId, [FromQuery] string? severity, [FromQuery] DateTimeOffset? since, [FromQuery] long? after, [FromQuery] int? limit, CancellationToken cancellationToken) => { var tenantId = GetTenantId(ctx); var options = new TimelineQueryOptions { EventType = eventType, CorrelationId = correlationId, TraceId = traceId, Severity = severity, Since = since, AfterEventSeq = after, Limit = limit ?? 100 }; var items = await service.QueryAsync(tenantId, options, cancellationToken).ConfigureAwait(false); return Results.Ok(items); }) .RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead); app.MapGet("/timeline/{eventId}", async ( HttpContext ctx, ITimelineQueryService service, string eventId, CancellationToken cancellationToken) => { var tenantId = GetTenantId(ctx); var item = await service.GetAsync(tenantId, eventId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound() : Results.Ok(item); }) .RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead); app.MapPost("/timeline/events", () => Results.Accepted("/timeline/events", new { status = "indexed" })) .RequireAuthorization(StellaOpsResourceServerPolicies.TimelineWrite); app.Run(); static string GetTenantId(HttpContext ctx) { // Temporary: allow explicit header override; fallback to claim if present. if (ctx.Request.Headers.TryGetValue("X-Tenant", out var header) && !string.IsNullOrWhiteSpace(header)) { return header!; } var tenant = ctx.User.FindFirst("tenant")?.Value; if (!string.IsNullOrWhiteSpace(tenant)) { return tenant!; } throw new InvalidOperationException("Tenant not provided"); }