using Htmx.ApiDemo; using Htmx.ApiDemo.Data; using Immediate.Apis; using Immediate.Apis.Shared; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; // ── Explicit BsonClassMap — no AutoMap() reflection, fully AOT-safe ─────── BsonClassMap.RegisterClassMap(cm => { cm.MapIdProperty(u => u.Id).SetSerializer(new ObjectIdSerializer()); cm.MapProperty(u => u.Email).SetElementName("email"); cm.MapProperty(u => u.NormalizedEmail).SetElementName("normalizedEmail"); cm.MapProperty(u => u.PasswordHash).SetElementName("passwordHash"); cm.MapProperty(u => u.DisplayName).SetElementName("displayName"); cm.MapProperty(u => u.CreatedAtUtc).SetElementName("createdAtUtc"); cm.SetIgnoreExtraElements(true); }); var builder = WebApplication.CreateSlimBuilder(args); // ── Antiforgery ─────────────────────────────────────────────────────────── builder.Services.AddAntiforgery(); // ── JSON ────────────────────────────────────────────────────────────────── builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); }); // ── MongoDB ─────────────────────────────────────────────────────────────── builder.Services.AddSingleton( new MongoClient(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); // ── Cookie Authentication ───────────────────────────────────────────────── builder.Services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.LogoutPath = "/logout"; options.AccessDeniedPath = "/login"; options.SlidingExpiration = true; options.ExpireTimeSpan = TimeSpan.FromHours(8); }); builder.Services.AddScoped, PasswordHasher>(); builder.Services.AddScoped(); // ── App services ────────────────────────────────────────────────────────── builder.Services.AddHttpContextAccessor(); builder.Services .AddHtmxApiDemoBehaviors() .AddHtmxApiDemoHandlers(); builder.Services.AddOpenApi(); builder.Services.AddAuthorization(); var app = builder.Build(); // Ensure the unique index on NormalizedEmail exists (runs once on startup, idempotent). using (var scope = app.Services.CreateScope()) await scope.ServiceProvider.GetRequiredService().EnsureIndexesAsync(); if (app.Environment.IsDevelopment()) app.MapOpenApi(); app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); // ── Guard: redirect unauthenticated users to /login ─────────────────────── app.Use(async (context, next) => { var path = context.Request.Path.Value ?? ""; bool isPublic = path.StartsWith("/login", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/register", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/logout", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/css/", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/js/", StringComparison.OrdinalIgnoreCase); if (!isPublic && context.User.Identity?.IsAuthenticated != true) { context.Response.Redirect("/login"); return; } await next(); }); app.MapHtmxApiDemoEndpoints(); app.Run();