Compare commits
No commits in common. "84e499103089090a37909eb8879933a720faf746" and "76e8af4987dd8e1f980d8e8794ce2a7d28ad6fde" have entirely different histories.
84e4991030
...
76e8af4987
15 changed files with 78 additions and 723 deletions
|
|
@ -1,78 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
@ -19,10 +19,6 @@
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
|
|
||||||
// DbSets for domain entities
|
// DbSets for domain entities
|
||||||
public DbSet<Lesson> Lessons { get; set; } = null!;
|
public DbSet<Lesson> Lessons { get; set; } = null!;
|
||||||
public DbSet<User> Users { get; set; } = null!;
|
|
||||||
|
|
||||||
// Note: Value objects are not stored directly as entities.
|
// Note: Value objects are not stored directly as entities.
|
||||||
// They are owned by entities and stored as part of the entity's data.
|
// They are owned by entities and stored as part of the entity's data.
|
||||||
|
|
@ -44,23 +43,6 @@ 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
|
// Seed data (optional) - Note: For EF Core, we need to set properties directly
|
||||||
// In a real application, use migrations or a separate seeding mechanism
|
// In a real application, use migrations or a separate seeding mechanism
|
||||||
// modelBuilder.Entity<Lesson>().HasData(
|
// modelBuilder.Entity<Lesson>().HasData(
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
// <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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
// <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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
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,64 +3,25 @@ using GermanApp.Application.UseCases.Commands;
|
||||||
using GermanApp.Domain.Interfaces;
|
using GermanApp.Domain.Interfaces;
|
||||||
using GermanApp.Infrastructure.Data.DbContext;
|
using GermanApp.Infrastructure.Data.DbContext;
|
||||||
using GermanApp.Infrastructure.Data.Repositories;
|
using GermanApp.Infrastructure.Data.Repositories;
|
||||||
using GermanApp.Infrastructure.Data.SeedData;
|
|
||||||
using GermanApp.Presentation.Endpoints;
|
using GermanApp.Presentation.Endpoints;
|
||||||
using GermanApp.Shared.Middleware;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
// Configure Serilog
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
|
||||||
.MinimumLevel.Debug()
|
|
||||||
.WriteTo.Console()
|
|
||||||
.CreateBootstrapLogger();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
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.
|
// Add services to the container.
|
||||||
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
// Add Health Checks
|
|
||||||
builder.Services.AddHealthChecks()
|
|
||||||
.AddDbContextCheck<AppDbContext>();
|
|
||||||
|
|
||||||
// Configure CORS
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowAll", builder =>
|
|
||||||
{
|
|
||||||
builder.AllowAnyOrigin()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add services for Minimal APIs
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen();
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// INFRASTRUCTURE LAYER - Database & External Services
|
// INFRASTRUCTURE LAYER - Database & External Services
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Add DbContext with PostgreSQL
|
// Add DbContext with SQLite (can be changed to SQL Server, PostgreSQL, etc.)
|
||||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
{
|
{
|
||||||
// Using PostgreSQL for production and development
|
// Using SQLite for development - configure in appsettings.json for production
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")
|
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")
|
||||||
?? "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres");
|
?? "Data Source=germanapp.db");
|
||||||
|
|
||||||
// Enable sensitive data logging in development
|
// Enable sensitive data logging in development
|
||||||
if (builder.Environment.IsDevelopment())
|
if (builder.Environment.IsDevelopment())
|
||||||
|
|
@ -80,13 +41,17 @@ try
|
||||||
// Register command handlers
|
// Register command handlers
|
||||||
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// PRESENTATION LAYER - API
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Add services for Minimal APIs
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|
||||||
// Use exception middleware first (to catch all exceptions)
|
|
||||||
app.UseExceptionMiddleware();
|
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
|
|
@ -94,14 +59,13 @@ try
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use CORS
|
// Initialize database (in development)
|
||||||
app.UseCors("AllowAll");
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
// Use Health Checks
|
using var scope = app.Services.CreateScope();
|
||||||
app.MapHealthChecks("/health");
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
dbContext.Database.EnsureCreated();
|
||||||
// Seed database with initial data
|
}
|
||||||
app.SeedDatabase();
|
|
||||||
|
|
||||||
// Map Clean Architecture endpoints
|
// Map Clean Architecture endpoints
|
||||||
app.MapLessonsEndpoints();
|
app.MapLessonsEndpoints();
|
||||||
|
|
@ -127,15 +91,6 @@ try
|
||||||
.WithName("GetWeatherForecast");
|
.WithName("GetWeatherForecast");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Log.CloseAndFlush();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Existing record for reference
|
// Existing record for reference
|
||||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Text.Json;
|
|
||||||
using GermanApp.Shared.Models;
|
|
||||||
|
|
||||||
namespace GermanApp.Shared.Middleware;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Global exception handling middleware.
|
|
||||||
/// Catches exceptions and returns consistent error responses.
|
|
||||||
/// </summary>
|
|
||||||
public class ExceptionMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
|
||||||
|
|
||||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension method to add exception middleware.
|
|
||||||
/// </summary>
|
|
||||||
public static class ExceptionMiddlewareExtensions
|
|
||||||
{
|
|
||||||
public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder builder) =>
|
|
||||||
builder.UseMiddleware<ExceptionMiddleware>();
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
namespace GermanApp.Shared.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base API response wrapper for consistent API responses.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The data type of the response</typeparam>
|
|
||||||
public record ApiResponse<T>(T Data, string Message = "Success", bool Success = true)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a successful response.
|
|
||||||
/// </summary>
|
|
||||||
public static ApiResponse<T> Ok(T data, string message = "Success") =>
|
|
||||||
new ApiResponse<T>(data, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// API error response for consistent error handling.
|
|
||||||
/// </summary>
|
|
||||||
public record ApiErrorResponse(string Message, int StatusCode, string? Details = null)
|
|
||||||
{
|
|
||||||
public bool Success => false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an error response.
|
|
||||||
/// </summary>
|
|
||||||
public static ApiErrorResponse Error(string message, int statusCode, string? details = null) =>
|
|
||||||
new ApiErrorResponse(message, statusCode, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Paginated API response.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The data type of the items</typeparam>
|
|
||||||
public record PaginatedResponse<T>(IEnumerable<T> Items, int PageNumber, int PageSize, int TotalCount, int TotalPages)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a paginated response.
|
|
||||||
/// </summary>
|
|
||||||
public static PaginatedResponse<T> Create(IEnumerable<T> items, int pageNumber, int pageSize, int totalCount) =>
|
|
||||||
new PaginatedResponse<T>(
|
|
||||||
items,
|
|
||||||
pageNumber,
|
|
||||||
pageSize,
|
|
||||||
totalCount,
|
|
||||||
(int)Math.Ceiling(totalCount / (double)pageSize)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Information"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Warning",
|
|
||||||
"Microsoft.AspNetCore": "Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=prod-db;Port=5432;Database=DeutschLernen;Username=postgres;Password=${DB_PASSWORD}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=staging-db;Port=5432;Database=DeutschLernen;Username=postgres;Password=${DB_PASSWORD}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,8 +5,5 @@
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*"
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Feature: Infrastructure Setup
|
# Feature: Infrastructure Setup
|
||||||
|
|
||||||
> **Status**: 🚀 In Progress
|
> **Status**: ⏳ Planned
|
||||||
> **Priority**: High
|
> **Priority**: High
|
||||||
> **Complexity**: Medium
|
> **Complexity**: Medium
|
||||||
> **Estimate**: 10-14 hours
|
> **Estimate**: 10-14 hours
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue