using System.Text; using System.Text.Json; using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using ROLAC.API.Data; using ROLAC.API.Data.Interceptors; using ROLAC.API.Entities; using ROLAC.API.Json; using ROLAC.API.Services; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration; // --------------------------------------------------------------------------- // Database // --------------------------------------------------------------------------- builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddDbContext((sp, opt) => opt.UseNpgsql(config.GetConnectionString("DefaultConnection")) .AddInterceptors(sp.GetRequiredService())); // --------------------------------------------------------------------------- // Identity (API-only — no cookie auth; JWT is the default scheme) // --------------------------------------------------------------------------- builder.Services .AddIdentityCore(opt => { opt.Password.RequiredLength = 8; opt.Password.RequireDigit = true; opt.Password.RequireUppercase = true; opt.Password.RequireLowercase = true; opt.Password.RequireNonAlphanumeric = true; opt.User.RequireUniqueEmail = true; }) .AddRoles() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); // --------------------------------------------------------------------------- // JWT Authentication // --------------------------------------------------------------------------- var jwtKey = config["Jwt:SecretKey"] ?? throw new InvalidOperationException("Jwt:SecretKey is not configured."); builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { // Keep JWT claim names exactly as written ("role", "sub", "email"). // Without this, .NET 8's JsonWebTokenHandler may remap "role" to the // long ClaimTypes.Role URI, which conflicts with RoleClaimType = "role". opt.MapInboundClaims = false; opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = config["Jwt:Issuer"], ValidAudience = config["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)), NameClaimType = "sub", RoleClaimType = "role", ClockSkew = TimeSpan.FromMinutes(1), }; // Diagnostic events — visible in the API console while debugging 401s. opt.Events = new JwtBearerEvents { OnAuthenticationFailed = ctx => { Console.WriteLine( $"[JWT] Auth failed: {ctx.Exception.GetType().Name} — {ctx.Exception.Message}"); return Task.CompletedTask; }, OnChallenge = ctx => { // Fires when a 401 challenge is about to be sent. Console.WriteLine( $"[JWT] Challenge: error={ctx.Error}, description={ctx.ErrorDescription}"); return Task.CompletedTask; }, OnForbidden = ctx => { // Fires when user IS authenticated but lacks the required role (403). Console.WriteLine( $"[JWT] Forbidden: user={ctx.HttpContext.User.Identity?.Name}, " + $"roles=[{string.Join(',', ctx.HttpContext.User.Claims .Where(c => c.Type == "role") .Select(c => c.Value))}]"); return Task.CompletedTask; }, }; }); // --------------------------------------------------------------------------- // CORS // --------------------------------------------------------------------------- var allowedOrigins = config.GetSection("Cors:AllowedOrigins").Get() ?? ["http://localhost:4200"]; builder.Services.AddCors(opt => opt.AddPolicy("Angular", policy => policy.WithOrigins(allowedOrigins) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); // --------------------------------------------------------------------------- // Application services // --------------------------------------------------------------------------- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // --------------------------------------------------------------------------- // Swagger / MVC // --------------------------------------------------------------------------- builder.Services .AddControllers() .AddJsonOptions(opt => { // camelCase in/out + tolerant DateOnly (accepts "yyyy-MM-dd" or full ISO datetime). opt.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; opt.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; opt.JsonSerializerOptions.PropertyNameCaseInsensitive = true; opt.JsonSerializerOptions.Converters.Add(new TolerantDateOnlyConverter()); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(opt => { opt.SwaggerDoc("v1", new() { Title = "ROLAC API", Version = "v1" }); // Enable JWT in Swagger UI opt.AddSecurityDefinition("Bearer", new() { Name = "Authorization", Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, Scheme = "Bearer", BearerFormat = "JWT", In = Microsoft.OpenApi.Models.ParameterLocation.Header, Description = "Enter your JWT access token.", }); opt.AddSecurityRequirement(new() { { new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, [] } }); }); // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- var app = builder.Build(); // Apply migrations + seed on startup using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); await DbSeeder.SeedAsync(scope.ServiceProvider); } if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors("Angular"); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();