< Summary

Information
Class: OzonOrdersWeb.Areas.Admin.Controllers.RolesController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Controllers/RolesController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 304
Coverable lines: 304
Total lines: 510
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 120
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Index()100%210%
Create()100%210%
Create()0%4260%
Edit()0%620%
Edit()0%4260%
Delete()0%2040%
Pages()0%4260%
SavePagePermission()0%420200%
SavePagePermissionsBulk()0%506220%
Fields()0%210140%
SaveFieldPermission()0%4260%
SetAllFieldPermission()0%210140%
DataScopes()0%7280%
SaveDataScopes()0%7280%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Controllers/RolesController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Mvc;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models.Authorization;
 4using OzonOrdersWeb.Areas.Admin.Models;
 5using OzonOrdersWeb.Authorization;
 6using OzonRepositories.Context;
 7using Services.Authorization;
 8using Services.Authorization.Services;
 9
 10namespace OzonOrdersWeb.Areas.Admin.Controllers;
 11
 12/// <summary>
 13/// Управление прикладными ролями (AppRole): CRUD + матрицы прав на страницы, поля и наборы данных.
 14/// </summary>
 15public class RolesController : AdminAreaControllerBase
 16{
 17    private readonly ApplicationDbContext _db;
 18    private readonly IAuditService _audit;
 19    private readonly IPermissionService _permissions;
 20    private readonly ILogger<RolesController> _logger;
 21
 022    public RolesController(
 023        ApplicationDbContext db,
 024        IAuditService audit,
 025        IPermissionService permissions,
 026        ILogger<RolesController> logger)
 27    {
 028        _db = db;
 029        _audit = audit;
 030        _permissions = permissions;
 031        _logger = logger;
 032    }
 33
 34    public async Task<IActionResult> Index(CancellationToken ct)
 35    {
 036        ViewData["Title"] = "Роли";
 037        ViewData["Breadcrumb"] = new List<(string, string?)>
 038        {
 039            ("Админка", Url.Action("Index", "Home")),
 040            ("Роли", null),
 041        };
 42
 043        var roles = await _db.AppRoles
 044            .OrderBy(r => r.DisplayName)
 045            .Select(r => new RoleListItemVm
 046            {
 047                Id = r.Id,
 048                Name = r.Name,
 049                DisplayName = r.DisplayName,
 050                Description = r.Description,
 051                IsSystem = r.IsSystem,
 052                IsActive = r.IsActive,
 053                UserCount = r.UserLinks.Count,
 054                PagePermissionCount = r.PagePermissions.Count(p => p.CanView || p.CanCreate || p.CanEdit || p.CanDelete 
 055            })
 056            .ToListAsync(ct);
 57
 058        return View(roles);
 059    }
 60
 61    [HttpGet]
 62    public IActionResult Create()
 63    {
 064        ViewData["Title"] = "Новая роль";
 065        return View("Edit", new RoleEditVm { IsActive = true });
 66    }
 67
 68    [HttpPost]
 69    [ValidateAntiForgeryToken]
 70    public async Task<IActionResult> Create(RoleEditVm model, CancellationToken ct)
 71    {
 072        if (!ModelState.IsValid) { ViewData["Title"] = "Новая роль"; return View("Edit", model); }
 73
 074        if (await _db.AppRoles.AnyAsync(r => r.Name == model.Name, ct))
 75        {
 076            ModelState.AddModelError(nameof(model.Name), "Роль с таким системным именем уже существует");
 077            return View("Edit", model);
 78        }
 79
 080        var role = new AppRole
 081        {
 082            Name = model.Name.Trim(),
 083            DisplayName = model.DisplayName.Trim(),
 084            Description = model.Description,
 085            IsSystem = IsSuperAdmin && model.IsSystem,
 086            IsActive = model.IsActive,
 087        };
 088        _db.AppRoles.Add(role);
 089        await _db.SaveChangesAsync(ct);
 90
 091        await _audit.LogAsync(AuthorizationConstants.AuditActionRoleCreated, nameof(AppRole), role.Id.ToString(), null, 
 092        _permissions.InvalidateAll();
 093        TempData["SuccessMessage"] = "Роль создана";
 094        return RedirectToAction(nameof(Edit), new { id = role.Id });
 095    }
 96
 97    [HttpGet]
 98    public async Task<IActionResult> Edit(int id, CancellationToken ct)
 99    {
 0100        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0101        if (role == null) return NotFound();
 102
 0103        ViewData["Title"] = $"Роль: {role.DisplayName}";
 0104        ViewData["Breadcrumb"] = new List<(string, string?)>
 0105        {
 0106            ("Админка", Url.Action("Index", "Home")),
 0107            ("Роли", Url.Action("Index")),
 0108            (role.DisplayName, null),
 0109        };
 110
 0111        return View(new RoleEditVm
 0112        {
 0113            Id = role.Id,
 0114            Name = role.Name,
 0115            DisplayName = role.DisplayName,
 0116            Description = role.Description,
 0117            IsSystem = role.IsSystem,
 0118            IsActive = role.IsActive,
 0119        });
 0120    }
 121
 122    [HttpPost]
 123    [ValidateAntiForgeryToken]
 124    public async Task<IActionResult> Edit(int id, RoleEditVm model, CancellationToken ct)
 125    {
 0126        if (!ModelState.IsValid) return View(model);
 127
 0128        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0129        if (role == null) return NotFound();
 130
 0131        var oldJson = System.Text.Json.JsonSerializer.Serialize(new { role.Name, role.DisplayName, role.Description, rol
 132
 0133        role.DisplayName = model.DisplayName.Trim();
 0134        role.Description = model.Description;
 0135        role.IsActive = model.IsActive;
 0136        role.UpdatedAt = DateTime.UtcNow;
 0137        if (!role.IsSystem)
 138        {
 0139            role.Name = model.Name.Trim();
 140        }
 141
 0142        await _db.SaveChangesAsync(ct);
 0143        var newJson = System.Text.Json.JsonSerializer.Serialize(new { role.Name, role.DisplayName, role.Description, rol
 0144        await _audit.LogAsync(AuthorizationConstants.AuditActionRoleUpdated, nameof(AppRole), role.Id.ToString(), oldJso
 0145        _permissions.InvalidateAll();
 146
 0147        TempData["SuccessMessage"] = "Изменения сохранены";
 0148        return RedirectToAction(nameof(Edit), new { id = role.Id });
 0149    }
 150
 151    [HttpPost]
 152    [ValidateAntiForgeryToken]
 153    public async Task<IActionResult> Delete(int id, CancellationToken ct)
 154    {
 0155        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0156        if (role == null) return NotFound();
 0157        if (role.IsSystem)
 158        {
 0159            TempData["ErrorMessage"] = "Системную роль удалить нельзя";
 0160            return RedirectToAction(nameof(Index));
 161        }
 162
 0163        _db.AppRoles.Remove(role);
 0164        await _db.SaveChangesAsync(ct);
 0165        await _audit.LogAsync(AuthorizationConstants.AuditActionRoleDeleted, nameof(AppRole), id.ToString(), role.Name, 
 0166        _permissions.InvalidateAll();
 167
 0168        TempData["SuccessMessage"] = "Роль удалена";
 0169        return RedirectToAction(nameof(Index));
 0170    }
 171
 172    // ---------------------- МАТРИЦА: СТРАНИЦЫ ----------------------
 173
 174    [HttpGet]
 175    public async Task<IActionResult> Pages(int id, CancellationToken ct)
 176    {
 0177        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0178        if (role == null) return NotFound();
 179
 0180        ViewData["Title"] = $"Роль «{role.DisplayName}» — права на страницы";
 0181        ViewData["RoleId"] = role.Id;
 0182        ViewData["RoleName"] = role.DisplayName;
 183
 0184        var allPages = await _db.AppPages.AsNoTracking().OrderBy(p => p.SortOrder).ThenBy(p => p.DisplayName).ToListAsyn
 0185        var perms = await _db.Set<RolePagePermission>().AsNoTracking().Where(p => p.AppRoleId == id).ToListAsync(ct);
 0186        var permByPage = perms.ToDictionary(p => p.AppPageId);
 187
 0188        var rows = new List<RolePageMatrixRow>();
 189        void Walk(int? parentId, int depth)
 190        {
 0191            foreach (var p in allPages.Where(x => x.ParentId == parentId).OrderBy(x => x.SortOrder))
 192            {
 193                permByPage.TryGetValue(p.Id, out var pm);
 194                rows.Add(new RolePageMatrixRow
 195                {
 196                    PageId = p.Id,
 197                    ParentId = p.ParentId,
 198                    Depth = depth,
 199                    Code = p.Code,
 200                    DisplayName = p.DisplayName,
 201                    Icon = p.Icon,
 202                    AccentClass = p.AccentClass,
 203                    IsSectionHeader = p.IsSectionHeader,
 204                    CanView = pm?.CanView ?? false,
 205                    CanCreate = pm?.CanCreate ?? false,
 206                    CanEdit = pm?.CanEdit ?? false,
 207                    CanDelete = pm?.CanDelete ?? false,
 208                    CanExport = pm?.CanExport ?? false,
 209                });
 210                Walk(p.Id, depth + 1);
 211            }
 212        }
 0213        Walk(null, 0);
 214
 0215        ViewBag.Role = role;
 0216        ViewBag.Studio2RolePagePresetsJson = Studio2RolePageMenuCatalog.GetRolePagePresetsJson();
 0217        return View(rows);
 0218    }
 219
 220    [HttpPost]
 221    [ValidateAntiForgeryToken]
 222    public async Task<IActionResult> SavePagePermission(int roleId, [FromBody] RolePagePermissionInput input, Cancellati
 223    {
 0224        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == roleId, ct);
 0225        if (role == null) return NotFound();
 226
 0227        var perm = await _db.Set<RolePagePermission>().FirstOrDefaultAsync(p => p.AppRoleId == roleId && p.AppPageId == 
 0228        var oldJson = perm == null ? null : System.Text.Json.JsonSerializer.Serialize(new { perm.CanView, perm.CanCreate
 0229        var hasAny = input.CanView || input.CanCreate || input.CanEdit || input.CanDelete || input.CanExport;
 230
 0231        if (perm == null)
 232        {
 0233            if (!hasAny) return Json(new { ok = true });
 0234            perm = new RolePagePermission
 0235            {
 0236                AppRoleId = roleId,
 0237                AppPageId = input.PageId,
 0238                CanView = input.CanView,
 0239                CanCreate = input.CanCreate,
 0240                CanEdit = input.CanEdit,
 0241                CanDelete = input.CanDelete,
 0242                CanExport = input.CanExport,
 0243            };
 0244            _db.Add(perm);
 245        }
 0246        else if (!hasAny)
 247        {
 0248            _db.Remove(perm);
 249        }
 250        else
 251        {
 0252            perm.CanView = input.CanView;
 0253            perm.CanCreate = input.CanCreate;
 0254            perm.CanEdit = input.CanEdit;
 0255            perm.CanDelete = input.CanDelete;
 0256            perm.CanExport = input.CanExport;
 257        }
 258
 0259        await _db.SaveChangesAsync(ct);
 0260        var newJson = hasAny ? System.Text.Json.JsonSerializer.Serialize(input) : null;
 0261        await _audit.LogAsync(AuthorizationConstants.AuditActionPagePermissionChanged, nameof(RolePagePermission),
 0262            $"role={roleId};page={input.PageId}", oldJson, newJson, ct: ct);
 0263        _permissions.InvalidateAll();
 0264        return Json(new { ok = true });
 0265    }
 266
 267    /// <summary>
 268    /// Пакетное сохранение матрицы страниц (один round-trip вместо N AJAX при «выбрать всё»).
 269    /// </summary>
 270    [HttpPost]
 271    [ValidateAntiForgeryToken]
 272    public async Task<IActionResult> SavePagePermissionsBulk(int roleId, [FromBody] List<RolePagePermissionInput>? items
 273    {
 0274        if (items is null || items.Count == 0)
 0275            return Json(new { ok = true, saved = 0 });
 276
 277        // На одну страницу — последняя запись в списке
 0278        var byPageId = items
 0279            .GroupBy(i => i.PageId)
 0280            .ToDictionary(g => g.Key, g => g.Last());
 281
 0282        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == roleId, ct);
 0283        if (role == null) return NotFound();
 284
 0285        var pageIds = byPageId.Keys.ToList();
 0286        var existing = await _db.Set<RolePagePermission>()
 0287            .Where(p => p.AppRoleId == roleId && pageIds.Contains(p.AppPageId))
 0288            .ToListAsync(ct);
 0289        var existingByPage = existing.ToDictionary(p => p.AppPageId);
 290
 0291        foreach (var input in byPageId.Values)
 292        {
 0293            var hasAny = input.CanView || input.CanCreate || input.CanEdit || input.CanDelete || input.CanExport;
 0294            existingByPage.TryGetValue(input.PageId, out var perm);
 295
 0296            if (perm == null)
 297            {
 0298                if (!hasAny) continue;
 0299                _db.Add(new RolePagePermission
 0300                {
 0301                    AppRoleId = roleId,
 0302                    AppPageId = input.PageId,
 0303                    CanView = input.CanView,
 0304                    CanCreate = input.CanCreate,
 0305                    CanEdit = input.CanEdit,
 0306                    CanDelete = input.CanDelete,
 0307                    CanExport = input.CanExport,
 0308                });
 309            }
 0310            else if (!hasAny)
 311            {
 0312                _db.Remove(perm);
 313            }
 314            else
 315            {
 0316                perm.CanView = input.CanView;
 0317                perm.CanCreate = input.CanCreate;
 0318                perm.CanEdit = input.CanEdit;
 0319                perm.CanDelete = input.CanDelete;
 0320                perm.CanExport = input.CanExport;
 321            }
 322        }
 323
 0324        await _db.SaveChangesAsync(ct);
 0325        await _audit.LogAsync(
 0326            AuthorizationConstants.AuditActionPagePermissionChanged,
 0327            nameof(RolePagePermission),
 0328            $"role={roleId};bulk",
 0329            null,
 0330            $"rows={byPageId.Count}",
 0331            comment: "Массовое изменение матрицы страниц",
 0332            ct: ct);
 0333        _permissions.InvalidateAll();
 0334        return Json(new { ok = true, saved = byPageId.Count });
 0335    }
 336
 337    // ---------------------- МАТРИЦА: ПОЛЯ ----------------------
 338
 339    [HttpGet]
 340    public async Task<IActionResult> Fields(int id, string? entity, CancellationToken ct)
 341    {
 0342        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0343        if (role == null) return NotFound();
 344
 0345        var entities = await _db.AppFields
 0346            .Where(f => !string.IsNullOrWhiteSpace(f.EntityName))
 0347            .Select(f => f.EntityName.Trim())
 0348            .Distinct()
 0349            .OrderBy(x => x)
 0350            .ToListAsync(ct);
 351
 0352        if (string.IsNullOrWhiteSpace(entity))
 353        {
 0354            entity = entities.FirstOrDefault(e =>
 0355                string.Equals(e, "OrderList", StringComparison.OrdinalIgnoreCase))
 0356                ?? entities.FirstOrDefault();
 357        }
 358
 0359        var fields = string.IsNullOrEmpty(entity)
 0360            ? new List<AppField>()
 0361            : await _db.AppFields.Where(f => f.EntityName == entity).OrderBy(f => f.SortOrder).ThenBy(f => f.FieldName).
 362
 0363        var perms = await _db.Set<RoleFieldPermission>().Where(p => p.AppRoleId == id).ToListAsync(ct);
 0364        var byField = perms.ToDictionary(p => p.AppFieldId);
 365
 0366        var rows = fields.Select(f => new RoleFieldMatrixRow
 0367        {
 0368            FieldId = f.Id,
 0369            EntityName = f.EntityName,
 0370            FieldName = f.FieldName,
 0371            DisplayName = f.DisplayName,
 0372            CanRead = byField.TryGetValue(f.Id, out var pm) ? pm.CanRead : true,
 0373            CanWrite = byField.TryGetValue(f.Id, out var pm2) ? pm2.CanWrite : true,
 0374        }).ToList();
 375
 0376        ViewData["Title"] = $"Роль «{role.DisplayName}» — права на поля";
 0377        ViewBag.Role = role;
 0378        ViewBag.Entities = entities;
 0379        ViewBag.SelectedEntity = entity;
 0380        return View(rows);
 0381    }
 382
 383    [HttpPost]
 384    [ValidateAntiForgeryToken]
 385    public async Task<IActionResult> SaveFieldPermission(int roleId, [FromBody] RoleFieldPermissionInput input, Cancella
 386    {
 0387        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == roleId, ct);
 0388        if (role == null) return NotFound();
 389
 0390        var perm = await _db.Set<RoleFieldPermission>().FirstOrDefaultAsync(p => p.AppRoleId == roleId && p.AppFieldId =
 0391        var oldJson = perm == null ? null : System.Text.Json.JsonSerializer.Serialize(new { perm.CanRead, perm.CanWrite 
 392
 0393        if (perm == null)
 394        {
 0395            perm = new RoleFieldPermission
 0396            {
 0397                AppRoleId = roleId,
 0398                AppFieldId = input.FieldId,
 0399                CanRead = input.CanRead,
 0400                CanWrite = input.CanWrite,
 0401            };
 0402            _db.Add(perm);
 403        }
 404        else
 405        {
 0406            perm.CanRead = input.CanRead;
 0407            perm.CanWrite = input.CanWrite;
 408        }
 409
 0410        await _db.SaveChangesAsync(ct);
 0411        var newJson = System.Text.Json.JsonSerializer.Serialize(input);
 0412        await _audit.LogAsync(AuthorizationConstants.AuditActionFieldPermissionChanged, nameof(RoleFieldPermission),
 0413            $"role={roleId};field={input.FieldId}", oldJson, newJson, ct: ct);
 0414        _permissions.InvalidateAll();
 0415        return Json(new { ok = true });
 0416    }
 417
 418    /// <summary>
 419    /// Массовое проставление галок по текущему набору полей (сущность на экране).
 420    /// </summary>
 421    [HttpPost]
 422    [ValidateAntiForgeryToken]
 423    public async Task<IActionResult> SetAllFieldPermission(int roleId, [FromForm] string entity, [FromForm] bool read, [
 424    {
 0425        if (string.IsNullOrEmpty(entity)) return BadRequest();
 0426        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == roleId, ct);
 0427        if (role == null) return NotFound();
 428
 0429        var fields = await _db.AppFields.Where(f => f.EntityName == entity).Select(f => f.Id).ToListAsync(ct);
 0430        if (fields.Count == 0) return Json(new { ok = true, updated = 0 });
 431
 0432        var existing = await _db.Set<RoleFieldPermission>()
 0433            .Where(p => p.AppRoleId == roleId && fields.Contains(p.AppFieldId))
 0434            .ToListAsync(ct);
 0435        var byField = existing.ToDictionary(p => p.AppFieldId);
 0436        var updated = 0;
 0437        foreach (var fid in fields)
 438        {
 0439            if (byField.TryGetValue(fid, out var perm))
 440            {
 0441                if (perm.CanRead != read || perm.CanWrite != write) updated++;
 0442                perm.CanRead = read;
 0443                perm.CanWrite = write;
 444            }
 445            else
 446            {
 0447                _db.Add(new RoleFieldPermission
 0448                {
 0449                    AppRoleId = roleId,
 0450                    AppFieldId = fid,
 0451                    CanRead = read,
 0452                    CanWrite = write,
 0453                });
 0454                updated++;
 455            }
 456        }
 457
 0458        await _db.SaveChangesAsync(ct);
 0459        await _audit.LogAsync(AuthorizationConstants.AuditActionFieldPermissionChanged, nameof(RoleFieldPermission),
 0460            $"role={roleId};entity={entity}", null, $"all read={read} write={write} fields={fields.Count}", ct: ct);
 0461        _permissions.InvalidateAll();
 0462        return Json(new { ok = true, updated });
 0463    }
 464
 465    // ---------------------- МАТРИЦА: ДАННЫЕ ----------------------
 466
 467    [HttpGet]
 468    public async Task<IActionResult> DataScopes(int id, CancellationToken ct)
 469    {
 0470        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0471        if (role == null) return NotFound();
 472
 0473        ViewData["Title"] = $"Роль «{role.DisplayName}» — наборы данных";
 0474        var all = await _db.DataScopes.OrderBy(s => s.EntityName).ThenBy(s => s.Code).ToListAsync(ct);
 0475        var selected = await _db.Set<RoleDataScope>().Where(r => r.AppRoleId == id).Select(r => r.DataScopeId).ToListAsy
 476
 0477        ViewBag.Role = role;
 0478        ViewBag.AllScopes = all;
 0479        ViewBag.SelectedScopeIds = selected.ToHashSet();
 0480        return View();
 0481    }
 482
 483    [HttpPost]
 484    [ValidateAntiForgeryToken]
 485    public async Task<IActionResult> SaveDataScopes(int id, [FromForm] int[] dataScopeIds, CancellationToken ct)
 486    {
 0487        var role = await _db.AppRoles.FirstOrDefaultAsync(r => r.Id == id, ct);
 0488        if (role == null) return NotFound();
 489
 0490        var current = await _db.Set<RoleDataScope>().Where(r => r.AppRoleId == id).ToListAsync(ct);
 0491        var oldIds = current.Select(c => c.DataScopeId).ToList();
 0492        var newIds = (dataScopeIds ?? Array.Empty<int>()).Distinct().ToList();
 493
 0494        _db.RemoveRange(current.Where(c => !newIds.Contains(c.DataScopeId)));
 0495        foreach (var sid in newIds.Where(s => !oldIds.Contains(s)))
 496        {
 0497            _db.Add(new RoleDataScope { AppRoleId = id, DataScopeId = sid });
 498        }
 499
 0500        await _db.SaveChangesAsync(ct);
 0501        await _audit.LogAsync(AuthorizationConstants.AuditActionDataScopeChanged, nameof(RoleDataScope),
 0502            $"role={id}",
 0503            System.Text.Json.JsonSerializer.Serialize(oldIds),
 0504            System.Text.Json.JsonSerializer.Serialize(newIds), ct: ct);
 0505        _permissions.InvalidateAll();
 506
 0507        TempData["SuccessMessage"] = "Наборы данных обновлены";
 0508        return RedirectToAction(nameof(DataScopes), new { id });
 0509    }
 510}