using FluentValidation; using FluentValidation.AspNetCore; using GermanApp.Application.DTOs; using GermanApp.Application.Services; using GermanApp.Application.Interfaces; using GermanApp.Application.UseCases.Commands; using GermanApp.Domain.Entities; using GermanApp.Domain.Interfaces; using GermanApp.Infrastructure.Data.DbContext; using GermanApp.Infrastructure.Data.Repositories; using GermanApp.Infrastructure.Data.SeedData; using GermanApp.Infrastructure.Services; using GermanApp.Infrastructure.Configuration; using GermanApp.Presentation.Controllers; using GermanApp.Presentation.Validators; using GermanApp.Presentation.Endpoints; using GermanApp.Shared.Middleware; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Serilog; using System.Text; // Configure Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() .CreateBootstrapLogger(); try { var builder = WebApplication.CreateBuilder(args); // Add Serilog to the builder builder.Host.UseSerilog((ctx, lc) => lc .MinimumLevel.Debug() .WriteTo.Console() .ReadFrom.Configuration(ctx.Configuration)); // ============================================ // PRESENTATION LAYER - API Configuration // ============================================ // Add services to the container. builder.Services.AddOpenApi(); // Add Health Checks builder.Services.AddHealthChecks() .AddDbContextCheck(); // Add Password Hasher for custom User entity builder.Services.AddScoped, PasswordHasher>(); // Configure JWT Authentication var jwtKey = builder.Configuration["Jwt:Key"] ?? "super-secret-key-at-least-32-characters"; var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "DeutschLernen"; var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "DeutschLernen"; builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtIssuer, ValidAudience = jwtAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)), ClockSkew = TimeSpan.Zero }; }); // Add Authorization builder.Services.AddAuthorization(); // Register AuthService builder.Services.AddScoped(); // Configure CORS builder.Services.AddCors(options => { options.AddPolicy("AllowAll", builder => { builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); builder.Services.AddControllers(); // ============================================ // PRESENTATION LAYER - Validation // ============================================ // Add FluentValidation builder.Services.AddValidatorsFromAssemblyContaining(); builder.Services.AddFluentValidationAutoValidation(); // Add services for Minimal APIs builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // ============================================ // INFRASTRUCTURE LAYER - Database & External Services // ============================================ // Add DbContext with PostgreSQL builder.Services.AddDbContext(options => { // Using PostgreSQL for production and development options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres"); // Enable sensitive data logging in development if (builder.Environment.IsDevelopment()) { options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); } }); // Register repositories (Infrastructure implementations of Domain interfaces) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ============================================ // INFRASTRUCTURE LAYER - AI Services // ============================================ // Add Memory Cache for caching (used by AI services) builder.Services.AddMemoryCache(); // Add Mistral API configuration builder.Services.Configure(builder.Configuration.GetSection("Mistral")); // Add HttpClient for Mistral API builder.Services.AddHttpClient("MistralClient"); // Register Mistral Connector builder.Services.AddScoped(provider => { var httpClient = provider.GetRequiredService().CreateClient("MistralClient"); var config = provider.GetRequiredService>().Value; var logger = provider.GetRequiredService>(); var cache = provider.GetRequiredService(); return new MistralConnector(httpClient, config, logger, cache); }); // ============================================ // APPLICATION LAYER - Use Cases & Services // ============================================ // Register command handlers // Register application services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped, CreateLessonCommandHandler>(); var app = builder.Build(); // Configure the HTTP request pipeline. // Use exception middleware first (to catch all exceptions) app.UseExceptionMiddleware(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.UseSwagger(); app.UseSwaggerUI(); } // Use Authentication & Authorization app.UseAuthentication(); app.UseAuthorization(); // Use CORS app.UseCors("AllowAll"); // Use Health Checks app.MapHealthChecks("/health"); // Map Auth endpoints app.MapControllerRoute( name: "api", pattern: "api/{controller}/{action}/{id?}" ); // Map Clean Architecture endpoints app.MapLessonsEndpoints(); // Seed database with initial data if (app.Environment.IsDevelopment()) { await app.SeedDatabaseAsync(); } // Keep original WeatherForecast endpoint for reference app.MapGet("/weatherforecast", () => { var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 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.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); } // Existing record for reference record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }