using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using TarninaHub.Application.Abstractions; using TarninaHub.Application.Dtos; using TarninaHub.Application.Mappings; using TarninaHub.Application.Security; using TarninaHub.Domain.Rules; namespace TarninaHub.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/submissions")] [Authorize] public class SubmissionsController : ControllerBase { private readonly ISubmissionService _submissionService; private readonly IMatchmakingService _matchmakingService; private readonly INotificationService _notifications; public SubmissionsController( ISubmissionService submissionService, IMatchmakingService matchmakingService, INotificationService notifications) { _submissionService = submissionService; _matchmakingService = matchmakingService; _notifications = notifications; } [HttpPost] public async Task> Create( CreateSubmissionRequest request, CancellationToken cancellationToken) { var userId = User.GetUserId(); if (string.IsNullOrWhiteSpace(userId)) { return Unauthorized(); } var created = await _submissionService.CreateAsync(userId, request, cancellationToken); await _matchmakingService.IndexSubmissionAsync(created.Id, cancellationToken); await _notifications.NotifyStaffAsync( "submission.created", new Dictionary { ["submissionId"] = created.Id, ["title"] = created.Title, ["authorId"] = created.AuthorId, }, cancellationToken); return CreatedAtAction(nameof(GetById), new { submissionId = created.Id }, created); } [HttpGet] public async Task>> List( [FromQuery] string? status, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default) { var userId = User.GetUserId(); if (string.IsNullOrWhiteSpace(userId)) { return Unauthorized(); } page = page < 1 ? 1 : page; pageSize = pageSize is < 1 or > 100 ? 20 : pageSize; var result = await _submissionService.ListAsync( userId, IsStaff(), status, page, pageSize, cancellationToken); return Ok(result); } [HttpGet("{submissionId}")] public async Task> GetById( string submissionId, CancellationToken cancellationToken) { var userId = User.GetUserId(); if (string.IsNullOrWhiteSpace(userId)) { return Unauthorized(); } var submission = await _submissionService.GetAsync(submissionId, cancellationToken); if (submission is null) { return NotFound(); } if (!IsStaff() && !string.Equals(submission.AuthorId, userId, StringComparison.Ordinal)) { return Forbid(); } return Ok(submission); } [HttpPut("{submissionId}/status")] [Authorize(Policy = Policies.OperatorOrAdmin)] public async Task> UpdateStatus( string submissionId, UpdateSubmissionStatusRequest request, CancellationToken cancellationToken) { var current = await _submissionService.GetAsync(submissionId, cancellationToken); if (current is null) { return NotFound(); } var from = SubmissionStatusWire.Parse(current.Status); var to = SubmissionStatusWire.Parse(request.Status); if (to is null) { ModelState.AddModelError(nameof(request.Status), "Unknown status value."); return ValidationProblem(ModelState); } if (from is null || !SubmissionWorkflow.CanTransition(from.Value, to.Value)) { ModelState.AddModelError( nameof(request.Status), $"Transition from '{current.Status}' to '{request.Status}' is not allowed."); return ValidationProblem(ModelState); } var updated = await _submissionService.UpdateStatusAsync(submissionId, to.Value.ToWire(), cancellationToken); return updated is null ? NotFound() : Ok(updated); } [HttpPost("{submissionId}/match")] public async Task> Match( string submissionId, [FromBody] MatchRequest? request, CancellationToken cancellationToken) { var access = await AuthorizeSubmissionAccessAsync(submissionId, cancellationToken); if (access is not null) { return access; } var result = await _matchmakingService.MatchAsync( submissionId, request ?? new MatchRequest(null, null), cancellationToken); return result is null ? NotFound() : Ok(result); } [HttpGet("{submissionId}/matches")] public async Task> Matches( string submissionId, [FromQuery] string? challengeId, [FromQuery] int? maxResults, CancellationToken cancellationToken) { var access = await AuthorizeSubmissionAccessAsync(submissionId, cancellationToken); if (access is not null) { return access; } var result = await _matchmakingService.MatchAsync( submissionId, new MatchRequest(maxResults, challengeId), cancellationToken); return result is null ? NotFound() : Ok(result); } private async Task AuthorizeSubmissionAccessAsync( string submissionId, CancellationToken cancellationToken) { var userId = User.GetUserId(); if (string.IsNullOrWhiteSpace(userId)) { return Unauthorized(); } var submission = await _submissionService.GetAsync(submissionId, cancellationToken); if (submission is null) { return NotFound(); } if (!IsStaff() && !string.Equals(submission.AuthorId, userId, StringComparison.Ordinal)) { return Forbid(); } return null; } private bool IsStaff() => User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin); }