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>
305 lines
8.5 KiB
C#
305 lines
8.5 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using SportsDivision.Application.DTOs;
|
|
using SportsDivision.Application.Interfaces;
|
|
using SportsDivision.Domain.Enums;
|
|
using SportsDivision.Web.Helpers;
|
|
|
|
namespace SportsDivision.Web.Controllers;
|
|
|
|
[Authorize]
|
|
public class TournamentController : Controller
|
|
{
|
|
private readonly ITournamentService _tournamentService;
|
|
private readonly IEventService _eventService;
|
|
|
|
public TournamentController(ITournamentService tournamentService, IEventService eventService)
|
|
{
|
|
_tournamentService = tournamentService;
|
|
_eventService = eventService;
|
|
}
|
|
|
|
public async Task<IActionResult> Index(TournamentStatus? status, bool showArchived = false, int page = 1, int pageSize = PaginationHelper.PageSize)
|
|
{
|
|
var all = await _tournamentService.GetAllAsync(includeArchived: true);
|
|
var archivedCount = all.Count(t => t.IsArchived);
|
|
|
|
var tournaments = showArchived ? all : all.Where(t => !t.IsArchived);
|
|
|
|
if (status.HasValue)
|
|
{
|
|
tournaments = tournaments.Where(t => t.Status == status.Value);
|
|
}
|
|
|
|
ViewBag.SelectedStatus = status;
|
|
ViewBag.ShowArchived = showArchived;
|
|
ViewBag.ArchivedCount = archivedCount;
|
|
|
|
return View(this.Page(tournaments, page, pageSize));
|
|
}
|
|
|
|
public async Task<IActionResult> Details(int id)
|
|
{
|
|
try
|
|
{
|
|
var tournament = await _tournamentService.GetByIdAsync(id);
|
|
if (tournament == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var eventLevels = await _tournamentService.GetEventLevelsAsync(id);
|
|
ViewBag.EventLevels = eventLevels;
|
|
ViewBag.Events = await _eventService.GetAllAsync();
|
|
ViewBag.Levels = await _tournamentService.GetAllEventLevelsAsync();
|
|
|
|
return View(tournament);
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpGet]
|
|
public IActionResult Create()
|
|
{
|
|
return View(new TournamentCreateDto());
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Create(TournamentCreateDto dto)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(dto);
|
|
}
|
|
|
|
try
|
|
{
|
|
var tournament = await _tournamentService.CreateAsync(dto);
|
|
TempData["SuccessMessage"] = "Tournament created successfully.";
|
|
return RedirectToAction(nameof(Details), new { id = tournament.TournamentId });
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
return View(dto);
|
|
}
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpGet]
|
|
public async Task<IActionResult> Edit(int id)
|
|
{
|
|
try
|
|
{
|
|
var tournament = await _tournamentService.GetByIdAsync(id);
|
|
if (tournament == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var dto = new TournamentUpdateDto
|
|
{
|
|
TournamentId = tournament.TournamentId,
|
|
Name = tournament.Name,
|
|
StartDate = tournament.StartDate,
|
|
EndDate = tournament.EndDate,
|
|
ZoneId = tournament.ZoneId,
|
|
SchoolLevel = tournament.SchoolLevel
|
|
};
|
|
|
|
return View(dto);
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Edit(TournamentUpdateDto dto)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(dto);
|
|
}
|
|
|
|
try
|
|
{
|
|
await _tournamentService.UpdateAsync(dto);
|
|
TempData["SuccessMessage"] = "Tournament updated successfully.";
|
|
return RedirectToAction(nameof(Details), new { id = dto.TournamentId });
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
return View(dto);
|
|
}
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Delete(int id)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.DeleteAsync(id);
|
|
TempData["SuccessMessage"] = "Tournament deleted successfully.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> AddEventLevel(TournamentEventLevelCreateDto dto)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.AddEventLevelAsync(dto);
|
|
TempData["SuccessMessage"] = "Event level added to tournament successfully.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Details), new { id = dto.TournamentId });
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> RemoveEventLevel(int tournamentEventLevelId, int id)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.RemoveEventLevelAsync(tournamentEventLevelId);
|
|
TempData["SuccessMessage"] = "Event level removed from tournament successfully.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Details), new { id });
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> UpdateStatus(int id, TournamentStatus status)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.UpdateStatusAsync(id, status);
|
|
TempData["SuccessMessage"] = $"Tournament status updated to {status}.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Details), new { id });
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Archive(int id)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.ArchiveAsync(id);
|
|
TempData["SuccessMessage"] = "Tournament archived successfully.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Unarchive(int id)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.UnarchiveAsync(id);
|
|
TempData["SuccessMessage"] = "Tournament unarchived successfully.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[Authorize(Roles = "Admin,Official")]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ToggleAgeWaiver(int tournamentEventLevelId, int id)
|
|
{
|
|
try
|
|
{
|
|
await _tournamentService.ToggleAgeWaiverAsync(tournamentEventLevelId);
|
|
TempData["SuccessMessage"] = "Age waiver toggled successfully.";
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
TempData["ErrorMessage"] = ex.Message;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Details), new { id });
|
|
}
|
|
}
|