feature/infrastructure-setup #1
7 changed files with 462 additions and 7 deletions
78
GermanApp/Domain/Entities/User.cs
Normal file
78
GermanApp/Domain/Entities/User.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
namespace GermanApp.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user in the DeutschLernen system.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for EF Core deserialization.
|
||||
/// </summary>
|
||||
private User() { }
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create a new user.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates user's gamification stats.
|
||||
/// </summary>
|
||||
public void AddPoints(int points)
|
||||
{
|
||||
TotalPoints += points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates user's streak.
|
||||
/// </summary>
|
||||
public void UpdateStreak(int streak)
|
||||
{
|
||||
Streak = streak;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates user's current level.
|
||||
/// </summary>
|
||||
public void UpdateLevel(string level)
|
||||
{
|
||||
CurrentLevel = level;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes user's email.
|
||||
/// </summary>
|
||||
public void ChangeEmail(string newEmail)
|
||||
{
|
||||
Email = newEmail.ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes user's password hash.
|
||||
/// </summary>
|
||||
public void ChangePassword(string newPasswordHash)
|
||||
{
|
||||
PasswordHash = newPasswordHash;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
|||
|
||||
// DbSets for domain entities
|
||||
public DbSet<Lesson> Lessons { get; set; } = null!;
|
||||
public DbSet<User> 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<User>(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<Lesson>().HasData(
|
||||
|
|
|
|||
116
GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.Designer.cs
generated
Normal file
116
GermanApp/Infrastructure/Data/Migrations/20260531174249_InitialCreate.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Lessons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("CurrentLevel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasDefaultValue("A1");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<int>("Streak")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("TotalPoints")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GermanApp.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Lessons",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
Level = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Username = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
CurrentLevel = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false, defaultValue: "A1"),
|
||||
Streak = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||
TotalPoints = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||
CreatedAt = table.Column<DateTime>(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Lessons");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// <auto-generated />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Lessons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("CurrentLevel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasDefaultValue("A1");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<int>("Streak")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("TotalPoints")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
60
GermanApp/Infrastructure/Data/SeedData/SeedDataExtension.cs
Normal file
60
GermanApp/Infrastructure/Data/SeedData/SeedDataExtension.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using GermanApp.Domain.Entities;
|
||||
using GermanApp.Infrastructure.Data.DbContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GermanApp.Infrastructure.Data.SeedData;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for seeding the database.
|
||||
/// </summary>
|
||||
public static class SeedDataExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds the database with initial data.
|
||||
/// </summary>
|
||||
/// <param name="app">The web application</param>
|
||||
public static void SeedDatabase(this WebApplication app)
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppDbContext>();
|
||||
dbContext.Database.EnsureCreated();
|
||||
}
|
||||
// Seed database with initial data
|
||||
app.SeedDatabase();
|
||||
|
||||
// Map Clean Architecture endpoints
|
||||
app.MapLessonsEndpoints();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue