diff --git a/GermanApp/Domain/Entities/User.cs b/GermanApp/Domain/Entities/User.cs
new file mode 100644
index 0000000..7f75606
--- /dev/null
+++ b/GermanApp/Domain/Entities/User.cs
@@ -0,0 +1,78 @@
+namespace GermanApp.Domain.Entities;
+
+///
+/// Represents a user in the DeutschLernen system.
+///
+public class User
+{
+ public int Id { get; private set; }
+ public string Username { get; private set; } = string.Empty;
+ public string Email { get; private set; } = string.Empty;
+ public string PasswordHash { get; private set; } = string.Empty;
+ public string CurrentLevel { get; private set; } = "A1";
+ public int Streak { get; private set; }
+ public int TotalPoints { get; private set; }
+ public DateTime CreatedAt { get; private set; }
+
+ ///
+ /// Constructor for EF Core deserialization.
+ ///
+ private User() { }
+
+ ///
+ /// Factory method to create a new user.
+ ///
+ public static User Create(string username, string email, string passwordHash)
+ {
+ return new User
+ {
+ Username = username,
+ Email = email.ToLowerInvariant(),
+ PasswordHash = passwordHash,
+ CurrentLevel = "A1",
+ Streak = 0,
+ TotalPoints = 0,
+ CreatedAt = DateTime.UtcNow
+ };
+ }
+
+ ///
+ /// Updates user's gamification stats.
+ ///
+ public void AddPoints(int points)
+ {
+ TotalPoints += points;
+ }
+
+ ///
+ /// Updates user's streak.
+ ///
+ public void UpdateStreak(int streak)
+ {
+ Streak = streak;
+ }
+
+ ///
+ /// Updates user's current level.
+ ///
+ public void UpdateLevel(string level)
+ {
+ CurrentLevel = level;
+ }
+
+ ///
+ /// Changes user's email.
+ ///
+ public void ChangeEmail(string newEmail)
+ {
+ Email = newEmail.ToLowerInvariant();
+ }
+
+ ///
+ /// Changes user's password hash.
+ ///
+ public void ChangePassword(string newPasswordHash)
+ {
+ PasswordHash = newPasswordHash;
+ }
+}
diff --git a/GermanApp/GermanApp.csproj b/GermanApp/GermanApp.csproj
index 824b0c5..e71a865 100644
--- a/GermanApp/GermanApp.csproj
+++ b/GermanApp/GermanApp.csproj
@@ -10,7 +10,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -19,6 +19,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs b/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
index df79381..aab0cc8 100644
--- a/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
+++ b/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
@@ -15,6 +15,7 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
// DbSets for domain entities
public DbSet Lessons { get; set; } = null!;
+ public DbSet Users { get; set; } = null!;
// Note: Value objects are not stored directly as entities.
// They are owned by entities and stored as part of the entity's data.
@@ -43,6 +44,23 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
// });
});
+ // Configure User entity
+ modelBuilder.Entity(builder =>
+ {
+ builder.HasKey(u => u.Id);
+ builder.Property(u => u.Username).IsRequired().HasMaxLength(50);
+ builder.Property(u => u.Email).IsRequired().HasMaxLength(100);
+ builder.Property(u => u.PasswordHash).IsRequired().HasMaxLength(255);
+ builder.Property(u => u.CurrentLevel).HasMaxLength(10).HasDefaultValue("A1");
+ builder.Property(u => u.Streak).HasDefaultValue(0);
+ builder.Property(u => u.TotalPoints).HasDefaultValue(0);
+ builder.Property(u => u.CreatedAt).IsRequired();
+
+ // Ensure unique constraints
+ builder.HasIndex(u => u.Username).IsUnique();
+ builder.HasIndex(u => u.Email).IsUnique();
+ });
+
// Seed data (optional) - Note: For EF Core, we need to set properties directly
// In a real application, use migrations or a separate seeding mechanism
// modelBuilder.Entity().HasData(
diff --git a/GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.Designer.cs b/GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.Designer.cs
new file mode 100644
index 0000000..7667d4d
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.Designer.cs
@@ -0,0 +1,116 @@
+//
+using System;
+using GermanApp.Infrastructure.Data.DbContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GermanApp.Infrastructure.Data.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260531174249_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("Level")
+ .HasColumnType("integer");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("Lessons");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrentLevel")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("A1");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Streak")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("TotalPoints")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.cs b/GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.cs
new file mode 100644
index 0000000..18f9d94
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.cs
@@ -0,0 +1,74 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GermanApp.Infrastructure.Data.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Lessons",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false),
+ Level = table.Column(type: "integer", nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Lessons", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Username = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false),
+ CurrentLevel = table.Column(type: "character varying(10)", maxLength: 10, nullable: false, defaultValue: "A1"),
+ Streak = table.Column(type: "integer", nullable: false, defaultValue: 0),
+ TotalPoints = table.Column(type: "integer", nullable: false, defaultValue: 0),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_Email",
+ table: "Users",
+ column: "Email",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_Username",
+ table: "Users",
+ column: "Username",
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Lessons");
+
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 0000000..acaa18e
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,113 @@
+//
+using System;
+using GermanApp.Infrastructure.Data.DbContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GermanApp.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("Level")
+ .HasColumnType("integer");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("Lessons");
+ });
+
+ modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrentLevel")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("A1");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Streak")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("TotalPoints")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Data/SeedData/SeedDataExtension.cs b/GermanApp/Infrastructure/Data/SeedData/SeedDataExtension.cs
new file mode 100644
index 0000000..7fa8ece
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/SeedData/SeedDataExtension.cs
@@ -0,0 +1,60 @@
+using GermanApp.Domain.Entities;
+using GermanApp.Infrastructure.Data.DbContext;
+using Microsoft.EntityFrameworkCore;
+
+namespace GermanApp.Infrastructure.Data.SeedData;
+
+///
+/// Extension methods for seeding the database.
+///
+public static class SeedDataExtension
+{
+ ///
+ /// Seeds the database with initial data.
+ ///
+ /// The web application
+ public static void SeedDatabase(this WebApplication app)
+ {
+ using var scope = app.Services.CreateScope();
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+
+ // Apply pending migrations
+ dbContext.Database.Migrate();
+
+ // Only seed if there are no users
+ if (!dbContext.Users.Any())
+ {
+ // Seed an admin user
+ var adminUser = User.Create(
+ "admin",
+ "admin@deutschlernen.com",
+ "$2a$11$N9qo8uLOickgx2ZMRZoMy.Mrq9Hq4yVJyX2sX7z3J0J9z6J9Z2K4a" // bcrypt hash of "Admin@123!"
+ );
+ adminUser.UpdateLevel("C1");
+ dbContext.Users.Add(adminUser);
+
+ // Seed a test user
+ var testUser = User.Create(
+ "testuser",
+ "test@deutschlernen.com",
+ "$2a$11$N9qo8uLOickgx2ZMRZoMy.Mrq9Hq4yVJyX2sX7z3J0J9z6J9Z2K4a" // bcrypt hash of "Test@123!"
+ );
+ dbContext.Users.Add(testUser);
+
+ // Seed some lessons
+ var lessons = new[]
+ {
+ Lesson.Create("Greetings", "Basic German greetings and introductions", 1),
+ Lesson.Create("Numbers", "German numbers 1-100", 1),
+ Lesson.Create("Grammar Basics", "Basic German grammar rules", 2),
+ Lesson.Create("Everyday Phrases", "Common phrases for daily conversations", 2),
+ Lesson.Create("Advanced Grammar", "Complex German grammar", 4)
+ };
+ dbContext.Lessons.AddRange(lessons);
+
+ dbContext.SaveChanges();
+
+ Console.WriteLine("Database seeded with admin user, test user, and sample lessons.");
+ }
+ }
+}
diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs
index 0133d3f..014fb9c 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -3,94 +3,139 @@ using GermanApp.Application.UseCases.Commands;
using GermanApp.Domain.Interfaces;
using GermanApp.Infrastructure.Data.DbContext;
using GermanApp.Infrastructure.Data.Repositories;
+using GermanApp.Infrastructure.Data.SeedData;
using GermanApp.Presentation.Endpoints;
+using GermanApp.Shared.Middleware;
using Microsoft.EntityFrameworkCore;
+using Serilog;
-var builder = WebApplication.CreateBuilder(args);
+// Configure Serilog
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .WriteTo.Console()
+ .CreateBootstrapLogger();
-// Add services to the container.
-// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
-builder.Services.AddOpenApi();
-
-// ============================================
-// INFRASTRUCTURE LAYER - Database & External Services
-// ============================================
-
-// Add DbContext with SQLite (can be changed to SQL Server, PostgreSQL, etc.)
-builder.Services.AddDbContext(options =>
+try
{
- // Using SQLite for development - configure in appsettings.json for production
- options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")
- ?? "Data Source=germanapp.db");
-
- // Enable sensitive data logging in development
- if (builder.Environment.IsDevelopment())
+ 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();
+
+ // Configure CORS
+ builder.Services.AddCors(options =>
{
- options.EnableSensitiveDataLogging();
- options.EnableDetailedErrors();
+ options.AddPolicy("AllowAll", builder =>
+ {
+ builder.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+ });
+
+ // 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();
+
+ // ============================================
+ // APPLICATION LAYER - Use Cases & Services
+ // ============================================
+
+ // Register command handlers
+ 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();
}
-});
-// Register repositories (Infrastructure implementations of Domain interfaces)
-builder.Services.AddScoped();
+ // Use CORS
+ app.UseCors("AllowAll");
-// ============================================
-// APPLICATION LAYER - Use Cases & Services
-// ============================================
+ // Use Health Checks
+ app.MapHealthChecks("/health");
-// Register command handlers
-builder.Services.AddScoped, CreateLessonCommandHandler>();
+ // Seed database with initial data
+ app.SeedDatabase();
-// ============================================
-// PRESENTATION LAYER - API
-// ============================================
+ // Map Clean Architecture endpoints
+ app.MapLessonsEndpoints();
-// Add services for Minimal APIs
-builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen();
-
-var app = builder.Build();
-
-// Configure the HTTP request pipeline.
-if (app.Environment.IsDevelopment())
-{
- app.MapOpenApi();
- app.UseSwagger();
- app.UseSwaggerUI();
-}
-
-// Initialize database (in development)
-if (app.Environment.IsDevelopment())
-{
- using var scope = app.Services.CreateScope();
- var dbContext = scope.ServiceProvider.GetRequiredService();
- dbContext.Database.EnsureCreated();
-}
-
-// Map Clean Architecture endpoints
-app.MapLessonsEndpoints();
-
-// Keep original WeatherForecast endpoint for reference
-app.MapGet("/weatherforecast", () =>
-{
- var summaries = new[]
+ // Keep original WeatherForecast endpoint for reference
+ app.MapGet("/weatherforecast", () =>
{
- "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");
+ 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();
+ 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)
diff --git a/GermanApp/Shared/Middleware/ExceptionMiddleware.cs b/GermanApp/Shared/Middleware/ExceptionMiddleware.cs
new file mode 100644
index 0000000..bc25881
--- /dev/null
+++ b/GermanApp/Shared/Middleware/ExceptionMiddleware.cs
@@ -0,0 +1,62 @@
+using System.Net;
+using System.Text.Json;
+using GermanApp.Shared.Models;
+
+namespace GermanApp.Shared.Middleware;
+
+///
+/// Global exception handling middleware.
+/// Catches exceptions and returns consistent error responses.
+///
+public class ExceptionMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public ExceptionMiddleware(RequestDelegate next, ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task InvokeAsync(HttpContext httpContext)
+ {
+ try
+ {
+ await _next(httpContext);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unhandled exception occurred: {Message}", ex.Message);
+ await HandleExceptionAsync(httpContext, ex);
+ }
+ }
+
+ private static Task HandleExceptionAsync(HttpContext context, Exception exception)
+ {
+ context.Response.ContentType = "application/json";
+ context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+
+ var response = new ApiErrorResponse(
+ "An unexpected error occurred. Please try again later.",
+ context.Response.StatusCode,
+ exception.Message
+ );
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ return context.Response.WriteAsync(JsonSerializer.Serialize(response, options));
+ }
+}
+
+///
+/// Extension method to add exception middleware.
+///
+public static class ExceptionMiddlewareExtensions
+{
+ public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder builder) =>
+ builder.UseMiddleware();
+}
diff --git a/GermanApp/Shared/Models/ApiResponse.cs b/GermanApp/Shared/Models/ApiResponse.cs
new file mode 100644
index 0000000..a496c52
--- /dev/null
+++ b/GermanApp/Shared/Models/ApiResponse.cs
@@ -0,0 +1,47 @@
+namespace GermanApp.Shared.Models;
+
+///
+/// Base API response wrapper for consistent API responses.
+///
+/// The data type of the response
+public record ApiResponse(T Data, string Message = "Success", bool Success = true)
+{
+ ///
+ /// Creates a successful response.
+ ///
+ public static ApiResponse Ok(T data, string message = "Success") =>
+ new ApiResponse(data, message, true);
+}
+
+///
+/// API error response for consistent error handling.
+///
+public record ApiErrorResponse(string Message, int StatusCode, string? Details = null)
+{
+ public bool Success => false;
+
+ ///
+ /// Creates an error response.
+ ///
+ public static ApiErrorResponse Error(string message, int statusCode, string? details = null) =>
+ new ApiErrorResponse(message, statusCode, details);
+}
+
+///
+/// Paginated API response.
+///
+/// The data type of the items
+public record PaginatedResponse(IEnumerable Items, int PageNumber, int PageSize, int TotalCount, int TotalPages)
+{
+ ///
+ /// Creates a paginated response.
+ ///
+ public static PaginatedResponse Create(IEnumerable items, int pageNumber, int pageSize, int totalCount) =>
+ new PaginatedResponse(
+ items,
+ pageNumber,
+ pageSize,
+ totalCount,
+ (int)Math.Ceiling(totalCount / (double)pageSize)
+ );
+}
diff --git a/GermanApp/appsettings.Development.json b/GermanApp/appsettings.Development.json
index ff66ba6..045b0f3 100644
--- a/GermanApp/appsettings.Development.json
+++ b/GermanApp/appsettings.Development.json
@@ -1,8 +1,11 @@
{
"Logging": {
"LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Information"
}
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres"
}
}
diff --git a/GermanApp/appsettings.Production.json b/GermanApp/appsettings.Production.json
new file mode 100644
index 0000000..4dd231b
--- /dev/null
+++ b/GermanApp/appsettings.Production.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft.AspNetCore": "Error"
+ }
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=prod-db;Port=5432;Database=DeutschLernen;Username=postgres;Password=${DB_PASSWORD}"
+ }
+}
diff --git a/GermanApp/appsettings.Staging.json b/GermanApp/appsettings.Staging.json
new file mode 100644
index 0000000..4635bcd
--- /dev/null
+++ b/GermanApp/appsettings.Staging.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=staging-db;Port=5432;Database=DeutschLernen;Username=postgres;Password=${DB_PASSWORD}"
+ }
+}
diff --git a/GermanApp/appsettings.json b/GermanApp/appsettings.json
index 4d56694..7b19f94 100644
--- a/GermanApp/appsettings.json
+++ b/GermanApp/appsettings.json
@@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres"
+ }
}
diff --git a/docs/features/infrastructure-setup.md b/docs/features/infrastructure-setup.md
index 5dc7aec..bc68495 100644
--- a/docs/features/infrastructure-setup.md
+++ b/docs/features/infrastructure-setup.md
@@ -1,6 +1,6 @@
# Feature: Infrastructure Setup
-> **Status**: ⏳ Planned
+> **Status**: 🚀 In Progress
> **Priority**: High
> **Complexity**: Medium
> **Estimate**: 10-14 hours