From a66a3f7cb06bfc0c95fb60656fff11cce2eea618 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Mon, 25 May 2026 19:05:02 -0700 Subject: [PATCH] feat: add AppDbContext (Identity + RefreshTokens) and DbSeeder (13 roles + dev admin) --- API/ROLAC.API/Data/AppDbContext.cs | 51 +++++++++++++++++++++++ API/ROLAC.API/Data/DbSeeder.cs | 67 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 API/ROLAC.API/Data/AppDbContext.cs create mode 100644 API/ROLAC.API/Data/DbSeeder.cs diff --git a/API/ROLAC.API/Data/AppDbContext.cs b/API/ROLAC.API/Data/AppDbContext.cs new file mode 100644 index 0000000..2472e7b --- /dev/null +++ b/API/ROLAC.API/Data/AppDbContext.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using ROLAC.API.Entities; + +namespace ROLAC.API.Data; + +public class AppDbContext : IdentityDbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet RefreshTokens => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity(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(entity => + { + entity.Property(e => e.LanguagePreference).HasMaxLength(10).HasDefaultValue("en"); + }); + + builder.Entity(entity => + { + entity.Property(e => e.Description).HasMaxLength(500); + }); + } +} diff --git a/API/ROLAC.API/Data/DbSeeder.cs b/API/ROLAC.API/Data/DbSeeder.cs new file mode 100644 index 0000000..458e000 --- /dev/null +++ b/API/ROLAC.API/Data/DbSeeder.cs @@ -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 roleManager) + { + foreach (var (name, description) in Roles) + { + if (!await roleManager.RoleExistsAsync(name)) + { + await roleManager.CreateAsync(new AppRole + { + Name = name, + Description = description, + }); + } + } + } + + /// + /// 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! + /// + public static async Task SeedAdminUserAsync(UserManager 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"); + } + } +}