OPC # 0001: Extract Clarity into standalone repo

This commit is contained in:
amadzarak
2026-04-25 17:26:35 -04:00
commit 60821e219c
65 changed files with 10203 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
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<EnvelopeEncryptionInterceptor>();
//builder.Services.AddSingleton<TenantKeyProvider>();
builder.Services.AddSingleton<IPIIVault, PIIVault>();
#endregion
#region POSTGRESQL
//builder.AddNpgsqlDbContext<ApplicationDbContext>(connectionName: "postgresdb");
builder.AddNpgsqlDbContext<ApplicationDbContext>(
connectionName: "postgresdb",
configureDbContextOptions: options =>
{
options.AddInterceptors(encryptionInterceptor);
});
#endregion
#region MINIO
builder.AddMinioClient("minio");
#endregion
builder.Services.AddScoped<ProfileService>();
// 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<InternalKeycloakHandler>();
builder.Services.AddHttpClient("KeycloakBackchannel")
.AddHttpMessageHandler<InternalKeycloakHandler>();
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<ApplicationDbContext>();
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();
/// <summary>
/// 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.
/// </summary>
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<HttpResponseMessage> 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);
}
}