Fix all 45 audited bugs from bug-fixes.md
Verified every finding against the code, then fixed correctness (redirects, advancement reset, duplicate heats, high-jump attempt entry, placement ranking with tie handling, delete-dependency 500s), security (secrets out of config, config-driven admin seed, login lockout, role authorization with school scoping, heat-time IDOR, forwarded headers, last-admin guards), schema integrity (nullable+filtered ExistingStudentId, unique indexes for rounds/heats/bar heights via SchemaIntegrityFixes migration), performance (N+1 removal in high jump/reports/standings/dashboard, SQL-side student paging), and hygiene (duplicate notifications, auto-dismiss scope, local bootstrap-icons, orphaned files, test-data.sql tournament creation). FluentValidation is now registered; AutoMapper bumped to 14.0.0 (advisory fully patched only in licence-changed 15.1.1 — documented). 11 new tests; 63/63 passing. Credential rotation and deploy-time DB_CONNECTION_STRING are required manual follow-ups, documented in bug-fixes.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,10 +31,20 @@ public class UserManagementController : Controller
|
||||
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<string, string>();
|
||||
foreach (var roleName in roleNames)
|
||||
{
|
||||
foreach (var member in await _userManager.GetUsersInRoleAsync(roleName))
|
||||
{
|
||||
roleByUserId.TryAdd(member.Id, roleName);
|
||||
}
|
||||
}
|
||||
|
||||
var userDtos = new List<UserDto>();
|
||||
foreach (var user in users)
|
||||
{
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
var dto = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
@@ -42,7 +52,7 @@ public class UserManagementController : Controller
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
FullName = user.FullName,
|
||||
Role = roles.FirstOrDefault() ?? "None",
|
||||
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
|
||||
@@ -152,6 +162,21 @@ public class UserManagementController : Controller
|
||||
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;
|
||||
@@ -195,6 +220,20 @@ public class UserManagementController : Controller
|
||||
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);
|
||||
|
||||
@@ -202,6 +241,14 @@ public class UserManagementController : Controller
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
/// <summary>True when <paramref name="user"/> is an active Admin and no other active Admin exists.</summary>
|
||||
private async Task<bool> 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<IActionResult> ResetPassword(string id)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user