feat(auth): эффективные права пользователя в /api/me (единый источник)
- RolePermissions.Effective(role, isSuperAdmin, isAdmin, advancedMode) — флаги с учётом роли и простого/расширенного режима; AdvancedOnly — единый список advanced-only прав (зеркалит [RequiresAdvancedMode]). - /api/me теперь отдаёт permissions (имя→bool). Мобильное и веб скрывают одно и то же. - Веб: хук useCan() читает me.permissions (раньше гейтил только по именам ролей). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7231c83d23
commit
2a4ba14d38
|
|
@ -718,6 +718,11 @@ [new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
|||
// false по умолчанию → UI показывает только «простое» меню.
|
||||
// Включает только SuperAdmin через /api/super-admin/organizations/{id}/advanced-mode.
|
||||
bool advancedMode = false;
|
||||
// Sprint (mobile-api): эффективные права текущего пользователя —
|
||||
// единый источник для мобильного и веба (скрывают одно и то же).
|
||||
foodmarket.Domain.Organizations.RolePermissions? rolePerms = null;
|
||||
var isSuperAdmin = roles.Contains(HttpContextTenantContext.SuperAdminRole);
|
||||
var isAdmin = roles.Contains("Admin");
|
||||
if (Guid.TryParse(orgIdRaw, out var orgId))
|
||||
{
|
||||
var org = await db.Organizations.IgnoreQueryFilters()
|
||||
|
|
@ -731,11 +736,18 @@ [new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
|||
}
|
||||
if (Guid.TryParse(subRaw, out var sub))
|
||||
{
|
||||
hasActiveEmployee = await db.Employees.IgnoreQueryFilters()
|
||||
.AnyAsync(e => e.OrganizationId == orgId && e.UserId == sub && e.IsActive);
|
||||
var emp = await db.Employees.IgnoreQueryFilters()
|
||||
.Where(e => e.OrganizationId == orgId && e.UserId == sub && e.IsActive && !e.IsDeleted)
|
||||
.Select(e => new { e.Role })
|
||||
.FirstOrDefaultAsync();
|
||||
hasActiveEmployee = emp is not null;
|
||||
rolePerms = emp?.Role?.Permissions;
|
||||
}
|
||||
}
|
||||
|
||||
var permissions = foodmarket.Domain.Organizations.RolePermissions.Effective(
|
||||
rolePerms, isSuperAdmin, isAdmin, advancedMode);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
sub = subRaw,
|
||||
|
|
@ -746,6 +758,9 @@ [new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
|||
hasLiveOrg,
|
||||
hasActiveEmployee,
|
||||
advancedMode,
|
||||
// Эффективные права (имя→bool) с учётом роли и простого/расширенного
|
||||
// режима. Клиенты (моб./веб) гейтят UI по этим флагам.
|
||||
permissions,
|
||||
});
|
||||
}).RequireAuthorization();
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,43 @@ public class RolePermissions
|
|||
public bool MoneyEdit { get; set; }
|
||||
public bool CashShiftOperate { get; set; }
|
||||
|
||||
/// <summary>Права, доступные только в расширенном режиме организации
|
||||
/// (в простом режиме скрыты и на сервере, и в UI). Единый источник правды,
|
||||
/// зеркалит контроллеры с <c>[RequiresAdvancedMode]</c>: Оптовые отгрузки,
|
||||
/// Перемещения, Лояльность, Акции, Типы цен.</summary>
|
||||
public static readonly IReadOnlySet<string> AdvancedOnly = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
nameof(DemandsView), nameof(DemandsEdit), nameof(DemandsPost),
|
||||
nameof(TransferEdit), nameof(LoyaltyManage), nameof(PromotionsManage),
|
||||
nameof(PriceTypesManage),
|
||||
};
|
||||
|
||||
/// <summary>Все bool-флаги прав в виде словаря имя→значение (через рефлексию).</summary>
|
||||
public Dictionary<string, bool> ToDictionary()
|
||||
{
|
||||
var dict = new Dictionary<string, bool>(StringComparer.Ordinal);
|
||||
foreach (var p in typeof(RolePermissions).GetProperties())
|
||||
if (p.PropertyType == typeof(bool) && p.CanRead)
|
||||
dict[p.Name] = (bool)p.GetValue(this)!;
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>Эффективные права текущего пользователя для клиента (моб./веб):
|
||||
/// SuperAdmin — всё без ограничений; Admin — всё, но в простом режиме
|
||||
/// advanced-only скрыты; обычная роль — её флаги, в простом режиме с
|
||||
/// обнулением advanced-only. Единый источник, чтобы клиенты скрывали одно и то же.</summary>
|
||||
public static Dictionary<string, bool> Effective(RolePermissions? role, bool isSuperAdmin, bool isAdmin, bool advancedMode)
|
||||
{
|
||||
var basePerms = (isSuperAdmin || isAdmin) ? All() : (role ?? new RolePermissions());
|
||||
var dict = basePerms.ToDictionary();
|
||||
// SuperAdmin обходит гейт расширенного режима; остальным в простом режиме
|
||||
// advanced-only права выключаем.
|
||||
if (!isSuperAdmin && !advancedMode)
|
||||
foreach (var name in AdvancedOnly)
|
||||
dict[name] = false;
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>Полный набор всех true — для системной роли «Администратор».</summary>
|
||||
public static RolePermissions All() => new()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ export interface MeResponse {
|
|||
* модули: Demand, Transfers, Loyalty, Promotions, ABC, Аудит,
|
||||
* пресеты, Telegram-бот, SSO, несколько типов цен. */
|
||||
advancedMode: boolean
|
||||
/** Эффективные права текущего пользователя (имя флага RolePermissions →
|
||||
* bool) с учётом роли и простого/расширенного режима. Единый источник
|
||||
* для веба и мобильного — скрываем одно и то же. Советуй `useCan()`. */
|
||||
permissions?: Record<string, boolean>
|
||||
}
|
||||
|
||||
/** Текущий залогиненный юзер с ролями. Используется для гейтов
|
||||
|
|
@ -33,3 +37,14 @@ export function useIsSuperAdmin(): boolean {
|
|||
const me = useMe()
|
||||
return !!me.data?.roles?.includes('SuperAdmin')
|
||||
}
|
||||
|
||||
/** Проверка эффективного права текущего пользователя по серверному источнику
|
||||
* (`/api/me` → permissions). Тот же набор, что у мобильного приложения, —
|
||||
* веб и моб. скрывают одно и то же. `can('ProductsEdit')` и т.п.
|
||||
* SuperAdmin и Admin получают все флаги (advanced-only — только в расш. режиме),
|
||||
* это уже учтено сервером. */
|
||||
export function useCan(): (permission: string) => boolean {
|
||||
const me = useMe()
|
||||
const perms = me.data?.permissions
|
||||
return (permission: string) => !!perms && perms[permission] === true
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue