using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SportsDivision.Application.DTOs; using SportsDivision.Application.Interfaces; using SportsDivision.Infrastructure.Identity; namespace SportsDivision.Web.Controllers; [Authorize(Roles = "Admin")] public class UserManagementController : Controller { private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly ISchoolService _schoolService; public UserManagementController( UserManager userManager, RoleManager roleManager, ISchoolService schoolService) { _userManager = userManager; _roleManager = roleManager; _schoolService = schoolService; } public async Task Index(string? role, bool? isActive) { var users = await _userManager.Users.ToListAsync(); var schools = await _schoolService.GetAllAsync(); var schoolLookup = schools.ToDictionary(s => s.SchoolId, s => s.Name); // One query per role instead of one per user. var roleNames = await _roleManager.Roles.Select(r => r.Name!).ToListAsync(); var roleByUserId = new Dictionary(); foreach (var roleName in roleNames) { foreach (var member in await _userManager.GetUsersInRoleAsync(roleName)) { roleByUserId.TryAdd(member.Id, roleName); } } var userDtos = new List(); foreach (var user in users) { var dto = new UserDto { Id = user.Id, Email = user.Email ?? string.Empty, FirstName = user.FirstName, LastName = user.LastName, FullName = user.FullName, Role = roleByUserId.GetValueOrDefault(user.Id, "None"), SchoolId = user.SchoolId, SchoolName = user.SchoolId.HasValue && schoolLookup.TryGetValue(user.SchoolId.Value, out var name) ? name : null, IsActive = user.IsActive }; userDtos.Add(dto); } if (!string.IsNullOrEmpty(role)) { userDtos = userDtos.Where(u => u.Role == role).ToList(); } if (isActive.HasValue) { userDtos = userDtos.Where(u => u.IsActive == isActive.Value).ToList(); } ViewBag.Roles = await _roleManager.Roles.Select(r => r.Name).ToListAsync(); ViewBag.SelectedRole = role; ViewBag.SelectedIsActive = isActive; return View(userDtos); } [HttpGet] public async Task Create() { await PopulateDropdowns(); return View(new UserCreateDto()); } [HttpPost] [ValidateAntiForgeryToken] public async Task Create(UserCreateDto dto) { if (!ModelState.IsValid) { await PopulateDropdowns(); return View(dto); } var user = new ApplicationUser { UserName = dto.Email, Email = dto.Email, FirstName = dto.FirstName, LastName = dto.LastName, SchoolId = dto.SchoolId, IsActive = true, EmailConfirmed = true }; var result = await _userManager.CreateAsync(user, dto.Password); if (!result.Succeeded) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } await PopulateDropdowns(); return View(dto); } if (!string.IsNullOrEmpty(dto.Role)) { await _userManager.AddToRoleAsync(user, dto.Role); } TempData["SuccessMessage"] = "User created successfully."; return RedirectToAction(nameof(Index)); } [HttpGet] public async Task Edit(string id) { var user = await _userManager.FindByIdAsync(id); if (user == null) return NotFound(); var roles = await _userManager.GetRolesAsync(user); var dto = new UserUpdateDto { Id = user.Id, FirstName = user.FirstName, LastName = user.LastName, Role = roles.FirstOrDefault() ?? string.Empty, SchoolId = user.SchoolId, IsActive = user.IsActive }; ViewBag.Email = user.Email; await PopulateDropdowns(); return View(dto); } [HttpPost] [ValidateAntiForgeryToken] public async Task Edit(UserUpdateDto dto) { if (!ModelState.IsValid) { var u = await _userManager.FindByIdAsync(dto.Id); ViewBag.Email = u?.Email; await PopulateDropdowns(); return View(dto); } var user = await _userManager.FindByIdAsync(dto.Id); if (user == null) return NotFound(); // Guard against locking the system out of administration: an admin may not // deactivate or demote themselves, and the last active Admin must remain. var currentRoles0 = await _userManager.GetRolesAsync(user); var losesAdmin = currentRoles0.Contains("Admin") && dto.Role != "Admin"; if (user.Id == _userManager.GetUserId(User) && (!dto.IsActive || losesAdmin)) { TempData["ErrorMessage"] = "You cannot deactivate or demote your own account."; return RedirectToAction(nameof(Index)); } if ((!dto.IsActive || losesAdmin) && await IsLastActiveAdminAsync(user)) { TempData["ErrorMessage"] = "This is the last active Admin account — it cannot be deactivated or demoted."; return RedirectToAction(nameof(Index)); } user.FirstName = dto.FirstName; user.LastName = dto.LastName; user.SchoolId = dto.SchoolId; user.IsActive = dto.IsActive; var updateResult = await _userManager.UpdateAsync(user); if (!updateResult.Succeeded) { foreach (var error in updateResult.Errors) { ModelState.AddModelError(string.Empty, error.Description); } ViewBag.Email = user.Email; await PopulateDropdowns(); return View(dto); } // Update role if changed var currentRoles = await _userManager.GetRolesAsync(user); var currentRole = currentRoles.FirstOrDefault(); if (currentRole != dto.Role) { if (!string.IsNullOrEmpty(currentRole)) { await _userManager.RemoveFromRoleAsync(user, currentRole); } if (!string.IsNullOrEmpty(dto.Role)) { await _userManager.AddToRoleAsync(user, dto.Role); } } TempData["SuccessMessage"] = "User updated successfully."; return RedirectToAction(nameof(Index)); } [HttpPost] [ValidateAntiForgeryToken] public async Task ToggleActive(string id) { var user = await _userManager.FindByIdAsync(id); if (user == null) return NotFound(); if (user.IsActive) { if (user.Id == _userManager.GetUserId(User)) { TempData["ErrorMessage"] = "You cannot deactivate your own account."; return RedirectToAction(nameof(Index)); } if (await IsLastActiveAdminAsync(user)) { TempData["ErrorMessage"] = "This is the last active Admin account — it cannot be deactivated."; return RedirectToAction(nameof(Index)); } } user.IsActive = !user.IsActive; await _userManager.UpdateAsync(user); TempData["SuccessMessage"] = $"User {(user.IsActive ? "activated" : "deactivated")} successfully."; return RedirectToAction(nameof(Index)); } /// True when is an active Admin and no other active Admin exists. private async Task IsLastActiveAdminAsync(ApplicationUser user) { if (!user.IsActive || !await _userManager.IsInRoleAsync(user, "Admin")) return false; var admins = await _userManager.GetUsersInRoleAsync("Admin"); return !admins.Any(a => a.IsActive && a.Id != user.Id); } [HttpGet] public async Task ResetPassword(string id) { var user = await _userManager.FindByIdAsync(id); if (user == null) return NotFound(); return View(new ResetPasswordDto { Id = user.Id, Email = user.Email ?? string.Empty, FullName = user.FullName }); } [HttpPost] [ValidateAntiForgeryToken] public async Task ResetPassword(ResetPasswordDto dto) { var user = await _userManager.FindByIdAsync(dto.Id); if (user == null) return NotFound(); // Keep display fields populated regardless of the posted values. dto.Email = user.Email ?? string.Empty; dto.FullName = user.FullName; if (!ModelState.IsValid) { return View(dto); } var token = await _userManager.GeneratePasswordResetTokenAsync(user); var result = await _userManager.ResetPasswordAsync(user, token, dto.NewPassword); if (!result.Succeeded) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } return View(dto); } TempData["SuccessMessage"] = $"Password reset successfully for {user.Email}."; return RedirectToAction(nameof(Index)); } private async Task PopulateDropdowns() { ViewBag.Roles = await _roleManager.Roles.Select(r => r.Name).ToListAsync(); ViewBag.Schools = (await _schoolService.GetAllAsync()).Where(s => s.IsActive).ToList(); } }