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/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 a7c0db3..d672186 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -3,6 +3,7 @@ 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 Microsoft.EntityFrameworkCore;
@@ -59,13 +60,8 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
-// Initialize database (in development)
-if (app.Environment.IsDevelopment())
-{
- using var scope = app.Services.CreateScope();
- var dbContext = scope.ServiceProvider.GetRequiredService();
- dbContext.Database.EnsureCreated();
-}
+// Seed database with initial data
+app.SeedDatabase();
// Map Clean Architecture endpoints
app.MapLessonsEndpoints();