Skip to content

Commit e41f34a

Browse files
committed
Add role management and permission filtering
1 parent 7c9b292 commit e41f34a

12 files changed

Lines changed: 282 additions & 35 deletions

File tree

website/MyWebApp.Tests/LayoutServiceTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.Caching.Memory;
33
using MyWebApp.Services;
44
using System.Collections.Generic;
5+
using Microsoft.AspNetCore.Http;
56
using Xunit;
67

78
public class LayoutServiceTests
@@ -12,8 +13,9 @@ public void CanReadZonesFromConfig()
1213
var config = new ConfigurationBuilder().Build();
1314
var memory = new MemoryCache(new MemoryCacheOptions());
1415
var cache = new CacheService(memory);
15-
var tokens = new TokenRenderService();
16-
var service = new LayoutService(cache, tokens);
16+
var accessor = new HttpContextAccessor();
17+
var tokens = new TokenRenderService(accessor);
18+
var service = new LayoutService(cache, tokens, accessor);
1719

1820
Assert.True(LayoutService.LayoutZones.ContainsKey("single-column"));
1921
Assert.Contains("sidebar", LayoutService.LayoutZones["two-column-sidebar"]);

website/MyWebApp.Tests/NavigationTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using MyWebApp.Data;
66
using MyWebApp.Models;
77
using MyWebApp.Services;
8+
using Microsoft.AspNetCore.Http;
89
using System.Text.RegularExpressions;
910
using System.Threading.Tasks;
1011
using Xunit;
@@ -23,7 +24,8 @@ public async Task PublishingPage_ShowsTitleOnceInHeader()
2324
context.Database.EnsureCreated();
2425
var memory = new MemoryCache(new MemoryCacheOptions());
2526
var cache = new CacheService(memory);
26-
var tokens = new TokenRenderService();
27+
var accessor = new HttpContextAccessor();
28+
var tokens = new TokenRenderService(accessor);
2729
var config = new ConfigurationBuilder()
2830
.AddInMemoryCollection(new Dictionary<string, string?>
2931
{
@@ -32,7 +34,7 @@ public async Task PublishingPage_ShowsTitleOnceInHeader()
3234
{"Layouts:two-column-sidebar:1", "sidebar"}
3335
})
3436
.Build();
35-
var layout = new LayoutService(cache, tokens);
37+
var layout = new LayoutService(cache, tokens, accessor);
3638

3739
context.Pages.Add(new Page { Slug = "about", Title = "About", Layout = "single-column", IsPublished = true });
3840
context.SaveChanges();

website/MyWebApp.Tests/SanitizationTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessin
2323
ctx.Database.EnsureCreated();
2424
var memory = new MemoryCache(new MemoryCacheOptions());
2525
var cache = new CacheService(memory);
26-
var tokens = new TokenRenderService();
26+
var accessor = new HttpContextAccessor();
27+
var tokens = new TokenRenderService(accessor);
2728
var config = new ConfigurationBuilder()
2829
.AddInMemoryCollection(new Dictionary<string, string?>
2930
{
@@ -32,7 +33,7 @@ private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessin
3233
{"Layouts:two-column-sidebar:1", "sidebar"}
3334
})
3435
.Build();
35-
var layout = new LayoutService(cache, tokens);
36+
var layout = new LayoutService(cache, tokens, accessor);
3637
var sanitizer = new HtmlSanitizerService();
3738
var content = new ContentProcessingService(sanitizer);
3839
return (ctx, layout, content, tokens, sanitizer);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.EntityFrameworkCore;
3+
using MyWebApp.Data;
4+
using MyWebApp.Filters;
5+
using MyWebApp.Models;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
9+
namespace MyWebApp.Controllers;
10+
11+
[RoleAuthorize("Admin")]
12+
public class AdminRoleController : Controller
13+
{
14+
private readonly ApplicationDbContext _db;
15+
16+
public AdminRoleController(ApplicationDbContext db)
17+
{
18+
_db = db;
19+
}
20+
21+
public async Task<IActionResult> Index()
22+
{
23+
var roles = await _db.Roles.AsNoTracking().OrderBy(r => r.Name).ToListAsync();
24+
return View(roles);
25+
}
26+
27+
public IActionResult Create()
28+
{
29+
return View(new Role());
30+
}
31+
32+
[HttpPost]
33+
[ValidateAntiForgeryToken]
34+
public async Task<IActionResult> Create(Role model)
35+
{
36+
if (!ModelState.IsValid) return View(model);
37+
_db.Roles.Add(model);
38+
await _db.SaveChangesAsync();
39+
return RedirectToAction(nameof(Index));
40+
}
41+
42+
public async Task<IActionResult> Edit(int id)
43+
{
44+
var role = await _db.Roles
45+
.Include(r => r.Permissions)
46+
.FirstOrDefaultAsync(r => r.Id == id);
47+
if (role == null) return NotFound();
48+
var permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync();
49+
var vm = new RoleEditViewModel
50+
{
51+
Role = role,
52+
SelectedPermissions = role.Permissions.Select(p => p.PermissionId).ToList()
53+
};
54+
ViewBag.Permissions = permissions;
55+
return View(vm);
56+
}
57+
58+
[HttpPost]
59+
[ValidateAntiForgeryToken]
60+
public async Task<IActionResult> Edit(RoleEditViewModel model)
61+
{
62+
var role = await _db.Roles
63+
.Include(r => r.Permissions)
64+
.FirstOrDefaultAsync(r => r.Id == model.Role.Id);
65+
if (role == null) return NotFound();
66+
role.Name = model.Role.Name;
67+
_db.RolePermissions.RemoveRange(role.Permissions);
68+
role.Permissions.Clear();
69+
foreach (var pid in model.SelectedPermissions.Distinct())
70+
{
71+
role.Permissions.Add(new RolePermission { RoleId = role.Id, PermissionId = pid });
72+
}
73+
await _db.SaveChangesAsync();
74+
return RedirectToAction(nameof(Index));
75+
}
76+
77+
public async Task<IActionResult> Delete(int id)
78+
{
79+
var role = await _db.Roles.FindAsync(id);
80+
if (role == null) return NotFound();
81+
return View(role);
82+
}
83+
84+
[HttpPost, ActionName("Delete")]
85+
[ValidateAntiForgeryToken]
86+
public async Task<IActionResult> DeleteConfirmed(int id)
87+
{
88+
var role = await _db.Roles.FindAsync(id);
89+
if (role != null)
90+
{
91+
_db.Roles.Remove(role);
92+
await _db.SaveChangesAsync();
93+
}
94+
return RedirectToAction(nameof(Index));
95+
}
96+
}

website/MyWebApp/Models/AdminModels.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,10 @@ public class FileStatsViewModel
4040
public DownloadFile File { get; set; } = new DownloadFile();
4141
public int DownloadCount { get; set; }
4242
}
43+
44+
public class RoleEditViewModel
45+
{
46+
public Role Role { get; set; } = new Role();
47+
public IList<int> SelectedPermissions { get; set; } = new List<int>();
48+
}
4349
}

website/MyWebApp/Services/LayoutService.cs

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.AspNetCore.Http;
23
using System.Linq;
34
using MyWebApp.Data;
45

@@ -8,6 +9,7 @@ public class LayoutService
89
{
910
private readonly CacheService _cache;
1011
private readonly TokenRenderService _tokens;
12+
private readonly IHttpContextAccessor _accessor;
1113
private const string HeaderKey = "layout_header";
1214
private const string FooterKey = "layout_footer";
1315

@@ -27,50 +29,94 @@ public static string[] GetZones(string layout)
2729
return LayoutZones.TryGetValue(layout, out var zones) ? zones : Array.Empty<string>();
2830
}
2931

30-
public LayoutService(CacheService cache, TokenRenderService tokens)
32+
public LayoutService(CacheService cache, TokenRenderService tokens, IHttpContextAccessor accessor)
3133
{
3234
_cache = cache;
3335
_tokens = tokens;
36+
_accessor = accessor;
37+
}
38+
39+
private string[] GetRoles()
40+
{
41+
var roles = _accessor.HttpContext?.Session.GetString("Roles");
42+
return string.IsNullOrWhiteSpace(roles) ? Array.Empty<string>() : roles.Split(',');
43+
}
44+
45+
private async Task<List<int>> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles)
46+
{
47+
if (roles.Length == 0) return new List<int>();
48+
return await db.RolePermissions.AsNoTracking()
49+
.Where(rp => roles.Contains(rp.Role!.Name))
50+
.Select(rp => rp.PermissionId)
51+
.Distinct()
52+
.ToListAsync();
3453
}
3554

3655
public async Task<string> GetHeaderAsync(ApplicationDbContext db)
3756
{
38-
return await _cache.GetOrCreateAsync(HeaderKey, async e =>
57+
var roles = GetRoles();
58+
if (roles.Length == 0)
3959
{
40-
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
41-
var parts = await db.PageSections.AsNoTracking()
42-
.Where(s => s.Page.Slug == "layout" && s.Zone == "header")
43-
.OrderBy(s => s.SortOrder)
44-
.Select(s => s.Html)
45-
.ToListAsync();
46-
var html = string.Join(System.Environment.NewLine, parts);
47-
return await _tokens.RenderAsync(db, html);
48-
});
60+
return await _cache.GetOrCreateAsync(HeaderKey, async e =>
61+
{
62+
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
63+
var parts = await db.PageSections.AsNoTracking()
64+
.Where(s => s.Page.Slug == "layout" && s.Zone == "header" && s.PermissionId == null)
65+
.OrderBy(s => s.SortOrder)
66+
.Select(s => s.Html)
67+
.ToListAsync();
68+
var html = string.Join(System.Environment.NewLine, parts);
69+
return await _tokens.RenderAsync(db, html);
70+
});
71+
}
72+
73+
var allowed = await GetAllowedPermissionsAsync(db, roles);
74+
var query = db.PageSections.AsNoTracking()
75+
.Where(s => s.Page.Slug == "layout" && s.Zone == "header");
76+
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
77+
var parts2 = await query.OrderBy(s => s.SortOrder).Select(s => s.Html).ToListAsync();
78+
var html2 = string.Join(System.Environment.NewLine, parts2);
79+
return await _tokens.RenderAsync(db, html2);
4980
}
5081

5182
public async Task<string> GetFooterAsync(ApplicationDbContext db)
5283
{
53-
return await _cache.GetOrCreateAsync(FooterKey, async e =>
84+
var roles = GetRoles();
85+
if (roles.Length == 0)
5486
{
55-
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
56-
var parts = await db.PageSections.AsNoTracking()
57-
.Where(s => s.Page.Slug == "layout" && s.Zone == "footer")
58-
.OrderBy(s => s.SortOrder)
59-
.Select(s => s.Html)
60-
.ToListAsync();
61-
var html = string.Join(System.Environment.NewLine, parts);
62-
return await _tokens.RenderAsync(db, html);
63-
});
87+
return await _cache.GetOrCreateAsync(FooterKey, async e =>
88+
{
89+
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
90+
var parts = await db.PageSections.AsNoTracking()
91+
.Where(s => s.Page.Slug == "layout" && s.Zone == "footer" && s.PermissionId == null)
92+
.OrderBy(s => s.SortOrder)
93+
.Select(s => s.Html)
94+
.ToListAsync();
95+
var html = string.Join(System.Environment.NewLine, parts);
96+
return await _tokens.RenderAsync(db, html);
97+
});
98+
}
99+
100+
var allowed = await GetAllowedPermissionsAsync(db, roles);
101+
var query = db.PageSections.AsNoTracking()
102+
.Where(s => s.Page.Slug == "layout" && s.Zone == "footer");
103+
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
104+
var parts2 = await query.OrderBy(s => s.SortOrder).Select(s => s.Html).ToListAsync();
105+
var html2 = string.Join(System.Environment.NewLine, parts2);
106+
return await _tokens.RenderAsync(db, html2);
64107
}
65108

66109
public async Task<string> GetSectionAsync(ApplicationDbContext db, int pageId, string zone)
67110
{
68-
69-
var parts = await db.PageSections.AsNoTracking()
70-
.Where(s => s.PageId == pageId && s.Zone == zone)
71-
.OrderBy(s => s.SortOrder)
72-
.Select(s => s.Html)
73-
.ToListAsync();
111+
var roles = GetRoles();
112+
var allowed = await GetAllowedPermissionsAsync(db, roles);
113+
var query = db.PageSections.AsNoTracking()
114+
.Where(s => s.PageId == pageId && s.Zone == zone);
115+
if (allowed.Count == 0)
116+
query = query.Where(s => s.PermissionId == null);
117+
else
118+
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
119+
var parts = await query.OrderBy(s => s.SortOrder).Select(s => s.Html).ToListAsync();
74120
var html = string.Join(System.Environment.NewLine, parts);
75121
return await _tokens.RenderAsync(db, html);
76122

website/MyWebApp/Services/TokenRenderService.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text.RegularExpressions;
22
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.AspNetCore.Http;
34
using System.Linq;
45
using MyWebApp.Data;
56

@@ -8,6 +9,28 @@ namespace MyWebApp.Services;
89
public class TokenRenderService
910
{
1011
private static readonly Regex TokenRegex = new(@"\{\{(block|section):([^{}]+)\}\}|\{\{nav\}\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
12+
private readonly IHttpContextAccessor _accessor;
13+
14+
public TokenRenderService(IHttpContextAccessor accessor)
15+
{
16+
_accessor = accessor;
17+
}
18+
19+
private string[] GetRoles()
20+
{
21+
var roles = _accessor.HttpContext?.Session.GetString("Roles");
22+
return string.IsNullOrWhiteSpace(roles) ? Array.Empty<string>() : roles.Split(',');
23+
}
24+
25+
private async Task<List<int>> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles)
26+
{
27+
if (roles.Length == 0) return new List<int>();
28+
return await db.RolePermissions.AsNoTracking()
29+
.Where(rp => roles.Contains(rp.Role!.Name))
30+
.Select(rp => rp.PermissionId)
31+
.Distinct()
32+
.ToListAsync();
33+
}
1134

1235
public Task<string> RenderAsync(ApplicationDbContext db, string html)
1336
{
@@ -51,8 +74,15 @@ async Task<string> Replace(Match match)
5174
if (parts.Length == 2 && int.TryParse(parts[0], out var pageId))
5275
{
5376
var zone = parts[1];
54-
var htmlParts = await db.PageSections.AsNoTracking()
55-
.Where(s => s.PageId == pageId && s.Zone == zone)
77+
var roles = GetRoles();
78+
var allowed = await GetAllowedPermissionsAsync(db, roles);
79+
var query = db.PageSections.AsNoTracking()
80+
.Where(s => s.PageId == pageId && s.Zone == zone);
81+
if (allowed.Count == 0)
82+
query = query.Where(s => s.PermissionId == null);
83+
else
84+
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
85+
var htmlParts = await query
5686
.OrderBy(s => s.SortOrder)
5787
.Select(s => s.Html)
5888
.ToListAsync();

website/MyWebApp/Views/Admin/_AdminLayout.cshtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<a asp-controller="Files" asp-action="Index">Files</a>
2929
<a asp-controller="Media" asp-action="Index">Media</a>
3030
<a asp-controller="AdminBlockTemplate" asp-action="Index">Blocks</a>
31+
<a asp-controller="AdminRole" asp-action="Index">Roles</a>
3132
<a asp-controller="AdminContent" asp-action="Index">Pages</a>
3233
<a asp-controller="Account" asp-action="Logout">Logout</a>
3334
</nav>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@model MyWebApp.Models.Role
2+
@{
3+
ViewData["Title"] = "Create Role";
4+
Layout = "../Admin/_AdminLayout";
5+
}
6+
<h2>Create Role</h2>
7+
<form asp-action="Create" method="post">
8+
<div><label>Name</label><input asp-for="Name" /></div>
9+
<button type="submit">Save</button>
10+
</form>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@model MyWebApp.Models.Role
2+
@{
3+
ViewData["Title"] = "Delete Role";
4+
Layout = "../Admin/_AdminLayout";
5+
}
6+
<h2>Delete Role</h2>
7+
<form asp-action="Delete" method="post">
8+
<input type="hidden" asp-for="Id" />
9+
<p>Are you sure you want to delete "@Model.Name"?</p>
10+
<button type="submit">Delete</button> |
11+
<a asp-action="Index">Cancel</a>
12+
</form>

0 commit comments

Comments
 (0)