feat(backend): add problem/need submission module with workflow and events
Add submission CRUD with pagination and role scoping, challenge/location binding, automatic keyword extraction, a validated status workflow, and domain events for matchmaking and notifications. Create a problem record together with each submission.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
|
||||
public SubmissionsController(ISubmissionService submissionService)
|
||||
{
|
||||
_submissionService = submissionService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<SubmissionResponse>> Create(
|
||||
CreateSubmissionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var created = await _submissionService.CreateAsync(userId, request, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { submissionId = created.Id }, created);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResponse<SubmissionResponse>>> 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<ActionResult<SubmissionResponse>> 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<ActionResult<SubmissionResponse>> 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);
|
||||
}
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
Reference in New Issue
Block a user