Fix db seeding, migration, repository services
This commit is contained in:
parent
872dc1e263
commit
6b87902ca7
22 changed files with 606 additions and 64 deletions
|
@ -5,10 +5,10 @@ using CSR.Domain.Entities;
|
|||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> GetByIdAsync(int id);
|
||||
Task<User?> GetByUsernameAsync(string username);
|
||||
// Task<User?> GetByEmailAsync(string email);
|
||||
// Task<User?> GetByUsernameAsync(string username);
|
||||
// Task<IEnumerable<User>> GetAllAsync();
|
||||
Task AddAsync(User user);
|
||||
Task<IEnumerable<User>?> GetAllByRoleIdAsync(int roleId);
|
||||
Task<User?> AddAsync(User user);
|
||||
Task UpdateAsync(User user);
|
||||
Task DeleteAsync(int id);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,12 @@ namespace CSR.Application.Interfaces;
|
|||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,12 +3,51 @@ namespace CSR.Application.Services;
|
|||
using CSR.Application.Interfaces;
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,18 +16,28 @@ public record Role
|
|||
return new Role(id, name);
|
||||
}
|
||||
|
||||
private static Role? _admin = null;
|
||||
public static Role Admin
|
||||
{
|
||||
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
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Role(2, "User");
|
||||
if (_user == null)
|
||||
{
|
||||
_user = new Role(2, "User");
|
||||
}
|
||||
return _user!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,26 @@ public class User
|
|||
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)
|
||||
{
|
||||
|
@ -44,21 +64,16 @@ public class User
|
|||
|
||||
if (validityCheck.IsValid)
|
||||
{
|
||||
PasswordHash = passwordHasher.HashPassword(password);
|
||||
PasswordHash = passwordHasher.HashPassword(this, password);
|
||||
}
|
||||
|
||||
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")
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only admins or the same user can verify passwords.");
|
||||
}
|
||||
|
||||
return passwordHasher.VerifyHashedPassword(PasswordHash, providedPassword);
|
||||
return passwordHasher.VerifyHashedPassword(this, PasswordHash, providedPassword);
|
||||
}
|
||||
|
||||
|
||||
|
@ -121,7 +136,7 @@ public class User
|
|||
|
||||
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.");
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
namespace CSR.Domain.Interfaces;
|
||||
|
||||
using CSR.Domain.Entities;
|
||||
|
||||
public interface IPasswordHasher
|
||||
{
|
||||
string HashPassword(string password);
|
||||
bool VerifyHashedPassword(string hashedPassword, string providedPassword);
|
||||
string HashPassword(User user, string password);
|
||||
bool VerifyHashedPassword(User user, string hashedPassword, string providedPassword);
|
||||
}
|
||||
|
|
13
CSR.Infrastructure/.config/dotnet-tools.json
Normal file
13
CSR.Infrastructure/.config/dotnet-tools.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.5",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
55
CSR.Infrastructure/Data/DbSeeder.cs
Normal file
55
CSR.Infrastructure/Data/DbSeeder.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
CSR.Infrastructure/Migrations/20250520020330_InitialCreate.Designer.cs
generated
Normal file
92
CSR.Infrastructure/Migrations/20250520020330_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
89
CSR.Infrastructure/Migrations/CSRDbContextModelSnapshot.cs
Normal file
89
CSR.Infrastructure/Migrations/CSRDbContextModelSnapshot.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
36
CSR.Infrastructure/Persistence/CSRDbContextFactory.cs
Normal file
36
CSR.Infrastructure/Persistence/CSRDbContextFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
18
CSR.Infrastructure/Services/PasswordHasherService.cs
Normal file
18
CSR.Infrastructure/Services/PasswordHasherService.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,10 @@
|
|||
<ProjectReference Include="..\CSR.Infrastructure\CSR.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
using CSR.Infrastructure.Persistence;
|
||||
using CSR.Infrastructure.Persistence.Repositories;
|
||||
using CSR.Application.Services;
|
||||
using CSR.Application.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Csr.Infrastructure.Persistence.Repositories;
|
||||
using CSR.Infrastructure.Data;
|
||||
|
||||
|
||||
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
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"}.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddKeyPerFile("/run/secrets", optional: true);
|
||||
|
||||
|
@ -28,22 +33,31 @@ builder.Services.AddDbContext<CSRDbContext>(options =>
|
|||
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();
|
||||
|
||||
// apply migrations and seed the database
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var config = services.GetRequiredService<IConfiguration>();
|
||||
|
||||
var context = services.GetRequiredService<CSRDbContext>();
|
||||
var userService = services.GetRequiredService<UserService>();
|
||||
|
||||
var adminUsername = config["Admin:Username"] ?? "admin";
|
||||
var adminEmail = config["Admin:Email"] ?? "";
|
||||
var adminPassword = config["Admin:Password"] ?? "password";
|
||||
|
||||
userService.CreateUser(adminUsername, adminEmail, adminPassword, true);
|
||||
try
|
||||
{
|
||||
var context = services.GetRequiredService<CSRDbContext>();
|
||||
context.Database.Migrate();
|
||||
await DbInitializer.SeedDatabase(services);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogError(ex, "An error occurred while seeding the database");
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
|
|
@ -5,5 +5,8 @@
|
|||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"Path": "/home/danial23/dl/csr.db"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue