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.IO; 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() .AddCheck("ai_services"); // 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 => { // AllowAll policy - for production without credentials // Note: Cannot use AllowAnyOrigin() with AllowCredentials() options.AddPolicy("AllowAll", builder => { builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); // Note: No AllowCredentials() - cannot combine with AllowAnyOrigin() }); // Development policy - for Docker and local development with credentials options.AddPolicy("Development", builder => { builder.WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:5175", "http://localhost:3000") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); // Docker policy - for when frontend is served from Docker nginx options.AddPolicy("Docker", builder => { builder.WithOrigins("http://localhost:3000") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); 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(); builder.Services.AddScoped(); builder.Services.AddScoped(); 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"); // APPLICATION LAYER - Use Cases & Services // ============================================ // 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); }); // Add AI service configurations builder.Services.Configure(builder.Configuration.GetSection("Vosk")); builder.Services.Configure(builder.Configuration.GetSection("Coqui")); // Validate AI service configurations try { ValidateAiConfigurations(builder.Configuration); } catch (Exception) { // Skip validation during EF migrations and design-time when configs may not be fully set } // Register AI services (Infrastructure implementations of Domain interfaces) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ============================================ // APPLICATION LAYER - Use Cases & Services // ======================================================================================== // APPLICATION LAYER - Use Cases & Services // ============================================ // Register command handlers // Register application services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Register AI higher-level services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Register Story services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped, CreateLessonCommandHandler>(); // Note: QuizQuestionService requires IQuizRepository, ILessonRepository, and ProgressService which are registered above // Register Quiz command handlers builder.Services.AddScoped, CreateQuizCommandHandler>(); builder.Services.AddScoped, UpdateQuizCommandHandler>(); builder.Services.AddScoped, DeleteQuizCommandHandler>(); builder.Services.AddScoped, GetQuizWithQuestionsCommandHandler>(); var app = builder.Build(); // Create static file directories bool isEfDesignTime = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true"; if (!isEfDesignTime) { try { Directory.CreateDirectory(Path.Combine("wwwroot", "audio", "story")); } catch { // Skip during migrations } } // Configure the HTTP request pipeline. // Use exception middleware first (to catch all exceptions) app.UseExceptionMiddleware(); // Use CORS - must be early in the pipeline, before UseAuthorization // In Docker, use "Docker" policy to allow credentials from localhost:3000 // In development without Docker, use "Development" policy app.UseCors(app.Environment.IsDevelopment() ? "Development" : "Docker"); // Serve static files (audio, etc.) app.UseStaticFiles(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.UseSwagger(); app.UseSwaggerUI(); } // Use Authentication & Authorization app.UseAuthentication(); app.UseAuthorization(); // 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 // Run migrations and seed in Development, or just migrations in Production/Docker // In Docker, we can control seeding via environment variable var shouldSeed = app.Environment.IsDevelopment() || (app.Configuration.GetValue("SeedDatabase")); if (shouldSeed) { await app.SeedDatabaseAsync(); } else { // In Production/Docker, ensure migrations are applied with retry logic await ApplyMigrationsWithRetry(app, maxRetries: 10, delaySeconds: 5); } // 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(); // Helper method to validate AI service configurations static void ValidateAiConfigurations(IConfiguration configuration) { // Skip validation during EF migrations if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_EF") == "true") { return; } // Validate Mistral configuration (only if API key is set) var mistralConfig = configuration.GetSection("Mistral").Get() ?? new MistralConfig(); if (!string.IsNullOrWhiteSpace(mistralConfig.ApiKey)) { mistralConfig.Validate(); } // Validate Vosk configuration (only if model path is set) var voskConfig = configuration.GetSection("Vosk").Get() ?? new VoskConfig(); if (!string.IsNullOrWhiteSpace(voskConfig.ModelPath)) { voskConfig.Validate(); } // Validate Coqui configuration (only if Python path is set) var coquiConfig = configuration.GetSection("Coqui").Get() ?? new CoquiConfig(); if (!string.IsNullOrWhiteSpace(coquiConfig.PythonPath)) { coquiConfig.Validate(); } Log.Information("All AI service configurations validated successfully"); } // Helper method to apply migrations with retry logic for Docker static async Task ApplyMigrationsWithRetry(WebApplication app, int maxRetries, int delaySeconds) { int retryCount = 0; while (retryCount < maxRetries) { try { using var scope = app.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.Database.Migrate(); Log.Information("Database migrations applied in Production environment"); return; } catch (Exception ex) { retryCount++; Log.Warning(ex, "Failed to apply database migrations (attempt {Attempt}/{MaxAttempts}). Retrying in {DelaySeconds} seconds...", retryCount, maxRetries, delaySeconds); if (retryCount >= maxRetries) { Log.Fatal(ex, "Failed to apply database migrations after {MaxAttempts} attempts", maxRetries); throw; } await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); } } } } 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); }