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

@ -5,10 +5,10 @@ using CSR.Domain.Entities;
public interface IUserRepository public interface IUserRepository
{ {
Task<User?> GetByIdAsync(int id); Task<User?> GetByIdAsync(int id);
Task<User?> GetByUsernameAsync(string username);
// Task<User?> GetByEmailAsync(string email); // Task<User?> GetByEmailAsync(string email);
// Task<User?> GetByUsernameAsync(string username); Task<IEnumerable<User>?> GetAllByRoleIdAsync(int roleId);
// Task<IEnumerable<User>> GetAllAsync(); Task<User?> AddAsync(User user);
Task AddAsync(User user);
Task UpdateAsync(User user); Task UpdateAsync(User user);
Task DeleteAsync(int id); Task DeleteAsync(int id);
} }

View file

@ -2,7 +2,12 @@ namespace CSR.Application.Interfaces;
public interface IUserService public interface IUserService
{ {
public record CreateUserResult(Domain.Entities.User? User, string? ErrorMessage); record RegisterNewUserResult(Domain.Entities.User? User, IEnumerable<string>? Errors);
public void CreateUser(string username, string email, string password, bool isAdmin = false); Task<RegisterNewUserResult> RegisterNewUser
(
string username,
string email,
string password
);
} }

View file

@ -3,12 +3,51 @@ namespace CSR.Application.Services;
using CSR.Application.Interfaces; using CSR.Application.Interfaces;
using CSR.Domain.Entities; using CSR.Domain.Entities;
public class UserService(IUserRepository userRepository) : IUserService public class UserService(IUserRepository userRepository, Domain.Interfaces.IPasswordHasher passwordHasher) : IUserService
{ {
private readonly IUserRepository _userRepository = userRepository; private readonly IUserRepository _userRepository = userRepository;
private readonly Domain.Interfaces.IPasswordHasher _passwordHasher = passwordHasher;
public void CreateNewUser(string username, string email, string password, bool isAdmin = false) public async Task<IUserService.RegisterNewUserResult> RegisterNewUser
(
string username,
string email,
string password
)
{ {
var errors = new List<string>();
var (IsValid, Errors) = User.IsValidUsername(username);
if (!IsValid)
{
errors.AddRange(Errors!);
}
if (!User.IsValidEmail(email))
{
errors.Add("Invalid email address.");
}
(IsValid, Errors) = User.IsValidPassword(password);
if (!IsValid)
{
errors.AddRange(Errors!);
}
var existingUser = await _userRepository.GetByUsernameAsync(username);
if (existingUser != null)
{
errors.Add("Username already exists.");
}
if (errors.Count > 0)
{
return new IUserService.RegisterNewUserResult(null, errors);
}
// create the new user
var user = User.CreateNew(username, email, password, _passwordHasher);
user = await _userRepository.AddAsync(user);
return new IUserService.RegisterNewUserResult(user, null);
} }
} }

View file

@ -16,18 +16,28 @@ public record Role
return new Role(id, name); return new Role(id, name);
} }
private static Role? _admin = null;
public static Role Admin public static Role Admin
{ {
get get
{ {
return new Role(1, "Admin"); if (_admin == null)
{
_admin = new Role(1, "Admin");
}
return _admin!;
} }
} }
private static Role? _user = null;
public static Role User public static Role User
{ {
get get
{ {
return new Role(2, "User"); if (_user == null)
{
_user = new Role(2, "User");
}
return _user!;
} }
} }
} }

View file

@ -32,6 +32,26 @@ public class User
return new User(id, username, email, passwordHash, roleId, role); return new User(id, username, email, passwordHash, roleId, role);
} }
public static User CreateNew(string username, string email, string password, IPasswordHasher passwordHasher)
{
if (!IsValidUsername(username).IsValid)
{
throw new ArgumentException("Invalid username.");
}
if (!IsValidEmail(email))
{
throw new ArgumentException("Invalid email.");
}
if (!IsValidPassword(password).IsValid)
{
throw new ArgumentException("Invalid password.");
}
var user = new User(0, username, email, password, DefaultRole.Id, DefaultRole);
user.PasswordHash = passwordHasher.HashPassword(user, password);
return user;
}
public (bool Success, IEnumerable<string>? Errors) UpdatePassword(string password, IPasswordHasher passwordHasher, User requestingUser) public (bool Success, IEnumerable<string>? Errors) UpdatePassword(string password, IPasswordHasher passwordHasher, User requestingUser)
{ {
@ -44,21 +64,16 @@ public class User
if (validityCheck.IsValid) if (validityCheck.IsValid)
{ {
PasswordHash = passwordHasher.HashPassword(password); PasswordHash = passwordHasher.HashPassword(this, password);
} }
return validityCheck; return validityCheck;
} }
public bool VerifyPasswordAgainstHash(string providedPassword, IPasswordHasher passwordHasher, User requestingUser) public bool VerifyPasswordAgainstHash(string providedPassword, IPasswordHasher passwordHasher)
{ {
if (requestingUser.Id != Id || requestingUser.Role.Name != "Admin") return passwordHasher.VerifyHashedPassword(this, PasswordHash, providedPassword);
{
throw new UnauthorizedAccessException("Only admins or the same user can verify passwords.");
}
return passwordHasher.VerifyHashedPassword(PasswordHash, providedPassword);
} }
@ -121,7 +136,7 @@ public class User
public (bool Success, IEnumerable<string>? Errors) UpdateUsername(string username, User requestingUser) public (bool Success, IEnumerable<string>? Errors) UpdateUsername(string username, User requestingUser)
{ {
if (requestingUser.Id != Id || requestingUser.Role.Name != "Admin") if (requestingUser.Role.Name != "Admin")
{ {
throw new UnauthorizedAccessException("Only admins can update username."); throw new UnauthorizedAccessException("Only admins can update username.");
} }

View file

@ -1,7 +1,9 @@
namespace CSR.Domain.Interfaces; namespace CSR.Domain.Interfaces;
using CSR.Domain.Entities;
public interface IPasswordHasher public interface IPasswordHasher
{ {
string HashPassword(string password); string HashPassword(User user, string password);
bool VerifyHashedPassword(string hashedPassword, string providedPassword); bool VerifyHashedPassword(User user, string hashedPassword, string providedPassword);
} }

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" /> <ProjectReference Include="..\CSR.Domain\CSR.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </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> </ItemGroup>
<PropertyGroup> <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 Microsoft.EntityFrameworkCore;
using CSR.Infrastructure.Persistence.Configurations; using CSR.Infrastructure.Persistence.Configurations;
public class CSRDbContext(DbContextOptions<CSRDbContext> options) : DbContext(options) public class CSRDbContext(DbContextOptions<CSRDbContext> options) : DbContext(options)
{ {
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
@ -23,14 +22,9 @@ public class CSRDbContext(DbContextOptions<CSRDbContext> options) : DbContext(op
.WithMany(r => r.Users) .WithMany(r => r.Users)
.HasForeignKey(u => u.RoleId); .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>() modelBuilder.Entity<Role>()
.HasData(adminRole, userRole); .HasIndex(r => r.Name)
.IsUnique();
base.OnModelCreating(modelBuilder); 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) public void Configure(EntityTypeBuilder<Role> builder)
{ {
builder.Property(u => u.Id) builder.Property(u => u.Id)
.HasColumnName("RoleId") .HasColumnName("RoleId");
.IsRequired();
builder.Property(u => u.Name) builder.Property(u => u.Name)
.HasColumnName("RoleName") .HasColumnName("RoleName");
.IsRequired();
} }
} }

View file

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

View file

@ -32,7 +32,6 @@ public class RoleRepository(CSR.Infrastructure.Persistence.CSRDbContext context)
{ {
var roleEntity = new CSR.Infrastructure.Persistence.Role var roleEntity = new CSR.Infrastructure.Persistence.Role
{ {
Id = role.Id,
Name = role.Name 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.Domain.Entities;
using CSR.Application.Interfaces; using CSR.Application.Interfaces;
using Microsoft.EntityFrameworkCore; 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) 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, Username = user.Username,
Email = user.Email, Email = user.Email,
PasswordHash = user.PasswordHash, PasswordHash = user.PasswordHash,
RoleId = user.RoleId, RoleId = user.RoleId,
Role = new CSR.Infrastructure.Persistence.Role Role = roleEntity
{
Id = user.Role.Id,
Name = user.Role.Name
}
}; };
_context.Users.Add(userEntity); try
{
_context.Users.Add(userEntity);
}
catch (DbUpdateException)
{
return null;
}
await _context.SaveChangesAsync(); 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.Email = user.Email;
userEntity.PasswordHash = user.PasswordHash; userEntity.PasswordHash = user.PasswordHash;
userEntity.RoleId = user.RoleId; 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 // Prevent EF from trying to update the Role entity
_context.Entry(userEntity.Role).State = EntityState.Unchanged; _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) public async Task DeleteAsync(int id)
{ {
var userEntity = new CSR.Infrastructure.Persistence.User var userEntity = new Persistence.User
{ {
Id = id, Id = id,
Username = string.Empty, Username = string.Empty,
Email = string.Empty, Email = string.Empty,
PasswordHash = string.Empty, PasswordHash = string.Empty,
RoleId = 0, RoleId = 0
Role = new CSR.Infrastructure.Persistence.Role
{
Id = 0,
Name = string.Empty
}
}; };
_context.Users.Remove(userEntity); _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;
}
}

View file

@ -5,6 +5,10 @@
<ProjectReference Include="..\CSR.Infrastructure\CSR.Infrastructure.csproj" /> <ProjectReference Include="..\CSR.Infrastructure\CSR.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

View file

@ -1,6 +1,10 @@
using CSR.Infrastructure.Persistence; using CSR.Infrastructure.Persistence;
using CSR.Infrastructure.Persistence.Repositories;
using CSR.Application.Services; using CSR.Application.Services;
using CSR.Application.Interfaces;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Csr.Infrastructure.Persistence.Repositories;
using CSR.Infrastructure.Data;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -8,6 +12,7 @@ var builder = WebApplication.CreateBuilder(args);
// Get configuration from appsettings.json, environment variables, and Docker secrets in that order // Get configuration from appsettings.json, environment variables, and Docker secrets in that order
builder.Configuration builder.Configuration
.AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"}.json", optional: true)
.AddEnvironmentVariables() .AddEnvironmentVariables()
.AddKeyPerFile("/run/secrets", optional: true); .AddKeyPerFile("/run/secrets", optional: true);
@ -28,22 +33,31 @@ builder.Services.AddDbContext<CSRDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"); options.UseSqlite($"Data Source={dbPath}");
}); });
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<Microsoft.AspNetCore.Identity.IPasswordHasher<CSR.Domain.Entities.User>, Microsoft.AspNetCore.Identity.PasswordHasher<CSR.Domain.Entities.User>>();
builder.Services.AddScoped<CSR.Domain.Interfaces.IPasswordHasher, CSR.Infrastructure.Services.PasswordHasherService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IRoleRepository, RoleRepository>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
var app = builder.Build(); var app = builder.Build();
// apply migrations and seed the database
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var services = scope.ServiceProvider; var services = scope.ServiceProvider;
var config = services.GetRequiredService<IConfiguration>(); try
{
var context = services.GetRequiredService<CSRDbContext>(); var context = services.GetRequiredService<CSRDbContext>();
var userService = services.GetRequiredService<UserService>(); context.Database.Migrate();
await DbInitializer.SeedDatabase(services);
var adminUsername = config["Admin:Username"] ?? "admin"; }
var adminEmail = config["Admin:Email"] ?? ""; catch (Exception ex)
var adminPassword = config["Admin:Password"] ?? "password"; {
var logger = services.GetRequiredService<ILogger<Program>>();
userService.CreateUser(adminUsername, adminEmail, adminPassword, true); logger.LogError(ex, "An error occurred while seeding the database");
}
} }
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.

View file

@ -5,5 +5,8 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"Database": {
"Path": "/home/danial23/dl/csr.db"
} }
} }