Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions website/MyWebApp.Tests/LayoutServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Caching.Memory;
using MyWebApp.Services;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Xunit;

public class LayoutServiceTests
Expand All @@ -12,8 +13,9 @@ public void CanReadZonesFromConfig()
var config = new ConfigurationBuilder().Build();
var memory = new MemoryCache(new MemoryCacheOptions());
var cache = new CacheService(memory);
var tokens = new TokenRenderService();
var service = new LayoutService(cache, tokens);
var accessor = new HttpContextAccessor();
var tokens = new TokenRenderService(accessor);
var service = new LayoutService(cache, tokens, accessor);

Assert.True(LayoutService.LayoutZones.ContainsKey("single-column"));
Assert.Contains("sidebar", LayoutService.LayoutZones["two-column-sidebar"]);
Expand Down
6 changes: 4 additions & 2 deletions website/MyWebApp.Tests/NavigationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using MyWebApp.Data;
using MyWebApp.Models;
using MyWebApp.Services;
using Microsoft.AspNetCore.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
Expand All @@ -23,7 +24,8 @@ public async Task PublishingPage_ShowsTitleOnceInHeader()
context.Database.EnsureCreated();
var memory = new MemoryCache(new MemoryCacheOptions());
var cache = new CacheService(memory);
var tokens = new TokenRenderService();
var accessor = new HttpContextAccessor();
var tokens = new TokenRenderService(accessor);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
Expand All @@ -32,7 +34,7 @@ public async Task PublishingPage_ShowsTitleOnceInHeader()
{"Layouts:two-column-sidebar:1", "sidebar"}
})
.Build();
var layout = new LayoutService(cache, tokens);
var layout = new LayoutService(cache, tokens, accessor);

context.Pages.Add(new Page { Slug = "about", Title = "About", Layout = "single-column", IsPublished = true });
context.SaveChanges();
Expand Down
5 changes: 3 additions & 2 deletions website/MyWebApp.Tests/SanitizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessin
ctx.Database.EnsureCreated();
var memory = new MemoryCache(new MemoryCacheOptions());
var cache = new CacheService(memory);
var tokens = new TokenRenderService();
var accessor = new HttpContextAccessor();
var tokens = new TokenRenderService(accessor);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
Expand All @@ -32,7 +33,7 @@ private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessin
{"Layouts:two-column-sidebar:1", "sidebar"}
})
.Build();
var layout = new LayoutService(cache, tokens);
var layout = new LayoutService(cache, tokens, accessor);
var sanitizer = new HtmlSanitizerService();
var content = new ContentProcessingService(sanitizer);
return (ctx, layout, content, tokens, sanitizer);
Expand Down
96 changes: 96 additions & 0 deletions website/MyWebApp/Controllers/AdminRoleController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyWebApp.Data;
using MyWebApp.Filters;
using MyWebApp.Models;
using System.Linq;
using System.Threading.Tasks;

namespace MyWebApp.Controllers;

[RoleAuthorize("Admin")]
public class AdminRoleController : Controller
{
private readonly ApplicationDbContext _db;

public AdminRoleController(ApplicationDbContext db)
{
_db = db;
}

public async Task<IActionResult> Index()
{
var roles = await _db.Roles.AsNoTracking().OrderBy(r => r.Name).ToListAsync();
return View(roles);
}

public IActionResult Create()
{
return View(new Role());
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Role model)
{
if (!ModelState.IsValid) return View(model);
_db.Roles.Add(model);
await _db.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

public async Task<IActionResult> Edit(int id)
{
var role = await _db.Roles
.Include(r => r.Permissions)
.FirstOrDefaultAsync(r => r.Id == id);
if (role == null) return NotFound();
var permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync();
var vm = new RoleEditViewModel
{
Role = role,
SelectedPermissions = role.Permissions.Select(p => p.PermissionId).ToList()
};
ViewBag.Permissions = permissions;
return View(vm);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(RoleEditViewModel model)
{
var role = await _db.Roles
.Include(r => r.Permissions)
.FirstOrDefaultAsync(r => r.Id == model.Role.Id);
if (role == null) return NotFound();
role.Name = model.Role.Name;
_db.RolePermissions.RemoveRange(role.Permissions);
role.Permissions.Clear();
foreach (var pid in model.SelectedPermissions.Distinct())
{
role.Permissions.Add(new RolePermission { RoleId = role.Id, PermissionId = pid });
}
await _db.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

public async Task<IActionResult> Delete(int id)
{
var role = await _db.Roles.FindAsync(id);
if (role == null) return NotFound();
return View(role);
}

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var role = await _db.Roles.FindAsync(id);
if (role != null)
{
_db.Roles.Remove(role);
await _db.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
}
6 changes: 6 additions & 0 deletions website/MyWebApp/Models/AdminModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ public class FileStatsViewModel
public DownloadFile File { get; set; } = new DownloadFile();
public int DownloadCount { get; set; }
}

public class RoleEditViewModel
{
public Role Role { get; set; } = new Role();
public IList<int> SelectedPermissions { get; set; } = new List<int>();
}
}
100 changes: 73 additions & 27 deletions website/MyWebApp/Services/LayoutService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
using System.Linq;
using MyWebApp.Data;

Expand All @@ -8,6 +9,7 @@
{
private readonly CacheService _cache;
private readonly TokenRenderService _tokens;
private readonly IHttpContextAccessor _accessor;
private const string HeaderKey = "layout_header";
private const string FooterKey = "layout_footer";

Expand All @@ -27,50 +29,94 @@
return LayoutZones.TryGetValue(layout, out var zones) ? zones : Array.Empty<string>();
}

public LayoutService(CacheService cache, TokenRenderService tokens)
public LayoutService(CacheService cache, TokenRenderService tokens, IHttpContextAccessor accessor)
{
_cache = cache;
_tokens = tokens;
_accessor = accessor;
}

private string[] GetRoles()
{
var roles = _accessor.HttpContext?.Session.GetString("Roles");
return string.IsNullOrWhiteSpace(roles) ? Array.Empty<string>() : roles.Split(',');
}

private async Task<List<int>> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles)
{
if (roles.Length == 0) return new List<int>();
return await db.RolePermissions.AsNoTracking()
.Where(rp => roles.Contains(rp.Role!.Name))
.Select(rp => rp.PermissionId)
.Distinct()
.ToListAsync();
}

public async Task<string> GetHeaderAsync(ApplicationDbContext db)
{
return await _cache.GetOrCreateAsync(HeaderKey, async e =>
var roles = GetRoles();
if (roles.Length == 0)
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var parts = await db.PageSections.AsNoTracking()
.Where(s => s.Page.Slug == "layout" && s.Zone == "header")
.OrderBy(s => s.SortOrder)
.Select(s => s.Html)
.ToListAsync();
var html = string.Join(System.Environment.NewLine, parts);
return await _tokens.RenderAsync(db, html);
});
return await _cache.GetOrCreateAsync(HeaderKey, async e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var parts = await db.PageSections.AsNoTracking()
.Where(s => s.Page.Slug == "layout" && s.Zone == "header" && s.PermissionId == null)

Check warning on line 64 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 64 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
.OrderBy(s => s.SortOrder)
.Select(s => s.Html)
.ToListAsync();
var html = string.Join(System.Environment.NewLine, parts);
return await _tokens.RenderAsync(db, html);
});
}

var allowed = await GetAllowedPermissionsAsync(db, roles);
var query = db.PageSections.AsNoTracking()
.Where(s => s.Page.Slug == "layout" && s.Zone == "header");

Check warning on line 75 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 75 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
var parts2 = await query.OrderBy(s => s.SortOrder).Select(s => s.Html).ToListAsync();
var html2 = string.Join(System.Environment.NewLine, parts2);
return await _tokens.RenderAsync(db, html2);
}

public async Task<string> GetFooterAsync(ApplicationDbContext db)
{
return await _cache.GetOrCreateAsync(FooterKey, async e =>
var roles = GetRoles();
if (roles.Length == 0)
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var parts = await db.PageSections.AsNoTracking()
.Where(s => s.Page.Slug == "layout" && s.Zone == "footer")
.OrderBy(s => s.SortOrder)
.Select(s => s.Html)
.ToListAsync();
var html = string.Join(System.Environment.NewLine, parts);
return await _tokens.RenderAsync(db, html);
});
return await _cache.GetOrCreateAsync(FooterKey, async e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var parts = await db.PageSections.AsNoTracking()
.Where(s => s.Page.Slug == "layout" && s.Zone == "footer" && s.PermissionId == null)

Check warning on line 91 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 91 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
.OrderBy(s => s.SortOrder)
.Select(s => s.Html)
.ToListAsync();
var html = string.Join(System.Environment.NewLine, parts);
return await _tokens.RenderAsync(db, html);
});
}

var allowed = await GetAllowedPermissionsAsync(db, roles);
var query = db.PageSections.AsNoTracking()
.Where(s => s.Page.Slug == "layout" && s.Zone == "footer");

Check warning on line 102 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 102 in website/MyWebApp/Services/LayoutService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
var parts2 = await query.OrderBy(s => s.SortOrder).Select(s => s.Html).ToListAsync();
var html2 = string.Join(System.Environment.NewLine, parts2);
return await _tokens.RenderAsync(db, html2);
}

public async Task<string> GetSectionAsync(ApplicationDbContext db, int pageId, string zone)
{

var parts = await db.PageSections.AsNoTracking()
.Where(s => s.PageId == pageId && s.Zone == zone)
.OrderBy(s => s.SortOrder)
.Select(s => s.Html)
.ToListAsync();
var roles = GetRoles();
var allowed = await GetAllowedPermissionsAsync(db, roles);
var query = db.PageSections.AsNoTracking()
.Where(s => s.PageId == pageId && s.Zone == zone);
if (allowed.Count == 0)
query = query.Where(s => s.PermissionId == null);
else
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
var parts = await query.OrderBy(s => s.SortOrder).Select(s => s.Html).ToListAsync();
var html = string.Join(System.Environment.NewLine, parts);
return await _tokens.RenderAsync(db, html);

Expand Down
34 changes: 32 additions & 2 deletions website/MyWebApp/Services/TokenRenderService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
using System.Linq;
using MyWebApp.Data;

Expand All @@ -8,6 +9,28 @@ namespace MyWebApp.Services;
public class TokenRenderService
{
private static readonly Regex TokenRegex = new(@"\{\{(block|section):([^{}]+)\}\}|\{\{nav\}\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly IHttpContextAccessor _accessor;

public TokenRenderService(IHttpContextAccessor accessor)
{
_accessor = accessor;
}

private string[] GetRoles()
{
var roles = _accessor.HttpContext?.Session.GetString("Roles");
return string.IsNullOrWhiteSpace(roles) ? Array.Empty<string>() : roles.Split(',');
}

private async Task<List<int>> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles)
{
if (roles.Length == 0) return new List<int>();
return await db.RolePermissions.AsNoTracking()
.Where(rp => roles.Contains(rp.Role!.Name))
.Select(rp => rp.PermissionId)
.Distinct()
.ToListAsync();
}

public Task<string> RenderAsync(ApplicationDbContext db, string html)
{
Expand Down Expand Up @@ -51,8 +74,15 @@ async Task<string> Replace(Match match)
if (parts.Length == 2 && int.TryParse(parts[0], out var pageId))
{
var zone = parts[1];
var htmlParts = await db.PageSections.AsNoTracking()
.Where(s => s.PageId == pageId && s.Zone == zone)
var roles = GetRoles();
var allowed = await GetAllowedPermissionsAsync(db, roles);
var query = db.PageSections.AsNoTracking()
.Where(s => s.PageId == pageId && s.Zone == zone);
if (allowed.Count == 0)
query = query.Where(s => s.PermissionId == null);
else
query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value));
var htmlParts = await query
.OrderBy(s => s.SortOrder)
.Select(s => s.Html)
.ToListAsync();
Expand Down
1 change: 1 addition & 0 deletions website/MyWebApp/Views/Admin/_AdminLayout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<a asp-controller="Files" asp-action="Index">Files</a>
<a asp-controller="Media" asp-action="Index">Media</a>
<a asp-controller="AdminBlockTemplate" asp-action="Index">Blocks</a>
<a asp-controller="AdminRole" asp-action="Index">Roles</a>
<a asp-controller="AdminContent" asp-action="Index">Pages</a>
<a asp-controller="Account" asp-action="Logout">Logout</a>
</nav>
Expand Down
10 changes: 10 additions & 0 deletions website/MyWebApp/Views/AdminRole/Create.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@model MyWebApp.Models.Role
@{
ViewData["Title"] = "Create Role";
Layout = "../Admin/_AdminLayout";
}
<h2>Create Role</h2>
<form asp-action="Create" method="post">
<div><label>Name</label><input asp-for="Name" /></div>
<button type="submit">Save</button>
</form>
12 changes: 12 additions & 0 deletions website/MyWebApp/Views/AdminRole/Delete.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@model MyWebApp.Models.Role
@{
ViewData["Title"] = "Delete Role";
Layout = "../Admin/_AdminLayout";
}
<h2>Delete Role</h2>
<form asp-action="Delete" method="post">
<input type="hidden" asp-for="Id" />
<p>Are you sure you want to delete "@Model.Name"?</p>
<button type="submit">Delete</button> |
<a asp-action="Index">Cancel</a>
</form>
Loading
Loading