180 lines
6.8 KiB
C#
180 lines
6.8 KiB
C#
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);
|
|
}
|
|
}
|
|
|