using System.Text; using FlatRender.ContentSvc.Application.Services; using FlatRender.ContentSvc.Domain.Enums; using FlatRender.ContentSvc.Infrastructure.Data; using FlatRender.ContentSvc.Middleware; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Npgsql; var builder = WebApplication.CreateBuilder(args); // ── Database ────────────────────────────────────────────────────────────────── // Native PostgreSQL enums are mapped on the EF provider so Npgsql can read/write // them at runtime (HasPostgresEnum in the model alone is not enough on Npgsql 8+). // PG labels match the C# enum member names exactly, so preserve case verbatim. var enumTr = PreserveCaseNameTranslator.Instance; builder.Services.AddDbContext(options => options.UseNpgsql( builder.Configuration.GetConnectionString("Postgres"), npgsql => { npgsql.MapEnum("choose_mode", "content", enumTr); npgsql.MapEnum("resolution_kind", "content", enumTr); npgsql.MapEnum("scene_kind", "content", enumTr); npgsql.MapEnum("content_element_type", "content", enumTr); npgsql.MapEnum("justify_kind", "content", enumTr); npgsql.MapEnum("ai_input_type", "content", enumTr); npgsql.MapEnum("repeat_sort_strategy", "content", enumTr); npgsql.MapEnum("attr_value_kind", "content", enumTr); npgsql.MapEnum("blog_kind", "content", enumTr); npgsql.MapEnum("slide_type", "content", enumTr); }) .UseSnakeCaseNamingConvention()); // ── JWT Auth ────────────────────────────────────────────────────────────────── builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)), // The token's "role" claim is auto-mapped to ClaimTypes.Role by the default // inbound claim mapping, which is what [Authorize(Roles = "Admin")] reads. }; }); builder.Services.AddAuthorization(); // ── Application Services ────────────────────────────────────────────────────── builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // HTTP client for the OpenAI-compatible AI provider (base URL is per-tenant config). builder.Services.AddHttpClient("openai"); // ── HTTP ────────────────────────────────────────────────────────────────────── builder.Services.AddRouting(opts => { opts.LowercaseUrls = true; opts.AppendTrailingSlash = false; // prevent 301 redirects from gateway calls }); builder.Services.AddControllers() .AddJsonOptions(opts => { opts.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "FlatRender Content API", Version = "v1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", Description = "JWT Bearer token" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); builder.Services.AddHealthChecks() .AddCheck("db", () => HealthCheckResult.Healthy()); builder.Services.AddCors(opts => opts.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); // ── Build ───────────────────────────────────────────────────────────────────── var app = builder.Build(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); } app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapHealthChecks("/health", new HealthCheckOptions { AllowCachingResponses = false }); app.Run();