Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
202 lines
5.5 KiB
C#
202 lines
5.5 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/ideas")]
|
|
[Authorize]
|
|
public class IdeasController : ControllerBase
|
|
{
|
|
private readonly IIdeaService _ideaService;
|
|
private readonly IIdeaAssistant _ideaAssistant;
|
|
private readonly INotificationService _notifications;
|
|
|
|
public IdeasController(
|
|
IIdeaService ideaService,
|
|
IIdeaAssistant ideaAssistant,
|
|
INotificationService notifications)
|
|
{
|
|
_ideaService = ideaService;
|
|
_ideaAssistant = ideaAssistant;
|
|
_notifications = notifications;
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<ActionResult<IdeaResponse>> Create(
|
|
CreateIdeaRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = User.GetUserId();
|
|
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
{
|
|
return Unauthorized();
|
|
}
|
|
|
|
var created = await _ideaService.CreateAsync(userId, request, cancellationToken);
|
|
|
|
await _notifications.NotifyStaffAsync(
|
|
"idea.created",
|
|
new Dictionary<string, string?>
|
|
{
|
|
["ideaId"] = created.Id,
|
|
["title"] = created.Title,
|
|
["authorId"] = created.AuthorId,
|
|
},
|
|
cancellationToken);
|
|
|
|
return CreatedAtAction(nameof(GetById), new { ideaId = created.Id }, created);
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<IReadOnlyList<IdeaResponse>>> ListMine(CancellationToken cancellationToken)
|
|
{
|
|
var userId = User.GetUserId();
|
|
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
{
|
|
return Unauthorized();
|
|
}
|
|
|
|
var ideas = await _ideaService.ListForAuthorAsync(userId, cancellationToken);
|
|
|
|
return Ok(ideas);
|
|
}
|
|
|
|
[HttpGet("{ideaId}")]
|
|
public async Task<ActionResult<IdeaResponse>> GetById(string ideaId, CancellationToken cancellationToken)
|
|
{
|
|
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
|
|
|
if (idea is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!IsOwner(idea) && !IsStaff())
|
|
{
|
|
return Forbid();
|
|
}
|
|
|
|
return Ok(idea);
|
|
}
|
|
|
|
[HttpPut("{ideaId}")]
|
|
public async Task<ActionResult<IdeaResponse>> Update(
|
|
string ideaId,
|
|
UpdateIdeaRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var current = await _ideaService.GetAsync(ideaId, cancellationToken);
|
|
|
|
if (current is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!IsOwner(current))
|
|
{
|
|
return Forbid();
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.Status))
|
|
{
|
|
var from = IdeaStatusWire.Parse(current.Status);
|
|
var to = IdeaStatusWire.Parse(request.Status);
|
|
|
|
if (to is null || from is null || !IdeaWorkflow.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 _ideaService.UpdateAsync(ideaId, request, cancellationToken);
|
|
|
|
return updated is null ? NotFound() : Ok(updated);
|
|
}
|
|
|
|
[HttpGet("{ideaId}/card")]
|
|
public async Task<ActionResult<IdeaCardResponse>> GetCard(string ideaId, CancellationToken cancellationToken)
|
|
{
|
|
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
|
|
|
if (idea is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!IsOwner(idea) && !IsStaff())
|
|
{
|
|
return Forbid();
|
|
}
|
|
|
|
var card = await _ideaService.GetCardAsync(ideaId, cancellationToken);
|
|
|
|
return card is null ? NotFound() : Ok(card);
|
|
}
|
|
|
|
[HttpPut("{ideaId}/card")]
|
|
public async Task<ActionResult<IdeaCardResponse>> UpdateCard(
|
|
string ideaId,
|
|
IdeaCardRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
|
|
|
if (idea is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!IsOwner(idea))
|
|
{
|
|
return Forbid();
|
|
}
|
|
|
|
var card = await _ideaService.UpdateCardAsync(ideaId, request, cancellationToken);
|
|
|
|
return card is null ? NotFound() : Ok(card);
|
|
}
|
|
|
|
[HttpPost("{ideaId}/assist")]
|
|
public async Task<ActionResult<IdeaAssistResponse>> Assist(
|
|
string ideaId,
|
|
IdeaAssistRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
|
|
|
if (idea is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!IsOwner(idea) && !IsStaff())
|
|
{
|
|
return Forbid();
|
|
}
|
|
|
|
var assist = await _ideaAssistant.AssistAsync(ideaId, request, cancellationToken);
|
|
|
|
return assist is null ? NotFound() : Ok(assist);
|
|
}
|
|
|
|
private bool IsOwner(IdeaResponse idea) =>
|
|
string.Equals(idea.AuthorId, User.GetUserId(), StringComparison.Ordinal);
|
|
|
|
private bool IsStaff() =>
|
|
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
|
}
|