255 lines
11 KiB
C#
255 lines
11 KiB
C#
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ROLAC.API.Authorization;
|
|
using ROLAC.API.Entities;
|
|
|
|
namespace ROLAC.API.Data;
|
|
|
|
public static class DbSeeder
|
|
{
|
|
private static readonly (string En, string Zh, int Sort)[] GivingCategorySeed =
|
|
[
|
|
("Tithe", "什一奉獻", 1),
|
|
("General Offering", "一般奉獻", 2),
|
|
("Special Offering", "特別奉獻", 3),
|
|
("Building Fund", "建堂基金", 4),
|
|
("Mission", "宣教奉獻", 5),
|
|
];
|
|
|
|
private static readonly (string En, string Zh, int Sort)[] MinistrySeed =
|
|
[
|
|
("Administration", "行政", 1),
|
|
("Preaching", "講道", 2),
|
|
("Emcee", "司會", 3),
|
|
("Worship", "敬拜", 4),
|
|
("PPT/Media", "PPT/影音", 5),
|
|
("Sound", "音控", 6),
|
|
("Facility", "場地組", 7),
|
|
("Hospitality", "招待", 8),
|
|
("Children", "兒牧", 9),
|
|
("Catering", "餐飲", 10),
|
|
];
|
|
|
|
// (GroupEn, GroupZh, Sort, SubItems[(SubEn, SubZh)])
|
|
private static readonly (string En, string Zh, int Sort, (string En, string Zh)[] Subs)[] ExpenseCategorySeed =
|
|
[
|
|
("Equipment", "設備", 1, [("Purchase","購置"),("Rental","租借"),("Maintenance & Repair","維修")]),
|
|
("Consumables", "消耗品", 2, [("Batteries","電池"),("Accessories","配件"),("Cleaning Supplies","清潔用品"),("Office Supplies","文具")]),
|
|
("Food & Beverage", "餐飲", 3, [("Catering","出餐費用"),("Food Ingredients","食材採購"),("Utensils","器具"),("Consumables","消耗品")]),
|
|
("Training", "培訓", 4, [("Course Fees","課程費用"),("Books","書籍"),("Conference","研討會"),("Travel","差旅")]),
|
|
("Materials", "教材", 5, [("Printing","印刷費用"),("Craft Supplies","手工材料"),("Copyright & Licensing","版權購買")]),
|
|
("Facility", "場地", 6, [("Rent","場地租金"),("Utilities","水電"),("Property Insurance","財產保險"),("Decoration","裝飾")]),
|
|
("Printing", "印刷", 7, [("Bulletins","週報"),("Order of Service","程序單"),("Posters","海報")]),
|
|
("Missions", "宣教", 8, [("Offering Transfer","奉獻轉帳"),("Missionary Support","宣教士支援"),("Travel","差旅")]),
|
|
("Benevolence", "關懷救助", 9, [("Emergency Aid","急難救助"),("Condolence Gifts","慰問禮品"),("Visit Expenses","探訪費用")]),
|
|
("Other", "其他", 10, [("Miscellaneous","雜支")]),
|
|
("Personnel", "人事", 11, [("Salary & Wages","薪資"),("Payroll Taxes","薪資稅費"),("Employee Benefits","員工福利"),("Workers Compensation","勞工保險"),("Honorarium","酬庸"),("Staff Training","同工進修"),("Contract Labor","外包勞務")]),
|
|
];
|
|
|
|
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"),
|
|
];
|
|
|
|
// Default permission matrix — mirrors the hard-coded [Authorize(Roles=...)] rules that
|
|
// existed before the configurable RBAC system, so day-one behavior is unchanged.
|
|
// super_admin is intentionally absent: it bypasses all checks (see PermissionAuthorizationHandler).
|
|
// R=Read, W=Write, D=Delete, A=Approve. Rows are inserted only if missing, so an admin's
|
|
// later edits via the Permissions UI are never clobbered on restart.
|
|
private static readonly (string Role, string Module, bool R, bool W, bool D, bool A)[] RolePermissionSeed =
|
|
[
|
|
// Secretary — manages member data.
|
|
("secretary", Modules.Members, true, true, true, false),
|
|
|
|
// Pastor — read-only overview of members and all expenses.
|
|
("pastor", Modules.Members, true, false, false, false),
|
|
("pastor", Modules.Expenses, true, false, false, false),
|
|
|
|
// Finance — full control over the finance modules.
|
|
("finance", Modules.Givings, true, true, true, false),
|
|
("finance", Modules.GivingCategories, true, true, true, false),
|
|
("finance", Modules.Expenses, true, true, true, true),
|
|
("finance", Modules.ExpenseCategories, true, true, true, false),
|
|
("finance", Modules.OfferingSessions, true, true, true, true),
|
|
("finance", Modules.FinanceDashboard, true, false, false, false),
|
|
("finance", Modules.MonthlyStatements, true, true, false, true),
|
|
("finance", Modules.ChurchProfile, true, true, false, false),
|
|
("finance", Modules.Disbursements, true, true, true, true),
|
|
];
|
|
|
|
public static async Task SeedRolePermissionsAsync(AppDbContext db)
|
|
{
|
|
var rolesByName = await db.Roles
|
|
.Where(r => r.Name != null)
|
|
.ToDictionaryAsync(r => r.Name!, r => r.Id);
|
|
|
|
foreach (var (role, module, read, write, delete, approve) in RolePermissionSeed)
|
|
{
|
|
if (!rolesByName.TryGetValue(role, out var roleId))
|
|
continue;
|
|
|
|
var exists = await db.RolePermissions.AnyAsync(p => p.RoleId == roleId && p.Module == module);
|
|
if (exists)
|
|
continue; // never clobber an admin's edit
|
|
|
|
db.RolePermissions.Add(new RolePermission
|
|
{
|
|
RoleId = roleId,
|
|
Module = module,
|
|
CanRead = read,
|
|
CanWrite = write,
|
|
CanDelete = delete,
|
|
CanApprove = approve,
|
|
});
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public static async Task SeedGivingCategoriesAsync(AppDbContext db)
|
|
{
|
|
foreach (var (en, zh, sort) in GivingCategorySeed)
|
|
{
|
|
if (!await db.GivingCategories.AnyAsync(c => c.Name_en == en))
|
|
{
|
|
db.GivingCategories.Add(new GivingCategory
|
|
{
|
|
Name_en = en,
|
|
Name_zh = zh,
|
|
SortOrder = sort,
|
|
IsActive = true,
|
|
// Audit fields are stamped by AuditSaveChangesInterceptor on save.
|
|
});
|
|
}
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
public static async Task SeedMinistriesAsync(AppDbContext db)
|
|
{
|
|
foreach (var (en, zh, sort) in MinistrySeed)
|
|
{
|
|
if (!await db.Ministries.AnyAsync(m => m.Name_en == en))
|
|
db.Ministries.Add(new Ministry { Name_en = en, Name_zh = zh, SortOrder = sort, IsActive = true });
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
public static async Task SeedExpenseCategoriesAsync(AppDbContext db)
|
|
{
|
|
foreach (var (gEn, gZh, gSort, subs) in ExpenseCategorySeed)
|
|
{
|
|
var group = await db.ExpenseCategoryGroups.FirstOrDefaultAsync(g => g.Name_en == gEn);
|
|
if (group is null)
|
|
{
|
|
group = new ExpenseCategoryGroup { Name_en = gEn, Name_zh = gZh, SortOrder = gSort, IsActive = true };
|
|
db.ExpenseCategoryGroups.Add(group);
|
|
await db.SaveChangesAsync(); // assign group.Id
|
|
}
|
|
|
|
var sub = 1;
|
|
foreach (var (sEn, sZh) in subs)
|
|
{
|
|
if (!await db.ExpenseSubCategories.AnyAsync(s => s.GroupId == group.Id && s.Name_en == sEn))
|
|
db.ExpenseSubCategories.Add(new ExpenseSubCategory
|
|
{ GroupId = group.Id, Name_en = sEn, Name_zh = sZh, SortOrder = sub, IsActive = true });
|
|
sub++;
|
|
}
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
public static async Task SeedChurchProfileAsync(AppDbContext db)
|
|
{
|
|
// Singleton row used by the disbursement module (issuer info + check counter).
|
|
if (!await db.ChurchProfiles.AnyAsync())
|
|
{
|
|
db.ChurchProfiles.Add(new ChurchProfile
|
|
{
|
|
Name = "River Of Life Christian Church",
|
|
City = "Arcadia",
|
|
State = "CA",
|
|
NextCheckNumber = 1001,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds roles and (in Development) the default admin account.
|
|
/// Called once on application startup after migrations have been applied.
|
|
/// </summary>
|
|
public static async Task SeedAsync(IServiceProvider services)
|
|
{
|
|
var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
|
|
var userManager = services.GetRequiredService<UserManager<AppUser>>();
|
|
var env = services.GetRequiredService<IWebHostEnvironment>();
|
|
|
|
await SeedRolesAsync(roleManager);
|
|
|
|
var db = services.GetRequiredService<AppDbContext>();
|
|
await SeedRolePermissionsAsync(db);
|
|
await SeedGivingCategoriesAsync(db);
|
|
await SeedMinistriesAsync(db);
|
|
await SeedExpenseCategoriesAsync(db);
|
|
await SeedChurchProfileAsync(db);
|
|
|
|
if (env.IsDevelopment())
|
|
await SeedAdminUserAsync(userManager);
|
|
}
|
|
|
|
/// <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");
|
|
}
|
|
}
|
|
}
|