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:
2026-08-11 08:30:53 -04:00
parent 78c526ee35
commit 810f721e48
93 changed files with 3251 additions and 1606 deletions

View File

@@ -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)
{