diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs
index 296acaf..b88e3bc 100644
--- a/src/food-market.api/Program.cs
+++ b/src/food-market.api/Program.cs
@@ -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();
diff --git a/src/food-market.domain/Organizations/RolePermissions.cs b/src/food-market.domain/Organizations/RolePermissions.cs
index 53a4acc..b53c394 100644
--- a/src/food-market.domain/Organizations/RolePermissions.cs
+++ b/src/food-market.domain/Organizations/RolePermissions.cs
@@ -68,6 +68,43 @@ public class RolePermissions
public bool MoneyEdit { get; set; }
public bool CashShiftOperate { get; set; }
+ /// Права, доступные только в расширенном режиме организации
+ /// (в простом режиме скрыты и на сервере, и в UI). Единый источник правды,
+ /// зеркалит контроллеры с [RequiresAdvancedMode]: Оптовые отгрузки,
+ /// Перемещения, Лояльность, Акции, Типы цен.
+ public static readonly IReadOnlySet AdvancedOnly = new HashSet(StringComparer.Ordinal)
+ {
+ nameof(DemandsView), nameof(DemandsEdit), nameof(DemandsPost),
+ nameof(TransferEdit), nameof(LoyaltyManage), nameof(PromotionsManage),
+ nameof(PriceTypesManage),
+ };
+
+ /// Все bool-флаги прав в виде словаря имя→значение (через рефлексию).
+ public Dictionary ToDictionary()
+ {
+ var dict = new Dictionary(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;
+ }
+
+ /// Эффективные права текущего пользователя для клиента (моб./веб):
+ /// SuperAdmin — всё без ограничений; Admin — всё, но в простом режиме
+ /// advanced-only скрыты; обычная роль — её флаги, в простом режиме с
+ /// обнулением advanced-only. Единый источник, чтобы клиенты скрывали одно и то же.
+ public static Dictionary 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;
+ }
+
/// Полный набор всех true — для системной роли «Администратор».
public static RolePermissions All() => new()
{
diff --git a/src/food-market.web/src/lib/useMe.ts b/src/food-market.web/src/lib/useMe.ts
index 64a63e4..d2d91f6 100644
--- a/src/food-market.web/src/lib/useMe.ts
+++ b/src/food-market.web/src/lib/useMe.ts
@@ -17,6 +17,10 @@ export interface MeResponse {
* модули: Demand, Transfers, Loyalty, Promotions, ABC, Аудит,
* пресеты, Telegram-бот, SSO, несколько типов цен. */
advancedMode: boolean
+ /** Эффективные права текущего пользователя (имя флага RolePermissions →
+ * bool) с учётом роли и простого/расширенного режима. Единый источник
+ * для веба и мобильного — скрываем одно и то же. Советуй `useCan()`. */
+ permissions?: Record
}
/** Текущий залогиненный юзер с ролями. Используется для гейтов
@@ -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
+}