Fix db seeding, migration, repository services

This commit is contained in:
danial23 2025-05-19 22:22:33 -04:00
parent 872dc1e263
commit 6b87902ca7
Signed by: danial23
SSH key fingerprint: SHA256:IJ8VP0j2WMUVweTYnzUUnEjNgPnGx+mAt+RhqWZ01bU
22 changed files with 606 additions and 64 deletions

View file

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.5",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View file

@ -5,11 +5,23 @@
<ProjectReference Include="..\CSR.Domain\CSR.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="9.0.5" />
</ItemGroup>
<PropertyGroup>

View file

@ -0,0 +1,55 @@
namespace CSR.Infrastructure.Data;
using CSR.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
public static class DbInitializer
{
public static async Task SeedDatabase(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var services = scope.ServiceProvider;
var context = services.GetRequiredService<CSRDbContext>();
var config = services.GetRequiredService<IConfiguration>();
var userService = services.GetRequiredService<Application.Interfaces.IUserService>();
var roleRepository = services.GetRequiredService<Application.Interfaces.IRoleRepository>();
var userRepository = services.GetRequiredService<Application.Interfaces.IUserRepository>();
// --- create roles if not exists --- //
if (!await context.Roles.AnyAsync())
{
await roleRepository.AddAsync(Domain.Entities.Role.Admin);
await roleRepository.AddAsync(Domain.Entities.Role.User);
}
// --- create admin user if not exists --- //
var adminUsername = config["Admin:Username"] ?? "admin";
var adminEmail = config["Admin:Email"] ?? "admin@example.com";
var adminPassword = config["Admin:Password"] ?? "Admin123";
var admins = await userRepository.GetAllByRoleIdAsync(Domain.Entities.Role.Admin.Id);
if (admins == null || !admins.Any())
{
var result = await userService.RegisterNewUser(adminUsername, adminEmail, adminPassword);
if (result.User == null)
{
Console.WriteLine($"Error creating admin user: {string.Join(", ", result.Errors!)}");
}
else
{
var adminEntity = await context.Users
.Include(u => u.Role)
.SingleAsync(u => u.Id == result.User.Id);
adminEntity.RoleId = Domain.Entities.Role.Admin.Id;
context.Users.Update(adminEntity);
await context.SaveChangesAsync();
Console.WriteLine("Admin user created successfully.");
}
}
}
}

View file

@ -0,0 +1,92 @@
// <auto-generated />
using CSR.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CSR.Infrastructure.Migrations
{
[DbContext(typeof(CSRDbContext))]
[Migration("20250520020330_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
modelBuilder.Entity("CSR.Infrastructure.Persistence.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("RoleId");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("RoleName");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Roles");
});
modelBuilder.Entity("CSR.Infrastructure.Persistence.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("Password");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("CSR.Infrastructure.Persistence.User", b =>
{
b.HasOne("CSR.Infrastructure.Persistence.Role", "Role")
.WithMany("Users")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("CSR.Infrastructure.Persistence.Role", b =>
{
b.Navigation("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,76 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CSR.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Roles",
columns: table => new
{
RoleId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RoleName = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Roles", x => x.RoleId);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Username = table.Column<string>(type: "TEXT", nullable: false),
Email = table.Column<string>(type: "TEXT", nullable: false),
Password = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
table.ForeignKey(
name: "FK_Users_Roles_RoleId",
column: x => x.RoleId,
principalTable: "Roles",
principalColumn: "RoleId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Roles_RoleName",
table: "Roles",
column: "RoleName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_RoleId",
table: "Users",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_Users_Username",
table: "Users",
column: "Username",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Roles");
}
}
}

View file

@ -0,0 +1,89 @@
// <auto-generated />
using CSR.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CSR.Infrastructure.Migrations
{
[DbContext(typeof(CSRDbContext))]
partial class CSRDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
modelBuilder.Entity("CSR.Infrastructure.Persistence.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("RoleId");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("RoleName");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Roles");
});
modelBuilder.Entity("CSR.Infrastructure.Persistence.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("Password");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("CSR.Infrastructure.Persistence.User", b =>
{
b.HasOne("CSR.Infrastructure.Persistence.Role", "Role")
.WithMany("Users")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("CSR.Infrastructure.Persistence.Role", b =>
{
b.Navigation("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -3,7 +3,6 @@ namespace CSR.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using CSR.Infrastructure.Persistence.Configurations;
public class CSRDbContext(DbContextOptions<CSRDbContext> options) : DbContext(options)
{
public DbSet<User> Users { get; set; }
@ -23,14 +22,9 @@ public class CSRDbContext(DbContextOptions<CSRDbContext> options) : DbContext(op
.WithMany(r => r.Users)
.HasForeignKey(u => u.RoleId);
// --- Seed data --- //
var adminRole = new Role { Id = 1, Name = "Admin" };
var userRole = new Role { Id = 2, Name = "User" };
modelBuilder.Entity<Role>()
.HasData(adminRole, userRole);
.HasIndex(r => r.Name)
.IsUnique();
base.OnModelCreating(modelBuilder);
}

View file

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace CSR.Infrastructure.Persistence
{
public class CSRDbContextFactory : IDesignTimeDbContextFactory<CSRDbContext>
{
public CSRDbContext CreateDbContext(string[] args)
{
// build configuration.
var configuration = new ConfigurationBuilder()
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "..", "CSR.WebUI"))
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"}.json", optional: true)
.AddEnvironmentVariables()
.AddKeyPerFile("/run/secrets", optional: true)
.Build();
// get the database path
var dbPath = configuration["Database:Path"];
if (string.IsNullOrEmpty(dbPath))
{
var folder = Environment.SpecialFolder.LocalApplicationData;
var path = Environment.GetFolderPath(folder);
dbPath = Path.Join(path, "csr.db");
}
// create DbContextOptions
var optionsBuilder = new DbContextOptionsBuilder<CSRDbContext>();
optionsBuilder.UseSqlite($"Data Source={dbPath}");
return new CSRDbContext(optionsBuilder.Options);
}
}
}

View file

@ -9,11 +9,9 @@ public class RoleConfiguration : IEntityTypeConfiguration<Role>
public void Configure(EntityTypeBuilder<Role> builder)
{
builder.Property(u => u.Id)
.HasColumnName("RoleId")
.IsRequired();
.HasColumnName("RoleId");
builder.Property(u => u.Name)
.HasColumnName("RoleName")
.IsRequired();
.HasColumnName("RoleName");
}
}

View file

@ -9,7 +9,6 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
public void Configure(EntityTypeBuilder<User> builder)
{
builder.Property(u => u.PasswordHash)
.HasColumnName("Password")
.IsRequired();
.HasColumnName("Password");
}
}

View file

@ -32,7 +32,6 @@ public class RoleRepository(CSR.Infrastructure.Persistence.CSRDbContext context)
{
var roleEntity = new CSR.Infrastructure.Persistence.Role
{
Id = role.Id,
Name = role.Name
};

View file

@ -1,12 +1,12 @@
namespace Csr.Infrastructure.Persistence.Repositories;
namespace CSR.Infrastructure.Persistence.Repositories;
using CSR.Domain.Entities;
using CSR.Application.Interfaces;
using Microsoft.EntityFrameworkCore;
public class UserRepository(CSR.Infrastructure.Persistence.CSRDbContext context) : IUserRepository
public class UserRepository(CSRDbContext context) : IUserRepository
{
private readonly CSR.Infrastructure.Persistence.CSRDbContext _context = context;
private readonly CSRDbContext _context = context;
public async Task<User?> GetByIdAsync(int id)
{
@ -35,23 +35,97 @@ public class UserRepository(CSR.Infrastructure.Persistence.CSRDbContext context)
}
public async Task AddAsync(User user)
public async Task<User?> GetByUsernameAsync(string username)
{
var userEntity = new CSR.Infrastructure.Persistence.User
var userEntity = await _context.Users
.Include(u => u.Role)
.SingleOrDefaultAsync(u => u.Username == username);
if (userEntity == null)
{
return null; // No entity found, return null domain model
}
var user = User.LoadExisting(
userEntity.Id,
userEntity.Username,
userEntity.Email,
userEntity.PasswordHash,
userEntity.RoleId,
Role.LoadExisting(
userEntity.Role.Id,
userEntity.Role.Name
)
);
return user;
}
public async Task<IEnumerable<User>?> GetAllByRoleIdAsync(int roleId)
{
var roleEntity = await _context.Roles
.Include(r => r.Users)
.FirstOrDefaultAsync(r => r.Id == roleId);
if (roleEntity == null)
{
return null; // No entity found, return null
}
var users = roleEntity.Users
.Select(userEntity => User.LoadExisting(
userEntity.Id,
userEntity.Username,
userEntity.Email,
userEntity.PasswordHash,
userEntity.RoleId,
Role.LoadExisting(
userEntity.Role.Id,
userEntity.Role.Name
)
));
return users;
}
public async Task<User?> AddAsync(User user)
{
var roleEntity = await _context.Roles
.SingleOrDefaultAsync(r => r.Id == user.RoleId)
?? throw new InvalidOperationException($"Role with ID {user.RoleId} does not exist.");
var userEntity = new Persistence.User
{
Username = user.Username,
Email = user.Email,
PasswordHash = user.PasswordHash,
RoleId = user.RoleId,
Role = new CSR.Infrastructure.Persistence.Role
{
Id = user.Role.Id,
Name = user.Role.Name
}
Role = roleEntity
};
_context.Users.Add(userEntity);
try
{
_context.Users.Add(userEntity);
}
catch (DbUpdateException)
{
return null;
}
await _context.SaveChangesAsync();
return User.LoadExisting(
userEntity.Id,
userEntity.Username,
userEntity.Email,
userEntity.PasswordHash,
userEntity.RoleId,
Role.LoadExisting(
userEntity.Role.Id,
userEntity.Role.Name
)
);
}
@ -73,7 +147,7 @@ public class UserRepository(CSR.Infrastructure.Persistence.CSRDbContext context)
userEntity.Email = user.Email;
userEntity.PasswordHash = user.PasswordHash;
userEntity.RoleId = user.RoleId;
userEntity.Role = new CSR.Infrastructure.Persistence.Role { Id = user.Role.Id, Name = user.Role.Name };
userEntity.Role = new Persistence.Role { Id = user.Role.Id, Name = user.Role.Name };
// Prevent EF from trying to update the Role entity
_context.Entry(userEntity.Role).State = EntityState.Unchanged;
@ -85,18 +159,13 @@ public class UserRepository(CSR.Infrastructure.Persistence.CSRDbContext context)
public async Task DeleteAsync(int id)
{
var userEntity = new CSR.Infrastructure.Persistence.User
var userEntity = new Persistence.User
{
Id = id,
Username = string.Empty,
Email = string.Empty,
PasswordHash = string.Empty,
RoleId = 0,
Role = new CSR.Infrastructure.Persistence.Role
{
Id = 0,
Name = string.Empty
}
RoleId = 0
};
_context.Users.Remove(userEntity);

View file

@ -0,0 +1,18 @@
namespace CSR.Infrastructure.Services;
using CSR.Domain.Entities;
public class PasswordHasherService(Microsoft.AspNetCore.Identity.IPasswordHasher<User> passwordHasher) : Domain.Interfaces.IPasswordHasher
{
private readonly Microsoft.AspNetCore.Identity.IPasswordHasher<User> _passwordHasher = passwordHasher;
public string HashPassword(User user, string password)
{
return _passwordHasher.HashPassword(user, password);
}
public bool VerifyHashedPassword(User user, string hashedPassword, string providedPassword)
{
return _passwordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword) != 0;
}
}