feat: add AppDbContext (Identity + RefreshTokens) and DbSeeder (13 roles + dev admin)

This commit is contained in:
Chris Chen
2026-05-25 19:05:02 -07:00
parent 40d740d6e0
commit a66a3f7cb0
2 changed files with 118 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Entities;
namespace ROLAC.API.Data;
public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<RefreshToken>(entity =>
{
entity.HasKey(e => e.Id);
// Unique index on hash — enables fast lookup and prevents duplicate tokens
entity.HasIndex(e => e.TokenHash).IsUnique();
entity.Property(e => e.TokenHash).HasMaxLength(64).IsRequired();
entity.Property(e => e.UserId).HasMaxLength(450).IsRequired();
entity.Property(e => e.DeviceInfo).HasMaxLength(200);
entity.Property(e => e.IpAddress).HasMaxLength(45);
entity.Property(e => e.ReplacedByHash).HasMaxLength(64);
entity.HasOne(e => e.User)
.WithMany(u => u.RefreshTokens)
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Computed properties are not DB columns
entity.Ignore(e => e.IsExpired);
entity.Ignore(e => e.IsRevoked);
entity.Ignore(e => e.IsActive);
});
builder.Entity<AppUser>(entity =>
{
entity.Property(e => e.LanguagePreference).HasMaxLength(10).HasDefaultValue("en");
});
builder.Entity<AppRole>(entity =>
{
entity.Property(e => e.Description).HasMaxLength(500);
});
}
}
+67
View File
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Identity;
using ROLAC.API.Entities;
namespace ROLAC.API.Data;
public static class DbSeeder
{
private static readonly (string Name, string Description)[] Roles =
[
("super_admin", "System administrator — full access"),
("pastor", "Pastor — full member and financial overview"),
("board_member", "Board member — church governance"),
("coworker_chair", "Coworker chair — coordinates ministry leaders"),
("ministry_leader", "Ministry leader — scoped to own ministry"),
("district_leader", "District leader — manages multiple cell groups"),
("cell_leader", "Cell leader — scoped to own cell group"),
("coworker", "Coworker — general worker in assigned ministry"),
("finance", "Finance — manages giving and expense reports"),
("secretary", "Secretary — manages member data and scheduling"),
("worship_leader", "Worship leader — manages song library and setlists (Phase deferred)"),
("member", "Member — views own profile and service roster"),
("visitor", "Visitor — public pages only"),
];
public static async Task SeedRolesAsync(RoleManager<AppRole> roleManager)
{
foreach (var (name, description) in Roles)
{
if (!await roleManager.RoleExistsAsync(name))
{
await roleManager.CreateAsync(new AppRole
{
Name = name,
Description = description,
});
}
}
}
/// <summary>
/// Creates a super_admin test account for local development.
/// DO NOT call this in production — remove or guard with IsDevelopment().
/// Credentials: admin@rolac.org / Admin1234!
/// </summary>
public static async Task SeedAdminUserAsync(UserManager<AppUser> userManager)
{
const string adminEmail = "admin@rolac.org";
const string adminPassword = "Admin1234!";
if (await userManager.FindByEmailAsync(adminEmail) is null)
{
var admin = new AppUser
{
UserName = adminEmail,
Email = adminEmail,
EmailConfirmed = true,
IsActive = true,
LanguagePreference = "en",
CreatedAt = DateTime.UtcNow,
};
var result = await userManager.CreateAsync(admin, adminPassword);
if (result.Succeeded)
await userManager.AddToRoleAsync(admin, "super_admin");
}
}
}