using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using WorkClub.Api.Auth; using WorkClub.Api.Endpoints.Clubs; using WorkClub.Api.Endpoints.Members; using WorkClub.Api.Endpoints.Shifts; using WorkClub.Api.Endpoints.Tasks; using WorkClub.Api.Middleware; using WorkClub.Api.Services; using WorkClub.Application.Interfaces; using WorkClub.Infrastructure.Data; using WorkClub.Infrastructure.Data.Interceptors; using WorkClub.Infrastructure.Services; using WorkClub.Infrastructure.Seed; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.Services.AddHttpContextAccessor(); 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.AddSingleton(); // Add CORS to allow frontend requests builder.Services.AddCors(options => { options.AddPolicy("AllowFrontend", policy => { policy.WithOrigins("http://localhost:3000") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = builder.Configuration["Keycloak:Authority"]; options.Audience = builder.Configuration["Keycloak:Audience"]; options.RequireHttpsMetadata = false; options.MapInboundClaims = false; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = false, // Disabled for local dev - external clients use localhost:8080, internal use keycloak:8080 ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { Console.WriteLine($"JWT Authentication Failed: {context.Exception.Message}"); if (context.Exception.InnerException != null) { Console.WriteLine($"Inner Exception: {context.Exception.InnerException.Message}"); } return Task.CompletedTask; }, OnTokenValidated = context => { Console.WriteLine($"JWT Token Validated for user: {context.Principal?.Identity?.Name ?? "unknown"}"); return Task.CompletedTask; }, OnChallenge = context => { Console.WriteLine($"JWT Challenge: {context.Error}"); return Task.CompletedTask; } }; }); builder.Services.AddScoped(); builder.Services.AddAuthorizationBuilder() .AddPolicy("RequireGlobalAdmin", policy => policy.RequireAssertion(context => { var realmAccess = context.User.FindFirst("realm_access")?.Value; if (string.IsNullOrEmpty(realmAccess)) return false; try { using var doc = System.Text.Json.JsonDocument.Parse(realmAccess); if (doc.RootElement.TryGetProperty("roles", out var rolesElement) && rolesElement.ValueKind == System.Text.Json.JsonValueKind.Array) { foreach (var role in rolesElement.EnumerateArray()) { if (role.GetString() == "admin") return true; } } } catch { // If JSON parsing fails, fallback to string contains check return realmAccess.Contains("admin", StringComparison.OrdinalIgnoreCase); } return false; })) .AddPolicy("RequireManager", policy => policy.RequireRole("Manager")) .AddPolicy("RequireMember", policy => policy.RequireRole("Manager", "Member")) .AddPolicy("RequireViewer", policy => policy.RequireAuthenticatedUser()); builder.Services.AddDbContext((sp, options) => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) .AddInterceptors( sp.GetRequiredService(), sp.GetRequiredService())); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); if (!string.IsNullOrEmpty(connectionString)) { builder.Services.AddHealthChecks() .AddNpgSql(connectionString); } else { builder.Services.AddHealthChecks(); } var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); using var scope = app.Services.CreateScope(); var seedService = scope.ServiceProvider.GetRequiredService(); await seedService.SeedAsync(); } app.UseHttpsRedirection(); app.UseCors("AllowFrontend"); app.UseAuthentication(); app.UseAuthorization(); app.UseMiddleware(); app.UseMiddleware(); app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = _ => false }); app.MapHealthChecks("/health/ready"); app.MapHealthChecks("/health/startup"); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast"); app.MapGet("/api/debug/claims", (HttpContext context) => { var claims = context.User.Claims.Select(c => new { c.Type, c.Value }).ToList(); var realmAccess = context.User.FindFirst("realm_access")?.Value; // Check if the authorization header is present var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); return Results.Ok(new { isAuthenticated = context.User.Identity?.IsAuthenticated ?? false, authenticationType = context.User.Identity?.AuthenticationType, claimCount = claims.Count, claims = claims, realmAccess = realmAccess, hasAuthHeader = !string.IsNullOrEmpty(authHeader), authHeaderPrefix = authHeader?.Substring(0, Math.Min(20, authHeader?.Length ?? 0)) }); }); app.MapTaskEndpoints(); app.MapShiftEndpoints(); app.MapClubEndpoints(); app.MapAdminClubEndpoints(); app.MapMemberEndpoints(); app.Run(); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } public partial class Program { }