using Clarity.Server; using Clarity.Server.Data; using Clarity.Server.Endpoints; using Clarity.Server.Extensions; using Clarity.Server.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; using System.Net; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); // Add service defaults & Aspire client integrations. builder.AddServiceDefaults(); if (!string.IsNullOrEmpty(builder.Configuration.GetConnectionString("cache"))) { builder.AddRedisClientBuilder("cache") .WithOutputCache(); } else { builder.Services.AddOutputCache(); } #region HASHICORP_VAULT var keyProvider = new TenantKeyProvider(); var encryptionInterceptor = new EnvelopeEncryptionInterceptor(keyProvider); // Add them to DI so your bootstrapper can still resolve them! builder.Services.AddSingleton(keyProvider); builder.Services.AddSingleton(encryptionInterceptor); builder.Services.AddClarityVaultCryptography(builder.Configuration); //builder.Services.AddSingleton(); //builder.Services.AddSingleton(); builder.Services.AddSingleton(); #endregion #region POSTGRESQL //builder.AddNpgsqlDbContext(connectionName: "postgresdb"); builder.AddNpgsqlDbContext( connectionName: "postgresdb", configureDbContextOptions: options => { options.AddInterceptors(encryptionInterceptor); }); #endregion #region MINIO builder.AddMinioClient("minio"); #endregion builder.Services.AddScoped(); // Add services to the container. builder.Services.AddProblemDetails(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); // Keycloak__BaseUrl and Keycloak__Realm are injected by ClarityContainerService at container creation. // Fall back to Aspire service-discovery var for local dev (running outside Docker). // BaseUrl = internal URL for fetching OIDC discovery (host.docker.internal:8080 inside Docker) // PublicUrl = public-facing issuer URL that appears in JWT `iss` claim (localhost:8080) // PublicUrl = browser-facing issuer (matches `iss` claim in JWT) e.g. https://keycloak.clarity.test // InternalUrl = container-internal URL for OIDC metadata fetch — avoids self-signed cert issues // BaseUrl is legacy fallback kept for local dev outside Docker var keycloakPublicUrl = builder.Configuration["Keycloak:BaseUrl"] ?? builder.Configuration["services__keycloak__http__0"] ?? "http://localhost:8080"; var keycloakInternalUrl = builder.Configuration["Keycloak:InternalUrl"] ?? "http://keycloak:8080"; var keycloakRealm = builder.Configuration["Keycloak:Realm"] ?? "clarity"; // AddKeycloakJwtBearer uses the named IHttpClientFactory client "KeycloakBackchannel" for all // backchannel requests (discovery + JWKS). Register our rewriting handler on that named client // so every request to keycloak.clarity.test[:port] is rewritten to http://keycloak:8080 before // it leaves the container — avoiding self-signed cert issues and DNS failures. builder.Services.AddTransient(); builder.Services.AddHttpClient("KeycloakBackchannel") .AddHttpMessageHandler(); builder.Services.AddAuthentication() .AddKeycloakJwtBearer("keycloak", realm: keycloakRealm, options => { // Authority = public issuer, must match `iss` claim in the JWT options.Authority = $"{keycloakPublicUrl}/realms/{keycloakRealm}"; options.Audience = "clarity-rest-api"; options.RequireHttpsMetadata = false; // Fetch OIDC discovery doc over internal HTTP to avoid self-signed cert trust issues options.MetadataAddress = $"{keycloakInternalUrl}/realms/{keycloakRealm}/.well-known/openid-configuration"; options.TokenValidationParameters.ValidIssuers = [ $"{keycloakPublicUrl}/realms/{keycloakRealm}", $"{keycloakInternalUrl}/realms/{keycloakRealm}", // Keycloak advertises http://keycloak.clarity.test:8080 as issuer when accessed // directly on port 8080 (before KC_HOSTNAME_URL takes full effect). $"http://keycloak.clarity.test:8080/realms/{keycloakRealm}", $"http://keycloak.clarity.test/realms/{keycloakRealm}", ]; }); builder.Services.AddAuthorization(); var app = builder.Build(); // Run EF Core migrations on every cold start — safe, idempotent, self-healing. // One container = one tenant = no concurrent migrator race. await using (var scope = app.Services.CreateAsyncScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); } await app.InitializeTenantSecurityAsync(); #region MINIO PROVISION BUCKETS await app.ProvisionBucketsAsync("clarity-documents", "clarity-profile-pictures"); #endregion // Configure the HTTP request pipeline. app.UseExceptionHandler(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseOutputCache(); app.UseAuthentication(); app.UseAuthorization(); // Serves Keycloak config to the frontend at runtime so the realm (which is per-tenant) // doesn't have to be baked in at image build time. app.MapGet("/api/config", (IConfiguration cfg) => Results.Ok(new { keycloakUrl = cfg["Keycloak:BaseUrl"] ?? "http://localhost:8080", realm = cfg["Keycloak:Realm"] ?? "clarity", clientId = "clarity-web-app", })).AllowAnonymous(); #if DEBUG app.MapDebugEndpoints(); #endif app.MapProfileEndpoints(); app.MapDefaultEndpoints(); app.UseFileServer(); app.Run(); /// /// Delegating handler registered on the "KeycloakBackchannel" IHttpClientFactory named client. /// Rewrites any backchannel request targeting the public Keycloak hostname /// (e.g. jwks_uri returned by the OIDC discovery doc) to the internal Docker DNS URL, /// avoiding self-signed cert issues inside tenant containers. /// sealed class InternalKeycloakHandler : DelegatingHandler { // Matches http:// or https:// + keycloak.clarity.test + optional :port private static readonly System.Text.RegularExpressions.Regex PublicHostPattern = new(@"https?://keycloak\.clarity\.test(:\d+)?", System.Text.RegularExpressions.RegexOptions.IgnoreCase); private const string InternalUrl = "http://keycloak:8080"; protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct) { if (request.RequestUri is not null) { var rewritten = PublicHostPattern.Replace(request.RequestUri.ToString(), InternalUrl); request.RequestUri = new Uri(rewritten); } return base.SendAsync(request, ct); } }