Add a pluggable embedding provider with an offline deterministic default, vector indexing for problems and innovations, cosine semantic ranking, similar cases and solution suggestions, area filtering and an admin reindex endpoint. Refresh the index when innovations or submissions change.
213 lines
6.0 KiB
C#
213 lines
6.0 KiB
C#
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;
|
|
|
|
public SubmissionsController(
|
|
ISubmissionService submissionService,
|
|
IMatchmakingService matchmakingService)
|
|
{
|
|
_submissionService = submissionService;
|
|
_matchmakingService = matchmakingService;
|
|
}
|
|
|
|
[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);
|
|
|
|
await _matchmakingService.IndexSubmissionAsync(created.Id, 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);
|
|
}
|
|
|
|
[HttpPost("{submissionId}/match")]
|
|
public async Task<ActionResult<MatchResponse>> 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<ActionResult<MatchResponse>> 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<ActionResult?> 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);
|
|
}
|