Some checks are pending
Раздел /super-admin в UI прячется за me.roles.includes('SuperAdmin').
Сидер при создании admin'а назначал только SystemRoles.Admin —
SuperAdmin висел как Identity-роль в роле-каталоге, но никому не был
выдан. Из-за этого SuperAdmin-консоль на стенде была не видна в меню.
Фикс: при создании admin'а сразу AddToRoleAsync(SuperAdmin). Для уже
развёрнутых стендов — догоняющая ветка else if (!IsInRoleAsync(SuperAdmin))
догоняет существующую учётку при следующем рестарте API.
На стенде роль уже выдана вручную через INSERT в AspNetUserRoles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
273 lines
12 KiB
C#
273 lines
12 KiB
C#
using foodmarket.Domain.Catalog;
|
||
using foodmarket.Domain.Organizations;
|
||
using foodmarket.Infrastructure.Identity;
|
||
using foodmarket.Infrastructure.Persistence;
|
||
using Microsoft.AspNetCore.Identity;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace foodmarket.Api.Seed;
|
||
|
||
public class DevDataSeeder : IHostedService
|
||
{
|
||
private readonly IServiceProvider _services;
|
||
private readonly IHostEnvironment _env;
|
||
|
||
public DevDataSeeder(IServiceProvider services, IHostEnvironment env)
|
||
{
|
||
_services = services;
|
||
_env = env;
|
||
}
|
||
|
||
public async Task StartAsync(CancellationToken ct)
|
||
{
|
||
// Idempotent — runs in all envs to bootstrap a usable admin + demo org.
|
||
// Once first real user/org is set up via UI, rename/disable demo.
|
||
// (Wired regardless of env so stage/prod first-deploy lands a working
|
||
// admin, otherwise nobody can log in.)
|
||
|
||
using var scope = _services.CreateScope();
|
||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
|
||
var roleMgr = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
|
||
|
||
foreach (var role in new[] { SystemRoles.SuperAdmin, SystemRoles.Admin, SystemRoles.Manager, SystemRoles.Cashier, SystemRoles.Storekeeper })
|
||
{
|
||
if (!await roleMgr.RoleExistsAsync(role))
|
||
{
|
||
await roleMgr.CreateAsync(new Role { Name = role });
|
||
}
|
||
}
|
||
|
||
var kzt = await db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
|
||
var demoOrg = await db.Organizations.FirstOrDefaultAsync(o => o.Name == "Demo Market", ct);
|
||
if (demoOrg is null)
|
||
{
|
||
demoOrg = new Organization
|
||
{
|
||
Name = "Demo Market",
|
||
CountryCode = "KZ",
|
||
Bin = "000000000000",
|
||
Address = "Алматы, ул. Пример 1",
|
||
Phone = "+7 (777) 000-00-00",
|
||
Email = "demo@food-market.local",
|
||
DefaultCurrencyId = kzt?.Id,
|
||
};
|
||
db.Organizations.Add(demoOrg);
|
||
await db.SaveChangesAsync(ct);
|
||
}
|
||
else if (demoOrg.DefaultCurrencyId is null && kzt is not null)
|
||
{
|
||
demoOrg.DefaultCurrencyId = kzt.Id;
|
||
await db.SaveChangesAsync(ct);
|
||
}
|
||
|
||
await SeedTenantReferencesAsync(db, demoOrg.Id, ct);
|
||
|
||
const string adminEmail = "admin@food-market.local";
|
||
var admin = await userMgr.FindByEmailAsync(adminEmail);
|
||
if (admin is null)
|
||
{
|
||
admin = new User
|
||
{
|
||
UserName = adminEmail,
|
||
Email = adminEmail,
|
||
EmailConfirmed = true,
|
||
FullName = "System Admin",
|
||
OrganizationId = demoOrg.Id,
|
||
};
|
||
var result = await userMgr.CreateAsync(admin, "Admin12345!");
|
||
if (result.Succeeded)
|
||
{
|
||
await userMgr.AddToRoleAsync(admin, SystemRoles.Admin);
|
||
await userMgr.AddToRoleAsync(admin, SystemRoles.SuperAdmin);
|
||
}
|
||
}
|
||
else if (!await userMgr.IsInRoleAsync(admin, SystemRoles.SuperAdmin))
|
||
{
|
||
// Существующий admin без SuperAdmin — догоняем (для уже развёрнутых стендов).
|
||
await userMgr.AddToRoleAsync(admin, SystemRoles.SuperAdmin);
|
||
}
|
||
|
||
await SeedAdminEmployeeAsync(db, demoOrg.Id, admin?.Id, ct);
|
||
}
|
||
|
||
/// <summary>Привязывает существующего admin@food-market.local к
|
||
/// Employee-записи с системной ролью «Администратор» — чтобы UI «Сотрудники»
|
||
/// сразу показывал учётку с правильной ролью, а не пустой список.</summary>
|
||
private static async Task SeedAdminEmployeeAsync(AppDbContext db, Guid orgId, Guid? adminUserId, CancellationToken ct)
|
||
{
|
||
if (adminUserId is null) return;
|
||
var existing = await db.Employees.IgnoreQueryFilters()
|
||
.FirstOrDefaultAsync(e => e.OrganizationId == orgId && e.UserId == adminUserId, ct);
|
||
if (existing is not null) return;
|
||
var adminRole = await db.EmployeeRoles.IgnoreQueryFilters()
|
||
.FirstOrDefaultAsync(r => r.OrganizationId == orgId && r.IsSystem && r.Name == "Администратор", ct);
|
||
if (adminRole is null) return;
|
||
db.Employees.Add(new Employee
|
||
{
|
||
OrganizationId = orgId,
|
||
UserId = adminUserId,
|
||
LastName = "Admin",
|
||
FirstName = "System",
|
||
Position = "Администратор",
|
||
Email = "admin@food-market.local",
|
||
RoleId = adminRole.Id,
|
||
IsActive = true,
|
||
});
|
||
await db.SaveChangesAsync(ct);
|
||
}
|
||
|
||
private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, CancellationToken ct)
|
||
{
|
||
var anyUnit = await db.UnitsOfMeasure.IgnoreQueryFilters().AnyAsync(u => u.OrganizationId == orgId, ct);
|
||
if (!anyUnit)
|
||
{
|
||
db.UnitsOfMeasure.AddRange(
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "796", Name = "штука" },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "166", Name = "килограмм" },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "112", Name = "литр" },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "006", Name = "метр" },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "625", Name = "упаковка" }
|
||
);
|
||
}
|
||
|
||
// Сидируем PriceType ТОЛЬКО если у организации не было ни одной записи.
|
||
// Если есть — никогда не создаём «системную копию», корректность IsSystem
|
||
// обеспечивает миграция Phase3b_FixPriceTypeIsSystem (выбор по факту:
|
||
// запись с максимумом ProductPrice).
|
||
var anyPriceType = await db.PriceTypes.IgnoreQueryFilters().AnyAsync(p => p.OrganizationId == orgId, ct);
|
||
if (!anyPriceType)
|
||
{
|
||
db.PriceTypes.AddRange(
|
||
new PriceType { OrganizationId = orgId, Name = "Розничная цена", IsSystem = true, IsRequired = true, IsRetail = true, SortOrder = 0 },
|
||
new PriceType { OrganizationId = orgId, Name = "Оптовая", SortOrder = 1 }
|
||
);
|
||
}
|
||
|
||
var mainStore = await db.Stores.IgnoreQueryFilters().FirstOrDefaultAsync(s => s.OrganizationId == orgId && s.IsMain, ct);
|
||
if (mainStore is null)
|
||
{
|
||
mainStore = new Store
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Основной склад",
|
||
Code = "MAIN",
|
||
IsMain = true,
|
||
Address = "Алматы, ул. Пример 1",
|
||
};
|
||
db.Stores.Add(mainStore);
|
||
await db.SaveChangesAsync(ct);
|
||
}
|
||
|
||
var anyRetail = await db.RetailPoints.IgnoreQueryFilters().AnyAsync(r => r.OrganizationId == orgId, ct);
|
||
if (!anyRetail)
|
||
{
|
||
db.RetailPoints.Add(new RetailPoint
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Касса 1",
|
||
Code = "POS-1",
|
||
StoreId = mainStore.Id,
|
||
});
|
||
}
|
||
|
||
await db.SaveChangesAsync(ct);
|
||
|
||
await SeedEmployeeRolesAsync(db, orgId, ct);
|
||
}
|
||
|
||
/// <summary>Системные роли (IsSystem=true): Администратор / Менеджер /
|
||
/// Кладовщик / Кассир / Закупщик / Бухгалтер. Сидируется один раз
|
||
/// per организацию; обновлять не пытаемся, чтобы не сбросить кастомные
|
||
/// правки галок которые админ мог сделать.</summary>
|
||
private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, CancellationToken ct)
|
||
{
|
||
var anyRole = await db.EmployeeRoles.IgnoreQueryFilters().AnyAsync(r => r.OrganizationId == orgId, ct);
|
||
if (anyRole) return;
|
||
|
||
var admin = new EmployeeRole
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Администратор",
|
||
Description = "Полный доступ ко всем разделам организации",
|
||
IsSystem = true, SortOrder = 0,
|
||
Permissions = RolePermissions.All(),
|
||
};
|
||
// Менеджер/Кладовщик/Закупщик/Бухгалтер — кастомные шаблоны (IsSystem=false),
|
||
// юзер может удалить или подкрутить под себя. Системные только Администратор + Кассир.
|
||
var manager = new EmployeeRole
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Менеджер",
|
||
Description = "Управление каталогом, документами и контрагентами",
|
||
IsSystem = false, SortOrder = 10,
|
||
Permissions = new RolePermissions
|
||
{
|
||
ProductsView = true, ProductsEdit = true, ProductGroupsManage = true, PriceTypesManage = true,
|
||
SuppliesView = true, SuppliesEdit = true, SuppliesPost = true,
|
||
CounterpartiesView = true, CounterpartiesEdit = true,
|
||
ReportsView = true, StocksView = true,
|
||
},
|
||
};
|
||
var keeper = new EmployeeRole
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Кладовщик",
|
||
Description = "Приёмки, инвентаризация, остатки",
|
||
IsSystem = false, SortOrder = 20,
|
||
Permissions = new RolePermissions
|
||
{
|
||
ProductsView = true,
|
||
SuppliesView = true, SuppliesEdit = true, SuppliesPost = true,
|
||
StocksView = true,
|
||
},
|
||
};
|
||
var cashier = new EmployeeRole
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Кассир",
|
||
Description = "Только работа на кассе. Без доступа к веб-админке.",
|
||
IsSystem = true, SortOrder = 30,
|
||
Permissions = new RolePermissions
|
||
{
|
||
ProductsView = true,
|
||
StocksView = true,
|
||
RetailSalesOperate = true,
|
||
// RetailSalesRefund по умолчанию false — админ включит при необходимости
|
||
},
|
||
};
|
||
var buyer = new EmployeeRole
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Закупщик",
|
||
Description = "Заказы поставщикам и приёмка товара",
|
||
IsSystem = false, SortOrder = 40,
|
||
Permissions = new RolePermissions
|
||
{
|
||
ProductsView = true,
|
||
SuppliesView = true, SuppliesEdit = true,
|
||
CounterpartiesView = true, CounterpartiesEdit = true,
|
||
},
|
||
};
|
||
var accountant = new EmployeeRole
|
||
{
|
||
OrganizationId = orgId,
|
||
Name = "Бухгалтер",
|
||
Description = "Просмотр всех данных и отчётов, без редактирования",
|
||
IsSystem = false, SortOrder = 50,
|
||
Permissions = new RolePermissions
|
||
{
|
||
ProductsView = true,
|
||
SuppliesView = true,
|
||
CounterpartiesView = true,
|
||
ReportsView = true, StocksView = true,
|
||
},
|
||
};
|
||
|
||
db.EmployeeRoles.AddRange(admin, manager, keeper, cashier, buyer, accountant);
|
||
await db.SaveChangesAsync(ct);
|
||
}
|
||
|
||
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
|
||
}
|