using Elternbeirat.Contracts; using Elternbeirat.PocketBase; using Elternbeirat.Web.Components; using Elternbeirat.Web.Features.Events; using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorComponents(); // The typed client reads content from PocketBase over its REST API. The base URL // comes from configuration: appsettings for local dev, the PocketBase__BaseUrl // env var on the server (see compose.yaml). var pocketBaseUrl = builder.Configuration["PocketBase:BaseUrl"] ?? throw new InvalidOperationException("PocketBase:BaseUrl is not configured."); builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(pocketBaseUrl)); var app = builder.Build(); // NPM terminates TLS and is the only way to reach the container (no port // mapping in production). Without UseForwardedHeaders the app sees every request // as HTTP and with the proxy IP instead of the client IP. // KnownNetworks/KnownProxies are deliberately empty because only NPM reaches // the container. app.UseForwardedHeaders( new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment() is false) { app.UseExceptionHandler("/Error", createScopeForErrors: true); // No UseHsts() and no UseHttpsRedirection(): NPM sets HSTS and terminates // TLS. Both here would create a redirect loop behind the proxy. } // No UseStatusCodePagesWithReExecute: the Router renders the NotFoundPage // (Pages.NotFound) in process for both an unmatched route and an explicit // NavigationManager.NotFound() from a component (see Routes.razor). Re-executing // every error code to /not-found would also turn a 503 (content source down) into // a "not found" page; leaving it out lets those components keep their own // "temporarily unavailable" markup and 503 status. app.UseAntiforgery(); app.MapStaticAssets(); app.MapRazorComponents(); // Subscribable calendar feed of all events. A minimal API endpoint rather than // a Razor page because it returns text/calendar, not HTML. Reads from PocketBase // per request; an unreachable source yields an empty calendar, not a 500. app.MapGet("/events.ics", async (PocketBaseClient pocketBase, CancellationToken token) => { IReadOnlyList events; try { events = await pocketBase.GetEventsAsync(token); } catch (PocketBaseUnavailableException) { events = []; } return Results.Text(IcsCalendar.Build(events), "text/calendar; charset=utf-8"); }); app.Run();