148 lines
6.8 KiB
C#
148 lines
6.8 KiB
C#
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 serializer registrations — force AOT to preserve constructors ─
|
|
BsonSerializer.RegisterSerializer(new ObjectIdSerializer());
|
|
BsonSerializer.RegisterSerializer(new StringSerializer());
|
|
BsonSerializer.RegisterSerializer(new DateTimeSerializer());
|
|
BsonSerializer.RegisterSerializer(new BooleanSerializer());
|
|
BsonSerializer.RegisterSerializer(new NullableSerializer<DateTime>(new DateTimeSerializer()));
|
|
// ── Explicit BsonClassMap — no AutoMap() reflection, fully AOT-safe ───────
|
|
BsonClassMap.RegisterClassMap<AppUser>(cm =>
|
|
{
|
|
cm.AutoMap();
|
|
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<IMongoClient>(
|
|
new MongoClient(builder.Configuration.GetConnectionString("DefaultConnection")));
|
|
builder.Services.AddScoped<MongoDbService>();
|
|
|
|
// ── 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<IPasswordHasher<AppUser>, PasswordHasher<AppUser>>();
|
|
builder.Services.AddScoped<AuthService>();
|
|
|
|
// ── 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<MongoDbService>().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();
|
|
});
|
|
|
|
// Explicit MapGet/MapPost so RequestDelegateGenerator can intercept and emit
|
|
// NativeAOT-safe endpoint code. Handlers return ValueTask<IResult> which the
|
|
// generator knows how to handle: it emits `await result.ExecuteAsync(httpContext)`.
|
|
app.MapGet("/", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.GetIndexHandler.Command cmd,
|
|
Htmx.ApiDemo.Templates.GetIndexHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(cmd, token));
|
|
|
|
app.MapGet("/greet/{username}/{count?}/{id?}", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.GetGreetingHandler.Query query,
|
|
Htmx.ApiDemo.Templates.GetGreetingHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(query, token));
|
|
|
|
app.MapGet("/login", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.GetLoginHandler.Query query,
|
|
Htmx.ApiDemo.Templates.GetLoginHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(query, token));
|
|
|
|
app.MapPost("/login", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.PostLoginHandler.Command cmd,
|
|
Htmx.ApiDemo.Templates.PostLoginHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(cmd, token));
|
|
|
|
app.MapGet("/register", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.GetRegisterHandler.Query query,
|
|
Htmx.ApiDemo.Templates.GetRegisterHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(query, token));
|
|
|
|
app.MapPost("/register", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.PostRegisterHandler.Command cmd,
|
|
Htmx.ApiDemo.Templates.PostRegisterHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(cmd, token));
|
|
|
|
app.MapPost("/logout", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.PostLogoutHandler.Command cmd,
|
|
Htmx.ApiDemo.Templates.PostLogoutHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(cmd, token));
|
|
|
|
app.MapGet("/ui-demo", static (
|
|
[AsParameters] Htmx.ApiDemo.Templates.GetUiDemoHandler.Query query,
|
|
Htmx.ApiDemo.Templates.GetUiDemoHandler.Handler handler,
|
|
CancellationToken token) => handler.HandleAsync(query, token));
|
|
|
|
app.Run(); |