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

@@ -11,5 +11,22 @@ public class StudentRepository : Repository<Student>, IStudentRepository
public async Task<Student?> GetByExistingIdAsync(string existingStudentId) => await _dbSet.Include(s => s.School).FirstOrDefaultAsync(s => s.ExistingStudentId == existingStudentId);
public async Task<IEnumerable<Student>> GetBySchoolAsync(int schoolId) => await _dbSet.Where(s => s.SchoolId == schoolId).Include(s => s.School).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToListAsync();
public async Task<Student?> GetWithRegistrationsAsync(int studentId) => await _dbSet.Include(s => s.School).Include(s => s.Registrations).ThenInclude(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(s => s.Registrations).ThenInclude(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).FirstOrDefaultAsync(s => s.StudentId == studentId);
public async Task<IEnumerable<Student>> SearchAsync(string searchTerm) => await _dbSet.Include(s => s.School).Where(s => s.FirstName.Contains(searchTerm) || s.LastName.Contains(searchTerm) || s.ExistingStudentId.Contains(searchTerm)).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).Take(50).ToListAsync();
public async Task<(IReadOnlyList<Student> Items, int TotalCount)> GetPagedAsync(int? schoolId, string? searchTerm, int page, int pageSize)
{
IQueryable<Student> query = _dbSet.Include(s => s.School);
if (schoolId.HasValue)
query = query.Where(s => s.SchoolId == schoolId.Value);
if (!string.IsNullOrWhiteSpace(searchTerm))
query = query.Where(s => s.FirstName.Contains(searchTerm)
|| s.LastName.Contains(searchTerm)
|| (s.ExistingStudentId != null && s.ExistingStudentId.Contains(searchTerm)));
var total = await query.CountAsync();
query = query.OrderBy(s => s.LastName).ThenBy(s => s.FirstName);
if (pageSize > 0)
query = query.Skip((page - 1) * pageSize).Take(pageSize);
return (await query.ToListAsync(), total);
}
}