From d16faa73757ade6a8a0150abb86abda67d4e3f0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:56:59 +0000 Subject: [PATCH 1/3] Initial plan From 8de745f6b00defd798f50e26e3639a58be1a1a25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:03:49 +0000 Subject: [PATCH 2/3] Add staff member role and user management feature Co-authored-by: JoeProgrammer88 <7156063+JoeProgrammer88@users.noreply.github.com> --- PC2/Controllers/AboutController.cs | 2 +- PC2/Controllers/AgencyController.cs | 2 +- PC2/Controllers/CalendarController.cs | 2 +- PC2/Controllers/UserManagementController.cs | 168 +++++++++ PC2/Data/IdentityHelper.cs | 2 + PC2/Models/UserManagementViewModel.cs | 12 + PC2/Program.cs | 2 +- PC2/Views/Shared/_Layout.cshtml | 17 +- PC2/Views/UserManagement/Create.cshtml | 34 ++ PC2/Views/UserManagement/Index.cshtml | 100 +++++ .../UserManagementControllerTests.cs | 345 ++++++++++++++++++ 11 files changed, 678 insertions(+), 8 deletions(-) create mode 100644 PC2/Controllers/UserManagementController.cs create mode 100644 PC2/Models/UserManagementViewModel.cs create mode 100644 PC2/Views/UserManagement/Create.cshtml create mode 100644 PC2/Views/UserManagement/Index.cshtml create mode 100644 PC2Tests/Controllers/UserManagementControllerTests.cs diff --git a/PC2/Controllers/AboutController.cs b/PC2/Controllers/AboutController.cs index 28729c5b..0834c31a 100644 --- a/PC2/Controllers/AboutController.cs +++ b/PC2/Controllers/AboutController.cs @@ -7,7 +7,7 @@ namespace PC2.Controllers; -[Authorize(Roles = IdentityHelper.Admin)] +[Authorize(Roles = IdentityHelper.AdminOrStaff)] public class AboutController : Controller { private readonly ApplicationDbContext _context; diff --git a/PC2/Controllers/AgencyController.cs b/PC2/Controllers/AgencyController.cs index 2544b569..0043e47c 100644 --- a/PC2/Controllers/AgencyController.cs +++ b/PC2/Controllers/AgencyController.cs @@ -6,7 +6,7 @@ namespace PC2.Controllers { - [Authorize(Roles = IdentityHelper.Admin)] + [Authorize(Roles = IdentityHelper.AdminOrStaff)] public class AgencyController : Controller { private readonly ApplicationDbContext _context; diff --git a/PC2/Controllers/CalendarController.cs b/PC2/Controllers/CalendarController.cs index 27d263af..5f9ca33a 100644 --- a/PC2/Controllers/CalendarController.cs +++ b/PC2/Controllers/CalendarController.cs @@ -5,7 +5,7 @@ namespace PC2.Controllers; -[Authorize(Roles = IdentityHelper.Admin)] +[Authorize(Roles = IdentityHelper.AdminOrStaff)] public class CalendarController : Controller { private readonly ApplicationDbContext _context; diff --git a/PC2/Controllers/UserManagementController.cs b/PC2/Controllers/UserManagementController.cs new file mode 100644 index 00000000..01f76a7b --- /dev/null +++ b/PC2/Controllers/UserManagementController.cs @@ -0,0 +1,168 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using PC2.Data; +using PC2.Models; + +namespace PC2.Controllers; + +/// +/// Controller for admin-only user management: add/remove staff users and promote/demote roles. +/// +[Authorize(Roles = IdentityHelper.Admin)] +public class UserManagementController : Controller +{ + private readonly UserManager _userManager; + + public UserManagementController(UserManager userManager) + { + _userManager = userManager; + } + + /// + /// Displays a list of all staff and admin users. + /// + public async Task Index() + { + var users = _userManager.Users.ToList(); + var viewModels = new List(); + + foreach (var user in users) + { + viewModels.Add(new UserManagementViewModel + { + UserId = user.Id, + Email = user.Email ?? string.Empty, + IsAdmin = await _userManager.IsInRoleAsync(user, IdentityHelper.Admin), + IsStaff = await _userManager.IsInRoleAsync(user, IdentityHelper.Staff) + }); + } + + return View(viewModels); + } + + /// + /// Displays the form to create a new staff user. + /// + public IActionResult Create() + { + return View(); + } + + /// + /// Creates a new staff user account. + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(string email, string password) + { + if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password)) + { + ModelState.AddModelError(string.Empty, "Email and password are required."); + return View(); + } + + var user = new IdentityUser { UserName = email, Email = email }; + var result = await _userManager.CreateAsync(user, password); + + if (result.Succeeded) + { + await _userManager.AddToRoleAsync(user, IdentityHelper.Staff); + TempData["SuccessMessage"] = $"Staff user '{email}' created successfully."; + return RedirectToAction(nameof(Index)); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + + return View(); + } + + /// + /// Promotes a staff user to the admin role. + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Promote(string userId) + { + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return NotFound(); + } + + if (!await _userManager.IsInRoleAsync(user, IdentityHelper.Admin)) + { + await _userManager.AddToRoleAsync(user, IdentityHelper.Admin); + } + if (await _userManager.IsInRoleAsync(user, IdentityHelper.Staff)) + { + await _userManager.RemoveFromRoleAsync(user, IdentityHelper.Staff); + } + + TempData["SuccessMessage"] = $"User '{user.Email}' has been promoted to Admin."; + return RedirectToAction(nameof(Index)); + } + + /// + /// Demotes an admin user back to the staff role. + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Demote(string userId) + { + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return NotFound(); + } + + // Prevent demoting yourself + var currentUser = await _userManager.GetUserAsync(User); + if (currentUser != null && currentUser.Id == userId) + { + TempData["ErrorMessage"] = "You cannot demote your own account."; + return RedirectToAction(nameof(Index)); + } + + if (await _userManager.IsInRoleAsync(user, IdentityHelper.Admin)) + { + await _userManager.RemoveFromRoleAsync(user, IdentityHelper.Admin); + } + if (!await _userManager.IsInRoleAsync(user, IdentityHelper.Staff)) + { + await _userManager.AddToRoleAsync(user, IdentityHelper.Staff); + } + + TempData["SuccessMessage"] = $"User '{user.Email}' has been demoted to Staff."; + return RedirectToAction(nameof(Index)); + } + + /// + /// Deletes a staff user account. Admins cannot delete their own account. + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(string userId) + { + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return NotFound(); + } + + // Prevent deleting yourself + var currentUser = await _userManager.GetUserAsync(User); + if (currentUser != null && currentUser.Id == userId) + { + TempData["ErrorMessage"] = "You cannot delete your own account."; + return RedirectToAction(nameof(Index)); + } + + await _userManager.DeleteAsync(user); + TempData["SuccessMessage"] = $"User '{user.Email}' has been deleted."; + return RedirectToAction(nameof(Index)); + } +} diff --git a/PC2/Data/IdentityHelper.cs b/PC2/Data/IdentityHelper.cs index 157aaf4e..4c24bc19 100644 --- a/PC2/Data/IdentityHelper.cs +++ b/PC2/Data/IdentityHelper.cs @@ -5,6 +5,8 @@ namespace PC2.Data public static class IdentityHelper { public const string Admin = "Admin"; + public const string Staff = "Staff"; + public const string AdminOrStaff = Admin + "," + Staff; internal static async Task CreateRoles(IServiceProvider provider, params string[] roles) { diff --git a/PC2/Models/UserManagementViewModel.cs b/PC2/Models/UserManagementViewModel.cs new file mode 100644 index 00000000..7805e499 --- /dev/null +++ b/PC2/Models/UserManagementViewModel.cs @@ -0,0 +1,12 @@ +namespace PC2.Models; + +/// +/// View model representing an application user and their roles for the user management page. +/// +public class UserManagementViewModel +{ + public string UserId { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public bool IsAdmin { get; set; } + public bool IsStaff { get; set; } +} diff --git a/PC2/Program.cs b/PC2/Program.cs index 73fe07d3..4731cde1 100644 --- a/PC2/Program.cs +++ b/PC2/Program.cs @@ -90,7 +90,7 @@ #if DEBUG var serviceProvider = app.Services.GetRequiredService().CreateScope(); -IdentityHelper.CreateRoles(serviceProvider.ServiceProvider, IdentityHelper.Admin) +IdentityHelper.CreateRoles(serviceProvider.ServiceProvider, IdentityHelper.Admin, IdentityHelper.Staff) .Wait(); IdentityHelper.CreateDefaultAdmin(serviceProvider.ServiceProvider, IdentityHelper.Admin) .Wait(); diff --git a/PC2/Views/Shared/_Layout.cshtml b/PC2/Views/Shared/_Layout.cshtml index 5a24769c..988a09f1 100644 --- a/PC2/Views/Shared/_Layout.cshtml +++ b/PC2/Views/Shared/_Layout.cshtml @@ -77,7 +77,7 @@