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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PC2/Controllers/AboutController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace PC2.Controllers;

[Authorize(Roles = IdentityHelper.Admin)]
[Authorize(Roles = IdentityHelper.AdminOrStaff)]
public class AboutController : Controller
{
private readonly ApplicationDbContext _context;
Expand Down
4 changes: 2 additions & 2 deletions PC2/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
namespace PC2.Controllers;

/// <summary>
/// Controller for admin-only features including analytics dashboard
/// Controller for analytics dashboard, accessible by admins and staff.
/// </summary>
[Authorize(Roles = IdentityHelper.Admin)]
[Authorize(Roles = IdentityHelper.AdminOrStaff)]
public class AdminController : Controller
{
private readonly AnalyticsService _analyticsService;
Expand Down
2 changes: 1 addition & 1 deletion PC2/Controllers/AgencyController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace PC2.Controllers
{
[Authorize(Roles = IdentityHelper.Admin)]
[Authorize(Roles = IdentityHelper.AdminOrStaff)]
public class AgencyController : Controller
{
private readonly ApplicationDbContext _context;
Expand Down
2 changes: 1 addition & 1 deletion PC2/Controllers/CalendarController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace PC2.Controllers;

[Authorize(Roles = IdentityHelper.Admin)]
[Authorize(Roles = IdentityHelper.AdminOrStaff)]
public class CalendarController : Controller
{
private readonly ApplicationDbContext _context;
Expand Down
2 changes: 1 addition & 1 deletion PC2/Controllers/PeopleController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace PC2.Controllers;

[Authorize(Roles = IdentityHelper.Admin)]
[Authorize(Roles = IdentityHelper.AdminOrStaff)]
public class PeopleController : Controller
{
private readonly ApplicationDbContext _context;
Expand Down
2 changes: 1 addition & 1 deletion PC2/Controllers/ProgramVideosController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace PC2.Controllers
{
[Authorize(Roles = IdentityHelper.Admin)]
[Authorize(Roles = IdentityHelper.AdminOrStaff)]
public class ProgramVideosController : Controller
{
private readonly ApplicationDbContext _context;
Expand Down
168 changes: 168 additions & 0 deletions PC2/Controllers/UserManagementController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for admin-only user management: add/remove staff users and promote/demote roles.
/// </summary>
[Authorize(Roles = IdentityHelper.Admin)]
public class UserManagementController : Controller
{
private readonly UserManager<IdentityUser> _userManager;

public UserManagementController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}

/// <summary>
/// Displays a list of all staff and admin users.
/// </summary>
public async Task<IActionResult> Index()
{
var users = _userManager.Users.ToList();
var viewModels = new List<UserManagementViewModel>();

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);
}

/// <summary>
/// Displays the form to create a new staff user.
/// </summary>
public IActionResult Create()
{
return View();
}

/// <summary>
/// Creates a new staff user account.
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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();
}

/// <summary>
/// Promotes a staff user to the admin role.
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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));
}

/// <summary>
/// Demotes an admin user back to the staff role.
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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));
}

/// <summary>
/// Deletes a staff user account. Admins cannot delete their own account.
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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));
}
}
2 changes: 2 additions & 0 deletions PC2/Data/IdentityHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
12 changes: 12 additions & 0 deletions PC2/Models/UserManagementViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace PC2.Models;

/// <summary>
/// View model representing an application user and their roles for the user management page.
/// </summary>
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; }
}
2 changes: 1 addition & 1 deletion PC2/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@

#if DEBUG
var serviceProvider = app.Services.GetRequiredService<IServiceProvider>().CreateScope();
IdentityHelper.CreateRoles(serviceProvider.ServiceProvider, IdentityHelper.Admin)
IdentityHelper.CreateRoles(serviceProvider.ServiceProvider, IdentityHelper.Admin, IdentityHelper.Staff)
.Wait();
IdentityHelper.CreateDefaultAdmin(serviceProvider.ServiceProvider, IdentityHelper.Admin)
.Wait();
Expand Down
8 changes: 7 additions & 1 deletion PC2/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<div id="right-ribbon" class="ribbon"></div>
<div class="navbar-collapse collapse d-lg-inline-flex flex-lg-row-reverse" id="navbarSupportedContent">
<ul class="navbar-nav flex-grow-1">
@if (User.IsInRole(IdentityHelper.Admin))
@if (User.IsInRole(IdentityHelper.Admin) || User.IsInRole(IdentityHelper.Staff))
{
<li class="nav-item menu-item">
<a class="nav-link" asp-controller="Agency" asp-action="Index">Agencies</a>
Expand Down Expand Up @@ -112,6 +112,12 @@
<li><a class="dropdown-item" asp-controller="ProgramVideos" asp-action="ManageVideos">Program Videos</a></li>
</ul>
</li>
@if (User.IsInRole(IdentityHelper.Admin))
{
<li class="nav-item menu-item">
<a class="nav-link" asp-controller="UserManagement" asp-action="Index">Manage Users</a>
</li>
}
}
else
{
Expand Down
34 changes: 34 additions & 0 deletions PC2/Views/UserManagement/Create.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@{
ViewData["Title"] = "Add Staff User";
}

<h1>@ViewData["Title"]</h1>
<div class="divider"></div>
<br />

<div asp-validation-summary="All" class="text-danger"></div>

<div class="row">
<div class="col-md-5">
<form asp-action="Create" method="post">
@Html.AntiForgeryToken()
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required />
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required />
<div class="form-text">
Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character.
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-person-plus-fill me-2"></i>Create Staff User
</button>
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
Loading
Loading