From b667c3f9c6e5d45e109881e38cb404ce9c6768a3 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:22:10 +0800 Subject: [PATCH 01/27] Rename PageSection Area to Zone and update references --- website/MyWebApp.Tests/PageSectionTests.cs | 4 ++-- website/MyWebApp.Tests/SanitizationTests.cs | 20 ++++++++-------- .../AdminBlockTemplateController.cs | 18 +++++++-------- .../Controllers/AdminContentController.cs | 8 +++---- .../Controllers/AdminPageSectionController.cs | 20 ++++------------ website/MyWebApp/Data/ApplicationDbContext.cs | 6 ++--- .../Migrations/20250617_RenameAreaToZone.cs | 23 +++++++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 14 +++++++++++ website/MyWebApp/Models/PageSection.cs | 2 +- website/MyWebApp/Program.cs | 12 +++++----- website/MyWebApp/Services/LayoutService.cs | 16 ++++++------- .../MyWebApp/Services/TokenRenderService.cs | 4 ++-- .../TagHelpers/PageBlocksTagHelper.cs | 4 ++-- .../Views/AdminContent/_SectionEditor.cshtml | 4 ++-- .../Views/AdminPageSection/Create.cshtml | 6 ++--- .../Views/AdminPageSection/Delete.cshtml | 2 +- .../Views/AdminPageSection/Edit.cshtml | 6 ++--- .../Views/AdminPageSection/Index.cshtml | 4 ++-- .../MyWebApp/wwwroot/js/page-section-area.js | 19 --------------- .../MyWebApp/wwwroot/js/page-section-zone.js | 19 +++++++++++++++ 20 files changed, 119 insertions(+), 92 deletions(-) create mode 100644 website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs create mode 100644 website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs delete mode 100644 website/MyWebApp/wwwroot/js/page-section-area.js create mode 100644 website/MyWebApp/wwwroot/js/page-section-zone.js diff --git a/website/MyWebApp.Tests/PageSectionTests.cs b/website/MyWebApp.Tests/PageSectionTests.cs index e6d1f72..29643b7 100644 --- a/website/MyWebApp.Tests/PageSectionTests.cs +++ b/website/MyWebApp.Tests/PageSectionTests.cs @@ -22,7 +22,7 @@ public void CanAddAndRetrievePageSection() context.Pages.Add(page); context.SaveChanges(); - context.PageSections.Add(new PageSection { PageId = page.Id, Area = "header", Html = "

hi

", Type = PageSectionType.Html }); + context.PageSections.Add(new PageSection { PageId = page.Id, Zone = "header", Html = "

hi

", Type = PageSectionType.Html }); context.SaveChanges(); } @@ -30,7 +30,7 @@ public void CanAddAndRetrievePageSection() using (var context = new ApplicationDbContext(options)) { var section = context.PageSections.Include(s => s.Page) - .Single(s => s.Area == "header" && s.Page!.Slug == "test"); + .Single(s => s.Zone == "header" && s.Page!.Slug == "test"); Assert.Equal("

hi

", section.Html); Assert.Equal("test", section.Page!.Slug); } diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs index 44e1adc..3246fa6 100644 --- a/website/MyWebApp.Tests/SanitizationTests.cs +++ b/website/MyWebApp.Tests/SanitizationTests.cs @@ -40,7 +40,7 @@ public async Task CreatePage_SanitizesHtml() Layout = "single-column", Sections = new List { - new PageSection { Area = "main", Html = "

b

" } + new PageSection { Zone = "main", Html = "

b

" } } }; var result = await controller.Create(model); @@ -55,7 +55,7 @@ public async Task CreateSection_SanitizesHtml() var (ctx, layout, sanitizer) = CreateServices(); var controller = new AdminPageSectionController(ctx, layout, sanitizer); - var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "test", Html = "
hi
", Type = PageSectionType.Html }; + var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "main", Html = "
hi
", Type = PageSectionType.Html }; var result = await controller.Create(model, null); Assert.IsType(result); @@ -73,7 +73,7 @@ public async Task EditPage_SanitizesHtml() Slug = "edit", Title = "Edit", Layout = "single-column", - Sections = new List { new PageSection { Area = "main", Html = "

a

" } } + Sections = new List { new PageSection { Zone = "main", Html = "

a

" } } }; await controller.Create(createModel); var page = ctx.Pages.Single(p => p.Slug == "edit"); @@ -83,7 +83,7 @@ public async Task EditPage_SanitizesHtml() Slug = page.Slug, Title = page.Title, Layout = page.Layout, - Sections = new List { new PageSection { Area = "main", Html = "

b

" } } + Sections = new List { new PageSection { Zone = "main", Html = "

b

" } } }; var result = await controller.Edit(model); var section = ctx.PageSections.Single(s => s.PageId == page.Id); @@ -96,10 +96,10 @@ public async Task CreateSection_MarkdownConverted() { var (ctx, layout, sanitizer) = CreateServices(); var controller = new AdminPageSectionController(ctx, layout, sanitizer); - var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; + var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; var result = await controller.Create(model, null); Assert.IsType(result); - var section = ctx.PageSections.First(s => s.Area == "md"); + var section = ctx.PageSections.First(s => s.Zone == "md"); Assert.Contains("

", section.Html); Assert.DoesNotContain("(result); - var section = ctx.PageSections.First(s => s.Area == "code"); + var section = ctx.PageSections.First(s => s.Zone == "code"); Assert.Contains("<b>test</b>", section.Html); } @@ -124,10 +124,10 @@ public async Task CreateSection_ImageStoresTag() var bytes = new byte[] {1,2,3}; using var stream = new System.IO.MemoryStream(bytes); var file = new FormFile(stream, 0, bytes.Length, "file", "img.png"); - var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "img", Type = PageSectionType.Image }; + var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "img", Type = PageSectionType.Image }; var result = await controller.Create(model, file); Assert.IsType(result); - var section = ctx.PageSections.First(s => s.Area == "img"); + var section = ctx.PageSections.First(s => s.Zone == "img"); Assert.Contains(" AddToPage(int id) [HttpPost] [ValidateAntiForgeryToken] - public async Task AddToPage(int id, int pageId, string area) + public async Task AddToPage(int id, int pageId, string zone) { var template = await _db.BlockTemplates.FindAsync(id); var page = await _db.Pages.FindAsync(pageId); @@ -143,23 +143,23 @@ public async Task AddToPage(int id, int pageId, string area) { return NotFound(); } - area = area?.Trim() ?? string.Empty; - if (string.IsNullOrEmpty(area)) + zone = zone?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(zone)) { await LoadPagesAsync(); ViewBag.BlockId = id; - ModelState.AddModelError("area", "Area required"); + ModelState.AddModelError("zone", "Zone required"); return View(); } var sort = await _db.PageSections - .Where(s => s.PageId == pageId && s.Area == area) + .Where(s => s.PageId == pageId && s.Zone == zone) .Select(s => s.SortOrder) .DefaultIfEmpty(-1) .MaxAsync() + 1; var section = new PageSection { PageId = pageId, - Area = area, + Zone = zone, SortOrder = sort, Html = template.Html, Type = PageSectionType.Html @@ -192,12 +192,12 @@ public async Task GetPages() [HttpGet] public async Task GetSections(int id) { - var areas = await _db.PageSections.AsNoTracking() + var zones = await _db.PageSections.AsNoTracking() .Where(s => s.PageId == id) - .Select(s => s.Area) + .Select(s => s.Zone) .Distinct() .OrderBy(a => a) .ToListAsync(); - return Json(areas); + return Json(zones); } } diff --git a/website/MyWebApp/Controllers/AdminContentController.cs b/website/MyWebApp/Controllers/AdminContentController.cs index 7e54fea..bd78896 100644 --- a/website/MyWebApp/Controllers/AdminContentController.cs +++ b/website/MyWebApp/Controllers/AdminContentController.cs @@ -65,11 +65,11 @@ public async Task Create(Page model) model.PublishDate = DateTime.UtcNow; } var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) { ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); } - if (!sections.Any(s => s.Area == "main")) + if (!sections.Any(s => s.Zone == "main")) { ModelState.AddModelError(string.Empty, "Main area cannot be empty."); } @@ -131,11 +131,11 @@ public async Task Edit(Page model) model.PublishDate = DateTime.UtcNow; } var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) { ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); } - if (!sections.Any(s => s.Area == "main")) + if (!sections.Any(s => s.Zone == "main")) { ModelState.AddModelError(string.Empty, "Main area cannot be empty."); } diff --git a/website/MyWebApp/Controllers/AdminPageSectionController.cs b/website/MyWebApp/Controllers/AdminPageSectionController.cs index d440e5f..fc748ee 100644 --- a/website/MyWebApp/Controllers/AdminPageSectionController.cs +++ b/website/MyWebApp/Controllers/AdminPageSectionController.cs @@ -31,9 +31,9 @@ public async Task Index(string? q) if (!string.IsNullOrWhiteSpace(q)) { q = q.ToLowerInvariant(); - query = query.Where(s => s.Area.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); + query = query.Where(s => s.Zone.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); } - var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Area).ToListAsync(); + var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Zone).ToListAsync(); ViewBag.Query = q; return View(sections); } @@ -59,11 +59,6 @@ public async Task Create(PageSection model, IFormFile? file) await LoadPagesAsync(); return View(model); } - var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); - if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } if (!ModelState.IsValid) { await LoadPagesAsync(); @@ -93,11 +88,6 @@ public async Task Edit(PageSection model, IFormFile? file) await LoadPagesAsync(); return View(model); } - var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); - if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } if (!ModelState.IsValid) { await LoadPagesAsync(); @@ -165,10 +155,10 @@ public async Task DeleteConfirmed(int id) } [HttpGet] - public async Task GetAreasForPage(int id) + public async Task GetZonesForPage(int id) { var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; - var areas = LayoutService.GetAreas(layout); - return Json(areas); + var zones = LayoutService.GetZones(layout); + return Json(zones); } } diff --git a/website/MyWebApp/Data/ApplicationDbContext.cs b/website/MyWebApp/Data/ApplicationDbContext.cs index 3a5f5e0..d1f2415 100644 --- a/website/MyWebApp/Data/ApplicationDbContext.cs +++ b/website/MyWebApp/Data/ApplicationDbContext.cs @@ -54,7 +54,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() - .HasIndex(s => new { s.PageId, s.Area, s.SortOrder }); + .HasIndex(s => new { s.PageId, s.Zone, s.SortOrder }); modelBuilder.Entity() @@ -105,7 +105,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { Id = 1, PageId = 1, - Area = "header", + Zone = "header", SortOrder = 0, Type = PageSectionType.Html, @@ -117,7 +117,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { Id = 2, PageId = 1, - Area = "footer", + Zone = "footer", SortOrder = 0, Type = PageSectionType.Html, diff --git a/website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs b/website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs new file mode 100644 index 0000000..0b2e978 --- /dev/null +++ b/website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace MyWebApp.Migrations +{ + public partial class _20250617_RenameAreaToZone : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Area", + table: "PageSections", + newName: "Zone"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Zone", + table: "PageSections", + newName: "Area"); + } + } +} diff --git a/website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs b/website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..872d909 --- /dev/null +++ b/website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using MyWebApp.Data; + +namespace MyWebApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + } + } +} diff --git a/website/MyWebApp/Models/PageSection.cs b/website/MyWebApp/Models/PageSection.cs index 294236d..f0cf8c5 100644 --- a/website/MyWebApp/Models/PageSection.cs +++ b/website/MyWebApp/Models/PageSection.cs @@ -21,7 +21,7 @@ public class PageSection [Required] [MaxLength(64)] - public string Area { get; set; } = string.Empty; + public string Zone { get; set; } = string.Empty; public int SortOrder { get; set; } diff --git a/website/MyWebApp/Program.cs b/website/MyWebApp/Program.cs index da50f5a..381fd7d 100644 --- a/website/MyWebApp/Program.cs +++ b/website/MyWebApp/Program.cs @@ -317,7 +317,7 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) db.Database.ExecuteSqlRaw(@"CREATE TABLE PageSections ( Id INTEGER PRIMARY KEY AUTOINCREMENT, PageId INTEGER NOT NULL, - Area TEXT NOT NULL, + Zone TEXT NOT NULL, SortOrder INTEGER NOT NULL DEFAULT 0, Type INTEGER NOT NULL DEFAULT 0, Html TEXT, @@ -327,8 +327,8 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) ViewCount INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(PageId) REFERENCES Pages(Id) ON DELETE CASCADE )"); - db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Area_SortOrder ON PageSections(PageId, Area, SortOrder)"); - db.Database.ExecuteSqlRaw(@"INSERT INTO PageSections (Id, PageId, Area, SortOrder, Type, Html) VALUES + db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Zone_SortOrder ON PageSections(PageId, Zone, SortOrder)"); + db.Database.ExecuteSqlRaw(@"INSERT INTO PageSections (Id, PageId, Zone, SortOrder, Type, Html) VALUES (1, 1, 'header', 0, 0, ''), (2, 1, 'footer', 0, 0, '
© 2025 - Screen Area Recorder Pro
')"); } @@ -365,8 +365,8 @@ FOREIGN KEY(PageId) REFERENCES Pages(Id) ON DELETE CASCADE idx.Close(); if (indexes.Contains("IX_PageSections_PageId_Area")) db.Database.ExecuteSqlRaw("DROP INDEX IX_PageSections_PageId_Area"); - if (!indexes.Contains("IX_PageSections_PageId_Area_SortOrder")) - db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Area_SortOrder ON PageSections(PageId, Area, SortOrder)"); + if (!indexes.Contains("IX_PageSections_PageId_Zone_SortOrder")) + db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Zone_SortOrder ON PageSections(PageId, Zone, SortOrder)"); } } catch (Exception ex) @@ -562,7 +562,7 @@ static void UpgradeLayoutHeader(ApplicationDbContext db) return; var section = db.PageSections - .FirstOrDefault(s => s.PageId == layoutId && s.Area == "header"); + .FirstOrDefault(s => s.PageId == layoutId && s.Zone == "header"); if (section == null) return; diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs index a53b4c6..a9aed2f 100644 --- a/website/MyWebApp/Services/LayoutService.cs +++ b/website/MyWebApp/Services/LayoutService.cs @@ -17,12 +17,12 @@ public class LayoutService ["two-column-sidebar"] = new[] { "main", "sidebar" } }; - public static bool IsValidArea(string layout, string area) + public static bool IsValidZone(string layout, string zone) { - return LayoutZones.TryGetValue(layout, out var zones) && zones.Contains(area); + return LayoutZones.TryGetValue(layout, out var zones) && zones.Contains(zone); } - public static string[] GetAreas(string layout) + public static string[] GetZones(string layout) { return LayoutZones.TryGetValue(layout, out var zones) ? zones : Array.Empty(); } @@ -39,7 +39,7 @@ public async Task GetHeaderAsync(ApplicationDbContext db) { e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); var parts = await db.PageSections.AsNoTracking() - .Where(s => s.Page.Slug == "layout" && s.Area == "header") + .Where(s => s.Page.Slug == "layout" && s.Zone == "header") .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); @@ -54,7 +54,7 @@ public async Task GetFooterAsync(ApplicationDbContext db) { e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); var parts = await db.PageSections.AsNoTracking() - .Where(s => s.Page.Slug == "layout" && s.Area == "footer") + .Where(s => s.Page.Slug == "layout" && s.Zone == "footer") .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); @@ -63,11 +63,11 @@ public async Task GetFooterAsync(ApplicationDbContext db) }); } - public async Task GetSectionAsync(ApplicationDbContext db, int pageId, string area) + public async Task GetSectionAsync(ApplicationDbContext db, int pageId, string zone) { - + var parts = await db.PageSections.AsNoTracking() - .Where(s => s.PageId == pageId && s.Area == area) + .Where(s => s.PageId == pageId && s.Zone == zone) .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); diff --git a/website/MyWebApp/Services/TokenRenderService.cs b/website/MyWebApp/Services/TokenRenderService.cs index a71ae00..cc6f4fb 100644 --- a/website/MyWebApp/Services/TokenRenderService.cs +++ b/website/MyWebApp/Services/TokenRenderService.cs @@ -50,9 +50,9 @@ async Task Replace(Match match) var parts = param.Split(':', 2); if (parts.Length == 2 && int.TryParse(parts[0], out var pageId)) { - var area = parts[1]; + var zone = parts[1]; var htmlParts = await db.PageSections.AsNoTracking() - .Where(s => s.PageId == pageId && s.Area == area) + .Where(s => s.PageId == pageId && s.Zone == zone) .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); diff --git a/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs b/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs index 6f8c8cb..2f7dc72 100644 --- a/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs +++ b/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs @@ -17,13 +17,13 @@ public PageBlocksTagHelper(ApplicationDbContext db, TokenRenderService tokens) } public int PageId { get; set; } - public string Area { get; set; } = string.Empty; + public string Zone { get; set; } = string.Empty; public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { output.TagName = null; var htmlParts = await _db.PageSections.AsNoTracking() - .Where(s => s.PageId == PageId && s.Area == Area) + .Where(s => s.PageId == PageId && s.Zone == Zone) .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); diff --git a/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml b/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml index 01027e7..beaff5f 100644 --- a/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml +++ b/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml @@ -15,8 +15,8 @@
- - + +
diff --git a/website/MyWebApp/Views/AdminPageSection/Create.cshtml b/website/MyWebApp/Views/AdminPageSection/Create.cshtml index 3e1d846..4f30413 100644 --- a/website/MyWebApp/Views/AdminPageSection/Create.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Create.cshtml @@ -12,12 +12,12 @@
- - + +
@await Html.PartialAsync("_SectionEditor", Model) - + diff --git a/website/MyWebApp/Views/AdminPageSection/Delete.cshtml b/website/MyWebApp/Views/AdminPageSection/Delete.cshtml index a93a86a..a426762 100644 --- a/website/MyWebApp/Views/AdminPageSection/Delete.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Delete.cshtml @@ -6,7 +6,7 @@

Delete Section

-

Are you sure you want to delete @Model.Area for page @Model.PageId?

+

Are you sure you want to delete @Model.Zone for page @Model.PageId?

Cancel
diff --git a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml index 641ab5c..590e245 100644 --- a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml @@ -13,12 +13,12 @@
- - + +
@await Html.PartialAsync("_SectionEditor", Model) - + diff --git a/website/MyWebApp/Views/AdminPageSection/Index.cshtml b/website/MyWebApp/Views/AdminPageSection/Index.cshtml index eb878dc..8115b47 100644 --- a/website/MyWebApp/Views/AdminPageSection/Index.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Index.cshtml @@ -12,7 +12,7 @@ - + @@ -20,7 +20,7 @@ { - + diff --git a/website/MyWebApp/wwwroot/js/page-section-area.js b/website/MyWebApp/wwwroot/js/page-section-area.js deleted file mode 100644 index c105b77..0000000 --- a/website/MyWebApp/wwwroot/js/page-section-area.js +++ /dev/null @@ -1,19 +0,0 @@ -window.addEventListener('load', () => { - const pageSelect = document.querySelector('select[name="PageId"]'); - const areaSelect = document.getElementById('area-select'); - if (!pageSelect || !areaSelect) return; - - function loadAreas() { - const id = pageSelect.value; - if (!id) { areaSelect.innerHTML = ''; return; } - fetch(`/AdminPageSection/GetAreasForPage/${id}`) - .then(r => r.json()) - .then(list => { - areaSelect.innerHTML = list.map(a => ``).join(''); - if (areaSelect.dataset.selected) - areaSelect.value = areaSelect.dataset.selected; - }); - } - loadAreas(); - pageSelect.addEventListener('change', loadAreas); -}); diff --git a/website/MyWebApp/wwwroot/js/page-section-zone.js b/website/MyWebApp/wwwroot/js/page-section-zone.js new file mode 100644 index 0000000..9b2d0d3 --- /dev/null +++ b/website/MyWebApp/wwwroot/js/page-section-zone.js @@ -0,0 +1,19 @@ +window.addEventListener('load', () => { + const pageSelect = document.querySelector('select[name="PageId"]'); + const zoneSelect = document.getElementById('zone-select'); + if (!pageSelect || !zoneSelect) return; + + function loadZones() { + const id = pageSelect.value; + if (!id) { zoneSelect.innerHTML = ''; return; } + fetch(`/AdminPageSection/GetZonesForPage/${id}`) + .then(r => r.json()) + .then(list => { + zoneSelect.innerHTML = list.map(a => ``).join(''); + if (zoneSelect.dataset.selected) + zoneSelect.value = zoneSelect.dataset.selected; + }); + } + loadZones(); + pageSelect.addEventListener('change', loadZones); +}); From 8dacfcbf2d9703ab4846ce5ee13408d2edf93eb1 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:30:20 +0800 Subject: [PATCH 02/27] Read layout zones from config --- website/MyWebApp.Tests/LayoutServiceTests.cs | 28 +++++++++++++++++++ website/MyWebApp.Tests/NavigationTests.cs | 11 +++++++- website/MyWebApp.Tests/SanitizationTests.cs | 11 +++++++- .../Controllers/AdminContentController.cs | 5 ++-- .../Controllers/AdminPageSectionController.cs | 2 +- .../MyWebApp/Controllers/PagesController.cs | 4 +-- website/MyWebApp/Services/LayoutService.cs | 20 ++++++------- .../Views/AdminContent/PageEditor.cshtml | 11 ++++++-- website/MyWebApp/appsettings.json | 4 +++ 9 files changed, 76 insertions(+), 20 deletions(-) create mode 100644 website/MyWebApp.Tests/LayoutServiceTests.cs diff --git a/website/MyWebApp.Tests/LayoutServiceTests.cs b/website/MyWebApp.Tests/LayoutServiceTests.cs new file mode 100644 index 0000000..2ca3585 --- /dev/null +++ b/website/MyWebApp.Tests/LayoutServiceTests.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Caching.Memory; +using MyWebApp.Services; +using System.Collections.Generic; +using Xunit; + +public class LayoutServiceTests +{ + [Fact] + public void CanReadZonesFromConfig() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"Layouts:single-column:0", "main"}, + {"Layouts:two-column-sidebar:0", "main"}, + {"Layouts:two-column-sidebar:1", "sidebar"} + }) + .Build(); + var memory = new MemoryCache(new MemoryCacheOptions()); + var cache = new CacheService(memory); + var tokens = new TokenRenderService(); + var service = new LayoutService(cache, tokens, config); + + Assert.True(service.LayoutZones.ContainsKey("single-column")); + Assert.Contains("sidebar", service.LayoutZones["two-column-sidebar"]); + } +} diff --git a/website/MyWebApp.Tests/NavigationTests.cs b/website/MyWebApp.Tests/NavigationTests.cs index 23677c1..dbbbeb8 100644 --- a/website/MyWebApp.Tests/NavigationTests.cs +++ b/website/MyWebApp.Tests/NavigationTests.cs @@ -1,6 +1,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; using MyWebApp.Data; using MyWebApp.Models; using MyWebApp.Services; @@ -23,7 +24,15 @@ public async Task PublishingPage_ShowsTitleOnceInHeader() var memory = new MemoryCache(new MemoryCacheOptions()); var cache = new CacheService(memory); var tokens = new TokenRenderService(); - var layout = new LayoutService(cache, tokens); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"Layouts:single-column:0", "main"}, + {"Layouts:two-column-sidebar:0", "main"}, + {"Layouts:two-column-sidebar:1", "sidebar"} + }) + .Build(); + var layout = new LayoutService(cache, tokens, config); context.Pages.Add(new Page { Slug = "about", Title = "About", Layout = "single-column", IsPublished = true }); context.SaveChanges(); diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs index 3246fa6..1ae0bf2 100644 --- a/website/MyWebApp.Tests/SanitizationTests.cs +++ b/website/MyWebApp.Tests/SanitizationTests.cs @@ -1,6 +1,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; using MyWebApp.Controllers; @@ -23,7 +24,15 @@ private static (ApplicationDbContext ctx, LayoutService layout, HtmlSanitizerSer var memory = new MemoryCache(new MemoryCacheOptions()); var cache = new CacheService(memory); var tokens = new TokenRenderService(); - var layout = new LayoutService(cache, tokens); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"Layouts:single-column:0", "main"}, + {"Layouts:two-column-sidebar:0", "main"}, + {"Layouts:two-column-sidebar:1", "sidebar"} + }) + .Build(); + var layout = new LayoutService(cache, tokens, config); var sanitizer = new HtmlSanitizerService(); return (ctx, layout, sanitizer); } diff --git a/website/MyWebApp/Controllers/AdminContentController.cs b/website/MyWebApp/Controllers/AdminContentController.cs index bd78896..f6315cb 100644 --- a/website/MyWebApp/Controllers/AdminContentController.cs +++ b/website/MyWebApp/Controllers/AdminContentController.cs @@ -33,6 +33,7 @@ private async Task LoadTemplatesAsync() .OrderBy(t => t.Name).ToListAsync(); ViewBag.Permissions = await _db.Permissions.AsNoTracking() .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; } public async Task Index() @@ -65,7 +66,7 @@ public async Task Create(Page model) model.PublishDate = DateTime.UtcNow; } var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) { ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); } @@ -131,7 +132,7 @@ public async Task Edit(Page model) model.PublishDate = DateTime.UtcNow; } var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) { ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); } diff --git a/website/MyWebApp/Controllers/AdminPageSectionController.cs b/website/MyWebApp/Controllers/AdminPageSectionController.cs index fc748ee..c435ae2 100644 --- a/website/MyWebApp/Controllers/AdminPageSectionController.cs +++ b/website/MyWebApp/Controllers/AdminPageSectionController.cs @@ -158,7 +158,7 @@ public async Task DeleteConfirmed(int id) public async Task GetZonesForPage(int id) { var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; - var zones = LayoutService.GetZones(layout); + var zones = _layout.GetZones(layout); return Json(zones); } } diff --git a/website/MyWebApp/Controllers/PagesController.cs b/website/MyWebApp/Controllers/PagesController.cs index 8d5cae0..a952cf7 100644 --- a/website/MyWebApp/Controllers/PagesController.cs +++ b/website/MyWebApp/Controllers/PagesController.cs @@ -42,9 +42,9 @@ public async Task Show(string? slug) } var layoutName = string.IsNullOrWhiteSpace(page.Layout) ? "single-column" : page.Layout; - if (!LayoutService.LayoutZones.TryGetValue(layoutName, out var zones)) + if (!_layout.LayoutZones.TryGetValue(layoutName, out var zones)) { - zones = LayoutService.LayoutZones["single-column"]; + zones = _layout.LayoutZones["single-column"]; } var zoneHtml = new Dictionary(); foreach (var z in zones) diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs index a9aed2f..48a8bb1 100644 --- a/website/MyWebApp/Services/LayoutService.cs +++ b/website/MyWebApp/Services/LayoutService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using System.Linq; using MyWebApp.Data; @@ -8,29 +9,28 @@ public class LayoutService { private readonly CacheService _cache; private readonly TokenRenderService _tokens; + private readonly Dictionary _zoneMap; private const string HeaderKey = "layout_header"; private const string FooterKey = "layout_footer"; - public static readonly Dictionary LayoutZones = new() - { - ["single-column"] = new[] { "main" }, - ["two-column-sidebar"] = new[] { "main", "sidebar" } - }; + public IReadOnlyDictionary LayoutZones => _zoneMap; - public static bool IsValidZone(string layout, string zone) + public bool IsValidZone(string layout, string zone) { - return LayoutZones.TryGetValue(layout, out var zones) && zones.Contains(zone); + return _zoneMap.TryGetValue(layout, out var zones) && zones.Contains(zone); } - public static string[] GetZones(string layout) + public string[] GetZones(string layout) { - return LayoutZones.TryGetValue(layout, out var zones) ? zones : Array.Empty(); + return _zoneMap.TryGetValue(layout, out var zones) ? zones : Array.Empty(); } - public LayoutService(CacheService cache, TokenRenderService tokens) + public LayoutService(CacheService cache, TokenRenderService tokens, IConfiguration configuration) { _cache = cache; _tokens = tokens; + _zoneMap = configuration.GetSection("Layouts").Get>() + ?? new Dictionary(); } public async Task GetHeaderAsync(ApplicationDbContext db) diff --git a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml index 77bd2d0..8aa38f5 100644 --- a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml +++ b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml @@ -17,8 +17,13 @@
@@ -62,7 +67,7 @@ @section Scripts { + + +} From 1143b2dafa80be233c849f2f01634ac854f62cf0 Mon Sep 17 00:00:00 2001 From: Denis-RZ Date: Tue, 17 Jun 2025 23:33:46 +0800 Subject: [PATCH 07/27] Merge remote-tracking branch 'origin/ndmn4k-codex/rename-area-property-to-zone' # Conflicts: # website/MyWebApp/Controllers/AdminContentController.cs # website/MyWebApp/Controllers/AdminPageSectionController.cs # website/MyWebApp/Services/LayoutService.cs # website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml # website/MyWebApp/Views/AdminPageSection/Index.cshtml --- rename-zone.patch | 777 ++++++++++++++++++ .../AdminContentController_BACKUP_200.cs | 237 ++++++ .../AdminContentController_BACKUP_263.cs | 237 ++++++ .../AdminContentController_BACKUP_389.cs | 237 ++++++ .../AdminContentController_BASE_200.cs | 228 +++++ .../AdminContentController_BASE_263.cs | 228 +++++ .../AdminContentController_BASE_389.cs | 228 +++++ .../AdminContentController_LOCAL_200.cs | 229 ++++++ .../AdminContentController_LOCAL_263.cs | 229 ++++++ .../AdminContentController_LOCAL_389.cs | 229 ++++++ .../AdminContentController_REMOTE_200.cs | 228 +++++ .../AdminContentController_REMOTE_263.cs | 228 +++++ .../AdminContentController_REMOTE_389.cs | 228 +++++ .../AdminPageSectionController_BACKUP_326.cs | 195 +++++ .../AdminPageSectionController_BASE_326.cs | 164 ++++ .../AdminPageSectionController_LOCAL_326.cs | 180 ++++ .../AdminPageSectionController_REMOTE_326.cs | 174 ++++ 17 files changed, 4256 insertions(+) create mode 100644 rename-zone.patch create mode 100644 website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_BASE_200.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_BASE_263.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_BASE_389.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs create mode 100644 website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs create mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs create mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs create mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs create mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs diff --git a/rename-zone.patch b/rename-zone.patch new file mode 100644 index 0000000..7b5c6e6 --- /dev/null +++ b/rename-zone.patch @@ -0,0 +1,777 @@ +diff --git a/website/MyWebApp.Tests/PageSectionTests.cs b/website/MyWebApp.Tests/PageSectionTests.cs +index e6d1f72..29643b7 100644 +--- a/website/MyWebApp.Tests/PageSectionTests.cs ++++ b/website/MyWebApp.Tests/PageSectionTests.cs +@@ -22,7 +22,7 @@ public class PageSectionTests + context.Pages.Add(page); + context.SaveChanges(); + +- context.PageSections.Add(new PageSection { PageId = page.Id, Area = "header", Html = "

hi

", Type = PageSectionType.Html }); ++ context.PageSections.Add(new PageSection { PageId = page.Id, Zone = "header", Html = "

hi

", Type = PageSectionType.Html }); + + context.SaveChanges(); + } +@@ -30,7 +30,7 @@ public class PageSectionTests + using (var context = new ApplicationDbContext(options)) + { + var section = context.PageSections.Include(s => s.Page) +- .Single(s => s.Area == "header" && s.Page!.Slug == "test"); ++ .Single(s => s.Zone == "header" && s.Page!.Slug == "test"); + Assert.Equal("

hi

", section.Html); + Assert.Equal("test", section.Page!.Slug); + } +diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs +index 44e1adc..3246fa6 100644 +--- a/website/MyWebApp.Tests/SanitizationTests.cs ++++ b/website/MyWebApp.Tests/SanitizationTests.cs +@@ -40,7 +40,7 @@ public class SanitizationTests + Layout = "single-column", + Sections = new List + { +- new PageSection { Area = "main", Html = "

b

" } ++ new PageSection { Zone = "main", Html = "

b

" } + } + }; + var result = await controller.Create(model); +@@ -55,7 +55,7 @@ public class SanitizationTests + var (ctx, layout, sanitizer) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, sanitizer); + +- var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "test", Html = "
hi
", Type = PageSectionType.Html }; ++ var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "main", Html = "
hi
", Type = PageSectionType.Html }; + var result = await controller.Create(model, null); + + Assert.IsType(result); +@@ -73,7 +73,7 @@ public class SanitizationTests + Slug = "edit", + Title = "Edit", + Layout = "single-column", +- Sections = new List { new PageSection { Area = "main", Html = "

a

" } } ++ Sections = new List { new PageSection { Zone = "main", Html = "

a

" } } + }; + await controller.Create(createModel); + var page = ctx.Pages.Single(p => p.Slug == "edit"); +@@ -83,7 +83,7 @@ public class SanitizationTests + Slug = page.Slug, + Title = page.Title, + Layout = page.Layout, +- Sections = new List { new PageSection { Area = "main", Html = "

b

" } } ++ Sections = new List { new PageSection { Zone = "main", Html = "

b

" } } + }; + var result = await controller.Edit(model); + var section = ctx.PageSections.Single(s => s.PageId == page.Id); +@@ -96,10 +96,10 @@ public class SanitizationTests + { + var (ctx, layout, sanitizer) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, sanitizer); +- var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; ++ var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; + var result = await controller.Create(model, null); + Assert.IsType(result); +- var section = ctx.PageSections.First(s => s.Area == "md"); ++ var section = ctx.PageSections.First(s => s.Zone == "md"); + Assert.Contains("

", section.Html); + Assert.DoesNotContain("(result); +- var section = ctx.PageSections.First(s => s.Area == "code"); ++ var section = ctx.PageSections.First(s => s.Zone == "code"); + Assert.Contains("<b>test</b>", section.Html); + } + +@@ -124,10 +124,10 @@ public class SanitizationTests + var bytes = new byte[] {1,2,3}; + using var stream = new System.IO.MemoryStream(bytes); + var file = new FormFile(stream, 0, bytes.Length, "file", "img.png"); +- var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "img", Type = PageSectionType.Image }; ++ var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "img", Type = PageSectionType.Image }; + var result = await controller.Create(model, file); + Assert.IsType(result); +- var section = ctx.PageSections.First(s => s.Area == "img"); ++ var section = ctx.PageSections.First(s => s.Zone == "img"); + Assert.Contains(" AddToPage(int id, int pageId, string area) ++ public async Task AddToPage(int id, int pageId, string zone) + { + var template = await _db.BlockTemplates.FindAsync(id); + var page = await _db.Pages.FindAsync(pageId); +@@ -143,23 +143,23 @@ public class AdminBlockTemplateController : Controller + { + return NotFound(); + } +- area = area?.Trim() ?? string.Empty; +- if (string.IsNullOrEmpty(area)) ++ zone = zone?.Trim() ?? string.Empty; ++ if (string.IsNullOrEmpty(zone)) + { + await LoadPagesAsync(); + ViewBag.BlockId = id; +- ModelState.AddModelError("area", "Area required"); ++ ModelState.AddModelError("zone", "Zone required"); + return View(); + } + var sort = await _db.PageSections +- .Where(s => s.PageId == pageId && s.Area == area) ++ .Where(s => s.PageId == pageId && s.Zone == zone) + .Select(s => s.SortOrder) + .DefaultIfEmpty(-1) + .MaxAsync() + 1; + var section = new PageSection + { + PageId = pageId, +- Area = area, ++ Zone = zone, + SortOrder = sort, + Html = template.Html, + Type = PageSectionType.Html +@@ -192,12 +192,12 @@ public class AdminBlockTemplateController : Controller + [HttpGet] + public async Task GetSections(int id) + { +- var areas = await _db.PageSections.AsNoTracking() ++ var zones = await _db.PageSections.AsNoTracking() + .Where(s => s.PageId == id) +- .Select(s => s.Area) ++ .Select(s => s.Zone) + .Distinct() + .OrderBy(a => a) + .ToListAsync(); +- return Json(areas); ++ return Json(zones); + } + } +diff --git a/website/MyWebApp/Controllers/AdminContentController.cs b/website/MyWebApp/Controllers/AdminContentController.cs +index 7e54fea..bd78896 100644 +--- a/website/MyWebApp/Controllers/AdminContentController.cs ++++ b/website/MyWebApp/Controllers/AdminContentController.cs +@@ -65,11 +65,11 @@ public class AdminContentController : Controller + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +- if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ++ if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } +- if (!sections.Any(s => s.Area == "main")) ++ if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } +@@ -131,11 +131,11 @@ public class AdminContentController : Controller + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +- if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ++ if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } +- if (!sections.Any(s => s.Area == "main")) ++ if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } +diff --git a/website/MyWebApp/Controllers/AdminPageSectionController.cs b/website/MyWebApp/Controllers/AdminPageSectionController.cs +index d440e5f..fc748ee 100644 +--- a/website/MyWebApp/Controllers/AdminPageSectionController.cs ++++ b/website/MyWebApp/Controllers/AdminPageSectionController.cs +@@ -31,9 +31,9 @@ public class AdminPageSectionController : Controller + if (!string.IsNullOrWhiteSpace(q)) + { + q = q.ToLowerInvariant(); +- query = query.Where(s => s.Area.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); ++ query = query.Where(s => s.Zone.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); + } +- var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Area).ToListAsync(); ++ var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Zone).ToListAsync(); + ViewBag.Query = q; + return View(sections); + } +@@ -59,11 +59,6 @@ public class AdminPageSectionController : Controller + await LoadPagesAsync(); + return View(model); + } +- var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); +- if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) +- { +- ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); +- } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); +@@ -93,11 +88,6 @@ public class AdminPageSectionController : Controller + await LoadPagesAsync(); + return View(model); + } +- var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); +- if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) +- { +- ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); +- } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); +@@ -165,10 +155,10 @@ public class AdminPageSectionController : Controller + } + + [HttpGet] +- public async Task GetAreasForPage(int id) ++ public async Task GetZonesForPage(int id) + { + var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; +- var areas = LayoutService.GetAreas(layout); +- return Json(areas); ++ var zones = LayoutService.GetZones(layout); ++ return Json(zones); + } + } +diff --git a/website/MyWebApp/Data/ApplicationDbContext.cs b/website/MyWebApp/Data/ApplicationDbContext.cs +index 3a5f5e0..d1f2415 100644 +--- a/website/MyWebApp/Data/ApplicationDbContext.cs ++++ b/website/MyWebApp/Data/ApplicationDbContext.cs +@@ -54,7 +54,7 @@ namespace MyWebApp.Data + + modelBuilder.Entity() + +- .HasIndex(s => new { s.PageId, s.Area, s.SortOrder }); ++ .HasIndex(s => new { s.PageId, s.Zone, s.SortOrder }); + + + modelBuilder.Entity() +@@ -105,7 +105,7 @@ namespace MyWebApp.Data + { + Id = 1, + PageId = 1, +- Area = "header", ++ Zone = "header", + SortOrder = 0, + + Type = PageSectionType.Html, +@@ -117,7 +117,7 @@ namespace MyWebApp.Data + { + Id = 2, + PageId = 1, +- Area = "footer", ++ Zone = "footer", + SortOrder = 0, + + Type = PageSectionType.Html, +diff --git a/website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs b/website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs +new file mode 100644 +index 0000000..0b2e978 +--- /dev/null ++++ b/website/MyWebApp/Migrations/20250617_RenameAreaToZone.cs +@@ -0,0 +1,23 @@ ++using Microsoft.EntityFrameworkCore.Migrations; ++ ++namespace MyWebApp.Migrations ++{ ++ public partial class _20250617_RenameAreaToZone : Migration ++ { ++ protected override void Up(MigrationBuilder migrationBuilder) ++ { ++ migrationBuilder.RenameColumn( ++ name: "Area", ++ table: "PageSections", ++ newName: "Zone"); ++ } ++ ++ protected override void Down(MigrationBuilder migrationBuilder) ++ { ++ migrationBuilder.RenameColumn( ++ name: "Zone", ++ table: "PageSections", ++ newName: "Area"); ++ } ++ } ++} +diff --git a/website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs b/website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs +new file mode 100644 +index 0000000..872d909 +--- /dev/null ++++ b/website/MyWebApp/Migrations/ApplicationDbContextModelSnapshot.cs +@@ -0,0 +1,14 @@ ++using Microsoft.EntityFrameworkCore; ++using Microsoft.EntityFrameworkCore.Infrastructure; ++using MyWebApp.Data; ++ ++namespace MyWebApp.Migrations ++{ ++ [DbContext(typeof(ApplicationDbContext))] ++ partial class ApplicationDbContextModelSnapshot : ModelSnapshot ++ { ++ protected override void BuildModel(ModelBuilder modelBuilder) ++ { ++ } ++ } ++} +diff --git a/website/MyWebApp/Models/PageSection.cs b/website/MyWebApp/Models/PageSection.cs +index 294236d..f0cf8c5 100644 +--- a/website/MyWebApp/Models/PageSection.cs ++++ b/website/MyWebApp/Models/PageSection.cs +@@ -21,7 +21,7 @@ public class PageSection + + [Required] + [MaxLength(64)] +- public string Area { get; set; } = string.Empty; ++ public string Zone { get; set; } = string.Empty; + + public int SortOrder { get; set; } + +diff --git a/website/MyWebApp/Program.cs b/website/MyWebApp/Program.cs +index da50f5a..650cc87 100644 +--- a/website/MyWebApp/Program.cs ++++ b/website/MyWebApp/Program.cs +@@ -317,7 +317,7 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) + db.Database.ExecuteSqlRaw(@"CREATE TABLE PageSections ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + PageId INTEGER NOT NULL, +- Area TEXT NOT NULL, ++ Zone TEXT NOT NULL, + SortOrder INTEGER NOT NULL DEFAULT 0, + Type INTEGER NOT NULL DEFAULT 0, + Html TEXT, +@@ -327,8 +327,8 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) + ViewCount INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(PageId) REFERENCES Pages(Id) ON DELETE CASCADE + )"); +- db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Area_SortOrder ON PageSections(PageId, Area, SortOrder)"); +- db.Database.ExecuteSqlRaw(@"INSERT INTO PageSections (Id, PageId, Area, SortOrder, Type, Html) VALUES ++ db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Zone_SortOrder ON PageSections(PageId, Zone, SortOrder)"); ++ db.Database.ExecuteSqlRaw(@"INSERT INTO PageSections (Id, PageId, Zone, SortOrder, Type, Html) VALUES + (1, 1, 'header', 0, 0, ''), + (2, 1, 'footer', 0, 0, '
© 2025 - Screen Area Recorder Pro
')"); + } +@@ -342,6 +342,8 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) + columns.Add(reader.GetString(1)); + } + reader.Close(); ++ if (columns.Contains("Area") && !columns.Contains("Zone")) ++ db.Database.ExecuteSqlRaw("ALTER TABLE PageSections RENAME COLUMN Area TO Zone"); + if (!columns.Contains("SortOrder")) + db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN SortOrder INTEGER NOT NULL DEFAULT 0"); + if (!columns.Contains("Type")) +@@ -365,8 +367,8 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) + idx.Close(); + if (indexes.Contains("IX_PageSections_PageId_Area")) + db.Database.ExecuteSqlRaw("DROP INDEX IX_PageSections_PageId_Area"); +- if (!indexes.Contains("IX_PageSections_PageId_Area_SortOrder")) +- db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Area_SortOrder ON PageSections(PageId, Area, SortOrder)"); ++ if (!indexes.Contains("IX_PageSections_PageId_Zone_SortOrder")) ++ db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Zone_SortOrder ON PageSections(PageId, Zone, SortOrder)"); + } + } + catch (Exception ex) +@@ -562,7 +564,7 @@ static void UpgradeLayoutHeader(ApplicationDbContext db) + return; + + var section = db.PageSections +- .FirstOrDefault(s => s.PageId == layoutId && s.Area == "header"); ++ .FirstOrDefault(s => s.PageId == layoutId && s.Zone == "header"); + if (section == null) + return; + +diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs +index a53b4c6..a9aed2f 100644 +--- a/website/MyWebApp/Services/LayoutService.cs ++++ b/website/MyWebApp/Services/LayoutService.cs +@@ -17,12 +17,12 @@ public class LayoutService + ["two-column-sidebar"] = new[] { "main", "sidebar" } + }; + +- public static bool IsValidArea(string layout, string area) ++ public static bool IsValidZone(string layout, string zone) + { +- return LayoutZones.TryGetValue(layout, out var zones) && zones.Contains(area); ++ return LayoutZones.TryGetValue(layout, out var zones) && zones.Contains(zone); + } + +- public static string[] GetAreas(string layout) ++ public static string[] GetZones(string layout) + { + return LayoutZones.TryGetValue(layout, out var zones) ? zones : Array.Empty(); + } +@@ -39,7 +39,7 @@ public class LayoutService + { + e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); + var parts = await db.PageSections.AsNoTracking() +- .Where(s => s.Page.Slug == "layout" && s.Area == "header") ++ .Where(s => s.Page.Slug == "layout" && s.Zone == "header") + .OrderBy(s => s.SortOrder) + .Select(s => s.Html) + .ToListAsync(); +@@ -54,7 +54,7 @@ public class LayoutService + { + e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); + var parts = await db.PageSections.AsNoTracking() +- .Where(s => s.Page.Slug == "layout" && s.Area == "footer") ++ .Where(s => s.Page.Slug == "layout" && s.Zone == "footer") + .OrderBy(s => s.SortOrder) + .Select(s => s.Html) + .ToListAsync(); +@@ -63,11 +63,11 @@ public class LayoutService + }); + } + +- public async Task GetSectionAsync(ApplicationDbContext db, int pageId, string area) ++ public async Task GetSectionAsync(ApplicationDbContext db, int pageId, string zone) + { +- ++ + var parts = await db.PageSections.AsNoTracking() +- .Where(s => s.PageId == pageId && s.Area == area) ++ .Where(s => s.PageId == pageId && s.Zone == zone) + .OrderBy(s => s.SortOrder) + .Select(s => s.Html) + .ToListAsync(); +diff --git a/website/MyWebApp/Services/TokenRenderService.cs b/website/MyWebApp/Services/TokenRenderService.cs +index a71ae00..cc6f4fb 100644 +--- a/website/MyWebApp/Services/TokenRenderService.cs ++++ b/website/MyWebApp/Services/TokenRenderService.cs +@@ -50,9 +50,9 @@ public class TokenRenderService + var parts = param.Split(':', 2); + if (parts.Length == 2 && int.TryParse(parts[0], out var pageId)) + { +- var area = parts[1]; ++ var zone = parts[1]; + var htmlParts = await db.PageSections.AsNoTracking() +- .Where(s => s.PageId == pageId && s.Area == area) ++ .Where(s => s.PageId == pageId && s.Zone == zone) + .OrderBy(s => s.SortOrder) + .Select(s => s.Html) + .ToListAsync(); +diff --git a/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs b/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs +index 6f8c8cb..2f7dc72 100644 +--- a/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs ++++ b/website/MyWebApp/TagHelpers/PageBlocksTagHelper.cs +@@ -17,13 +17,13 @@ public class PageBlocksTagHelper : TagHelper + } + + public int PageId { get; set; } +- public string Area { get; set; } = string.Empty; ++ public string Zone { get; set; } = string.Empty; + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + output.TagName = null; + var htmlParts = await _db.PageSections.AsNoTracking() +- .Where(s => s.PageId == PageId && s.Area == Area) ++ .Where(s => s.PageId == PageId && s.Zone == Zone) + .OrderBy(s => s.SortOrder) + .Select(s => s.Html) + .ToListAsync(); +diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +index a9c6d99..8e06142 100644 +--- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml ++++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +@@ -16,8 +16,8 @@ + + +
+- +- ++ ++ +
+ + +diff --git a/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml b/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml +index 01027e7..82e7f54 100644 +--- a/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml ++++ b/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml +@@ -15,8 +15,8 @@ + + +
+- +- ++ ++ +
+
+ +diff --git a/website/MyWebApp/Views/AdminPageSection/Create.cshtml b/website/MyWebApp/Views/AdminPageSection/Create.cshtml +index 3e1d846..4f30413 100644 +--- a/website/MyWebApp/Views/AdminPageSection/Create.cshtml ++++ b/website/MyWebApp/Views/AdminPageSection/Create.cshtml +@@ -12,12 +12,12 @@ + +
+
+- +- ++ ++ +
+ + @await Html.PartialAsync("_SectionEditor", Model) + + + +- ++ +diff --git a/website/MyWebApp/Views/AdminPageSection/Delete.cshtml b/website/MyWebApp/Views/AdminPageSection/Delete.cshtml +index a93a86a..a426762 100644 +--- a/website/MyWebApp/Views/AdminPageSection/Delete.cshtml ++++ b/website/MyWebApp/Views/AdminPageSection/Delete.cshtml +@@ -6,7 +6,7 @@ +

Delete Section

+
+ +-

Are you sure you want to delete @Model.Area for page @Model.PageId?

++

Are you sure you want to delete @Model.Zone for page @Model.PageId?

+ + Cancel + +diff --git a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +index 641ab5c..590e245 100644 +--- a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml ++++ b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +@@ -13,12 +13,12 @@ + + +
+- +- ++ ++ +
+ + @await Html.PartialAsync("_SectionEditor", Model) + + + +- ++ +diff --git a/website/MyWebApp/Views/AdminPageSection/Index.cshtml b/website/MyWebApp/Views/AdminPageSection/Index.cshtml +index eb878dc..8115b47 100644 +--- a/website/MyWebApp/Views/AdminPageSection/Index.cshtml ++++ b/website/MyWebApp/Views/AdminPageSection/Index.cshtml +@@ -12,7 +12,7 @@ +

PageAreaType
PageZoneType
@s.Page?.Slug@s.Area@s.Zone @s.Type
+ + +- ++ + + + +@@ -20,7 +20,7 @@ + { + + +- ++ + + + +diff --git a/website/MyWebApp/wwwroot/css/admin.css b/website/MyWebApp/wwwroot/css/admin.css +index ac0c040..e891877 100644 +--- a/website/MyWebApp/wwwroot/css/admin.css ++++ b/website/MyWebApp/wwwroot/css/admin.css +@@ -1278,15 +1278,15 @@ form.mb-3 { + background: #0ea5e9; + color: #fff; + } +-.area-group { ++.zone-group { + border: 1px solid #e2e8f0; + padding: 0.5rem; + margin-bottom: 1rem; + } +-.area-group h3 { ++.zone-group h3 { + margin: 0 0 0.5rem 0; + text-transform: capitalize; + } +-.area-sections { ++.zone-sections { + min-height: 10px; + } +diff --git a/website/MyWebApp/wwwroot/js/page-editor.js b/website/MyWebApp/wwwroot/js/page-editor.js +index 9dec3f2..533e07c 100644 +--- a/website/MyWebApp/wwwroot/js/page-editor.js ++++ b/website/MyWebApp/wwwroot/js/page-editor.js +@@ -10,32 +10,32 @@ window.addEventListener('load', () => { + + function buildGroups() { + container.innerHTML = ''; +- (layoutZones[currentLayout] || []).forEach(a => { ++ (layoutZones[currentLayout] || []).forEach(z => { + const group = document.createElement('div'); +- group.className = 'area-group'; +- group.dataset.area = a; ++ group.className = 'zone-group'; ++ group.dataset.zone = z; + const h = document.createElement('h3'); +- h.textContent = a; ++ h.textContent = z; + const div = document.createElement('div'); +- div.className = 'area-sections'; ++ div.className = 'zone-sections'; + group.appendChild(h); + group.appendChild(div); + container.appendChild(group); + }); + } + +- function populateAreas(select) { ++ function populateZones(select) { + if (!select) return; + const current = select.dataset.selected || select.value; +- select.innerHTML = (layoutZones[currentLayout] || []).map(a => ``).join(''); ++ select.innerHTML = (layoutZones[currentLayout] || []).map(z => ``).join(''); + if (current) select.value = current; + select.dataset.selected = ''; + } + + function placeSection(section) { +- const select = section.querySelector('.area-select'); +- const area = select ? select.value : 'main'; +- const group = container.querySelector(`.area-group[data-area='${area}'] .area-sections`); ++ const select = section.querySelector('.zone-select'); ++ const zone = select ? select.value : 'main'; ++ const group = container.querySelector(`.zone-group[data-zone='${zone}'] .zone-sections`); + if (group) group.appendChild(section); + } + +@@ -43,11 +43,11 @@ window.addEventListener('load', () => { + const preview = document.getElementById('layout-preview'); + if (!preview) return; + preview.innerHTML = ''; +- (layoutZones[currentLayout] || []).forEach(a => { ++ (layoutZones[currentLayout] || []).forEach(z => { + const div = document.createElement('div'); + div.className = 'preview-zone'; +- div.dataset.area = a; +- div.textContent = a; ++ div.dataset.zone = z; ++ div.textContent = z; + preview.appendChild(div); + }); + } +@@ -56,9 +56,9 @@ window.addEventListener('load', () => { + const zone = e.target.closest('.preview-zone'); + if (!zone) return; + if (activeIndex !== null) { +- const select = document.querySelector(`.area-select[data-index='${activeIndex}']`); ++ const select = document.querySelector(`.zone-select[data-index='${activeIndex}']`); + if (select) { +- select.value = zone.dataset.area; ++ select.value = zone.dataset.zone; + placeSection(select.closest('.section-editor')); + updateIndexes(); + } +@@ -91,7 +91,7 @@ window.addEventListener('load', () => { + buildGroups(); + existing.forEach(el => { + const idx = el.dataset.index; +- populateAreas(el.querySelector('.area-select')); ++ populateZones(el.querySelector('.zone-select')); + placeSection(el); + initSectionEditor(idx); + }); +@@ -105,7 +105,7 @@ window.addEventListener('load', () => { + currentLayout = layoutSelect.value; + buildGroups(); + document.querySelectorAll('.section-editor').forEach(sec => { +- populateAreas(sec.querySelector('.area-select')); ++ populateZones(sec.querySelector('.zone-select')); + placeSection(sec); + }); + updateIndexes(); +@@ -123,7 +123,7 @@ window.addEventListener('load', () => { + }); + + container.addEventListener('change', e => { +- if (e.target.classList.contains('area-select')) { ++ if (e.target.classList.contains('zone-select')) { + const section = e.target.closest('.section-editor'); + placeSection(section); + updateIndexes(); +@@ -137,7 +137,7 @@ window.addEventListener('load', () => { + temp.innerHTML = html; + const section = temp.firstElementChild; + section.dataset.index = index; +- populateAreas(section.querySelector('.area-select')); ++ populateZones(section.querySelector('.zone-select')); + placeSection(section); + initSectionEditor(index); + updateIndexes(); +@@ -163,7 +163,7 @@ window.addEventListener('load', () => { + dest.value = src.value; + } + }); +- populateAreas(clone.querySelector('.area-select')); ++ populateZones(clone.querySelector('.zone-select')); + placeSection(clone); + initSectionEditor(index); + if (editors[original.dataset.index]) { +diff --git a/website/MyWebApp/wwwroot/js/page-section-area.js b/website/MyWebApp/wwwroot/js/page-section-area.js +deleted file mode 100644 +index c105b77..0000000 +--- a/website/MyWebApp/wwwroot/js/page-section-area.js ++++ /dev/null +@@ -1,19 +0,0 @@ +-window.addEventListener('load', () => { +- const pageSelect = document.querySelector('select[name="PageId"]'); +- const areaSelect = document.getElementById('area-select'); +- if (!pageSelect || !areaSelect) return; +- +- function loadAreas() { +- const id = pageSelect.value; +- if (!id) { areaSelect.innerHTML = ''; return; } +- fetch(`/AdminPageSection/GetAreasForPage/${id}`) +- .then(r => r.json()) +- .then(list => { +- areaSelect.innerHTML = list.map(a => ``).join(''); +- if (areaSelect.dataset.selected) +- areaSelect.value = areaSelect.dataset.selected; +- }); +- } +- loadAreas(); +- pageSelect.addEventListener('change', loadAreas); +-}); +diff --git a/website/MyWebApp/wwwroot/js/page-section-zone.js b/website/MyWebApp/wwwroot/js/page-section-zone.js +new file mode 100644 +index 0000000..9b2d0d3 +--- /dev/null ++++ b/website/MyWebApp/wwwroot/js/page-section-zone.js +@@ -0,0 +1,19 @@ ++window.addEventListener('load', () => { ++ const pageSelect = document.querySelector('select[name="PageId"]'); ++ const zoneSelect = document.getElementById('zone-select'); ++ if (!pageSelect || !zoneSelect) return; ++ ++ function loadZones() { ++ const id = pageSelect.value; ++ if (!id) { zoneSelect.innerHTML = ''; return; } ++ fetch(`/AdminPageSection/GetZonesForPage/${id}`) ++ .then(r => r.json()) ++ .then(list => { ++ zoneSelect.innerHTML = list.map(a => ``).join(''); ++ if (zoneSelect.dataset.selected) ++ zoneSelect.value = zoneSelect.dataset.selected; ++ }); ++ } ++ loadZones(); ++ pageSelect.addEventListener('change', loadZones); ++}); diff --git a/website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs b/website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs new file mode 100644 index 0000000..4b53843 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs @@ -0,0 +1,237 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +<<<<<<< HEAD + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) +======= + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) +>>>>>>> parent of 88cb6ce (Revert database file changes) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +<<<<<<< HEAD + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) +======= + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) +>>>>>>> parent of 88cb6ce (Revert database file changes) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs b/website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs new file mode 100644 index 0000000..4b53843 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs @@ -0,0 +1,237 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +<<<<<<< HEAD + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) +======= + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) +>>>>>>> parent of 88cb6ce (Revert database file changes) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +<<<<<<< HEAD + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) +======= + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) +>>>>>>> parent of 88cb6ce (Revert database file changes) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs b/website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs new file mode 100644 index 0000000..4b53843 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs @@ -0,0 +1,237 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +<<<<<<< HEAD + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) +======= + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) +>>>>>>> parent of 88cb6ce (Revert database file changes) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); +<<<<<<< HEAD + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) +======= + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) +>>>>>>> parent of 88cb6ce (Revert database file changes) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_BASE_200.cs b/website/MyWebApp/Controllers/AdminContentController_BASE_200.cs new file mode 100644 index 0000000..bd78896 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_BASE_200.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_BASE_263.cs b/website/MyWebApp/Controllers/AdminContentController_BASE_263.cs new file mode 100644 index 0000000..bd78896 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_BASE_263.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_BASE_389.cs b/website/MyWebApp/Controllers/AdminContentController_BASE_389.cs new file mode 100644 index 0000000..bd78896 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_BASE_389.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs b/website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs new file mode 100644 index 0000000..f6315cb --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs @@ -0,0 +1,229 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs b/website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs new file mode 100644 index 0000000..f6315cb --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs @@ -0,0 +1,229 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs b/website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs new file mode 100644 index 0000000..f6315cb --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs @@ -0,0 +1,229 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + ViewBag.LayoutZones = _layout.LayoutZones; + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Zone == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs b/website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs new file mode 100644 index 0000000..7e54fea --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs b/website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs new file mode 100644 index 0000000..7e54fea --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs b/website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs new file mode 100644 index 0000000..7e54fea --- /dev/null +++ b/website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs @@ -0,0 +1,228 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using MyWebApp.Data; +using System.Collections.Generic; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using System.Linq; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminContentController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + private async Task LoadTemplatesAsync() + { + ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking() + .OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Index() + { + var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + return View(pages); + } + + public async Task Create() + { + + await LoadTemplatesAsync(); + ViewBag.Sections = new List(); + return View("PageEditor", new Page()); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Pages.Add(model); + await _db.SaveChangesAsync(); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + + await LoadTemplatesAsync(); + ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) + .OrderBy(s => s.SortOrder).ToListAsync(); + return View("PageEditor", page); + + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(Page model) + { + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = model.Sections; + return View("PageEditor", model); + } + if (model.IsPublished && model.PublishDate == null) + { + model.PublishDate = DateTime.UtcNow; + } + var sections = model.Sections?.ToList() ?? new List(); + if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!sections.Any(s => s.Area == "main")) + { + ModelState.AddModelError(string.Empty, "Main area cannot be empty."); + } + if (!ModelState.IsValid) + { + await LoadTemplatesAsync(); + ViewBag.Sections = sections; + model.Sections = sections; + return View("PageEditor", model); + } + model.Sections = new List(); + _db.Update(model); + await _db.SaveChangesAsync(); + var existing = _db.PageSections.Where(s => s.PageId == model.Id); + _db.PageSections.RemoveRange(existing); + if (sections.Count > 0) + { + var files = HttpContext.Request.Form.Files; + for (int i = 0; i < sections.Count; i++) + { + var s = sections[i]; + s.Id = 0; + s.PageId = model.Id; + var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); + await PrepareHtmlAsync(s, file); + _db.PageSections.Add(s); + } + await _db.SaveChangesAsync(); + } + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page == null) + { + return NotFound(); + } + return View(page); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var page = await _db.Pages.FindAsync(id); + if (page != null) + { + _db.Pages.Remove(page); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs new file mode 100644 index 0000000..94b1b74 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs @@ -0,0 +1,195 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using MyWebApp.Data; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminPageSectionController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + public async Task Index(string? q) + { + var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); + if (!string.IsNullOrWhiteSpace(q)) + { + q = q.ToLowerInvariant(); + query = query.Where(s => s.Area.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); + } + var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Area).ToListAsync(); + ViewBag.Query = q; + return View(sections); + } + + private async Task LoadPagesAsync() + { + ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Create() + { + await LoadPagesAsync(); + return View(new PageSection()); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); + if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.PageSections.Add(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + await LoadPagesAsync(); + return View(section); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); + if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.Update(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + return View(section); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section != null) + { + _db.PageSections.Remove(section); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } + + [HttpGet] + public async Task GetAreasForPage(int id) + { + var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; +<<<<<<< HEAD + var zones = _layout.GetZones(layout); + return Json(zones); +======= + var areas = LayoutService.GetAreas(layout); + return Json(areas); +>>>>>>> parent of 88cb6ce (Revert database file changes) + } + + [HttpPost] + public async Task Reorder([FromForm] IList ids) + { + using var tx = _db.Database.BeginTransaction(); + for (int i = 0; i < ids.Count; i++) + { + var sec = await _db.PageSections.FindAsync(ids[i]); + if (sec == null) continue; + sec.SortOrder = (i + 1) * 10; + _db.PageSections.Update(sec); + } + await _db.SaveChangesAsync(); + await tx.CommitAsync(); + return Ok(); + } +} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs new file mode 100644 index 0000000..fc748ee --- /dev/null +++ b/website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs @@ -0,0 +1,164 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using MyWebApp.Data; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminPageSectionController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + public async Task Index(string? q) + { + var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); + if (!string.IsNullOrWhiteSpace(q)) + { + q = q.ToLowerInvariant(); + query = query.Where(s => s.Zone.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); + } + var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Zone).ToListAsync(); + ViewBag.Query = q; + return View(sections); + } + + private async Task LoadPagesAsync() + { + ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Create() + { + await LoadPagesAsync(); + return View(new PageSection()); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.PageSections.Add(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + await LoadPagesAsync(); + return View(section); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.Update(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + return View(section); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section != null) + { + _db.PageSections.Remove(section); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } + + [HttpGet] + public async Task GetZonesForPage(int id) + { + var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; + var zones = LayoutService.GetZones(layout); + return Json(zones); + } +} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs new file mode 100644 index 0000000..efbebf3 --- /dev/null +++ b/website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs @@ -0,0 +1,180 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using MyWebApp.Data; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminPageSectionController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + public async Task Index(string? q) + { + var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); + if (!string.IsNullOrWhiteSpace(q)) + { + q = q.ToLowerInvariant(); + query = query.Where(s => s.Zone.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); + } + var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Zone).ToListAsync(); + ViewBag.Query = q; + return View(sections); + } + + private async Task LoadPagesAsync() + { + ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Create() + { + await LoadPagesAsync(); + return View(new PageSection()); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.PageSections.Add(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + await LoadPagesAsync(); + return View(section); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.Update(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + return View(section); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section != null) + { + _db.PageSections.Remove(section); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } + + [HttpGet] + public async Task GetZonesForPage(int id) + { + var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; + var zones = _layout.GetZones(layout); + return Json(zones); + } + + [HttpPost] + public async Task Reorder([FromForm] IList ids) + { + using var tx = _db.Database.BeginTransaction(); + for (int i = 0; i < ids.Count; i++) + { + var sec = await _db.PageSections.FindAsync(ids[i]); + if (sec == null) continue; + sec.SortOrder = (i + 1) * 10; + _db.PageSections.Update(sec); + } + await _db.SaveChangesAsync(); + await tx.CommitAsync(); + return Ok(); + } +} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs new file mode 100644 index 0000000..d440e5f --- /dev/null +++ b/website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs @@ -0,0 +1,174 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Http; +using Markdig; +using System.IO; +using MyWebApp.Data; +using MyWebApp.Filters; +using MyWebApp.Models; +using MyWebApp.Services; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class AdminPageSectionController : Controller +{ + private readonly ApplicationDbContext _db; + private readonly LayoutService _layout; + private readonly HtmlSanitizerService _sanitizer; + + public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + { + _db = db; + _layout = layout; + _sanitizer = sanitizer; + } + + public async Task Index(string? q) + { + var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); + if (!string.IsNullOrWhiteSpace(q)) + { + q = q.ToLowerInvariant(); + query = query.Where(s => s.Area.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); + } + var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Area).ToListAsync(); + ViewBag.Query = q; + return View(sections); + } + + private async Task LoadPagesAsync() + { + ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); + } + + public async Task Create() + { + await LoadPagesAsync(); + return View(new PageSection()); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); + if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.PageSections.Add(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + public async Task Edit(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + await LoadPagesAsync(); + return View(section); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(PageSection model, IFormFile? file) + { + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); + if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) + { + ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); + } + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } + await PrepareHtmlAsync(model, file); + _db.Update(model); + await _db.SaveChangesAsync(); + _layout.Reset(); + return RedirectToAction(nameof(Index)); + } + + private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } + + public async Task Delete(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section == null) return NotFound(); + return View(section); + } + + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var section = await _db.PageSections.FindAsync(id); + if (section != null) + { + _db.PageSections.Remove(section); + await _db.SaveChangesAsync(); + _layout.Reset(); + } + return RedirectToAction(nameof(Index)); + } + + [HttpGet] + public async Task GetAreasForPage(int id) + { + var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; + var areas = LayoutService.GetAreas(layout); + return Json(areas); + } +} From 7a530fab770789844c313e1bafb21db04646dfeb Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:59:19 +0800 Subject: [PATCH 08/27] Remove merge artifacts and fix build --- website/MyWebApp.Tests/LayoutServiceTests.cs | 15 +- website/MyWebApp.Tests/NavigationTests.cs | 2 +- website/MyWebApp.Tests/SanitizationTests.cs | 2 +- .../AdminContentController_BACKUP_200.cs | 237 ------------------ .../AdminContentController_BACKUP_263.cs | 237 ------------------ .../AdminContentController_BACKUP_389.cs | 237 ------------------ .../AdminContentController_BASE_200.cs | 228 ----------------- .../AdminContentController_BASE_263.cs | 228 ----------------- .../AdminContentController_BASE_389.cs | 228 ----------------- .../AdminContentController_LOCAL_200.cs | 229 ----------------- .../AdminContentController_LOCAL_263.cs | 229 ----------------- .../AdminContentController_LOCAL_389.cs | 229 ----------------- .../AdminContentController_REMOTE_200.cs | 228 ----------------- .../AdminContentController_REMOTE_263.cs | 228 ----------------- .../AdminContentController_REMOTE_389.cs | 228 ----------------- .../AdminPageSectionController_BACKUP_326.cs | 195 -------------- .../AdminPageSectionController_BASE_326.cs | 164 ------------ .../AdminPageSectionController_LOCAL_326.cs | 180 ------------- .../AdminPageSectionController_REMOTE_326.cs | 174 ------------- .../MyWebApp/Controllers/PagesController.cs | 4 +- 20 files changed, 8 insertions(+), 3494 deletions(-) delete mode 100644 website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_BASE_200.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_BASE_263.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_BASE_389.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs delete mode 100644 website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs delete mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs delete mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs delete mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs delete mode 100644 website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs diff --git a/website/MyWebApp.Tests/LayoutServiceTests.cs b/website/MyWebApp.Tests/LayoutServiceTests.cs index 2ca3585..906c6cb 100644 --- a/website/MyWebApp.Tests/LayoutServiceTests.cs +++ b/website/MyWebApp.Tests/LayoutServiceTests.cs @@ -9,20 +9,13 @@ public class LayoutServiceTests [Fact] public void CanReadZonesFromConfig() { - var config = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"Layouts:single-column:0", "main"}, - {"Layouts:two-column-sidebar:0", "main"}, - {"Layouts:two-column-sidebar:1", "sidebar"} - }) - .Build(); + 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, config); + var service = new LayoutService(cache, tokens); - Assert.True(service.LayoutZones.ContainsKey("single-column")); - Assert.Contains("sidebar", service.LayoutZones["two-column-sidebar"]); + Assert.True(LayoutService.LayoutZones.ContainsKey("single-column")); + Assert.Contains("sidebar", LayoutService.LayoutZones["two-column-sidebar"]); } } diff --git a/website/MyWebApp.Tests/NavigationTests.cs b/website/MyWebApp.Tests/NavigationTests.cs index dbbbeb8..67b3227 100644 --- a/website/MyWebApp.Tests/NavigationTests.cs +++ b/website/MyWebApp.Tests/NavigationTests.cs @@ -32,7 +32,7 @@ public async Task PublishingPage_ShowsTitleOnceInHeader() {"Layouts:two-column-sidebar:1", "sidebar"} }) .Build(); - var layout = new LayoutService(cache, tokens, config); + var layout = new LayoutService(cache, tokens); context.Pages.Add(new Page { Slug = "about", Title = "About", Layout = "single-column", IsPublished = true }); context.SaveChanges(); diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs index 1ae0bf2..9cc5073 100644 --- a/website/MyWebApp.Tests/SanitizationTests.cs +++ b/website/MyWebApp.Tests/SanitizationTests.cs @@ -32,7 +32,7 @@ private static (ApplicationDbContext ctx, LayoutService layout, HtmlSanitizerSer {"Layouts:two-column-sidebar:1", "sidebar"} }) .Build(); - var layout = new LayoutService(cache, tokens, config); + var layout = new LayoutService(cache, tokens); var sanitizer = new HtmlSanitizerService(); return (ctx, layout, sanitizer); } diff --git a/website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs b/website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs deleted file mode 100644 index 4b53843..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_BACKUP_200.cs +++ /dev/null @@ -1,237 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - ViewBag.LayoutZones = _layout.LayoutZones; - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); -<<<<<<< HEAD - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) -======= - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ->>>>>>> parent of 88cb6ce (Revert database file changes) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); -<<<<<<< HEAD - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) -======= - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ->>>>>>> parent of 88cb6ce (Revert database file changes) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs b/website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs deleted file mode 100644 index 4b53843..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_BACKUP_263.cs +++ /dev/null @@ -1,237 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - ViewBag.LayoutZones = _layout.LayoutZones; - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); -<<<<<<< HEAD - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) -======= - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ->>>>>>> parent of 88cb6ce (Revert database file changes) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); -<<<<<<< HEAD - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) -======= - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ->>>>>>> parent of 88cb6ce (Revert database file changes) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs b/website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs deleted file mode 100644 index 4b53843..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_BACKUP_389.cs +++ /dev/null @@ -1,237 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - ViewBag.LayoutZones = _layout.LayoutZones; - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); -<<<<<<< HEAD - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) -======= - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ->>>>>>> parent of 88cb6ce (Revert database file changes) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); -<<<<<<< HEAD - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) -======= - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) ->>>>>>> parent of 88cb6ce (Revert database file changes) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_BASE_200.cs b/website/MyWebApp/Controllers/AdminContentController_BASE_200.cs deleted file mode 100644 index bd78896..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_BASE_200.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_BASE_263.cs b/website/MyWebApp/Controllers/AdminContentController_BASE_263.cs deleted file mode 100644 index bd78896..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_BASE_263.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_BASE_389.cs b/website/MyWebApp/Controllers/AdminContentController_BASE_389.cs deleted file mode 100644 index bd78896..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_BASE_389.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs b/website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs deleted file mode 100644 index f6315cb..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_LOCAL_200.cs +++ /dev/null @@ -1,229 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - ViewBag.LayoutZones = _layout.LayoutZones; - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs b/website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs deleted file mode 100644 index f6315cb..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_LOCAL_263.cs +++ /dev/null @@ -1,229 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - ViewBag.LayoutZones = _layout.LayoutZones; - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs b/website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs deleted file mode 100644 index f6315cb..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_LOCAL_389.cs +++ /dev/null @@ -1,229 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - ViewBag.LayoutZones = _layout.LayoutZones; - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !_layout.IsValidZone(model.Layout, s.Zone))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Zone == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs b/website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs deleted file mode 100644 index 7e54fea..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_REMOTE_200.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs b/website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs deleted file mode 100644 index 7e54fea..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_REMOTE_263.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs b/website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs deleted file mode 100644 index 7e54fea..0000000 --- a/website/MyWebApp/Controllers/AdminContentController_REMOTE_389.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using MyWebApp.Data; -using System.Collections.Generic; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using System.Linq; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminContentController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - private async Task LoadTemplatesAsync() - { - ViewBag.Templates = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking() - .OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Index() - { - var pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - return View(pages); - } - - public async Task Create() - { - - await LoadTemplatesAsync(); - ViewBag.Sections = new List(); - return View("PageEditor", new Page()); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Pages.Add(model); - await _db.SaveChangesAsync(); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - - await LoadTemplatesAsync(); - ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) - .OrderBy(s => s.SortOrder).ToListAsync(); - return View("PageEditor", page); - - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(Page model) - { - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = model.Sections; - return View("PageEditor", model); - } - if (model.IsPublished && model.PublishDate == null) - { - model.PublishDate = DateTime.UtcNow; - } - var sections = model.Sections?.ToList() ?? new List(); - if (sections.Any(s => !LayoutService.IsValidArea(model.Layout, s.Area))) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!sections.Any(s => s.Area == "main")) - { - ModelState.AddModelError(string.Empty, "Main area cannot be empty."); - } - if (!ModelState.IsValid) - { - await LoadTemplatesAsync(); - ViewBag.Sections = sections; - model.Sections = sections; - return View("PageEditor", model); - } - model.Sections = new List(); - _db.Update(model); - await _db.SaveChangesAsync(); - var existing = _db.PageSections.Where(s => s.PageId == model.Id); - _db.PageSections.RemoveRange(existing); - if (sections.Count > 0) - { - var files = HttpContext.Request.Form.Files; - for (int i = 0; i < sections.Count; i++) - { - var s = sections[i]; - s.Id = 0; - s.PageId = model.Id; - var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); - _db.PageSections.Add(s); - } - await _db.SaveChangesAsync(); - } - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page == null) - { - return NotFound(); - } - return View(page); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var page = await _db.Pages.FindAsync(id); - if (page != null) - { - _db.Pages.Remove(page); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } -} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs deleted file mode 100644 index 94b1b74..0000000 --- a/website/MyWebApp/Controllers/AdminPageSectionController_BACKUP_326.cs +++ /dev/null @@ -1,195 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using MyWebApp.Data; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminPageSectionController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - public async Task Index(string? q) - { - var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); - if (!string.IsNullOrWhiteSpace(q)) - { - q = q.ToLowerInvariant(); - query = query.Where(s => s.Area.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); - } - var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Area).ToListAsync(); - ViewBag.Query = q; - return View(sections); - } - - private async Task LoadPagesAsync() - { - ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Create() - { - await LoadPagesAsync(); - return View(new PageSection()); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); - if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.PageSections.Add(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - await LoadPagesAsync(); - return View(section); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); - if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.Update(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - return View(section); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section != null) - { - _db.PageSections.Remove(section); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } - - [HttpGet] - public async Task GetAreasForPage(int id) - { - var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; -<<<<<<< HEAD - var zones = _layout.GetZones(layout); - return Json(zones); -======= - var areas = LayoutService.GetAreas(layout); - return Json(areas); ->>>>>>> parent of 88cb6ce (Revert database file changes) - } - - [HttpPost] - public async Task Reorder([FromForm] IList ids) - { - using var tx = _db.Database.BeginTransaction(); - for (int i = 0; i < ids.Count; i++) - { - var sec = await _db.PageSections.FindAsync(ids[i]); - if (sec == null) continue; - sec.SortOrder = (i + 1) * 10; - _db.PageSections.Update(sec); - } - await _db.SaveChangesAsync(); - await tx.CommitAsync(); - return Ok(); - } -} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs deleted file mode 100644 index fc748ee..0000000 --- a/website/MyWebApp/Controllers/AdminPageSectionController_BASE_326.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using MyWebApp.Data; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminPageSectionController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - public async Task Index(string? q) - { - var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); - if (!string.IsNullOrWhiteSpace(q)) - { - q = q.ToLowerInvariant(); - query = query.Where(s => s.Zone.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); - } - var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Zone).ToListAsync(); - ViewBag.Query = q; - return View(sections); - } - - private async Task LoadPagesAsync() - { - ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Create() - { - await LoadPagesAsync(); - return View(new PageSection()); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.PageSections.Add(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - await LoadPagesAsync(); - return View(section); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.Update(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - return View(section); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section != null) - { - _db.PageSections.Remove(section); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } - - [HttpGet] - public async Task GetZonesForPage(int id) - { - var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; - var zones = LayoutService.GetZones(layout); - return Json(zones); - } -} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs deleted file mode 100644 index efbebf3..0000000 --- a/website/MyWebApp/Controllers/AdminPageSectionController_LOCAL_326.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using MyWebApp.Data; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminPageSectionController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - public async Task Index(string? q) - { - var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); - if (!string.IsNullOrWhiteSpace(q)) - { - q = q.ToLowerInvariant(); - query = query.Where(s => s.Zone.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); - } - var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Zone).ToListAsync(); - ViewBag.Query = q; - return View(sections); - } - - private async Task LoadPagesAsync() - { - ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Create() - { - await LoadPagesAsync(); - return View(new PageSection()); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.PageSections.Add(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - await LoadPagesAsync(); - return View(section); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.Update(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - return View(section); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section != null) - { - _db.PageSections.Remove(section); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } - - [HttpGet] - public async Task GetZonesForPage(int id) - { - var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; - var zones = _layout.GetZones(layout); - return Json(zones); - } - - [HttpPost] - public async Task Reorder([FromForm] IList ids) - { - using var tx = _db.Database.BeginTransaction(); - for (int i = 0; i < ids.Count; i++) - { - var sec = await _db.PageSections.FindAsync(ids[i]); - if (sec == null) continue; - sec.SortOrder = (i + 1) * 10; - _db.PageSections.Update(sec); - } - await _db.SaveChangesAsync(); - await tx.CommitAsync(); - return Ok(); - } -} diff --git a/website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs b/website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs deleted file mode 100644 index d440e5f..0000000 --- a/website/MyWebApp/Controllers/AdminPageSectionController_REMOTE_326.cs +++ /dev/null @@ -1,174 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; -using MyWebApp.Data; -using MyWebApp.Filters; -using MyWebApp.Models; -using MyWebApp.Services; - -namespace MyWebApp.Controllers; - -[RoleAuthorize("Admin")] -public class AdminPageSectionController : Controller -{ - private readonly ApplicationDbContext _db; - private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; - - public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) - { - _db = db; - _layout = layout; - _sanitizer = sanitizer; - } - - public async Task Index(string? q) - { - var query = _db.PageSections.AsNoTracking().Include(s => s.Page).AsQueryable(); - if (!string.IsNullOrWhiteSpace(q)) - { - q = q.ToLowerInvariant(); - query = query.Where(s => s.Area.ToLower().Contains(q) || s.Html.ToLower().Contains(q) || s.Page.Slug.ToLower().Contains(q)); - } - var sections = await query.OrderBy(s => s.Page.Slug).ThenBy(s => s.Area).ToListAsync(); - ViewBag.Query = q; - return View(sections); - } - - private async Task LoadPagesAsync() - { - ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); - ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); - } - - public async Task Create() - { - await LoadPagesAsync(); - return View(new PageSection()); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Create(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); - if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.PageSections.Add(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - public async Task Edit(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - await LoadPagesAsync(); - return View(section); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Edit(PageSection model, IFormFile? file) - { - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - var pageLayout = await _db.Pages.Where(p => p.Id == model.PageId).Select(p => p.Layout).FirstOrDefaultAsync(); - if (!LayoutService.IsValidArea(pageLayout ?? "single-column", model.Area)) - { - ModelState.AddModelError(string.Empty, "Invalid area for selected layout."); - } - if (!ModelState.IsValid) - { - await LoadPagesAsync(); - return View(model); - } - await PrepareHtmlAsync(model, file); - _db.Update(model); - await _db.SaveChangesAsync(); - _layout.Reset(); - return RedirectToAction(nameof(Index)); - } - - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } - - public async Task Delete(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section == null) return NotFound(); - return View(section); - } - - [HttpPost, ActionName("Delete")] - [ValidateAntiForgeryToken] - public async Task DeleteConfirmed(int id) - { - var section = await _db.PageSections.FindAsync(id); - if (section != null) - { - _db.PageSections.Remove(section); - await _db.SaveChangesAsync(); - _layout.Reset(); - } - return RedirectToAction(nameof(Index)); - } - - [HttpGet] - public async Task GetAreasForPage(int id) - { - var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; - var areas = LayoutService.GetAreas(layout); - return Json(areas); - } -} diff --git a/website/MyWebApp/Controllers/PagesController.cs b/website/MyWebApp/Controllers/PagesController.cs index a952cf7..8d5cae0 100644 --- a/website/MyWebApp/Controllers/PagesController.cs +++ b/website/MyWebApp/Controllers/PagesController.cs @@ -42,9 +42,9 @@ public async Task Show(string? slug) } var layoutName = string.IsNullOrWhiteSpace(page.Layout) ? "single-column" : page.Layout; - if (!_layout.LayoutZones.TryGetValue(layoutName, out var zones)) + if (!LayoutService.LayoutZones.TryGetValue(layoutName, out var zones)) { - zones = _layout.LayoutZones["single-column"]; + zones = LayoutService.LayoutZones["single-column"]; } var zoneHtml = new Dictionary(); foreach (var z in zones) From e74ee30e6b4cb98494d25e2df148ad490160ed72 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:10:12 +0800 Subject: [PATCH 09/27] Run dotnet format for whitespace --- .../MyWebApp.Tests/BasicAuthAttributeTests.cs | 8 +++---- website/MyWebApp.Tests/PageSectionTests.cs | 4 ++-- website/MyWebApp.Tests/SanitizationTests.cs | 10 ++++---- .../Controllers/AdminContentController.cs | 6 ++--- website/MyWebApp/Data/ApplicationDbContext.cs | 17 ++++++------- website/MyWebApp/Models/PageSection.cs | 4 ++-- website/MyWebApp/Program.cs | 24 +++++++++---------- website/MyWebApp/Services/LayoutService.cs | 2 +- 8 files changed, 38 insertions(+), 37 deletions(-) diff --git a/website/MyWebApp.Tests/BasicAuthAttributeTests.cs b/website/MyWebApp.Tests/BasicAuthAttributeTests.cs index 3a4705c..0f96b1c 100644 --- a/website/MyWebApp.Tests/BasicAuthAttributeTests.cs +++ b/website/MyWebApp.Tests/BasicAuthAttributeTests.cs @@ -34,7 +34,7 @@ public void NoHeader_ReturnsUnauthorized() var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); services.Configure(o => { o.Username = "admin"; o.Password = "SecurePass123"; }); services.AddSingleton(new Microsoft.Extensions.Configuration.ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { {"AdminAuth:Username","admin"}, {"AdminAuth:Password","SecurePass123"} }).Build()); + .AddInMemoryCollection(new Dictionary { { "AdminAuth:Username", "admin" }, { "AdminAuth:Password", "SecurePass123" } }).Build()); var provider = services.BuildServiceProvider(); var http = new DefaultHttpContext { RequestServices = provider }; var ctx = new AuthorizationFilterContext( @@ -51,7 +51,7 @@ public void ValidHeader_AllowsAccess() var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); services.Configure(o => { o.Username = "admin"; o.Password = "SecurePass123"; }); services.AddSingleton(new Microsoft.Extensions.Configuration.ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { {"AdminAuth:Username","admin"}, {"AdminAuth:Password","SecurePass123"} }).Build()); + .AddInMemoryCollection(new Dictionary { { "AdminAuth:Username", "admin" }, { "AdminAuth:Password", "SecurePass123" } }).Build()); var provider = services.BuildServiceProvider(); var http = new DefaultHttpContext { RequestServices = provider }; var creds = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:SecurePass123")); @@ -71,7 +71,7 @@ public void WrongHeader_ReturnsUnauthorized() var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); services.Configure(o => { o.Username = "admin"; o.Password = "SecurePass123"; }); services.AddSingleton(new Microsoft.Extensions.Configuration.ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { {"AdminAuth:Username","admin"}, {"AdminAuth:Password","SecurePass123"} }).Build()); + .AddInMemoryCollection(new Dictionary { { "AdminAuth:Username", "admin" }, { "AdminAuth:Password", "SecurePass123" } }).Build()); var provider = services.BuildServiceProvider(); var http = new DefaultHttpContext { RequestServices = provider }; var creds = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:wrong")); @@ -103,7 +103,7 @@ public void Session_AllowsAccess() var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); services.Configure(o => { o.Username = "admin"; o.Password = "SecurePass123"; }); services.AddSingleton(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { {"AdminAuth:Username","admin"}, {"AdminAuth:Password","SecurePass123"} }).Build()); + .AddInMemoryCollection(new Dictionary { { "AdminAuth:Username", "admin" }, { "AdminAuth:Password", "SecurePass123" } }).Build()); var provider = services.BuildServiceProvider(); var http = new DefaultHttpContext { RequestServices = provider, Session = new DummySession() }; http.Session.SetString("IsAdmin", "true"); diff --git a/website/MyWebApp.Tests/PageSectionTests.cs b/website/MyWebApp.Tests/PageSectionTests.cs index 29643b7..4c99507 100644 --- a/website/MyWebApp.Tests/PageSectionTests.cs +++ b/website/MyWebApp.Tests/PageSectionTests.cs @@ -21,9 +21,9 @@ public void CanAddAndRetrievePageSection() var page = new Page { Slug = "test", Title = "Test", Layout = "single-column" }; context.Pages.Add(page); context.SaveChanges(); - + context.PageSections.Add(new PageSection { PageId = page.Id, Zone = "header", Html = "

hi

", Type = PageSectionType.Html }); - + context.SaveChanges(); } diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs index 9cc5073..97034e4 100644 --- a/website/MyWebApp.Tests/SanitizationTests.cs +++ b/website/MyWebApp.Tests/SanitizationTests.cs @@ -37,7 +37,7 @@ private static (ApplicationDbContext ctx, LayoutService layout, HtmlSanitizerSer return (ctx, layout, sanitizer); } - [Fact(Skip="Create sanitization covered by section tests")] + [Fact(Skip = "Create sanitization covered by section tests")] public async Task CreatePage_SanitizesHtml() { var (ctx, layout, sanitizer) = CreateServices(); @@ -63,16 +63,16 @@ public async Task CreateSection_SanitizesHtml() { var (ctx, layout, sanitizer) = CreateServices(); var controller = new AdminPageSectionController(ctx, layout, sanitizer); - + var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "main", Html = "
hi
", Type = PageSectionType.Html }; var result = await controller.Create(model, null); - + Assert.IsType(result); var section = ctx.PageSections.First(); Assert.DoesNotContain(" Index() public async Task Create() { - + await LoadTemplatesAsync(); ViewBag.Sections = new List(); return View("PageEditor", new Page()); - + } [HttpPost] @@ -113,7 +113,7 @@ public async Task Edit(int id) ViewBag.Sections = await _db.PageSections.Where(s => s.PageId == id) .OrderBy(s => s.SortOrder).ToListAsync(); return View("PageEditor", page); - + } [HttpPost] diff --git a/website/MyWebApp/Data/ApplicationDbContext.cs b/website/MyWebApp/Data/ApplicationDbContext.cs index d1f2415..0e941f7 100644 --- a/website/MyWebApp/Data/ApplicationDbContext.cs +++ b/website/MyWebApp/Data/ApplicationDbContext.cs @@ -53,9 +53,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsUnique(); modelBuilder.Entity() - + .HasIndex(s => new { s.PageId, s.Zone, s.SortOrder }); - + modelBuilder.Entity() .HasIndex(t => t.Token) @@ -107,11 +107,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) PageId = 1, Zone = "header", SortOrder = 0, - + Type = PageSectionType.Html, - + Html = "" - , ViewCount = 0 + , + ViewCount = 0 }, new PageSection { @@ -119,11 +120,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) PageId = 1, Zone = "footer", SortOrder = 0, - + Type = PageSectionType.Html, - + Html = "
© 2025 - Screen Area Recorder Pro
" - + }); modelBuilder.Entity().HasData( diff --git a/website/MyWebApp/Models/PageSection.cs b/website/MyWebApp/Models/PageSection.cs index f0cf8c5..7c2e402 100644 --- a/website/MyWebApp/Models/PageSection.cs +++ b/website/MyWebApp/Models/PageSection.cs @@ -25,9 +25,9 @@ public class PageSection public int SortOrder { get; set; } - + public PageSectionType Type { get; set; } = PageSectionType.Html; - + public string Html { get; set; } = string.Empty; diff --git a/website/MyWebApp/Program.cs b/website/MyWebApp/Program.cs index 650cc87..b9561ee 100644 --- a/website/MyWebApp/Program.cs +++ b/website/MyWebApp/Program.cs @@ -184,19 +184,19 @@ { app.Logger.LogInformation("Database schema created."); } - if (provider.Equals("sqlite", StringComparison.OrdinalIgnoreCase)) - { - db.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;"); - db.Database.ExecuteSqlRaw("PRAGMA synchronous=NORMAL;"); + if (provider.Equals("sqlite", StringComparison.OrdinalIgnoreCase)) + { + db.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;"); + db.Database.ExecuteSqlRaw("PRAGMA synchronous=NORMAL;"); - UpgradeDownloadFilesTable(db); - UpgradePageSectionsTable(db); - UpgradePagesTable(db); - UpgradeMediaItemsTable(db); - UpgradeBlockTemplatesTable(db); - UpgradePermissionsTable(db); - UpgradeLayoutHeader(db); - } + UpgradeDownloadFilesTable(db); + UpgradePageSectionsTable(db); + UpgradePagesTable(db); + UpgradeMediaItemsTable(db); + UpgradeBlockTemplatesTable(db); + UpgradePermissionsTable(db); + UpgradeLayoutHeader(db); + } if (db.Database.CanConnect()) { cacheService.WarmCache(db); diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs index a9aed2f..2e0ed82 100644 --- a/website/MyWebApp/Services/LayoutService.cs +++ b/website/MyWebApp/Services/LayoutService.cs @@ -73,7 +73,7 @@ public async Task GetSectionAsync(ApplicationDbContext db, int pageId, s .ToListAsync(); var html = string.Join(System.Environment.NewLine, parts); return await _tokens.RenderAsync(db, html); - + } public void Reset() From 2edafdba0adadc36fc59c74b2b0379d1a57ee253 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:28:03 +0800 Subject: [PATCH 10/27] Fix JsonSerializer usage in PageEditor --- website/MyWebApp/Views/AdminContent/PageEditor.cshtml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml index 0a4c777..ec26c72 100644 --- a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml +++ b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml @@ -66,7 +66,9 @@ @section Scripts { ", Type = PageSectionType.Html }; var result = await controller.Create(model, null); @@ -75,8 +76,8 @@ public async Task CreateSection_SanitizesHtml() [Fact(Skip = "Edit sanitization covered by section tests")] public async Task EditPage_SanitizesHtml() { - var (ctx, layout, sanitizer) = CreateServices(); - var controller = new AdminContentController(ctx, layout, sanitizer); + var (ctx, layout, content) = CreateServices(); + var controller = new AdminContentController(ctx, layout, content); var createModel = new Page { Slug = "edit", @@ -103,8 +104,8 @@ public async Task EditPage_SanitizesHtml() [Fact] public async Task CreateSection_MarkdownConverted() { - var (ctx, layout, sanitizer) = CreateServices(); - var controller = new AdminPageSectionController(ctx, layout, sanitizer); + var (ctx, layout, content) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, content); var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; var result = await controller.Create(model, null); Assert.IsType(result); @@ -116,8 +117,8 @@ public async Task CreateSection_MarkdownConverted() [Fact] public async Task CreateSection_CodeEncoded() { - var (ctx, layout, sanitizer) = CreateServices(); - var controller = new AdminPageSectionController(ctx, layout, sanitizer); + var (ctx, layout, content) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, content); var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "code", Html = "test", Type = PageSectionType.Code }; var result = await controller.Create(model, null); Assert.IsType(result); @@ -128,8 +129,8 @@ public async Task CreateSection_CodeEncoded() [Fact] public async Task CreateSection_ImageStoresTag() { - var (ctx, layout, sanitizer) = CreateServices(); - var controller = new AdminPageSectionController(ctx, layout, sanitizer); + var (ctx, layout, content) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, content); var bytes = new byte[] { 1, 2, 3 }; using var stream = new System.IO.MemoryStream(bytes); var file = new FormFile(stream, 0, bytes.Length, "file", "img.png"); diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index 52799a5..d73137d 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -169,35 +169,4 @@ public async Task AddToPage(int id, int pageId, string zone) return RedirectToAction(nameof(Index)); } - [HttpGet] - public async Task GetBlocks() - { - var items = await _db.BlockTemplates.AsNoTracking() - .OrderBy(t => t.Name) - .Select(t => new { t.Id, t.Name }) - .ToListAsync(); - return Json(items); - } - - [HttpGet] - public async Task GetPages() - { - var pages = await _db.Pages.AsNoTracking() - .OrderBy(p => p.Slug) - .Select(p => new { p.Id, p.Slug }) - .ToListAsync(); - return Json(pages); - } - - [HttpGet] - public async Task GetSections(int id) - { - var zones = await _db.PageSections.AsNoTracking() - .Where(s => s.PageId == id) - .Select(s => s.Zone) - .Distinct() - .OrderBy(a => a) - .ToListAsync(); - return Json(zones); - } } diff --git a/website/MyWebApp/Controllers/AdminContentController.cs b/website/MyWebApp/Controllers/AdminContentController.cs index 5102be2..094c672 100644 --- a/website/MyWebApp/Controllers/AdminContentController.cs +++ b/website/MyWebApp/Controllers/AdminContentController.cs @@ -7,8 +7,6 @@ using MyWebApp.Models; using MyWebApp.Services; using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; using System.Linq; namespace MyWebApp.Controllers; @@ -18,13 +16,13 @@ public class AdminContentController : Controller { private readonly ApplicationDbContext _db; private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; + private readonly ContentProcessingService _content; - public AdminContentController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content) { _db = db; _layout = layout; - _sanitizer = sanitizer; + _content = content; } private async Task LoadTemplatesAsync() @@ -92,7 +90,7 @@ public async Task Create(Page model) s.Id = 0; s.PageId = model.Id; var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); + await _content.PrepareHtmlAsync(s, file); _db.PageSections.Add(s); } await _db.SaveChangesAsync(); @@ -160,7 +158,7 @@ public async Task Edit(Page model) s.Id = 0; s.PageId = model.Id; var file = files.FirstOrDefault(f => f.Name == $"Sections[{i}].File"); - await PrepareHtmlAsync(s, file); + await _content.PrepareHtmlAsync(s, file); _db.PageSections.Add(s); } await _db.SaveChangesAsync(); @@ -169,38 +167,6 @@ public async Task Edit(Page model) return RedirectToAction(nameof(Index)); } - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } public async Task Delete(int id) { diff --git a/website/MyWebApp/Controllers/AdminPageSectionController.cs b/website/MyWebApp/Controllers/AdminPageSectionController.cs index fc748ee..05f74ff 100644 --- a/website/MyWebApp/Controllers/AdminPageSectionController.cs +++ b/website/MyWebApp/Controllers/AdminPageSectionController.cs @@ -1,9 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Http; -using Markdig; -using System.IO; using MyWebApp.Data; using MyWebApp.Filters; using MyWebApp.Models; @@ -16,13 +13,13 @@ public class AdminPageSectionController : Controller { private readonly ApplicationDbContext _db; private readonly LayoutService _layout; - private readonly HtmlSanitizerService _sanitizer; + private readonly ContentProcessingService _content; - public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, HtmlSanitizerService sanitizer) + public AdminPageSectionController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content) { _db = db; _layout = layout; - _sanitizer = sanitizer; + _content = content; } public async Task Index(string? q) @@ -64,7 +61,7 @@ public async Task Create(PageSection model, IFormFile? file) await LoadPagesAsync(); return View(model); } - await PrepareHtmlAsync(model, file); + await _content.PrepareHtmlAsync(model, file); _db.PageSections.Add(model); await _db.SaveChangesAsync(); _layout.Reset(); @@ -93,45 +90,13 @@ public async Task Edit(PageSection model, IFormFile? file) await LoadPagesAsync(); return View(model); } - await PrepareHtmlAsync(model, file); + await _content.PrepareHtmlAsync(model, file); _db.Update(model); await _db.SaveChangesAsync(); _layout.Reset(); return RedirectToAction(nameof(Index)); } - private async Task PrepareHtmlAsync(PageSection model, IFormFile? file) - { - switch (model.Type) - { - case PageSectionType.Html: - model.Html = _sanitizer.Sanitize(model.Html); - break; - case PageSectionType.Markdown: - var html = Markdig.Markdown.ToHtml(model.Html ?? string.Empty); - model.Html = _sanitizer.Sanitize(html); - break; - case PageSectionType.Code: - model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; - break; - case PageSectionType.Image: - case PageSectionType.Video: - if (file != null && file.Length > 0) - { - var uploads = Path.Combine("wwwroot", "uploads"); - Directory.CreateDirectory(uploads); - var name = Path.GetFileName(file.FileName); - var path = Path.Combine(uploads, name); - using var stream = new FileStream(path, FileMode.Create); - await file.CopyToAsync(stream); - if (model.Type == PageSectionType.Image) - model.Html = $""; - else - model.Html = $""; - } - break; - } - } public async Task Delete(int id) { @@ -154,11 +119,4 @@ public async Task DeleteConfirmed(int id) return RedirectToAction(nameof(Index)); } - [HttpGet] - public async Task GetZonesForPage(int id) - { - var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; - var zones = LayoutService.GetZones(layout); - return Json(zones); - } } diff --git a/website/MyWebApp/Controllers/ApiController.cs b/website/MyWebApp/Controllers/ApiController.cs new file mode 100644 index 0000000..717e707 --- /dev/null +++ b/website/MyWebApp/Controllers/ApiController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MyWebApp.Data; +using MyWebApp.Filters; +using MyWebApp.Services; + +namespace MyWebApp.Controllers; + +[RoleAuthorize("Admin")] +public class ApiController : Controller +{ + private readonly ApplicationDbContext _db; + + public ApiController(ApplicationDbContext db) + { + _db = db; + } + + [HttpGet] + public async Task GetBlocks() + { + var items = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name) + .Select(t => new { t.Id, t.Name }) + .ToListAsync(); + return Json(items); + } + + [HttpGet] + public async Task GetPages() + { + var pages = await _db.Pages.AsNoTracking() + .OrderBy(p => p.Slug) + .Select(p => new { p.Id, p.Slug }) + .ToListAsync(); + return Json(pages); + } + + [HttpGet] + public async Task GetSections(int id) + { + var zones = await _db.PageSections.AsNoTracking() + .Where(s => s.PageId == id) + .Select(s => s.Zone) + .Distinct() + .OrderBy(a => a) + .ToListAsync(); + return Json(zones); + } + + [HttpGet] + public async Task GetZonesForPage(int id) + { + var layout = await _db.Pages.Where(p => p.Id == id).Select(p => p.Layout).FirstOrDefaultAsync() ?? "single-column"; + var zones = LayoutService.GetZones(layout); + return Json(zones); + } +} diff --git a/website/MyWebApp/Data/ApplicationDbContext.cs b/website/MyWebApp/Data/ApplicationDbContext.cs index 0e941f7..a64914a 100644 --- a/website/MyWebApp/Data/ApplicationDbContext.cs +++ b/website/MyWebApp/Data/ApplicationDbContext.cs @@ -111,8 +111,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Type = PageSectionType.Html, Html = "" - , - ViewCount = 0 + }, new PageSection { diff --git a/website/MyWebApp/Models/PageSection.cs b/website/MyWebApp/Models/PageSection.cs index 7c2e402..2e3600e 100644 --- a/website/MyWebApp/Models/PageSection.cs +++ b/website/MyWebApp/Models/PageSection.cs @@ -37,7 +37,6 @@ public class PageSection public int? PermissionId { get; set; } - public int ViewCount { get; set; } public Page? Page { get; set; } diff --git a/website/MyWebApp/Program.cs b/website/MyWebApp/Program.cs index b9561ee..49b657b 100644 --- a/website/MyWebApp/Program.cs +++ b/website/MyWebApp/Program.cs @@ -149,6 +149,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); var smtpSection = builder.Configuration.GetSection("Smtp"); @@ -324,7 +325,6 @@ static void UpgradePageSectionsTable(ApplicationDbContext db) StartDate TEXT, EndDate TEXT, PermissionId INTEGER, - ViewCount INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(PageId) REFERENCES Pages(Id) ON DELETE CASCADE )"); db.Database.ExecuteSqlRaw("CREATE INDEX IX_PageSections_PageId_Zone_SortOrder ON PageSections(PageId, Zone, SortOrder)"); @@ -354,8 +354,6 @@ FOREIGN KEY(PageId) REFERENCES Pages(Id) ON DELETE CASCADE db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN EndDate TEXT"); if (!columns.Contains("PermissionId")) db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN PermissionId INTEGER"); - if (!columns.Contains("ViewCount")) - db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN ViewCount INTEGER NOT NULL DEFAULT 0"); cmd.CommandText = "PRAGMA index_list('PageSections')"; using var idx = cmd.ExecuteReader(); diff --git a/website/MyWebApp/Services/ContentProcessingService.cs b/website/MyWebApp/Services/ContentProcessingService.cs new file mode 100644 index 0000000..d121c3b --- /dev/null +++ b/website/MyWebApp/Services/ContentProcessingService.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http; +using System.IO; +using System.Threading.Tasks; +using Markdig; +using MyWebApp.Models; + +namespace MyWebApp.Services; + +public class ContentProcessingService +{ + private readonly HtmlSanitizerService _sanitizer; + + public ContentProcessingService(HtmlSanitizerService sanitizer) + { + _sanitizer = sanitizer; + } + + public async Task PrepareHtmlAsync(PageSection model, IFormFile? file) + { + switch (model.Type) + { + case PageSectionType.Html: + model.Html = _sanitizer.Sanitize(model.Html); + break; + case PageSectionType.Markdown: + var html = Markdown.ToHtml(model.Html ?? string.Empty); + model.Html = _sanitizer.Sanitize(html); + break; + case PageSectionType.Code: + model.Html = $"
{System.Net.WebUtility.HtmlEncode(model.Html)}
"; + break; + case PageSectionType.Image: + case PageSectionType.Video: + if (file != null && file.Length > 0) + { + var uploads = Path.Combine("wwwroot", "uploads"); + Directory.CreateDirectory(uploads); + var name = Path.GetFileName(file.FileName); + var path = Path.Combine(uploads, name); + using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + if (model.Type == PageSectionType.Image) + model.Html = $""; + else + model.Html = $""; + } + break; + } + } +} diff --git a/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml b/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml deleted file mode 100644 index 82e7f54..0000000 --- a/website/MyWebApp/Views/AdminContent/_SectionEditor.cshtml +++ /dev/null @@ -1,66 +0,0 @@ -@model MyWebApp.Models.PageSection -@using MyWebApp.Models -@{ - var index = ViewData["Index"]?.ToString() ?? "0"; - var quillId = $"quill-editor-{index}"; - var typeSelectId = $"type-select-{index}"; - var htmlInputId = $"Html-{index}"; - var htmlEditorId = $"html-editor-{index}"; - var markdownId = $"markdown-editor-{index}"; - var codeId = $"code-editor-{index}"; - var fileId = $"file-editor-{index}"; -} -
- - - -
- - -
-
- - -
-
-
- -
-
- -
-
- -
-
- -
-
- - -
-
- - -
-
- - -
- - -
diff --git a/website/MyWebApp/wwwroot/js/block-editor.js b/website/MyWebApp/wwwroot/js/block-editor.js index e2d7440..b9ab34a 100644 --- a/website/MyWebApp/wwwroot/js/block-editor.js +++ b/website/MyWebApp/wwwroot/js/block-editor.js @@ -8,7 +8,7 @@ window.addEventListener('load', () => { const sectionSelect = document.getElementById('section-select'); blockBtn?.addEventListener('click', () => { - fetch('/AdminBlockTemplate/GetBlocks') + fetch('/Api/GetBlocks') .then(r => r.json()) .then(list => { blockSelect.innerHTML = '' + @@ -26,7 +26,7 @@ window.addEventListener('load', () => { }); sectionBtn?.addEventListener('click', () => { - fetch('/AdminBlockTemplate/GetPages') + fetch('/Api/GetPages') .then(r => r.json()) .then(list => { pageSelect.innerHTML = '' + @@ -40,7 +40,7 @@ window.addEventListener('load', () => { pageSelect?.addEventListener('change', () => { const id = pageSelect.value; if (!id) return; - fetch(`/AdminBlockTemplate/GetSections/${id}`) + fetch(`/Api/GetSections/${id}`) .then(r => r.json()) .then(list => { sectionSelect.innerHTML = '' + diff --git a/website/MyWebApp/wwwroot/js/page-section-zone.js b/website/MyWebApp/wwwroot/js/page-section-zone.js index 9b2d0d3..ed662bb 100644 --- a/website/MyWebApp/wwwroot/js/page-section-zone.js +++ b/website/MyWebApp/wwwroot/js/page-section-zone.js @@ -6,7 +6,7 @@ window.addEventListener('load', () => { function loadZones() { const id = pageSelect.value; if (!id) { zoneSelect.innerHTML = ''; return; } - fetch(`/AdminPageSection/GetZonesForPage/${id}`) + fetch(`/Api/GetZonesForPage/${id}`) .then(r => r.json()) .then(list => { zoneSelect.innerHTML = list.map(a => ``).join(''); From 52e84d379f4ca5ec16ac30d15001e342c846f530 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:06:56 +0800 Subject: [PATCH 12/27] Enhance page editor zones --- website/MyWebApp/wwwroot/css/admin.css | 25 +++++++++++ website/MyWebApp/wwwroot/js/page-editor.js | 49 ++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/website/MyWebApp/wwwroot/css/admin.css b/website/MyWebApp/wwwroot/css/admin.css index e891877..2b83383 100644 --- a/website/MyWebApp/wwwroot/css/admin.css +++ b/website/MyWebApp/wwwroot/css/admin.css @@ -1290,3 +1290,28 @@ form.mb-3 { .zone-sections { min-height: 10px; } +.zone-group.drag-over { + border-color: #0ea5e9; + background: #f0f9ff; +} +.drop-indicator { + height: 4px; + background: #0ea5e9; + margin: 4px 0; + border-radius: 2px; +} +.section-editor { + border: 1px solid #cbd5e1; + background: #fff; + padding: 0.5rem; + margin-bottom: 0.5rem; +} +.section-editor:hover { + box-shadow: 0 0 0 2px #0ea5e9 inset; +} +.layout-preview { + border: 1px solid #e2e8f0; + padding: 0.5rem; +} +.zone-group[data-zone="main"] { border-color: #2563eb; } +.zone-group[data-zone="sidebar"] { border-color: #16a34a; } diff --git a/website/MyWebApp/wwwroot/js/page-editor.js b/website/MyWebApp/wwwroot/js/page-editor.js index 533e07c..989fbe2 100644 --- a/website/MyWebApp/wwwroot/js/page-editor.js +++ b/website/MyWebApp/wwwroot/js/page-editor.js @@ -15,7 +15,7 @@ window.addEventListener('load', () => { group.className = 'zone-group'; group.dataset.zone = z; const h = document.createElement('h3'); - h.textContent = z; + h.innerHTML = `${z} `; const div = document.createElement('div'); div.className = 'zone-sections'; group.appendChild(h); @@ -24,6 +24,14 @@ window.addEventListener('load', () => { }); } + function updateZoneCounts() { + document.querySelectorAll('.zone-group').forEach(g => { + const count = g.querySelectorAll('.section-editor').length; + const span = g.querySelector('.zone-count'); + if (span) span.textContent = `(${count})`; + }); + } + function populateZones(select) { if (!select) return; const current = select.dataset.selected || select.value; @@ -47,7 +55,8 @@ window.addEventListener('load', () => { const div = document.createElement('div'); div.className = 'preview-zone'; div.dataset.zone = z; - div.textContent = z; + const count = container.querySelectorAll(`.zone-group[data-zone='${z}'] .section-editor`).length; + div.textContent = `${z} (${count})`; preview.appendChild(div); }); } @@ -55,6 +64,8 @@ window.addEventListener('load', () => { document.getElementById('layout-preview')?.addEventListener('click', e => { const zone = e.target.closest('.preview-zone'); if (!zone) return; + const group = container.querySelector(`.zone-group[data-zone='${zone.dataset.zone}']`); + if (group) group.scrollIntoView({ behavior: 'smooth' }); if (activeIndex !== null) { const select = document.querySelector(`.zone-select[data-index='${activeIndex}']`); if (select) { @@ -96,6 +107,7 @@ window.addEventListener('load', () => { initSectionEditor(idx); }); updatePreview(); + updateZoneCounts(); document.getElementById('add-section').addEventListener('click', () => { addSection(); @@ -110,12 +122,14 @@ window.addEventListener('load', () => { }); updateIndexes(); updatePreview(); + updateZoneCounts(); }); container.addEventListener('click', e => { if (e.target.classList.contains('remove-section')) { e.target.closest('.section-editor').remove(); updateIndexes(); + updateZoneCounts(); } else if (e.target.classList.contains('duplicate-section')) { const original = e.target.closest('.section-editor'); duplicateSection(original); @@ -127,6 +141,7 @@ window.addEventListener('load', () => { const section = e.target.closest('.section-editor'); placeSection(section); updateIndexes(); + updateZoneCounts(); } }); @@ -142,6 +157,7 @@ window.addEventListener('load', () => { initSectionEditor(index); updateIndexes(); updatePreview(); + updateZoneCounts(); } function duplicateSection(original) { @@ -173,6 +189,7 @@ window.addEventListener('load', () => { } updateIndexes(); updatePreview(); + updateZoneCounts(); } function updateIndexes() { @@ -187,22 +204,48 @@ window.addEventListener('load', () => { } let dragged = null; + const dropIndicator = document.createElement('div'); + dropIndicator.className = 'drop-indicator'; + container.addEventListener('dragstart', e => { dragged = e.target.closest('.section-editor'); + if (dragged) { + dragged.classList.add('dragging'); + document.querySelectorAll('.zone-group').forEach(z => z.classList.add('drag-over')); + } e.dataTransfer.effectAllowed = 'move'; }); + container.addEventListener('dragover', e => { e.preventDefault(); + const zone = e.target.closest('.zone-group'); const target = e.target.closest('.section-editor'); + if (zone) zone.classList.add('drag-over'); if (dragged && target && target !== dragged) { const rect = target.getBoundingClientRect(); const next = (e.clientY - rect.top) > (rect.height / 2); - target.parentNode.insertBefore(dragged, next ? target.nextSibling : target); + target.parentNode.insertBefore(dropIndicator, next ? target.nextSibling : target); } }); + + ['dragleave', 'drop'].forEach(evt => { + container.addEventListener(evt, e => { + const zone = e.target.closest('.zone-group'); + if (zone) zone.classList.remove('drag-over'); + }); + }); + container.addEventListener('drop', e => { e.preventDefault(); + if (dropIndicator.parentNode) { + dropIndicator.parentNode.insertBefore(dragged, dropIndicator); + dropIndicator.remove(); + } + document.querySelectorAll('.zone-group.drag-over').forEach(z => z.classList.remove('drag-over')); + if (dragged) dragged.classList.remove('dragging'); + dragged = null; updateIndexes(); + updateZoneCounts(); }); const form = document.querySelector('form'); From e5a3bd9ba4e7cf3b072f731ae0afa791204867f9 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:18:55 +0800 Subject: [PATCH 13/27] Add block library panel with drag-and-drop --- .../AdminBlockTemplateController.cs | 24 +++++++++ .../Views/AdminContent/PageEditor.cshtml | 11 +++- .../Views/Shared/_SectionEditor.cshtml | 9 ++-- website/MyWebApp/wwwroot/css/admin.css | 40 +++++++++++++++ website/MyWebApp/wwwroot/js/block-library.js | 42 +++++++++++++++ website/MyWebApp/wwwroot/js/page-editor.js | 51 ++++++++++++++++++- 6 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 website/MyWebApp/wwwroot/js/block-library.js diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index d73137d..0afa0de 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -124,6 +124,30 @@ public async Task Import(IFormFile? file) return RedirectToAction(nameof(Index)); } + [HttpGet] + public async Task GetBlocks() + { + var items = await _db.BlockTemplates.AsNoTracking() + .OrderBy(t => t.Name) + .Select(t => new { t.Id, t.Name, Preview = t.Html.Length > 200 ? t.Html.Substring(0, 200) + "..." : t.Html }) + .ToListAsync(); + return Json(items); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task CreateFromSection(string name, string html) + { + if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(html)) + return BadRequest(); + html = _sanitizer.Sanitize(html); + var t = new BlockTemplate { Name = name, Html = html }; + _db.BlockTemplates.Add(t); + _db.BlockTemplateVersions.Add(new BlockTemplateVersion { Template = t, Html = html }); + await _db.SaveChangesAsync(); + return Json(new { t.Id }); + } + public async Task AddToPage(int id) { var item = await _db.BlockTemplates.FindAsync(id); diff --git a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml index ec26c72..1a50ac9 100644 --- a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml +++ b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml @@ -8,7 +8,12 @@ ViewData["Title"] = isNew ? "Create Page" : "Edit Page"; }

@ViewData["Title"]

-
+
+ +
@@ -63,7 +68,8 @@ @await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", new PageSection(), new ViewDataDictionary(ViewData) { ["Index"] = "__index__" })
- + +
@section Scripts { + ", Type = PageSectionType.Html }; @@ -76,8 +76,8 @@ public async Task CreateSection_SanitizesHtml() [Fact(Skip = "Edit sanitization covered by section tests")] public async Task EditPage_SanitizesHtml() { - var (ctx, layout, content) = CreateServices(); - var controller = new AdminContentController(ctx, layout, content); + var (ctx, layout, content, tokens, sanitizer) = CreateServices(); + var controller = new AdminContentController(ctx, layout, content, tokens, sanitizer); var createModel = new Page { Slug = "edit", @@ -104,7 +104,7 @@ public async Task EditPage_SanitizesHtml() [Fact] public async Task CreateSection_MarkdownConverted() { - var (ctx, layout, content) = CreateServices(); + var (ctx, layout, content, _, _) = CreateServices(); var controller = new AdminPageSectionController(ctx, layout, content); var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; var result = await controller.Create(model, null); @@ -117,7 +117,7 @@ public async Task CreateSection_MarkdownConverted() [Fact] public async Task CreateSection_CodeEncoded() { - var (ctx, layout, content) = CreateServices(); + var (ctx, layout, content, _, _) = CreateServices(); var controller = new AdminPageSectionController(ctx, layout, content); var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "code", Html = "test", Type = PageSectionType.Code }; var result = await controller.Create(model, null); @@ -129,7 +129,7 @@ public async Task CreateSection_CodeEncoded() [Fact] public async Task CreateSection_ImageStoresTag() { - var (ctx, layout, content) = CreateServices(); + var (ctx, layout, content, _, _) = CreateServices(); var controller = new AdminPageSectionController(ctx, layout, content); var bytes = new byte[] { 1, 2, 3 }; using var stream = new System.IO.MemoryStream(bytes); diff --git a/website/MyWebApp/Controllers/AdminContentController.cs b/website/MyWebApp/Controllers/AdminContentController.cs index 8c6b058..21f53d4 100644 --- a/website/MyWebApp/Controllers/AdminContentController.cs +++ b/website/MyWebApp/Controllers/AdminContentController.cs @@ -18,13 +18,15 @@ public class AdminContentController : Controller private readonly LayoutService _layout; private readonly ContentProcessingService _content; private readonly TokenRenderService _tokens; + private readonly HtmlSanitizerService _sanitizer; - public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content, TokenRenderService tokens) + public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content, TokenRenderService tokens, HtmlSanitizerService sanitizer) { _db = db; _layout = layout; _content = content; _tokens = tokens; + _sanitizer = sanitizer; } private async Task LoadTemplatesAsync() @@ -104,15 +106,19 @@ public async Task Create(Page model) [HttpPost] public async Task Preview([FromBody] PreviewRequest model) { + var layout = string.IsNullOrWhiteSpace(model.Layout) ? "single-column" : model.Layout; var zones = new Dictionary(); foreach (var kv in model.Zones ?? new Dictionary()) { - zones[kv.Key] = await _tokens.RenderAsync(_db, kv.Value ?? string.Empty); + if (!LayoutService.IsValidZone(layout, kv.Key)) continue; + var clean = _sanitizer.Sanitize(kv.Value ?? string.Empty); + zones[kv.Key] = await _tokens.RenderAsync(_db, clean); } ViewBag.HeaderHtml = await _layout.GetHeaderAsync(_db); ViewBag.FooterHtml = await _layout.GetFooterAsync(_db); - ViewBag.PageLayout = string.IsNullOrWhiteSpace(model.Layout) ? "single-column" : model.Layout; + ViewBag.PageLayout = layout; ViewBag.ZoneHtml = zones; + Response.Headers["Content-Security-Policy"] = "default-src 'self'"; return View("~/Views/Pages/Show.cshtml", new Page { Title = model.Title }); } diff --git a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml index bd8c89b..83ba5fd 100644 --- a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml +++ b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml @@ -90,6 +90,7 @@ ViewBag.LayoutZones as IReadOnlyDictionary ?? new Dictionary())); + diff --git a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml index b715880..5707297 100644 --- a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml @@ -21,4 +21,5 @@ + diff --git a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml index 73293bf..082477b 100644 --- a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml +++ b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml @@ -1,42 +1,33 @@ @model MyWebApp.Models.PageSection @using MyWebApp.Models -
- - -
-
-
- -
-
- -
-
- -
-
- +@{ + var idxObj = ViewData["Index"]; + var idx = idxObj?.ToString() ?? "0"; + var prefix = idxObj != null ? $"Sections[{idx}]." : string.Empty; +} +
+ + +
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
- +
diff --git a/website/MyWebApp/wwwroot/js/block-library.js b/website/MyWebApp/wwwroot/js/block-library.js index a700963..38fa7f5 100644 --- a/website/MyWebApp/wwwroot/js/block-library.js +++ b/website/MyWebApp/wwwroot/js/block-library.js @@ -36,7 +36,8 @@ window.addEventListener('load', () => { list.addEventListener('dragstart', e => { const card = e.target.closest('.block-card'); if (!card) return; - window.draggedBlockId = card.dataset.id; + const ev = new CustomEvent('blockdragstart', { detail: card.dataset.id }); + document.dispatchEvent(ev); e.dataTransfer.effectAllowed = 'copy'; }); }); diff --git a/website/MyWebApp/wwwroot/js/page-editor.js b/website/MyWebApp/wwwroot/js/page-editor.js index 459e4b1..1280a9c 100644 --- a/website/MyWebApp/wwwroot/js/page-editor.js +++ b/website/MyWebApp/wwwroot/js/page-editor.js @@ -3,7 +3,11 @@ window.addEventListener('load', () => { if (!container) return; const templateHtml = document.getElementById('section-template').innerHTML.trim(); let sectionCount = container.querySelectorAll('.section-editor').length; - const editors = {}; + const editors = new Map(); + document.addEventListener('section-editor:ready', e => { + editors.set(e.detail.index, e.detail.quill); + e.detail.quill.on('text-change', schedulePreview); + }); let activeIndex = null; const layoutSelect = document.getElementById('layout-select'); let currentLayout = layoutSelect ? layoutSelect.value : 'single-column'; @@ -50,7 +54,8 @@ window.addEventListener('load', () => { const idx = sec.dataset.index; const typeSel = document.getElementById(`type-select-${idx}`); if (typeSel && typeSel.value === 'Html') { - return editors[idx].root.innerHTML; + const q = editors.get(idx); + return q ? q.root.innerHTML : ''; } const inp = document.getElementById(`Html-${idx}`); return inp ? inp.value : ''; @@ -138,7 +143,7 @@ window.addEventListener('load', () => { templateSelect.addEventListener('change', () => { const id = templateSelect.value; if (!id) return; - if (activeIndex === null || !editors[activeIndex]) { + if (activeIndex === null || !editors.has(activeIndex)) { alert('Select a section first'); templateSelect.value = ''; return; @@ -146,7 +151,7 @@ window.addEventListener('load', () => { fetch(`/AdminBlockTemplate/Html/${id}`) .then(r => r.text()) .then(html => { - const quill = editors[activeIndex]; + const quill = editors.get(activeIndex); quill.root.innerHTML = html; const input = document.getElementById(`Html-${activeIndex}`); if (input) input.value = html; @@ -161,7 +166,7 @@ window.addEventListener('load', () => { const idx = el.dataset.index; populateZones(el.querySelector('.zone-select')); placeSection(el); - initSectionEditor(idx); + document.dispatchEvent(new CustomEvent('section-editor:add', { detail: el })); }); updatePreview(); updateZoneCounts(); @@ -195,7 +200,8 @@ window.addEventListener('load', () => { const idx = section.dataset.index; const name = prompt('Block name'); if (!name) return; - const html = editors[idx].root.innerHTML; + const q = editors.get(idx); + const html = q ? q.root.innerHTML : ''; const token = document.querySelector('input[name="__RequestVerificationToken"]').value; fetch('/AdminBlockTemplate/CreateFromSection', { method: 'POST', @@ -223,9 +229,10 @@ window.addEventListener('load', () => { section.dataset.index = index; populateZones(section.querySelector('.zone-select')); placeSection(section); - initSectionEditor(index); + document.dispatchEvent(new CustomEvent('section-editor:add', { detail: section })); if (htmlContent) { - editors[index].root.innerHTML = htmlContent; + const q = editors.get(index); + if (q) q.root.innerHTML = htmlContent; const input = document.getElementById(`Html-${index}`); if (input) input.value = htmlContent; } @@ -270,11 +277,13 @@ window.addEventListener('load', () => { }); populateZones(clone.querySelector('.zone-select')); placeSection(clone); - initSectionEditor(index); - if (editors[original.dataset.index]) { - editors[index].root.innerHTML = editors[original.dataset.index].root.innerHTML; + document.dispatchEvent(new CustomEvent('section-editor:add', { detail: clone })); + const orig = editors.get(original.dataset.index); + const copy = editors.get(index); + if (orig && copy) { + copy.root.innerHTML = orig.root.innerHTML; const destInput = clone.querySelector(`#Html-${index}`); - if (destInput) destInput.value = editors[index].root.innerHTML; + if (destInput) destInput.value = copy.root.innerHTML; } updateIndexes(); updatePreview(); @@ -294,6 +303,9 @@ window.addEventListener('load', () => { let dragged = null; let draggedBlockId = null; + document.addEventListener('blockdragstart', e => { + draggedBlockId = e.detail; + }); const dropIndicator = document.createElement('div'); dropIndicator.className = 'drop-indicator'; @@ -349,7 +361,7 @@ window.addEventListener('load', () => { const form = document.querySelector('form'); if (form) { form.addEventListener('submit', () => { - Object.entries(editors).forEach(([key, quill]) => { + editors.forEach((quill, key) => { const typeSelect = document.getElementById(`type-select-${key}`); if (typeSelect && typeSelect.value === 'Html') { const input = document.getElementById(`Html-${key}`); @@ -365,37 +377,6 @@ window.addEventListener('load', () => { fetch(form.action, { method: 'POST', body: fd }); } - function initSectionEditor(index) { - const typeSelect = document.getElementById(`type-select-${index}`); - const htmlDiv = document.getElementById(`html-editor-${index}`); - const markdownDiv = document.getElementById(`markdown-editor-${index}`); - const codeDiv = document.getElementById(`code-editor-${index}`); - const fileDiv = document.getElementById(`file-editor-${index}`); - const quillInput = document.getElementById(`Html-${index}`); - const markdown = markdownDiv ? markdownDiv.querySelector('textarea') : null; - const code = codeDiv ? codeDiv.querySelector('textarea') : null; - const quill = new Quill(`#quill-editor-${index}`, { theme: 'snow' }); - quill.root.innerHTML = quillInput.value || ''; - quill.root.addEventListener('click', () => { activeIndex = index; }); - quill.root.addEventListener('focus', () => { activeIndex = index; }); - editors[index] = quill; - quill.on('text-change', schedulePreview); - - function update() { - const type = typeSelect.value; - htmlDiv.style.display = type === 'Html' ? 'block' : 'none'; - markdownDiv.style.display = type === 'Markdown' ? 'block' : 'none'; - codeDiv.style.display = type === 'Code' ? 'block' : 'none'; - fileDiv.style.display = (type === 'Image' || type === 'Video') ? 'block' : 'none'; - if (markdown) markdown.disabled = type !== 'Markdown'; - if (code) code.disabled = type !== 'Code'; - quillInput.disabled = type !== 'Html'; - const fileInput = fileDiv ? fileDiv.querySelector('input[type="file"]') : null; - if (fileInput) fileInput.disabled = !(type === 'Image' || type === 'Video'); - } - update(); - typeSelect.addEventListener('change', update); - } modePreview?.addEventListener('click', () => { pageEditor?.classList.add('preview'); diff --git a/website/MyWebApp/wwwroot/js/section-editor.js b/website/MyWebApp/wwwroot/js/section-editor.js new file mode 100644 index 0000000..ed00676 --- /dev/null +++ b/website/MyWebApp/wwwroot/js/section-editor.js @@ -0,0 +1,39 @@ +(() => { + const editors = new WeakMap(); + + function init(container) { + if (!container) return; + const index = container.dataset.index; + const typeSelect = container.querySelector(`#type-select-${index}`); + const htmlDiv = container.querySelector(`#html-editor-${index}`); + const markdownDiv = container.querySelector(`#markdown-editor-${index}`); + const codeDiv = container.querySelector(`#code-editor-${index}`); + const fileDiv = container.querySelector(`#file-editor-${index}`); + const quillInput = container.querySelector(`#Html-${index}`); + if (!typeSelect || !quillInput) return; + const quill = new Quill(`#quill-editor-${index}`, { theme: 'snow' }); + quill.root.innerHTML = quillInput.value || ''; + quill.on('text-change', () => { + quillInput.value = quill.root.innerHTML; + container.dispatchEvent(new Event('input', { bubbles: true })); + }); + editors.set(container, quill); + container.dispatchEvent(new CustomEvent('section-editor:ready', { detail: { quill, index } })); + + function update() { + const type = typeSelect.value; + if (htmlDiv) htmlDiv.style.display = type === 'Html' ? 'block' : 'none'; + if (markdownDiv) markdownDiv.style.display = type === 'Markdown' ? 'block' : 'none'; + if (codeDiv) codeDiv.style.display = type === 'Code' ? 'block' : 'none'; + if (fileDiv) fileDiv.style.display = (type === 'Image' || type === 'Video') ? 'block' : 'none'; + } + typeSelect.addEventListener('change', update); + update(); + } + + document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.section-editor').forEach(init); + }); + + document.addEventListener('section-editor:add', e => init(e.detail)); +})(); From e41f34a1e48381eabadc23df33a5a4062004e2a3 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:47:01 +0800 Subject: [PATCH 16/27] Add role management and permission filtering --- website/MyWebApp.Tests/LayoutServiceTests.cs | 6 +- website/MyWebApp.Tests/NavigationTests.cs | 6 +- website/MyWebApp.Tests/SanitizationTests.cs | 5 +- .../Controllers/AdminRoleController.cs | 96 +++++++++++++++++ website/MyWebApp/Models/AdminModels.cs | 6 ++ website/MyWebApp/Services/LayoutService.cs | 100 +++++++++++++----- .../MyWebApp/Services/TokenRenderService.cs | 34 +++++- .../MyWebApp/Views/Admin/_AdminLayout.cshtml | 1 + .../MyWebApp/Views/AdminRole/Create.cshtml | 10 ++ .../MyWebApp/Views/AdminRole/Delete.cshtml | 12 +++ website/MyWebApp/Views/AdminRole/Edit.cshtml | 21 ++++ website/MyWebApp/Views/AdminRole/Index.cshtml | 20 ++++ 12 files changed, 282 insertions(+), 35 deletions(-) create mode 100644 website/MyWebApp/Controllers/AdminRoleController.cs create mode 100644 website/MyWebApp/Views/AdminRole/Create.cshtml create mode 100644 website/MyWebApp/Views/AdminRole/Delete.cshtml create mode 100644 website/MyWebApp/Views/AdminRole/Edit.cshtml create mode 100644 website/MyWebApp/Views/AdminRole/Index.cshtml diff --git a/website/MyWebApp.Tests/LayoutServiceTests.cs b/website/MyWebApp.Tests/LayoutServiceTests.cs index 906c6cb..b759b0f 100644 --- a/website/MyWebApp.Tests/LayoutServiceTests.cs +++ b/website/MyWebApp.Tests/LayoutServiceTests.cs @@ -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 @@ -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"]); diff --git a/website/MyWebApp.Tests/NavigationTests.cs b/website/MyWebApp.Tests/NavigationTests.cs index 67b3227..7ead488 100644 --- a/website/MyWebApp.Tests/NavigationTests.cs +++ b/website/MyWebApp.Tests/NavigationTests.cs @@ -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; @@ -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 { @@ -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(); diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs index da3536a..188ce5d 100644 --- a/website/MyWebApp.Tests/SanitizationTests.cs +++ b/website/MyWebApp.Tests/SanitizationTests.cs @@ -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 { @@ -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); diff --git a/website/MyWebApp/Controllers/AdminRoleController.cs b/website/MyWebApp/Controllers/AdminRoleController.cs new file mode 100644 index 0000000..31da78d --- /dev/null +++ b/website/MyWebApp/Controllers/AdminRoleController.cs @@ -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 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 Create(Role model) + { + if (!ModelState.IsValid) return View(model); + _db.Roles.Add(model); + await _db.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + + public async Task 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 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 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 DeleteConfirmed(int id) + { + var role = await _db.Roles.FindAsync(id); + if (role != null) + { + _db.Roles.Remove(role); + await _db.SaveChangesAsync(); + } + return RedirectToAction(nameof(Index)); + } +} diff --git a/website/MyWebApp/Models/AdminModels.cs b/website/MyWebApp/Models/AdminModels.cs index 7b98518..86273b7 100644 --- a/website/MyWebApp/Models/AdminModels.cs +++ b/website/MyWebApp/Models/AdminModels.cs @@ -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 SelectedPermissions { get; set; } = new List(); + } } diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs index 2e0ed82..09ffa7a 100644 --- a/website/MyWebApp/Services/LayoutService.cs +++ b/website/MyWebApp/Services/LayoutService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Http; using System.Linq; using MyWebApp.Data; @@ -8,6 +9,7 @@ public class LayoutService { private readonly CacheService _cache; private readonly TokenRenderService _tokens; + private readonly IHttpContextAccessor _accessor; private const string HeaderKey = "layout_header"; private const string FooterKey = "layout_footer"; @@ -27,50 +29,94 @@ public static string[] GetZones(string layout) return LayoutZones.TryGetValue(layout, out var zones) ? zones : Array.Empty(); } - 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() : roles.Split(','); + } + + private async Task> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles) + { + if (roles.Length == 0) return new List(); + return await db.RolePermissions.AsNoTracking() + .Where(rp => roles.Contains(rp.Role!.Name)) + .Select(rp => rp.PermissionId) + .Distinct() + .ToListAsync(); } public async Task 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) + .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"); + 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 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) + .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"); + 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 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); diff --git a/website/MyWebApp/Services/TokenRenderService.cs b/website/MyWebApp/Services/TokenRenderService.cs index cc6f4fb..9d0396d 100644 --- a/website/MyWebApp/Services/TokenRenderService.cs +++ b/website/MyWebApp/Services/TokenRenderService.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Http; using System.Linq; using MyWebApp.Data; @@ -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() : roles.Split(','); + } + + private async Task> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles) + { + if (roles.Length == 0) return new List(); + return await db.RolePermissions.AsNoTracking() + .Where(rp => roles.Contains(rp.Role!.Name)) + .Select(rp => rp.PermissionId) + .Distinct() + .ToListAsync(); + } public Task RenderAsync(ApplicationDbContext db, string html) { @@ -51,8 +74,15 @@ async Task 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(); diff --git a/website/MyWebApp/Views/Admin/_AdminLayout.cshtml b/website/MyWebApp/Views/Admin/_AdminLayout.cshtml index 1dbfc7e..538b4a3 100644 --- a/website/MyWebApp/Views/Admin/_AdminLayout.cshtml +++ b/website/MyWebApp/Views/Admin/_AdminLayout.cshtml @@ -28,6 +28,7 @@ Files Media Blocks + Roles Pages Logout diff --git a/website/MyWebApp/Views/AdminRole/Create.cshtml b/website/MyWebApp/Views/AdminRole/Create.cshtml new file mode 100644 index 0000000..e24893f --- /dev/null +++ b/website/MyWebApp/Views/AdminRole/Create.cshtml @@ -0,0 +1,10 @@ +@model MyWebApp.Models.Role +@{ + ViewData["Title"] = "Create Role"; + Layout = "../Admin/_AdminLayout"; +} +

Create Role

+
+
+ + diff --git a/website/MyWebApp/Views/AdminRole/Delete.cshtml b/website/MyWebApp/Views/AdminRole/Delete.cshtml new file mode 100644 index 0000000..64bca4a --- /dev/null +++ b/website/MyWebApp/Views/AdminRole/Delete.cshtml @@ -0,0 +1,12 @@ +@model MyWebApp.Models.Role +@{ + ViewData["Title"] = "Delete Role"; + Layout = "../Admin/_AdminLayout"; +} +

Delete Role

+
+ +

Are you sure you want to delete "@Model.Name"?

+ | + Cancel + diff --git a/website/MyWebApp/Views/AdminRole/Edit.cshtml b/website/MyWebApp/Views/AdminRole/Edit.cshtml new file mode 100644 index 0000000..0e2b95d --- /dev/null +++ b/website/MyWebApp/Views/AdminRole/Edit.cshtml @@ -0,0 +1,21 @@ +@model MyWebApp.Models.RoleEditViewModel +@{ + ViewData["Title"] = "Edit Role"; + Layout = "../Admin/_AdminLayout"; + var permissions = ViewBag.Permissions as List; +} +

Edit Role

+
+ +
+
+ + @foreach (var p in permissions) + { +
+ @p.Name +
+ } +
+ + diff --git a/website/MyWebApp/Views/AdminRole/Index.cshtml b/website/MyWebApp/Views/AdminRole/Index.cshtml new file mode 100644 index 0000000..5b94e09 --- /dev/null +++ b/website/MyWebApp/Views/AdminRole/Index.cshtml @@ -0,0 +1,20 @@ +@model IEnumerable +@{ + ViewData["Title"] = "Roles"; + Layout = "../Admin/_AdminLayout"; +} +

Roles

+

Create New

+
PageAreaType
PageZoneType
@s.Page?.Slug@s.Area@s.Zone@s.Type
+ + +@foreach (var r in Model) +{ + + + + + +} + +
Name
@r.NameEditDelete
From 709d4b384a392e8e24339b1a29f5482b10ab1fcb Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:56:32 +0800 Subject: [PATCH 17/27] Ensure role tables exist for SQLite --- website/MyWebApp/Program.cs | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/website/MyWebApp/Program.cs b/website/MyWebApp/Program.cs index 49b657b..817dbe8 100644 --- a/website/MyWebApp/Program.cs +++ b/website/MyWebApp/Program.cs @@ -195,6 +195,7 @@ UpgradePagesTable(db); UpgradeMediaItemsTable(db); UpgradeBlockTemplatesTable(db); + UpgradeRolesTable(db); UpgradePermissionsTable(db); UpgradeLayoutHeader(db); } @@ -512,6 +513,46 @@ FOREIGN KEY(BlockTemplateId) REFERENCES BlockTemplates(Id) ON DELETE CASCADE } } +static void UpgradeRolesTable(ApplicationDbContext db) +{ + try + { + using var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='Roles'"; + var exists = cmd.ExecuteScalar() != null; + if (!exists) + { + db.Database.ExecuteSqlRaw(@"CREATE TABLE Roles ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Name TEXT NOT NULL + )"); + db.Database.ExecuteSqlRaw("CREATE UNIQUE INDEX IX_Roles_Name ON Roles(Name)"); + db.Database.ExecuteSqlRaw(@"INSERT INTO Roles (Id, Name) VALUES + (1, 'Admin'), (2, 'User'), (3, 'Moderator')"); + } + + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='UserRoles'"; + exists = cmd.ExecuteScalar() != null; + if (!exists) + { + db.Database.ExecuteSqlRaw(@"CREATE TABLE UserRoles ( + SiteUserId INTEGER NOT NULL, + RoleId INTEGER NOT NULL, + PRIMARY KEY(SiteUserId, RoleId), + FOREIGN KEY(SiteUserId) REFERENCES SiteUsers(Id) ON DELETE CASCADE, + FOREIGN KEY(RoleId) REFERENCES Roles(Id) ON DELETE CASCADE + )"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Schema upgrade failed: {ex.Message}"); + } +} + static void UpgradePermissionsTable(ApplicationDbContext db) { try From 9ebea34cd2970c236be9981a546512d6b5ce0a51 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:21:41 +0800 Subject: [PATCH 18/27] Add role selection for pages and blocks --- .../AdminBlockTemplateController.cs | 6 ++-- .../Controllers/AdminContentController.cs | 2 ++ .../Controllers/AdminPageSectionController.cs | 1 + .../MyWebApp/Controllers/PagesController.cs | 9 ++++++ website/MyWebApp/Data/ApplicationDbContext.cs | 6 ++-- website/MyWebApp/Models/Page.cs | 4 +++ website/MyWebApp/Models/PageSection.cs | 4 +++ website/MyWebApp/Program.cs | 6 ++++ website/MyWebApp/Services/LayoutService.cs | 32 +++++++++++++++---- .../MyWebApp/Services/TokenRenderService.cs | 22 ++++++++++--- .../Views/AdminBlockTemplate/AddToPage.cshtml | 10 ++++++ .../Views/AdminContent/PageEditor.cshtml | 15 +++++++-- .../Views/AdminPageSection/Create.cshtml | 3 +- .../Views/AdminPageSection/Edit.cshtml | 3 +- .../Views/Shared/_SectionEditor.cshtml | 11 +++++++ 15 files changed, 115 insertions(+), 19 deletions(-) diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index 0afa0de..294481d 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -26,6 +26,7 @@ public AdminBlockTemplateController(ApplicationDbContext db, HtmlSanitizerServic private async Task LoadPagesAsync() { ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); + ViewBag.Roles = await _db.Roles.AsNoTracking().OrderBy(r => r.Name).ToListAsync(); } public async Task Index() @@ -159,7 +160,7 @@ public async Task AddToPage(int id) [HttpPost] [ValidateAntiForgeryToken] - public async Task AddToPage(int id, int pageId, string zone) + public async Task AddToPage(int id, int pageId, string zone, int? roleId) { var template = await _db.BlockTemplates.FindAsync(id); var page = await _db.Pages.FindAsync(pageId); @@ -186,7 +187,8 @@ public async Task AddToPage(int id, int pageId, string zone) Zone = zone, SortOrder = sort, Html = template.Html, - Type = PageSectionType.Html + Type = PageSectionType.Html, + RoleId = roleId }; _db.PageSections.Add(section); await _db.SaveChangesAsync(); diff --git a/website/MyWebApp/Controllers/AdminContentController.cs b/website/MyWebApp/Controllers/AdminContentController.cs index 21f53d4..99aef70 100644 --- a/website/MyWebApp/Controllers/AdminContentController.cs +++ b/website/MyWebApp/Controllers/AdminContentController.cs @@ -35,6 +35,8 @@ private async Task LoadTemplatesAsync() .OrderBy(t => t.Name).ToListAsync(); ViewBag.Permissions = await _db.Permissions.AsNoTracking() .OrderBy(p => p.Name).ToListAsync(); + ViewBag.Roles = await _db.Roles.AsNoTracking() + .OrderBy(r => r.Name).ToListAsync(); } public async Task Index() diff --git a/website/MyWebApp/Controllers/AdminPageSectionController.cs b/website/MyWebApp/Controllers/AdminPageSectionController.cs index 05f74ff..ecfa844 100644 --- a/website/MyWebApp/Controllers/AdminPageSectionController.cs +++ b/website/MyWebApp/Controllers/AdminPageSectionController.cs @@ -39,6 +39,7 @@ private async Task LoadPagesAsync() { ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); ViewBag.Permissions = await _db.Permissions.AsNoTracking().OrderBy(p => p.Name).ToListAsync(); + ViewBag.Roles = await _db.Roles.AsNoTracking().OrderBy(r => r.Name).ToListAsync(); } public async Task Create() diff --git a/website/MyWebApp/Controllers/PagesController.cs b/website/MyWebApp/Controllers/PagesController.cs index 8d5cae0..e4c62b8 100644 --- a/website/MyWebApp/Controllers/PagesController.cs +++ b/website/MyWebApp/Controllers/PagesController.cs @@ -29,6 +29,15 @@ public async Task Show(string? slug) { return NotFound(); } + var roles = HttpContext.Session.GetString("Roles")?.Split(',') ?? Array.Empty(); + if (page.RoleId != null) + { + var allowed = await Db.Roles.AsNoTracking().Where(r => roles.Contains(r.Name)).Select(r => r.Id).ToListAsync(); + if (!allowed.Contains(page.RoleId.Value)) + { + return Unauthorized(); + } + } var header = await _layout.GetSectionAsync(Db, page.Id, "header"); if (string.IsNullOrEmpty(header)) diff --git a/website/MyWebApp/Data/ApplicationDbContext.cs b/website/MyWebApp/Data/ApplicationDbContext.cs index a64914a..8b12015 100644 --- a/website/MyWebApp/Data/ApplicationDbContext.cs +++ b/website/MyWebApp/Data/ApplicationDbContext.cs @@ -90,14 +90,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Id = 1, Slug = "layout", Title = "Layout", - Layout = "single-column" + Layout = "single-column", + RoleId = null }, new Page { Id = 2, Slug = "home", Title = "Home", - Layout = "single-column" + Layout = "single-column", + RoleId = null }); modelBuilder.Entity().HasData( diff --git a/website/MyWebApp/Models/Page.cs b/website/MyWebApp/Models/Page.cs index ee8a599..71545c5 100644 --- a/website/MyWebApp/Models/Page.cs +++ b/website/MyWebApp/Models/Page.cs @@ -45,6 +45,10 @@ public class Page [MaxLength(256)] public string? FeaturedImage { get; set; } + public int? RoleId { get; set; } + + public Role? Role { get; set; } + public ICollection Sections { get; set; } = new List(); } } diff --git a/website/MyWebApp/Models/PageSection.cs b/website/MyWebApp/Models/PageSection.cs index 2e3600e..8b97aa4 100644 --- a/website/MyWebApp/Models/PageSection.cs +++ b/website/MyWebApp/Models/PageSection.cs @@ -37,8 +37,12 @@ public class PageSection public int? PermissionId { get; set; } + public int? RoleId { get; set; } + public Page? Page { get; set; } public Permission? Permission { get; set; } + + public Role? Role { get; set; } } diff --git a/website/MyWebApp/Program.cs b/website/MyWebApp/Program.cs index 817dbe8..3902050 100644 --- a/website/MyWebApp/Program.cs +++ b/website/MyWebApp/Program.cs @@ -355,6 +355,8 @@ FOREIGN KEY(PageId) REFERENCES Pages(Id) ON DELETE CASCADE db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN EndDate TEXT"); if (!columns.Contains("PermissionId")) db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN PermissionId INTEGER"); + if (!columns.Contains("RoleId")) + db.Database.ExecuteSqlRaw("ALTER TABLE PageSections ADD COLUMN RoleId INTEGER"); cmd.CommandText = "PRAGMA index_list('PageSections')"; using var idx = cmd.ExecuteReader(); @@ -438,6 +440,10 @@ static void UpgradePagesTable(ApplicationDbContext db) { db.Database.ExecuteSqlRaw("ALTER TABLE Pages ADD COLUMN FeaturedImage TEXT"); } + if (!columns.Contains("RoleId")) + { + db.Database.ExecuteSqlRaw("ALTER TABLE Pages ADD COLUMN RoleId INTEGER"); + } } catch (Exception ex) { diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs index 09ffa7a..1d16cf2 100644 --- a/website/MyWebApp/Services/LayoutService.cs +++ b/website/MyWebApp/Services/LayoutService.cs @@ -52,16 +52,26 @@ private async Task> GetAllowedPermissionsAsync(ApplicationDbContext db .ToListAsync(); } + private async Task> GetRoleIdsAsync(ApplicationDbContext db, string[] roles) + { + if (roles.Length == 0) return new List(); + return await db.Roles.AsNoTracking() + .Where(r => roles.Contains(r.Name)) + .Select(r => r.Id) + .ToListAsync(); + } + public async Task GetHeaderAsync(ApplicationDbContext db) { var roles = GetRoles(); + var roleIds = await GetRoleIdsAsync(db, roles); if (roles.Length == 0) { 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) + .Where(s => s.Page.Slug == "layout" && s.Zone == "header" && s.PermissionId == null && s.RoleId == null) .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); @@ -73,7 +83,9 @@ public async Task GetHeaderAsync(ApplicationDbContext db) var allowed = await GetAllowedPermissionsAsync(db, roles); var query = db.PageSections.AsNoTracking() .Where(s => s.Page.Slug == "layout" && s.Zone == "header"); - query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value)); + query = query.Where(s => + (s.PermissionId == null || allowed.Contains(s.PermissionId.Value)) && + (s.RoleId == null || roleIds.Contains(s.RoleId.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); @@ -82,13 +94,14 @@ public async Task GetHeaderAsync(ApplicationDbContext db) public async Task GetFooterAsync(ApplicationDbContext db) { var roles = GetRoles(); + var roleIds = await GetRoleIdsAsync(db, roles); if (roles.Length == 0) { 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) + .Where(s => s.Page.Slug == "layout" && s.Zone == "footer" && s.PermissionId == null && s.RoleId == null) .OrderBy(s => s.SortOrder) .Select(s => s.Html) .ToListAsync(); @@ -100,7 +113,9 @@ public async Task GetFooterAsync(ApplicationDbContext db) var allowed = await GetAllowedPermissionsAsync(db, roles); var query = db.PageSections.AsNoTracking() .Where(s => s.Page.Slug == "layout" && s.Zone == "footer"); - query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value)); + query = query.Where(s => + (s.PermissionId == null || allowed.Contains(s.PermissionId.Value)) && + (s.RoleId == null || roleIds.Contains(s.RoleId.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); @@ -109,13 +124,16 @@ public async Task GetFooterAsync(ApplicationDbContext db) public async Task GetSectionAsync(ApplicationDbContext db, int pageId, string zone) { var roles = GetRoles(); + var roleIds = await GetRoleIdsAsync(db, roles); 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); + if (allowed.Count == 0 && roleIds.Count == 0) + query = query.Where(s => s.PermissionId == null && s.RoleId == null); else - query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value)); + query = query.Where(s => + (s.PermissionId == null || allowed.Contains(s.PermissionId.Value)) && + (s.RoleId == null || roleIds.Contains(s.RoleId.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); diff --git a/website/MyWebApp/Services/TokenRenderService.cs b/website/MyWebApp/Services/TokenRenderService.cs index 9d0396d..da635ad 100644 --- a/website/MyWebApp/Services/TokenRenderService.cs +++ b/website/MyWebApp/Services/TokenRenderService.cs @@ -32,6 +32,15 @@ private async Task> GetAllowedPermissionsAsync(ApplicationDbContext db .ToListAsync(); } + private async Task> GetRoleIdsAsync(ApplicationDbContext db, string[] roles) + { + if (roles.Length == 0) return new List(); + return await db.Roles.AsNoTracking() + .Where(r => roles.Contains(r.Name)) + .Select(r => r.Id) + .ToListAsync(); + } + public Task RenderAsync(ApplicationDbContext db, string html) { return RenderAsync(db, html, new HashSet()); @@ -43,8 +52,10 @@ async Task Replace(Match match) { if (match.Value.StartsWith("{{nav", StringComparison.OrdinalIgnoreCase)) { + var roles = GetRoles(); + var roleIds = await GetRoleIdsAsync(db, roles); var pages = await db.Pages.AsNoTracking() - .Where(p => p.IsPublished && p.Slug != "layout" && p.Slug != "home") + .Where(p => p.IsPublished && p.Slug != "layout" && p.Slug != "home" && (p.RoleId == null || roleIds.Contains(p.RoleId.Value))) .OrderBy(p => p.Title) .Select(p => new { p.Slug, p.Title }) .ToListAsync(); @@ -75,13 +86,16 @@ async Task Replace(Match match) { var zone = parts[1]; var roles = GetRoles(); + var roleIds = await GetRoleIdsAsync(db, roles); 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); + if (allowed.Count == 0 && roleIds.Count == 0) + query = query.Where(s => s.PermissionId == null && s.RoleId == null); else - query = query.Where(s => s.PermissionId == null || allowed.Contains(s.PermissionId.Value)); + query = query.Where(s => + (s.PermissionId == null || allowed.Contains(s.PermissionId.Value)) && + (s.RoleId == null || roleIds.Contains(s.RoleId.Value))); var htmlParts = await query .OrderBy(s => s.SortOrder) .Select(s => s.Html) diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index 8e06142..c902368 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -19,5 +19,15 @@ +
+ + +
diff --git a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml index 83ba5fd..847a8e2 100644 --- a/website/MyWebApp/Views/AdminContent/PageEditor.cshtml +++ b/website/MyWebApp/Views/AdminContent/PageEditor.cshtml @@ -3,6 +3,7 @@ @using Microsoft.AspNetCore.Mvc.ViewFeatures @{ var sections = ViewBag.Sections as List ?? new List(); + var roles = ViewBag.Roles as List ?? new List(); Layout = "../Admin/_AdminLayout"; var isNew = Model.Id == 0; ViewData["Title"] = isNew ? "Create Page" : "Edit Page"; @@ -40,6 +41,16 @@
+
+ + +
@@ -56,7 +67,7 @@
@for (int i = 0; i < sections.Count; i++) { - var vd = new ViewDataDictionary(ViewData) { ["Index"] = i }; + var vd = new ViewDataDictionary(ViewData) { ["Index"] = i, ["Roles"] = roles }; @await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", sections[i], vd) }
@@ -74,7 +85,7 @@ } diff --git a/website/MyWebApp/Views/AdminPageSection/Create.cshtml b/website/MyWebApp/Views/AdminPageSection/Create.cshtml index 9fa8510..9550599 100644 --- a/website/MyWebApp/Views/AdminPageSection/Create.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Create.cshtml @@ -4,6 +4,7 @@ Layout = "../Admin/_AdminLayout"; var pages = ViewBag.Pages as List; var permissions = ViewBag.Permissions as List; + var roles = ViewBag.Roles as List ?? new List(); }

Create Section

@@ -16,7 +17,7 @@ -@await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", Model) +@await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", Model, new ViewDataDictionary(ViewData) { ["Roles"] = roles })
diff --git a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml index 5707297..05822c0 100644 --- a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml @@ -4,6 +4,7 @@ Layout = "../Admin/_AdminLayout"; var pages = ViewBag.Pages as List; var permissions = ViewBag.Permissions as List; + var roles = ViewBag.Roles as List ?? new List(); }

Edit Section

@@ -17,7 +18,7 @@ -@await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", Model) +@await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", Model, new ViewDataDictionary(ViewData) { ["Roles"] = roles })
diff --git a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml index 082477b..a5dbee2 100644 --- a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml +++ b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml @@ -4,6 +4,7 @@ var idxObj = ViewData["Index"]; var idx = idxObj?.ToString() ?? "0"; var prefix = idxObj != null ? $"Sections[{idx}]." : string.Empty; + var roles = ViewData["Roles"] as List ?? new List(); }
@@ -12,6 +13,16 @@
+
+ + +
From 33f67fc99920c7dc7689dcab33f23f20cb7f8a10 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:57:48 +0800 Subject: [PATCH 19/27] Fix role dropdown and default anonymous role --- website/MyWebApp/Controllers/AccountController.cs | 5 +++-- website/MyWebApp/Controllers/BaseController.cs | 5 +++-- website/MyWebApp/Controllers/PagesController.cs | 5 +++-- website/MyWebApp/Data/ApplicationDbContext.cs | 3 ++- website/MyWebApp/Filters/RoleAuthorizeAttribute.cs | 5 +++-- website/MyWebApp/Services/LayoutService.cs | 6 +++++- website/MyWebApp/Services/TokenRenderService.cs | 6 +++++- website/MyWebApp/Views/Account/Register.cshtml | 2 +- website/MyWebApp/Views/Shared/_SectionEditor.cshtml | 2 +- 9 files changed, 26 insertions(+), 13 deletions(-) diff --git a/website/MyWebApp/Controllers/AccountController.cs b/website/MyWebApp/Controllers/AccountController.cs index 63caa45..919a823 100644 --- a/website/MyWebApp/Controllers/AccountController.cs +++ b/website/MyWebApp/Controllers/AccountController.cs @@ -21,8 +21,9 @@ public class AccountController : Controller private bool HasRole(string role) { - var roles = HttpContext.Session.GetString("Roles")?.Split(',') ?? Array.Empty(); - return roles.Contains(role); + var roles = HttpContext.Session.GetString("Roles"); + var roleNames = string.IsNullOrWhiteSpace(roles) ? new[] { "Anonym" } : roles.Split(','); + return roleNames.Contains(role); } public AccountController(ApplicationDbContext db, CaptchaService captchaService, IEmailSender emailSender, ILogger logger) diff --git a/website/MyWebApp/Controllers/BaseController.cs b/website/MyWebApp/Controllers/BaseController.cs index d3e348b..e8ddd1f 100644 --- a/website/MyWebApp/Controllers/BaseController.cs +++ b/website/MyWebApp/Controllers/BaseController.cs @@ -33,8 +33,9 @@ protected bool CheckDatabase() protected bool HasRole(string role) { - var roles = HttpContext.Session.GetString("Roles")?.Split(',') ?? Array.Empty(); - return roles.Contains(role); + var roles = HttpContext.Session.GetString("Roles"); + var roleNames = string.IsNullOrWhiteSpace(roles) ? new[] { "Anonym" } : roles.Split(','); + return roleNames.Contains(role); } protected bool IsAdmin() diff --git a/website/MyWebApp/Controllers/PagesController.cs b/website/MyWebApp/Controllers/PagesController.cs index e4c62b8..dc94419 100644 --- a/website/MyWebApp/Controllers/PagesController.cs +++ b/website/MyWebApp/Controllers/PagesController.cs @@ -29,10 +29,11 @@ public async Task Show(string? slug) { return NotFound(); } - var roles = HttpContext.Session.GetString("Roles")?.Split(',') ?? Array.Empty(); + var roles = HttpContext.Session.GetString("Roles"); + var roleNames = string.IsNullOrWhiteSpace(roles) ? new[] { "Anonym" } : roles.Split(','); if (page.RoleId != null) { - var allowed = await Db.Roles.AsNoTracking().Where(r => roles.Contains(r.Name)).Select(r => r.Id).ToListAsync(); + var allowed = await Db.Roles.AsNoTracking().Where(r => roleNames.Contains(r.Name)).Select(r => r.Id).ToListAsync(); if (!allowed.Contains(page.RoleId.Value)) { return Unauthorized(); diff --git a/website/MyWebApp/Data/ApplicationDbContext.cs b/website/MyWebApp/Data/ApplicationDbContext.cs index 8b12015..4ef668a 100644 --- a/website/MyWebApp/Data/ApplicationDbContext.cs +++ b/website/MyWebApp/Data/ApplicationDbContext.cs @@ -131,7 +131,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasData( new Role { Id = 1, Name = "Admin" }, new Role { Id = 2, Name = "User" }, - new Role { Id = 3, Name = "Moderator" }); + new Role { Id = 3, Name = "Moderator" }, + new Role { Id = 4, Name = "Anonym" }); // provider specific optimizations var provider = Database.ProviderName ?? string.Empty; diff --git a/website/MyWebApp/Filters/RoleAuthorizeAttribute.cs b/website/MyWebApp/Filters/RoleAuthorizeAttribute.cs index dd09b5a..6bf3ebf 100644 --- a/website/MyWebApp/Filters/RoleAuthorizeAttribute.cs +++ b/website/MyWebApp/Filters/RoleAuthorizeAttribute.cs @@ -17,8 +17,9 @@ public RoleAuthorizeAttribute(params string[] roles) public void OnAuthorization(AuthorizationFilterContext context) { var session = context.HttpContext.Session; - var roles = session.GetString("Roles")?.Split(',') ?? Array.Empty(); - if (!_roles.Any(r => roles.Contains(r))) + var roles = session.GetString("Roles"); + var roleNames = string.IsNullOrWhiteSpace(roles) ? new[] { "Anonym" } : roles.Split(','); + if (!_roles.Any(r => roleNames.Contains(r))) { var returnUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; context.Result = new RedirectToActionResult("Login", "Account", new { returnUrl }); diff --git a/website/MyWebApp/Services/LayoutService.cs b/website/MyWebApp/Services/LayoutService.cs index 1d16cf2..5f4caac 100644 --- a/website/MyWebApp/Services/LayoutService.cs +++ b/website/MyWebApp/Services/LayoutService.cs @@ -39,7 +39,11 @@ public LayoutService(CacheService cache, TokenRenderService tokens, IHttpContext private string[] GetRoles() { var roles = _accessor.HttpContext?.Session.GetString("Roles"); - return string.IsNullOrWhiteSpace(roles) ? Array.Empty() : roles.Split(','); + if (string.IsNullOrWhiteSpace(roles)) + { + return new[] { "Anonym" }; + } + return roles.Split(','); } private async Task> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles) diff --git a/website/MyWebApp/Services/TokenRenderService.cs b/website/MyWebApp/Services/TokenRenderService.cs index da635ad..20f923a 100644 --- a/website/MyWebApp/Services/TokenRenderService.cs +++ b/website/MyWebApp/Services/TokenRenderService.cs @@ -19,7 +19,11 @@ public TokenRenderService(IHttpContextAccessor accessor) private string[] GetRoles() { var roles = _accessor.HttpContext?.Session.GetString("Roles"); - return string.IsNullOrWhiteSpace(roles) ? Array.Empty() : roles.Split(','); + if (string.IsNullOrWhiteSpace(roles)) + { + return new[] { "Anonym" }; + } + return roles.Split(','); } private async Task> GetAllowedPermissionsAsync(ApplicationDbContext db, string[] roles) diff --git a/website/MyWebApp/Views/Account/Register.cshtml b/website/MyWebApp/Views/Account/Register.cshtml index acb3ce7..b38238a 100644 --- a/website/MyWebApp/Views/Account/Register.cshtml +++ b/website/MyWebApp/Views/Account/Register.cshtml @@ -2,7 +2,7 @@ @using Microsoft.AspNetCore.Http @{ ViewData["Title"] = "Register"; - var roles = Context.Session.GetString("Roles") ?? string.Empty; + var roles = Context.Session.GetString("Roles") ?? "Anonym"; bool canSelectType = roles.Contains("Admin") || roles.Contains("Moderator"); }

Register

diff --git a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml index a5dbee2..f20e4bf 100644 --- a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml +++ b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml @@ -19,7 +19,7 @@ @foreach (var r in roles) { - + }
From de7379a33c2c8af0c2b735709135946f20217b3a Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:20:59 +0800 Subject: [PATCH 20/27] Enhance admin block UI --- .../AdminBlockTemplateController.cs | 49 ++++++++++++------- .../Views/AdminBlockTemplate/AddToPage.cshtml | 33 ++++++++++--- .../Views/AdminPageSection/Edit.cshtml | 4 ++ .../Views/Shared/_SectionEditor.cshtml | 11 ++--- 4 files changed, 65 insertions(+), 32 deletions(-) diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index 294481d..8e38870 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Http; using System.Collections.Generic; +using System.Linq; using System.IO; using System.Text.Json; using MyWebApp.Data; @@ -160,14 +161,19 @@ public async Task AddToPage(int id) [HttpPost] [ValidateAntiForgeryToken] - public async Task AddToPage(int id, int pageId, string zone, int? roleId) + public async Task AddToPage(int id, List pageIds, string zone, string role) { var template = await _db.BlockTemplates.FindAsync(id); - var page = await _db.Pages.FindAsync(pageId); - if (template == null || page == null) + if (template == null) return NotFound(); + if (pageIds == null || pageIds.Count == 0) { - return NotFound(); + await LoadPagesAsync(); + ViewBag.BlockId = id; + ModelState.AddModelError("pageIds", "Page selection required"); + return View(); } + var roleEntity = await _db.Roles.FirstOrDefaultAsync(r => r.Name == role); + int? roleId = roleEntity?.Id; zone = zone?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(zone)) { @@ -176,21 +182,28 @@ public async Task AddToPage(int id, int pageId, string zone, int? ModelState.AddModelError("zone", "Zone required"); return View(); } - var sort = await _db.PageSections - .Where(s => s.PageId == pageId && s.Zone == zone) - .Select(s => s.SortOrder) - .DefaultIfEmpty(-1) - .MaxAsync() + 1; - var section = new PageSection + if (pageIds.Contains(0)) { - PageId = pageId, - Zone = zone, - SortOrder = sort, - Html = template.Html, - Type = PageSectionType.Html, - RoleId = roleId - }; - _db.PageSections.Add(section); + pageIds = await _db.Pages.Select(p => p.Id).ToListAsync(); + } + foreach (var pageId in pageIds) + { + var sort = await _db.PageSections + .Where(s => s.PageId == pageId && s.Zone == zone) + .Select(s => s.SortOrder) + .DefaultIfEmpty(-1) + .MaxAsync() + 1; + var section = new PageSection + { + PageId = pageId, + Zone = zone, + SortOrder = sort, + Html = template.Html, + Type = PageSectionType.Html, + RoleId = roleId + }; + _db.PageSections.Add(section); + } await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index c902368..f574f8d 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -7,8 +7,13 @@
- - +
+
+ + - - @foreach (var r in ViewBag.Roles as List) - { - - } +
+ +@section Scripts { + +} diff --git a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml index 05822c0..6f30388 100644 --- a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml @@ -9,6 +9,10 @@

Edit Section

+
+ + +
diff --git a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml index f20e4bf..b6c82a7 100644 --- a/website/MyWebApp/Views/Shared/_SectionEditor.cshtml +++ b/website/MyWebApp/Views/Shared/_SectionEditor.cshtml @@ -15,12 +15,11 @@
- + + + +
From a3a0789a5d1c4276ff69cd64d952294bf596e183 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:52:23 +0800 Subject: [PATCH 21/27] Integrate block assignment into edit --- .../AdminBlockTemplateController.cs | 103 ++++++++++++++---- .../Views/AdminBlockTemplate/AddToPage.cshtml | 26 ++++- .../Views/AdminBlockTemplate/Create.cshtml | 2 + .../Views/AdminBlockTemplate/Edit.cshtml | 2 + .../Views/AdminBlockTemplate/Index.cshtml | 3 +- .../AdminBlockTemplate/_PageAssignment.cshtml | 41 +++++++ .../Views/AdminPageSection/Edit.cshtml | 4 + 7 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index 294481d..551618a 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Http; using System.Collections.Generic; +using System.Linq; using System.IO; using System.Text.Json; using MyWebApp.Data; @@ -35,20 +36,27 @@ public async Task Index() return View(items); } - public IActionResult Create() + public async Task Create() { + await LoadPagesAsync(); return View(new BlockTemplate()); } [HttpPost] [ValidateAntiForgeryToken] - public async Task Create(BlockTemplate model) + public async Task Create(BlockTemplate model, List? pageIds, string? zone, string? role) { - if (!ModelState.IsValid) return View(model); + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } model.Html = _sanitizer.Sanitize(model.Html); _db.BlockTemplates.Add(model); _db.BlockTemplateVersions.Add(new BlockTemplateVersion { Template = model, Html = model.Html }); await _db.SaveChangesAsync(); + await AddSectionsAsync(model, pageIds, zone, role); + await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } @@ -56,18 +64,25 @@ public async Task Edit(int id) { var item = await _db.BlockTemplates.FindAsync(id); if (item == null) return NotFound(); + await LoadPagesAsync(); return View(item); } [HttpPost] [ValidateAntiForgeryToken] - public async Task Edit(BlockTemplate model) + public async Task Edit(BlockTemplate model, List? pageIds, string? zone, string? role) { - if (!ModelState.IsValid) return View(model); + if (!ModelState.IsValid) + { + await LoadPagesAsync(); + return View(model); + } model.Html = _sanitizer.Sanitize(model.Html); _db.Update(model); _db.BlockTemplateVersions.Add(new BlockTemplateVersion { BlockTemplateId = model.Id, Html = model.Html }); await _db.SaveChangesAsync(); + await AddSectionsAsync(model, pageIds, zone, role); + await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } @@ -160,14 +175,19 @@ public async Task AddToPage(int id) [HttpPost] [ValidateAntiForgeryToken] - public async Task AddToPage(int id, int pageId, string zone, int? roleId) + public async Task AddToPage(int id, List pageIds, string zone, string role) { var template = await _db.BlockTemplates.FindAsync(id); - var page = await _db.Pages.FindAsync(pageId); - if (template == null || page == null) + if (template == null) return NotFound(); + if (pageIds == null || pageIds.Count == 0) { - return NotFound(); + await LoadPagesAsync(); + ViewBag.BlockId = id; + ModelState.AddModelError("pageIds", "Page selection required"); + return View(); } + var roleEntity = await _db.Roles.FirstOrDefaultAsync(r => r.Name == role); + int? roleId = roleEntity?.Id; zone = zone?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(zone)) { @@ -176,23 +196,60 @@ public async Task AddToPage(int id, int pageId, string zone, int? ModelState.AddModelError("zone", "Zone required"); return View(); } - var sort = await _db.PageSections - .Where(s => s.PageId == pageId && s.Zone == zone) - .Select(s => s.SortOrder) - .DefaultIfEmpty(-1) - .MaxAsync() + 1; - var section = new PageSection + if (pageIds.Contains(0)) + { + pageIds = await _db.Pages.Select(p => p.Id).ToListAsync(); + } + foreach (var pageId in pageIds) { - PageId = pageId, - Zone = zone, - SortOrder = sort, - Html = template.Html, - Type = PageSectionType.Html, - RoleId = roleId - }; - _db.PageSections.Add(section); + var sort = await _db.PageSections + .Where(s => s.PageId == pageId && s.Zone == zone) + .Select(s => s.SortOrder) + .DefaultIfEmpty(-1) + .MaxAsync() + 1; + var section = new PageSection + { + PageId = pageId, + Zone = zone, + SortOrder = sort, + Html = template.Html, + Type = PageSectionType.Html, + RoleId = roleId + }; + _db.PageSections.Add(section); + } await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } + private async Task AddSectionsAsync(BlockTemplate template, List? pageIds, string? zone, string? role) + { + if (pageIds == null || pageIds.Count == 0 || string.IsNullOrWhiteSpace(zone)) + return; + var roleEntity = await _db.Roles.FirstOrDefaultAsync(r => r.Name == role); + int? roleId = roleEntity?.Id; + zone = zone!.Trim(); + if (pageIds.Contains(0)) + { + pageIds = await _db.Pages.Select(p => p.Id).ToListAsync(); + } + foreach (var pageId in pageIds) + { + var sort = await _db.PageSections + .Where(s => s.PageId == pageId && s.Zone == zone) + .Select(s => s.SortOrder) + .DefaultIfEmpty(-1) + .MaxAsync() + 1; + var section = new PageSection + { + PageId = pageId, + Zone = zone, + SortOrder = sort, + Html = template.Html, + Type = PageSectionType.Html, + RoleId = roleId + }; + _db.PageSections.Add(section); + } + } } diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index c902368..6ea423b 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -7,8 +7,13 @@
- - +
+
+ + +
+ +@section Scripts { + +} diff --git a/website/MyWebApp/Views/AdminBlockTemplate/Create.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/Create.cshtml index 756c339..a53d5d4 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/Create.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/Create.cshtml @@ -17,6 +17,8 @@
+ + @await Html.PartialAsync("_PageAssignment") diff --git a/website/MyWebApp/Views/AdminBlockTemplate/Edit.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/Edit.cshtml index f82cedb..9047c6f 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/Edit.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/Edit.cshtml @@ -18,6 +18,8 @@ + + @await Html.PartialAsync("_PageAssignment") diff --git a/website/MyWebApp/Views/AdminBlockTemplate/Index.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/Index.cshtml index 5ad8c03..64f8481 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/Index.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/Index.cshtml @@ -10,7 +10,7 @@ - + @foreach (var t in Model) { @@ -18,7 +18,6 @@ - } diff --git a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml new file mode 100644 index 0000000..33945a6 --- /dev/null +++ b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml @@ -0,0 +1,41 @@ +@using MyWebApp.Models +
+

Page Assignment

+
+ + + +
+
+ + +
+
+ + +
+
+@section Scripts { + +} diff --git a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml index 05822c0..6f30388 100644 --- a/website/MyWebApp/Views/AdminPageSection/Edit.cshtml +++ b/website/MyWebApp/Views/AdminPageSection/Edit.cshtml @@ -9,6 +9,10 @@

Edit Section

+
+ + +
From e71c81325a8fe76c7e26eff827c95ea914cfb836 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:20:41 +0800 Subject: [PATCH 22/27] Add zone dropdowns for block templates --- .../MyWebApp/Controllers/AdminBlockTemplateController.cs | 5 +++++ website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml | 7 ++++++- .../Views/AdminBlockTemplate/_PageAssignment.cshtml | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index 551618a..da2c21f 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -28,6 +28,11 @@ private async Task LoadPagesAsync() { ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); ViewBag.Roles = await _db.Roles.AsNoTracking().OrderBy(r => r.Name).ToListAsync(); + ViewBag.Zones = await _db.PageSections.AsNoTracking() + .Select(s => s.Zone) + .Distinct() + .OrderBy(z => z) + .ToListAsync(); } public async Task Index() diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index c08e096..67a64ec 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -22,7 +22,12 @@
- +
diff --git a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml index 33945a6..75acbb0 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml @@ -14,7 +14,12 @@
- +
From 92d7f02626f4c730e170661dcefea9bd49063630 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:37:19 +0800 Subject: [PATCH 23/27] Fix zone sort retrieval --- .../AdminBlockTemplateController.cs | 21 ++++++++++++------- .../Views/AdminBlockTemplate/AddToPage.cshtml | 7 ++++++- .../AdminBlockTemplate/_PageAssignment.cshtml | 7 ++++++- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index 551618a..d799225 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -28,6 +28,11 @@ private async Task LoadPagesAsync() { ViewBag.Pages = await _db.Pages.AsNoTracking().OrderBy(p => p.Slug).ToListAsync(); ViewBag.Roles = await _db.Roles.AsNoTracking().OrderBy(r => r.Name).ToListAsync(); + ViewBag.Zones = await _db.PageSections.AsNoTracking() + .Select(s => s.Zone) + .Distinct() + .OrderBy(z => z) + .ToListAsync(); } public async Task Index() @@ -202,11 +207,11 @@ public async Task AddToPage(int id, List pageIds, string zon } foreach (var pageId in pageIds) { - var sort = await _db.PageSections + var maxSort = await _db.PageSections .Where(s => s.PageId == pageId && s.Zone == zone) - .Select(s => s.SortOrder) - .DefaultIfEmpty(-1) - .MaxAsync() + 1; + .Select(s => (int?)s.SortOrder) + .MaxAsync(); + var sort = (maxSort ?? -1) + 1; var section = new PageSection { PageId = pageId, @@ -235,11 +240,11 @@ private async Task AddSectionsAsync(BlockTemplate template, List? pageIds, } foreach (var pageId in pageIds) { - var sort = await _db.PageSections + var maxSort = await _db.PageSections .Where(s => s.PageId == pageId && s.Zone == zone) - .Select(s => s.SortOrder) - .DefaultIfEmpty(-1) - .MaxAsync() + 1; + .Select(s => (int?)s.SortOrder) + .MaxAsync(); + var sort = (maxSort ?? -1) + 1; var section = new PageSection { PageId = pageId, diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index c08e096..67a64ec 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -22,7 +22,12 @@
- +
diff --git a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml index 33945a6..75acbb0 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml @@ -14,7 +14,12 @@
- +
From da0d919b6b08e57b151cb8731fff19d9dfd340fa Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:55:22 +0800 Subject: [PATCH 24/27] Preserve block assignment selections --- .../AdminBlockTemplateControllerTests.cs | 70 +++++++++++++++++++ .../AdminBlockTemplateController.cs | 15 ++++ .../Views/AdminBlockTemplate/AddToPage.cshtml | 19 +++-- .../AdminBlockTemplate/_PageAssignment.cshtml | 15 ++-- 4 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs diff --git a/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs b/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs new file mode 100644 index 0000000..46f18a6 --- /dev/null +++ b/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs @@ -0,0 +1,70 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using MyWebApp.Controllers; +using MyWebApp.Data; +using MyWebApp.Models; +using MyWebApp.Services; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +public class AdminBlockTemplateControllerTests +{ + private static (AdminBlockTemplateController controller, ApplicationDbContext ctx, SqliteConnection conn) Create() + { + var conn = new SqliteConnection("DataSource=:memory:"); + conn.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(conn) + .Options; + var ctx = new ApplicationDbContext(options); + ctx.Database.EnsureCreated(); + var sanitizer = new HtmlSanitizerService(); + var controller = new AdminBlockTemplateController(ctx, sanitizer); + return (controller, ctx, conn); + } + + [Fact] + public async Task AddToPage_InvalidReturnsViewWithSelections() + { + var tuple = Create(); + using var connection = tuple.conn; + var ctx = tuple.ctx; + var controller = tuple.controller; + ctx.BlockTemplates.Add(new BlockTemplate { Id = 1, Name = "b", Html = "x" }); + ctx.Pages.Add(new Page { Id = 1, Slug = "home", Title = "Home", Layout = "single-column" }); + ctx.Roles.Add(new Role { Id = 1, Name = "Admin" }); + ctx.SaveChanges(); + + var result = await controller.AddToPage(1, new List { 1 }, "", "Admin"); + var view = Assert.IsType(result); + Assert.False(controller.ModelState.IsValid); + var selected = Assert.IsType>(controller.ViewBag.SelectedPageIds); + Assert.Contains(1, selected); + Assert.Equal("", controller.ViewBag.SelectedZone as string); + Assert.Equal("Admin", controller.ViewBag.SelectedRole as string); + } + + [Fact] + public async Task Create_InvalidModelPreservesSelections() + { + var tuple = Create(); + using var connection = tuple.conn; + var ctx = tuple.ctx; + var controller = tuple.controller; + ctx.Pages.Add(new Page { Id = 1, Slug = "home", Title = "Home", Layout = "single-column" }); + ctx.Roles.Add(new Role { Id = 1, Name = "Admin" }); + ctx.SaveChanges(); + + var model = new BlockTemplate(); + controller.ModelState.AddModelError("Name", "required"); + var result = await controller.Create(model, new List { 1 }, "main", "Admin"); + var view = Assert.IsType(result); + Assert.False(controller.ModelState.IsValid); + var selected = Assert.IsType>(controller.ViewBag.SelectedPageIds); + Assert.Contains(1, selected); + Assert.Equal("main", controller.ViewBag.SelectedZone as string); + Assert.Equal("Admin", controller.ViewBag.SelectedRole as string); + } +} diff --git a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs index d799225..34119a8 100644 --- a/website/MyWebApp/Controllers/AdminBlockTemplateController.cs +++ b/website/MyWebApp/Controllers/AdminBlockTemplateController.cs @@ -54,6 +54,9 @@ public async Task Create(BlockTemplate model, List? pageIds, if (!ModelState.IsValid) { await LoadPagesAsync(); + ViewBag.SelectedPageIds = pageIds ?? new List(); + ViewBag.SelectedZone = zone; + ViewBag.SelectedRole = role; return View(model); } model.Html = _sanitizer.Sanitize(model.Html); @@ -80,6 +83,9 @@ public async Task Edit(BlockTemplate model, List? pageIds, s if (!ModelState.IsValid) { await LoadPagesAsync(); + ViewBag.SelectedPageIds = pageIds ?? new List(); + ViewBag.SelectedZone = zone; + ViewBag.SelectedRole = role; return View(model); } model.Html = _sanitizer.Sanitize(model.Html); @@ -175,6 +181,9 @@ public async Task AddToPage(int id) if (item == null) return NotFound(); await LoadPagesAsync(); ViewBag.BlockId = id; + ViewBag.SelectedPageIds = new List(); + ViewBag.SelectedZone = string.Empty; + ViewBag.SelectedRole = string.Empty; return View(); } @@ -188,6 +197,9 @@ public async Task AddToPage(int id, List pageIds, string zon { await LoadPagesAsync(); ViewBag.BlockId = id; + ViewBag.SelectedPageIds = pageIds ?? new List(); + ViewBag.SelectedZone = zone; + ViewBag.SelectedRole = role; ModelState.AddModelError("pageIds", "Page selection required"); return View(); } @@ -198,6 +210,9 @@ public async Task AddToPage(int id, List pageIds, string zon { await LoadPagesAsync(); ViewBag.BlockId = id; + ViewBag.SelectedPageIds = pageIds; + ViewBag.SelectedZone = zone; + ViewBag.SelectedRole = role; ModelState.AddModelError("zone", "Zone required"); return View(); } diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index 67a64ec..55180b3 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -1,4 +1,9 @@ @using MyWebApp.Models +@{ + var selectedPages = ViewBag.SelectedPageIds as List ?? new List(); + var selectedZone = ViewBag.SelectedZone as string ?? string.Empty; + var selectedRole = ViewBag.SelectedRole as string ?? string.Empty; +} @{ ViewData["Title"] = "Add Block To Page"; Layout = "../Admin/_AdminLayout"; @@ -13,10 +18,10 @@
@@ -25,20 +30,20 @@
- +
diff --git a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml index 75acbb0..6e6c408 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml @@ -1,14 +1,19 @@ @using MyWebApp.Models +@{ + var selectedPages = ViewBag.SelectedPageIds as List ?? new List(); + var selectedZone = ViewBag.SelectedZone as string ?? string.Empty; + var selectedRole = ViewBag.SelectedRole as string ?? string.Empty; +}

Page Assignment

@@ -17,17 +22,17 @@
From 05add8775abd2ebac2e7c7a490bb5b7956f0d919 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:19:48 +0800 Subject: [PATCH 25/27] Fix tests to use seeded data --- .../AdminBlockTemplateControllerTests.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs b/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs index 46f18a6..d4632be 100644 --- a/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs +++ b/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs @@ -6,6 +6,7 @@ using MyWebApp.Models; using MyWebApp.Services; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -32,16 +33,16 @@ public async Task AddToPage_InvalidReturnsViewWithSelections() using var connection = tuple.conn; var ctx = tuple.ctx; var controller = tuple.controller; - ctx.BlockTemplates.Add(new BlockTemplate { Id = 1, Name = "b", Html = "x" }); - ctx.Pages.Add(new Page { Id = 1, Slug = "home", Title = "Home", Layout = "single-column" }); - ctx.Roles.Add(new Role { Id = 1, Name = "Admin" }); + var template = new BlockTemplate { Name = "b", Html = "x" }; + ctx.BlockTemplates.Add(template); ctx.SaveChanges(); - var result = await controller.AddToPage(1, new List { 1 }, "", "Admin"); + var homeId = ctx.Pages.Single(p => p.Slug == "home").Id; + var result = await controller.AddToPage(template.Id, new List { homeId }, "", "Admin"); var view = Assert.IsType(result); Assert.False(controller.ModelState.IsValid); var selected = Assert.IsType>(controller.ViewBag.SelectedPageIds); - Assert.Contains(1, selected); + Assert.Contains(homeId, selected); Assert.Equal("", controller.ViewBag.SelectedZone as string); Assert.Equal("Admin", controller.ViewBag.SelectedRole as string); } @@ -53,17 +54,14 @@ public async Task Create_InvalidModelPreservesSelections() using var connection = tuple.conn; var ctx = tuple.ctx; var controller = tuple.controller; - ctx.Pages.Add(new Page { Id = 1, Slug = "home", Title = "Home", Layout = "single-column" }); - ctx.Roles.Add(new Role { Id = 1, Name = "Admin" }); - ctx.SaveChanges(); - + var homeId = ctx.Pages.Single(p => p.Slug == "home").Id; var model = new BlockTemplate(); controller.ModelState.AddModelError("Name", "required"); - var result = await controller.Create(model, new List { 1 }, "main", "Admin"); + var result = await controller.Create(model, new List { homeId }, "main", "Admin"); var view = Assert.IsType(result); Assert.False(controller.ModelState.IsValid); var selected = Assert.IsType>(controller.ViewBag.SelectedPageIds); - Assert.Contains(1, selected); + Assert.Contains(homeId, selected); Assert.Equal("main", controller.ViewBag.SelectedZone as string); Assert.Equal("Admin", controller.ViewBag.SelectedRole as string); } From 30b1f2ab5ccbd2f9d502722ff4c3dfaa908812c9 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:48:15 +0800 Subject: [PATCH 26/27] Fix dropdown selections in block templates --- .../MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml | 10 +++++----- .../Views/AdminBlockTemplate/_PageAssignment.cshtml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml index 55180b3..a7a0231 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/AddToPage.cshtml @@ -18,10 +18,10 @@
@@ -30,7 +30,7 @@ @@ -38,10 +38,10 @@ diff --git a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml index 6e6c408..e2a40f5 100644 --- a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml +++ b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml @@ -10,10 +10,10 @@ @@ -22,17 +22,17 @@
From 023883328527cfe69dbc6f0d2599b86d1e1e8d74 Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Sat, 28 Jun 2025 19:04:18 +0800 Subject: [PATCH 27/27] Fix UI issues --- extension/html/drive-auth.html | 54 ++++---- extension/html/folder-picker.html | 46 +++--- extension/html/privacy-policy.html | 131 +----------------- extension/html/terms-of-service.html | 116 +--------------- extension/styles/privacy-policy.css | 128 +++++++++++++++++ extension/styles/terms-of-service.css | 113 +++++++++++++++ .../Views/Account/ForgotPassword.cshtml | 2 +- website/MyWebApp/Views/Account/Login.cshtml | 4 +- .../MyWebApp/Views/Account/Register.cshtml | 2 +- .../Views/Account/ResetPassword.cshtml | 2 +- website/MyWebApp/wwwroot/css/site.css | 44 +++--- 11 files changed, 323 insertions(+), 319 deletions(-) create mode 100644 extension/styles/privacy-policy.css create mode 100644 extension/styles/terms-of-service.css diff --git a/extension/html/drive-auth.html b/extension/html/drive-auth.html index 77ee7c5..8bf3aa7 100644 --- a/extension/html/drive-auth.html +++ b/extension/html/drive-auth.html @@ -1,8 +1,8 @@ - + - Загрузка в Google Drive + Upload to Google Drive +
diff --git a/extension/html/terms-of-service.html b/extension/html/terms-of-service.html index f04c436..a08fdff 100644 --- a/extension/html/terms-of-service.html +++ b/extension/html/terms-of-service.html @@ -4,121 +4,7 @@ Terms of Service - Screen Area Recorder Pro - +
diff --git a/extension/styles/privacy-policy.css b/extension/styles/privacy-policy.css new file mode 100644 index 0000000..6e6dbd8 --- /dev/null +++ b/extension/styles/privacy-policy.css @@ -0,0 +1,128 @@ + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #333; + max-width: 900px; + margin: 0 auto; + padding: 20px; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + min-height: 100vh; + } + .container { + background: white; + padding: 40px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + border: 1px solid rgba(255,255,255,0.2); + } + h1 { + color: #2c3e50; + border-bottom: 4px solid #3498db; + padding-bottom: 15px; + font-size: 2.2em; + margin-bottom: 10px; + } + h2 { + color: #34495e; + margin-top: 35px; + border-left: 5px solid #3498db; + padding-left: 20px; + font-size: 1.4em; + } + h3 { + color: #2c3e50; + margin-top: 25px; + font-size: 1.2em; + } + .highlight { + background: linear-gradient(135deg, #e8f4fd 0%, #c8e6f5 100%); + padding: 20px; + border-radius: 8px; + border-left: 5px solid #3498db; + margin: 25px 0; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + .warning { + background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); + padding: 20px; + border-radius: 8px; + border-left: 5px solid #f39c12; + margin: 25px 0; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + .success { + background: linear-gradient(135deg, #d4edda 0%, #a8e6cf 100%); + padding: 20px; + border-radius: 8px; + border-left: 5px solid #27ae60; + margin: 25px 0; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + ul, ol { + margin: 15px 0; + padding-left: 30px; + } + li { + margin: 8px 0; + line-height: 1.5; + } + .last-updated { + font-style: italic; + color: #7f8c8d; + text-align: center; + margin-bottom: 30px; + background: #ecf0f1; + padding: 10px; + border-radius: 5px; + } + .contact-info { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + padding: 25px; + border-radius: 8px; + margin: 30px 0; + border: 1px solid #dee2e6; + } + .emoji { + font-size: 1.2em; + margin-right: 8px; + } + .table-container { + overflow-x: auto; + margin: 20px 0; + } + table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + th, td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #ddd; + } + th { + background: #3498db; + color: white; + font-weight: 600; + } + .privacy-feature { + display: flex; + align-items: center; + margin: 10px 0; + padding: 10px; + background: #f8f9fa; + border-radius: 5px; + } + .check { + color: #27ae60; + font-size: 1.3em; + margin-right: 10px; + } + .cross { + color: #e74c3c; + font-size: 1.3em; + margin-right: 10px; + } diff --git a/extension/styles/terms-of-service.css b/extension/styles/terms-of-service.css new file mode 100644 index 0000000..6df02db --- /dev/null +++ b/extension/styles/terms-of-service.css @@ -0,0 +1,113 @@ + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #333; + max-width: 900px; + margin: 0 auto; + padding: 20px; + background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%); + min-height: 100vh; + } + .container { + background: white; + padding: 40px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + border: 1px solid rgba(255,255,255,0.2); + } + h1 { + color: #2c3e50; + border-bottom: 4px solid #e74c3c; + padding-bottom: 15px; + font-size: 2.2em; + margin-bottom: 10px; + } + h2 { + color: #34495e; + margin-top: 35px; + border-left: 5px solid #e74c3c; + padding-left: 20px; + font-size: 1.4em; + } + h3 { + color: #2c3e50; + margin-top: 25px; + font-size: 1.2em; + } + .highlight { + background: linear-gradient(135deg, #fff5f5 0%, #ffebee 100%); + padding: 20px; + border-radius: 8px; + border-left: 5px solid #e74c3c; + margin: 25px 0; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + .warning { + background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); + padding: 20px; + border-radius: 8px; + border-left: 5px solid #f39c12; + margin: 25px 0; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + .success { + background: linear-gradient(135deg, #d4edda 0%, #a8e6cf 100%); + padding: 20px; + border-radius: 8px; + border-left: 5px solid #27ae60; + margin: 25px 0; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + } + .legal-box { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + padding: 25px; + border-radius: 8px; + border: 2px solid #6c757d; + margin: 25px 0; + font-weight: 500; + } + ul, ol { + margin: 15px 0; + padding-left: 30px; + } + li { + margin: 8px 0; + line-height: 1.5; + } + .last-updated { + font-style: italic; + color: #7f8c8d; + text-align: center; + margin-bottom: 30px; + background: #ecf0f1; + padding: 10px; + border-radius: 5px; + } + .caps { + text-transform: uppercase; + font-weight: bold; + font-size: 1.1em; + } + .emoji { + font-size: 1.2em; + margin-right: 8px; + } + .prohibited-list { + background: #ffebee; + padding: 15px; + border-radius: 5px; + border-left: 4px solid #f44336; + } + .allowed-list { + background: #e8f5e8; + padding: 15px; + border-radius: 5px; + border-left: 4px solid #4caf50; + } + .contact-info { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + padding: 25px; + border-radius: 8px; + margin: 30px 0; + border: 1px solid #dee2e6; + } diff --git a/website/MyWebApp/Views/Account/ForgotPassword.cshtml b/website/MyWebApp/Views/Account/ForgotPassword.cshtml index d8ef692..a708af6 100644 --- a/website/MyWebApp/Views/Account/ForgotPassword.cshtml +++ b/website/MyWebApp/Views/Account/ForgotPassword.cshtml @@ -11,7 +11,7 @@
-
+
captcha
diff --git a/website/MyWebApp/Views/Account/Login.cshtml b/website/MyWebApp/Views/Account/Login.cshtml index 06bf5ac..97dec13 100644 --- a/website/MyWebApp/Views/Account/Login.cshtml +++ b/website/MyWebApp/Views/Account/Login.cshtml @@ -21,7 +21,7 @@
-
+
captcha
@@ -31,7 +31,7 @@
-
+ -
+
captcha
diff --git a/website/MyWebApp/Views/Account/ResetPassword.cshtml b/website/MyWebApp/Views/Account/ResetPassword.cshtml index 91af517..c02a867 100644 --- a/website/MyWebApp/Views/Account/ResetPassword.cshtml +++ b/website/MyWebApp/Views/Account/ResetPassword.cshtml @@ -8,7 +8,7 @@
-
+
captcha
diff --git a/website/MyWebApp/wwwroot/css/site.css b/website/MyWebApp/wwwroot/css/site.css index ec8965e..d12d09d 100644 --- a/website/MyWebApp/wwwroot/css/site.css +++ b/website/MyWebApp/wwwroot/css/site.css @@ -5,7 +5,7 @@ /* Form container */ form[method="post"] { max-width: 400px !important; - width: 400px !important; + width: 100% !important; margin: 1rem auto 0 auto !important; /* Remove bottom margin */ padding: 2rem !important; background: #ffffff !important; @@ -19,7 +19,7 @@ form[method="post"] { /* Wide forms */ form[action*="Register"] { max-width: 500px !important; - width: 500px !important; + width: 100% !important; } /* Input fields remain as is */ @@ -101,21 +101,21 @@ form[method="post"] input[type="checkbox"] { accent-color: #0d6efd !important; } -form[method="post"] > div:has(input[type="checkbox"]) { +.form-check { display: flex !important; align-items: center !important; margin-bottom: 1.5rem !important; cursor: pointer !important; } - form[method="post"] > div:has(input[type="checkbox"]) label { + .form-check label { margin: 0 !important; cursor: pointer !important; font-size: 0.9rem !important; } /* Captcha remains as is */ -form[method="post"] > div:has(img[src*="captcha"]) { +.captcha-container { display: flex !important; align-items: center !important; gap: 1rem !important; @@ -140,7 +140,7 @@ form[method="post"] img[src*="captcha" i] { border-color: #0d6efd !important; } -form[method="post"] > div:has(img[src*="captcha"]) input[type="text"] { +.captcha-container input[type="text"] { flex: 1 !important; margin-bottom: 0 !important; min-width: 100px !important; @@ -151,8 +151,8 @@ form[method="post"] > div:has(img[src*="captcha"]) input[type="text"] { ========================================== */ /* Links block - FULLY MERGED with form */ -form[method="post"] + div:has(a) { - width: 400px !important; +form[method="post"] + .form-links { + width: 100% !important; max-width: 400px !important; margin: 0 auto 2rem auto !important; /* WITHOUT negative margin */ text-align: center !important; @@ -168,8 +168,8 @@ form[method="post"] + div:has(a) { } /* For wide forms */ -form[action*="Register"] + div:has(a) { - width: 500px !important; +form[action*="Register"] + .form-links { + width: 100% !important; max-width: 500px !important; } @@ -204,7 +204,7 @@ form[method="post"] + div { padding: 1.5rem !important; } - form[method="post"] + div:has(a) { + form[method="post"] + .form-links { width: calc(100vw - 2rem) !important; max-width: none !important; margin: 0 1rem 2rem 1rem !important; @@ -217,13 +217,13 @@ form[method="post"] + div { font-size: 16px !important; } - form[method="post"] > div:has(img[src*="captcha"]) { + .captcha-container { flex-direction: column !important; align-items: center !important; gap: 0.75rem !important; } - form[method="post"] > div:has(img[src*="captcha"]) input[type="text"] { + .captcha-container input[type="text"] { width: 100% !important; } } @@ -232,13 +232,8 @@ form[method="post"] + div { ADDITIONAL FIXES ========================================== */ -/* Remove any margins between form and links block */ -form[method="post"]:has(+ div:has(a)) { - margin-bottom: 0 !important; -} - /* Ensure smooth transition */ -form[method="post"] + div:has(a):before { +form[method="post"] + .form-links:before { content: ""; position: absolute; top: -1px; @@ -248,3 +243,14 @@ form[method="post"] + div:has(a):before { background: #ffffff; z-index: 1; } + +/* Error styling shared across themes */ +.error { + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + padding: var(--space-md, 1rem); + border-radius: var(--radius-md, 0.375rem); + margin-bottom: var(--space-md, 1rem); + font-weight: 500; +}
Name
Name
@t.Name Edit DeleteAdd to Page