feat(backend): add knowledge base, innovation library and trends modules

Add knowledge item and innovation CRUD with search/filtering, published-only visibility for non-staff, admin publish/update, challenge dictionary, problem aggregation with trends (admin only) and canonical embedding text preparation for matchmaking.
This commit is contained in:
Rafał Tyński
2026-10-04 02:32:33 +02:00
parent fee137aee6
commit bad37a6ab9
19 changed files with 1074 additions and 0 deletions
@@ -0,0 +1,91 @@
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;
namespace TarninaHub.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/innovations")]
[Authorize]
public class InnovationsController : ControllerBase
{
private readonly IInnovationService _innovations;
public InnovationsController(IInnovationService innovations)
{
_innovations = innovations;
}
[HttpGet]
public async Task<ActionResult<IReadOnlyList<InnovationResponse>>> List(
[FromQuery] string? challengeId,
[FromQuery] string? stage,
[FromQuery] string? q,
CancellationToken cancellationToken)
{
var innovations = await _innovations.ListAsync(challengeId, stage, q, !IsStaff(), cancellationToken);
return Ok(innovations);
}
[HttpGet("{innovationId}")]
public async Task<ActionResult<InnovationResponse>> GetById(
string innovationId,
CancellationToken cancellationToken)
{
var innovation = await _innovations.GetAsync(innovationId, cancellationToken);
return innovation is null ? NotFound() : Ok(innovation);
}
[HttpPost]
[Authorize(Policy = Policies.OperatorOrAdmin)]
public async Task<ActionResult<InnovationResponse>> Create(
InnovationRequest request,
CancellationToken cancellationToken)
{
if (!ValidateStage(request.Stage))
{
return InvalidStage();
}
var created = await _innovations.CreateAsync(request, cancellationToken);
return CreatedAtAction(nameof(GetById), new { innovationId = created.Id }, created);
}
[HttpPut("{innovationId}")]
[Authorize(Policy = Policies.OperatorOrAdmin)]
public async Task<ActionResult<InnovationResponse>> Update(
string innovationId,
InnovationRequest request,
CancellationToken cancellationToken)
{
if (!ValidateStage(request.Stage))
{
return InvalidStage();
}
var updated = await _innovations.UpdateAsync(innovationId, request, cancellationToken);
return updated is null ? NotFound() : Ok(updated);
}
private bool ValidateStage(string? stage) => InnovationStageWire.IsValid(stage);
private ActionResult InvalidStage()
{
ModelState.AddModelError(nameof(InnovationRequest.Stage),
"Stage must be one of: idea, pilot, scaling, service.");
return ValidationProblem(ModelState);
}
private bool IsStaff() =>
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
}