From 788da5cb62543c8d403938790a258e82c3fd6412 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sun, 8 Dec 2024 18:42:35 +0100 Subject: [PATCH 1/4] Add moderator pages with limited CRUD operations Add a new "Moderator" directory under "Pages" with limited CRUD operations for moderators. * **Layout and Navigation:** - Add `_Layout.cshtml` for moderator pages. - Add `_ManageNav.cshtml` for moderator navigation with links to Reviews and Users index pages. * **Reviews Management:** - Add `Index.cshtml` and `Index.cshtml.cs` for listing reviews with limited actions. - Add `Edit.cshtml` and `Edit.cshtml.cs` for approving or denying reviews. * **Users Management:** - Add `Index.cshtml` and `Index.cshtml.cs` for listing users with limited actions. - Add `Edit.cshtml` and `Edit.cshtml.cs` for editing user information. --- .../Pages/Moderator/Reviews/Edit.cshtml | 65 ++++++++++ .../Pages/Moderator/Reviews/Edit.cshtml.cs | 118 ++++++++++++++++++ .../Pages/Moderator/Reviews/Index.cshtml | 43 +++++++ .../Pages/Moderator/Reviews/Index.cshtml.cs | 26 ++++ GameLibrary/Pages/Moderator/Users/Edit.cshtml | 58 +++++++++ .../Pages/Moderator/Users/Edit.cshtml.cs | 70 +++++++++++ .../Pages/Moderator/Users/Index.cshtml | 37 ++++++ .../Pages/Moderator/Users/Index.cshtml.cs | 26 ++++ GameLibrary/Pages/Moderator/_Layout.cshtml | 25 ++++ GameLibrary/Pages/Moderator/_ManageNav.cshtml | 5 + 10 files changed, 473 insertions(+) create mode 100644 GameLibrary/Pages/Moderator/Reviews/Edit.cshtml create mode 100644 GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs create mode 100644 GameLibrary/Pages/Moderator/Reviews/Index.cshtml create mode 100644 GameLibrary/Pages/Moderator/Reviews/Index.cshtml.cs create mode 100644 GameLibrary/Pages/Moderator/Users/Edit.cshtml create mode 100644 GameLibrary/Pages/Moderator/Users/Edit.cshtml.cs create mode 100644 GameLibrary/Pages/Moderator/Users/Index.cshtml create mode 100644 GameLibrary/Pages/Moderator/Users/Index.cshtml.cs create mode 100644 GameLibrary/Pages/Moderator/_Layout.cshtml create mode 100644 GameLibrary/Pages/Moderator/_ManageNav.cshtml diff --git a/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml new file mode 100644 index 0000000..eeb07dd --- /dev/null +++ b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml @@ -0,0 +1,65 @@ +@page +@model GameLibrary.Pages.Moderator.Reviews.EditModel + +@{ + ViewData["Title"] = "Edit Review"; +} + + + +
+

Edit Review

+
+ +
+
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + Back to List +
+
+
+
+ + diff --git a/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs new file mode 100644 index 0000000..bb3672f --- /dev/null +++ b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs @@ -0,0 +1,118 @@ +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Moderator.Reviews; + +public class EditModel : PageModel +{ + private readonly ApplicationDbContext _context; + + public EditModel(ApplicationDbContext context) + { + _context = context; + } + + public SelectList Games { get; set; } = null!; + public SelectList Users { get; set; } = null!; + + [BindProperty] + public Guid SelectedId { get; set; } + + [BindProperty] + public Guid SelectedGameId { get; set; } + + [BindProperty] + public Guid SelectedUserId { get; set; } + + [BindProperty] + public int SelectedRating { get; set; } + + [BindProperty] + public string? SelectedContent { get; set; } + + public async Task OnGetAsync(Guid? id) + { + if (id == null) + { + return NotFound(); + } + + var review = await _context.Reviews.AsNoTracking().FirstOrDefaultAsync(m => m.Id == id); + if (review == null) + { + return NotFound(); + } + + // Populate the bindable properties + SelectedId = review.Id; + SelectedGameId = review.GameId; + SelectedUserId = review.UserId; + SelectedRating = review.Rating; + SelectedContent = review.Content; + + Games = new SelectList(await _context.Games.ToListAsync(), "Id", "Title"); + Users = new SelectList(await _context.Users.ToListAsync(), "Id", "UserName"); + + return Page(); + } + + public async Task OnPostAsync(string submitButton) + { + if (!ModelState.IsValid) + { + Games = new SelectList(await _context.Games.ToListAsync(), "Id", "Title"); + Users = new SelectList(await _context.Users.ToListAsync(), "Id", "UserName"); + return Page(); + } + + var reviewToUpdate = await _context.Reviews.FindAsync(SelectedId); + if (reviewToUpdate == null) + { + return NotFound(); + } + + if (submitButton == "Approve") + { + reviewToUpdate.Status = "Approved"; + } + else if (submitButton == "Deny") + { + reviewToUpdate.Status = "Denied"; + } + + reviewToUpdate.GameId = SelectedGameId; + reviewToUpdate.UserId = SelectedUserId; + reviewToUpdate.Rating = SelectedRating; + reviewToUpdate.Content = SelectedContent ?? string.Empty; + reviewToUpdate.UpdatedAt = DateTime.UtcNow; + + _context.Attach(reviewToUpdate).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ReviewExists(SelectedId)) + { + return NotFound(); + } + else + { + throw; + } + } + + return RedirectToPage("./Index"); + } + + private bool ReviewExists(Guid id) + { + return _context.Reviews.Any(e => e.Id == id); + } +} diff --git a/GameLibrary/Pages/Moderator/Reviews/Index.cshtml b/GameLibrary/Pages/Moderator/Reviews/Index.cshtml new file mode 100644 index 0000000..a7b45c0 --- /dev/null +++ b/GameLibrary/Pages/Moderator/Reviews/Index.cshtml @@ -0,0 +1,43 @@ +@page +@model GameLibrary.Pages.Moderator.Reviews.IndexModel + +@{ + ViewData["Title"] = "Moderator Reviews"; + ViewData["ActivePage"] = ManageNavPages.Reviews; +} + + + +
+

Reviews

+
+ +
+ + + + + + + + + + + + @foreach (var item in Model.Reviews!) + { + + + + + + + + } + +
GameUserRatingContentActions
@Html.DisplayFor(modelItem => item.Game.Title)@Html.DisplayFor(modelItem => item.User.UserName)@Html.DisplayFor(modelItem => item.Rating)@Html.DisplayFor(modelItem => item.Content) + Approve/Deny +
+
+ + diff --git a/GameLibrary/Pages/Moderator/Reviews/Index.cshtml.cs b/GameLibrary/Pages/Moderator/Reviews/Index.cshtml.cs new file mode 100644 index 0000000..565188e --- /dev/null +++ b/GameLibrary/Pages/Moderator/Reviews/Index.cshtml.cs @@ -0,0 +1,26 @@ +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Moderator.Reviews; + +public class IndexModel : PageModel +{ + private readonly ApplicationDbContext _context; + + public IndexModel(ApplicationDbContext context) + { + _context = context; + } + + public IList? Reviews { get; set; } + + public async Task OnGetAsync() + { + Reviews = await _context.Reviews + .Include(r => r.Game) + .Include(r => r.User) + .ToListAsync(); + } +} diff --git a/GameLibrary/Pages/Moderator/Users/Edit.cshtml b/GameLibrary/Pages/Moderator/Users/Edit.cshtml new file mode 100644 index 0000000..58881c8 --- /dev/null +++ b/GameLibrary/Pages/Moderator/Users/Edit.cshtml @@ -0,0 +1,58 @@ +@page +@model GameLibrary.Pages.Moderator.Users.EditModel + +@{ + ViewData["Title"] = "Edit User"; +} + +
+

Edit User

+
+ +
+
+
+ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + Back to List +
+
+
diff --git a/GameLibrary/Pages/Moderator/Users/Edit.cshtml.cs b/GameLibrary/Pages/Moderator/Users/Edit.cshtml.cs new file mode 100644 index 0000000..2c8c9a9 --- /dev/null +++ b/GameLibrary/Pages/Moderator/Users/Edit.cshtml.cs @@ -0,0 +1,70 @@ +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Moderator.Users; + +public class EditModel : PageModel +{ + private readonly ApplicationDbContext _context; + + public EditModel(ApplicationDbContext context) + { + _context = context; + } + + [BindProperty] + public User User { get; set; } = null!; + + public async Task OnGetAsync(Guid? id) + { + if (id == null) + { + return NotFound(); + } + + User = await _context.Users.FirstOrDefaultAsync(m => m.Id == id); + + if (User == null) + { + return NotFound(); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + _context.Attach(User).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!UserExists(User.Id)) + { + return NotFound(); + } + else + { + throw; + } + } + + return RedirectToPage("./Index"); + } + + private bool UserExists(Guid id) + { + return _context.Users.Any(e => e.Id == id); + } +} diff --git a/GameLibrary/Pages/Moderator/Users/Index.cshtml b/GameLibrary/Pages/Moderator/Users/Index.cshtml new file mode 100644 index 0000000..ebde7c6 --- /dev/null +++ b/GameLibrary/Pages/Moderator/Users/Index.cshtml @@ -0,0 +1,37 @@ +@page +@model GameLibrary.Pages.Moderator.Users.IndexModel + +@{ + ViewData["Title"] = "Users"; + ViewData["ActivePage"] = ManageNavPages.Users; +} + +
+

Users

+
+ +
+ + + + + + + + + + + @foreach (var item in Model.IdentityUsers!) + { + + + + + + + } + +
User NameEmailRoleActions
@item.UserName@item.Email@(Model.UserRoles.ContainsKey(item.Id) ? Model.UserRoles[item.Id] : "No Role") + Edit +
+
diff --git a/GameLibrary/Pages/Moderator/Users/Index.cshtml.cs b/GameLibrary/Pages/Moderator/Users/Index.cshtml.cs new file mode 100644 index 0000000..65dbfa3 --- /dev/null +++ b/GameLibrary/Pages/Moderator/Users/Index.cshtml.cs @@ -0,0 +1,26 @@ +using GameLibrary.Data; +using GameLibrary.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Moderator.Users; + +public class IndexModel(ApplicationDbContext context, UserManager userManager) : PageModel +{ + public IList IdentityUsers { get; set; } = []; + public Dictionary UserRoles { get; set; } = []; + + public async Task OnGetAsync() + { + // Fetch all users + IdentityUsers = await context.Users.ToListAsync(); + + // Fetch roles for each user + foreach (var user in IdentityUsers) + { + var roles = await userManager.GetRolesAsync(user); + UserRoles[user.Id] = roles.FirstOrDefault() ?? "No Role"; + } + } +} diff --git a/GameLibrary/Pages/Moderator/_Layout.cshtml b/GameLibrary/Pages/Moderator/_Layout.cshtml new file mode 100644 index 0000000..4d7ed2c --- /dev/null +++ b/GameLibrary/Pages/Moderator/_Layout.cshtml @@ -0,0 +1,25 @@ +@{ + if (ViewData.TryGetValue("ParentLayout", out var parentLayout) && parentLayout != null) + { + Layout = parentLayout.ToString(); + } + else + { + Layout = "/Pages/Shared/_Layout.cshtml"; + } +} + +
+
+
+ +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} diff --git a/GameLibrary/Pages/Moderator/_ManageNav.cshtml b/GameLibrary/Pages/Moderator/_ManageNav.cshtml new file mode 100644 index 0000000..46e3a37 --- /dev/null +++ b/GameLibrary/Pages/Moderator/_ManageNav.cshtml @@ -0,0 +1,5 @@ + From f9b10795e8f725c71e3052091964bc44a58958f8 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sun, 8 Dec 2024 18:46:56 +0100 Subject: [PATCH 2/4] Add a navigation pages file for moderator * Include navigation logic for Reviews and Users * Define methods to get navigation class for Index, Reviews, and Users pages * Implement logic to determine active page based on ViewContext --- GameLibrary/Pages/Moderator/ManageNavPages.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 GameLibrary/Pages/Moderator/ManageNavPages.cs diff --git a/GameLibrary/Pages/Moderator/ManageNavPages.cs b/GameLibrary/Pages/Moderator/ManageNavPages.cs new file mode 100644 index 0000000..b326199 --- /dev/null +++ b/GameLibrary/Pages/Moderator/ManageNavPages.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace GameLibrary.Pages.Moderator; + +public static class ManageNavPages +{ + public static string Index => "Index"; + public static string Reviews => "Reviews/Index"; + public static string Users => "Users/Index"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + public static string ReviewsNavClass(ViewContext viewContext) => PageNavClass(viewContext, Reviews); + public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users); + + public static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } +} From 96b8d899ae8b7b836da92ecc7453045d0dc2ecd0 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sun, 8 Dec 2024 18:52:19 +0100 Subject: [PATCH 3/4] Add moderator index page and code-behind file * **Index.cshtml** - Add a page to display the moderator dashboard - Display total reviews and total users - Include a chart for review ratings * **Index.cshtml.cs** - Add a code-behind file to fetch and display summary information for moderators - Fetch total reviews and total users - Fetch review ratings and group them by rating --- GameLibrary/Pages/Moderator/Index.cshtml | 55 +++++++++++++++++++++ GameLibrary/Pages/Moderator/Index.cshtml.cs | 30 +++++++++++ 2 files changed, 85 insertions(+) create mode 100644 GameLibrary/Pages/Moderator/Index.cshtml create mode 100644 GameLibrary/Pages/Moderator/Index.cshtml.cs diff --git a/GameLibrary/Pages/Moderator/Index.cshtml b/GameLibrary/Pages/Moderator/Index.cshtml new file mode 100644 index 0000000..296a888 --- /dev/null +++ b/GameLibrary/Pages/Moderator/Index.cshtml @@ -0,0 +1,55 @@ +@page +@model GameLibrary.Pages.Moderator.IndexModel + +@{ + ViewData["Title"] = "Moderator Index"; + ViewData["ActivePage"] = ManageNavPages.Index; +} + +

Moderator Dashboard

+ +
+
+
+
Total Reviews
+
+
@Model.TotalReviews
+
+
+
+
+
+
Total Users
+
+
@Model.TotalUsers
+
+
+
+
+ +
+
+

Review Ratings

+ +
+
+ +@section Scripts { + + +} diff --git a/GameLibrary/Pages/Moderator/Index.cshtml.cs b/GameLibrary/Pages/Moderator/Index.cshtml.cs new file mode 100644 index 0000000..60ea3aa --- /dev/null +++ b/GameLibrary/Pages/Moderator/Index.cshtml.cs @@ -0,0 +1,30 @@ +using GameLibrary.Data; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace GameLibrary.Pages.Moderator; + +public class IndexModel : PageModel +{ + private readonly ApplicationDbContext _context; + + public IndexModel(ApplicationDbContext context) + { + _context = context; + } + + public int TotalReviews { get; set; } + public int TotalUsers { get; set; } + + public Dictionary ReviewRatings { get; set; } = new(); + + public async Task OnGetAsync() + { + TotalReviews = await _context.Reviews.CountAsync(); + TotalUsers = await _context.Users.CountAsync(); + + ReviewRatings = await _context.Reviews + .GroupBy(r => r.Rating) + .ToDictionaryAsync(r => r.Key, r => r.Count()); + } +} From 128569b09f496a8b0f15ae44bff82554f0b2d68f Mon Sep 17 00:00:00 2001 From: Mathias Date: Sun, 8 Dec 2024 19:33:28 +0100 Subject: [PATCH 4/4] Add Moderator Role Added: - Seeded Moderator role - Restriction of moderator pages - Moved Role tools into its own tab/section/page --- GameLibrary/Data/DbInitializer.cs | 31 +++++++++-- GameLibrary/Database.db-shm | Bin 32768 -> 32768 bytes GameLibrary/Database.db-wal | Bin 354352 -> 329632 bytes GameLibrary/Pages/Account/Manage/Index.cshtml | 5 -- .../Pages/Account/Manage/Index.cshtml.cs | 3 -- .../Pages/Account/Manage/ManageNavPages.cs | 10 ++++ .../Pages/Account/Manage/RoleTools.cshtml | 39 ++++++++++++++ .../Pages/Account/Manage/RoleTools.cshtml.cs | 33 ++++++++++++ .../Pages/Account/Manage/_ManageNav.cshtml | 1 + .../Pages/Moderator/Reviews/Edit.cshtml | 13 +++-- .../Pages/Moderator/Reviews/Edit.cshtml.cs | 48 ++++++++++-------- GameLibrary/Program.cs | 3 ++ 12 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 GameLibrary/Pages/Account/Manage/RoleTools.cshtml create mode 100644 GameLibrary/Pages/Account/Manage/RoleTools.cshtml.cs diff --git a/GameLibrary/Data/DbInitializer.cs b/GameLibrary/Data/DbInitializer.cs index 2f6d0a1..2dd6ad6 100644 --- a/GameLibrary/Data/DbInitializer.cs +++ b/GameLibrary/Data/DbInitializer.cs @@ -30,9 +30,16 @@ public static void Initialize(this IApplicationBuilder app) var roleManager = services.GetRequiredService>(); // Apply any pending migrations - if (context.Database.GetPendingMigrations().Any()) + try { - context.Database.Migrate(); + if (context.Database.GetPendingMigrations().Any()) + { + context.Database.Migrate(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Migration error: {ex.Message}"); } // Check if we already have games @@ -42,7 +49,7 @@ public static void Initialize(this IApplicationBuilder app) } // Ensure roles are created - var roles = new[] { "Administrator", "User" }; + var roles = new[] { "Administrator", "User", "Moderator" }; foreach (var role in roles) { if (!roleManager.RoleExistsAsync(role).Result) @@ -68,6 +75,24 @@ public static void Initialize(this IApplicationBuilder app) } } + var moderatorUser = new User + { + Id = Guid.Parse("71a23c2d-f82e-4b47-a843-cfcadbd65a77"), + UserName = "moderator@example.com", + Email = "moderator@example.com", + EmailConfirmed = true + }; + + if (userManager.FindByNameAsync(moderatorUser.UserName).Result == null) + { + var result = userManager.CreateAsync(moderatorUser, "Password123!").Result; + if (result.Succeeded) + { + userManager.AddToRoleAsync(moderatorUser, "Moderator").Wait(); + } + } + + context.SaveChanges(); // Add test games diff --git a/GameLibrary/Database.db-shm b/GameLibrary/Database.db-shm index e7825ece08ed0b67bb21a7a8f86cfce8b97766e8..4511a9573c86c2aac0b0b9fb7b3421371269d74e 100644 GIT binary patch delta 460 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 QY+U|M2dlOVw0-8k010RkPyhe` delta 6381 zcmeHM3s6+o8QvGT2ybC+dvS8H=^XFBancV?NL zJLfz9f4=`a-}%p_W8F1feK8!{6u#0ze15&f;<4zTsI2kX4O@;C#M-2|q+X53kF3V$ z#Ybkna^lpDv71(axZTAk-JW5<_|bgJYbS03-31<}CwlnlI}Z`r@!LLYAK6{#v3X)b zrkap5uco&b)poA{PET;2Gf$VtEn0w1bU&G8c>3BJs`SX#);GJm3Jg!xXLd9t9^c=H zW7<72Va7a-r6V>SdGe zD;XS{KLl_Y0jC;IymxE)*3sP^-xw$z{Z@{BZC6A=fKs;+!A{;)c>G9To7NX?H9+#u zJa_UKTI&r!qC+4nulsKK-iwENC?n#TJNEBlz97a05MzvpE!)4GoO!YKxCU;;kYM4! z1Jl=aiK~0G>@wryHSZ^9-pl9vw7cq!0Y2z?{ai-OU|b-;b;bav^Bz6c89lB{Tffl= zdABDfqp#*bd;l`m2r1mAdHY{<9M^ug#|TL*`u))hsa@X(AmfdYXAkT=dH&n2i?uWF z8zH&!#eLUL-*zqli8Vr^ZX9=Ven<8*TKu<0h~>T2M@O|zj}AcMjF7+PH+wxRyB25# zV{C?sd~*IV;lTd0c>%}`Mo77u)*1iD&kt)Wpb@fV%EL!j-jvfBfFu|pUw@@mSJS7D zXQdy)w}jkEfhF|#d=tyvkdGod!?En{X$UHwaTWo~TH z>JHh~x{HCzs(?ov!I3;mAW>!rffq#rlAMfWLFRdq^ewKa_svjlm961ES8}q&L^l4_ zj2k9tvoNQ#So`G@BXNE<_b!Wjmlm(sx4Ek)+I?0t8Q7BTz6cXxFC$^hz_qUEy4Fao z#GTwCT5~Xs()9mA|O6q^3AGMWj<2bIa4_Ev708q{W4$xzn;M z=a%G_(@QfmrJN$=76q0R7qP|DMQJ7@W-C*RXi+XKoF-=HNqi}o3#igEg6GQ+RtX1) zGQJ!U<$PsDJXyw-(^Po{UOh!q#?dVV?f@Qc@N@vGuI|+`2jcHKgD=uw-Q!NwA2nAW&%qNpK8RN$?f0{Dz7$ zy1W#Y&c&pJc>XtcCWw1H>-${VhF1F?cUwxU-D`Fe`Oo}ely=jnsf>(5O@VD1j#Gd$7Gr#c!aQ-0hyI#Q4u7TE2v&pu`qsWX+zDDg%wNELBPmAZDTa_ak|-1fWTVl^KZ_1ybAbwxiXGR^1ln@)fttCNR`XLnjLaMUn)G5CoPc@ah@FYq0O=1(vQCHqKoSWEm61pSF_KDfEK&$wKng&zDnnJfxXxjDiLwwdi5fVIwYf3{(kOfAD zEaFD$1I=hH!aq6@-ld;g(nrqp_k@i%BUvu}D~z+3s@uk^+{0z1N}oTy)qY`CIHq+O*;Xb(P!H{buw z;D5{vnyrag4_^H{e40!>etid;QafH3iY=?r(7;AVy%k1|)LpLd28>K$Gn{5jL#tiuD6!s%vxvdpppJXvwfnf`DNa2-yK9QY z{qs=?t|>S{(gm7J?GZkoubHq)78UZ?Nbw0nAqJl{ib&!POvOhJLJ|@<1QDyWfmVBB zf{6{<_SK&pvlQ&5V7Qy4QxTwOnx~qV+59nY$1z0}aHN2P&@>70ajIa`WJvr+1*)n* zR^@2Xe9SWgj#?a(GY+=ANVOhqSuGjz`>0CfN3yoEeJz9%p7H|&Frhq@n~s0-TauC}+U0rN zhp_k;FUceURlp#bAz4a<{u0kN(SfDXXz7F4(@G zCs9`v?1p!fKkQstvieFiI{))DhH%hIyk{XOv#)l( z{Z7oxAr9&-?s_bxD)-IJk=8K0Zv|{jv?9{@3We`X_;ShNdmkyYgoG4Gv$O(OFeIdz zW=eF4kmz&-QW<e$oYTm~)gkqtz5)S(^iL+>dP&8+b6sZf?R diff --git a/GameLibrary/Pages/Account/Manage/Index.cshtml b/GameLibrary/Pages/Account/Manage/Index.cshtml index dc78fb2..d774475 100644 --- a/GameLibrary/Pages/Account/Manage/Index.cshtml +++ b/GameLibrary/Pages/Account/Manage/Index.cshtml @@ -22,10 +22,5 @@ - - @if (Model.IsAdminRole) - { - Admin Dashboard - } diff --git a/GameLibrary/Pages/Account/Manage/Index.cshtml.cs b/GameLibrary/Pages/Account/Manage/Index.cshtml.cs index 28b8bf2..693c235 100644 --- a/GameLibrary/Pages/Account/Manage/Index.cshtml.cs +++ b/GameLibrary/Pages/Account/Manage/Index.cshtml.cs @@ -35,7 +35,6 @@ public IndexModel( _signInManager = signInManager; } - public bool IsAdminRole { get; set; } /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -92,8 +91,6 @@ public async Task OnGetAsync() return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - IsAdminRole = _userManager.IsInRoleAsync(user, "Administrator").Result; - await LoadAsync(user); return Page(); } diff --git a/GameLibrary/Pages/Account/Manage/ManageNavPages.cs b/GameLibrary/Pages/Account/Manage/ManageNavPages.cs index 4ef9be9..6d36434 100644 --- a/GameLibrary/Pages/Account/Manage/ManageNavPages.cs +++ b/GameLibrary/Pages/Account/Manage/ManageNavPages.cs @@ -72,6 +72,11 @@ public static class ManageNavPages /// public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// + public static string RoleTools => "RoleTools"; + /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -120,6 +125,11 @@ public static class ManageNavPages /// public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// + public static string RoleToolsNavClass(ViewContext viewContext) => PageNavClass(viewContext, RoleTools); + /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/GameLibrary/Pages/Account/Manage/RoleTools.cshtml b/GameLibrary/Pages/Account/Manage/RoleTools.cshtml new file mode 100644 index 0000000..8990153 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/RoleTools.cshtml @@ -0,0 +1,39 @@ +@page +@model RoleToolsModel +@{ + ViewData["Title"] = "Role-Based Tools"; + ViewData["ActivePage"] = ManageNavPages.RoleTools; +} + +
+
+
+
+
+

Role-Based Tools

+
+
+

Access tools and dashboards based on your roles.

+
+ @if (Model.IsAdminRole) + { + + Admin Dashboard + + } + @if (Model.IsModeratorRole) + { + + Moderator Dashboard + + } + @if (!Model.IsAdminRole && !Model.IsModeratorRole) + { +

You do not have any role-based tools available.

+ } +
+
+
+
+
+
diff --git a/GameLibrary/Pages/Account/Manage/RoleTools.cshtml.cs b/GameLibrary/Pages/Account/Manage/RoleTools.cshtml.cs new file mode 100644 index 0000000..ac6f070 --- /dev/null +++ b/GameLibrary/Pages/Account/Manage/RoleTools.cshtml.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using GameLibrary.Models; + +namespace GameLibrary.Pages.Account.Manage; + +public class RoleToolsModel : PageModel +{ + private readonly UserManager _userManager; + + public RoleToolsModel(UserManager userManager) + { + _userManager = userManager; + } + + public bool IsAdminRole { get; set; } + public bool IsModeratorRole { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + IsAdminRole = await _userManager.IsInRoleAsync(user, "Administrator"); + IsModeratorRole = await _userManager.IsInRoleAsync(user, "Moderator"); + + return Page(); + } +} diff --git a/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml b/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml index 6d8f6ec..ea4670c 100644 --- a/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml +++ b/GameLibrary/Pages/Account/Manage/_ManageNav.cshtml @@ -12,4 +12,5 @@ } + diff --git a/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml index eeb07dd..dc73518 100644 --- a/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml +++ b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml @@ -13,6 +13,13 @@
+ @if (ViewData["Status"] != null) + { +
+ Current Status: @ViewData["Status"] +
+ } +
@@ -54,9 +61,9 @@
diff --git a/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs index bb3672f..d485202 100644 --- a/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs +++ b/GameLibrary/Pages/Moderator/Reviews/Edit.cshtml.cs @@ -11,6 +11,8 @@ public class EditModel : PageModel { private readonly ApplicationDbContext _context; + public static Dictionary ReviewStatuses { get; set; } = []; + public EditModel(ApplicationDbContext context) { _context = context; @@ -47,7 +49,6 @@ public async Task OnGetAsync(Guid? id) return NotFound(); } - // Populate the bindable properties SelectedId = review.Id; SelectedGameId = review.GameId; SelectedUserId = review.UserId; @@ -57,6 +58,16 @@ public async Task OnGetAsync(Guid? id) Games = new SelectList(await _context.Games.ToListAsync(), "Id", "Title"); Users = new SelectList(await _context.Users.ToListAsync(), "Id", "UserName"); + // Retrieve status from the dictionary, if exists + if (ReviewStatuses.TryGetValue(SelectedId, out var status)) + { + ViewData["Status"] = status; + } + else + { + ViewData["Status"] = "Pending"; // Default status + } + return Page(); } @@ -75,37 +86,34 @@ public async Task OnPostAsync(string submitButton) return NotFound(); } + // Update the status in the dictionary if (submitButton == "Approve") { - reviewToUpdate.Status = "Approved"; + ReviewStatuses[SelectedId] = "Approved"; + + // Update review details + reviewToUpdate.GameId = SelectedGameId; + reviewToUpdate.UserId = SelectedUserId; + reviewToUpdate.Rating = SelectedRating; + reviewToUpdate.Content = SelectedContent ?? string.Empty; + reviewToUpdate.UpdatedAt = DateTime.UtcNow; + + _context.Attach(reviewToUpdate).State = EntityState.Modified; } else if (submitButton == "Deny") { - reviewToUpdate.Status = "Denied"; - } + ReviewStatuses[SelectedId] = "Denied"; - reviewToUpdate.GameId = SelectedGameId; - reviewToUpdate.UserId = SelectedUserId; - reviewToUpdate.Rating = SelectedRating; - reviewToUpdate.Content = SelectedContent ?? string.Empty; - reviewToUpdate.UpdatedAt = DateTime.UtcNow; - - _context.Attach(reviewToUpdate).State = EntityState.Modified; + _context.Reviews.Remove(reviewToUpdate); + } try { await _context.SaveChangesAsync(); } - catch (DbUpdateConcurrencyException) + catch (DbUpdateConcurrencyException) when (!ReviewExists(SelectedId)) { - if (!ReviewExists(SelectedId)) - { - return NotFound(); - } - else - { - throw; - } + return NotFound(); } return RedirectToPage("./Index"); diff --git a/GameLibrary/Program.cs b/GameLibrary/Program.cs index 6cc7c2f..8ebbfc9 100644 --- a/GameLibrary/Program.cs +++ b/GameLibrary/Program.cs @@ -46,10 +46,13 @@ public static void Main(string[] args) .AddRazorPagesOptions(options => { options.Conventions.AuthorizeFolder("/Admin", "RequireAdministratorRole"); + options.Conventions.AuthorizeFolder("/Moderator", "RequireModeratorRole"); }); + builder.Services.AddAuthorization(options => { options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator")); + options.AddPolicy("RequireModeratorRole", policy => policy.RequireRole("Moderator")); }); builder.Services.AddResponseCompression(options =>