mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 16:18:57 +00:00
6-domain audit (API, web, mobile, lambdas, infra, QA) with fixes: API security: scope internal API key middleware to allowed paths only, return 401 on invalid key instead of falling through, remove unvalidated JWT code path, sanitize error messages, add UpdateProposal validator, remove status field from UpdateProposalRequest to prevent over-posting, log swallowed exceptions in ProposalService. Infrastructure: enforce SSL on all S3 buckets, encrypt SQS queues, enable optional MFA on Cognito, add API Gateway access logging. Lambdas: fix _retry_request undefined variable across all 4 Lambdas, re-raise exceptions in pdf-extract/pdf-generate instead of swallowing, add idempotency guard to suggestions Lambda. Web: add ErrorBoundary, add auth loading state to ProtectedRoute, add mutation error toasts in AdminWorkspace, fix dead Cognito link. Mobile: add mutex to offline queue processing, distinguish permanent vs retryable failures, register all screens for both roles, log sync errors. Swagger/OpenAPI: add Swashbuckle with JWT bearer security definition, add ProducesResponseType attributes to key endpoints. Includes AUDIT-REPORT.md with complete findings and CLAUDE.md project instructions.
163 lines
5.6 KiB
C#
163 lines
5.6 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using ProposalSystem.Application.DTOs;
|
|
using ProposalSystem.Application.Interfaces;
|
|
|
|
namespace ProposalSystem.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/proposals")]
|
|
[Authorize]
|
|
public class ProposalsController : ControllerBase
|
|
{
|
|
private readonly IProposalService _proposalService;
|
|
private readonly IJobPublisher _jobPublisher;
|
|
private readonly ISimilarProposalService _similarService;
|
|
|
|
public ProposalsController(
|
|
IProposalService proposalService,
|
|
IJobPublisher jobPublisher,
|
|
ISimilarProposalService similarService)
|
|
{
|
|
_proposalService = proposalService;
|
|
_jobPublisher = jobPublisher;
|
|
_similarService = similarService;
|
|
}
|
|
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(ProposalResponse), 201)]
|
|
[ProducesResponseType(400)]
|
|
public async Task<ActionResult<ProposalResponse>> Create(
|
|
[FromBody] CreateProposalRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.CreateAsync(request, ct);
|
|
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
|
}
|
|
|
|
[HttpGet]
|
|
[ProducesResponseType(typeof(PagedResponse<ProposalListResponse>), 200)]
|
|
public async Task<ActionResult<PagedResponse<ProposalListResponse>>> GetAll(
|
|
[FromQuery] ProposalFilterRequest filter,
|
|
CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.GetAllAsync(filter, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("{id:guid}")]
|
|
[ProducesResponseType(typeof(ProposalResponse), 200)]
|
|
[ProducesResponseType(404)]
|
|
public async Task<ActionResult<ProposalResponse>> GetById(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.GetByIdAsync(id, ct);
|
|
if (result == null) return NotFound();
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPut("{id:guid}")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
[ProducesResponseType(typeof(ProposalResponse), 200)]
|
|
[ProducesResponseType(400)]
|
|
[ProducesResponseType(404)]
|
|
public async Task<ActionResult<ProposalResponse>> Update(
|
|
Guid id,
|
|
[FromBody] UpdateProposalRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.UpdateAsync(id, request, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("{id:guid}/approve")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
[ProducesResponseType(typeof(ProposalResponse), 200)]
|
|
[ProducesResponseType(400)]
|
|
[ProducesResponseType(404)]
|
|
public async Task<ActionResult<ProposalResponse>> Approve(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.ApproveAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("{id:guid}/return-to-review")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<ActionResult<ProposalResponse>> ReturnToReview(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.ReturnToReviewAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("{id:guid}/send")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<ActionResult<ProposalResponse>> MarkSent(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.MarkSentAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("{id:guid}/revise")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<ActionResult<ProposalResponse>> Revise(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.ReviseAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("{id:guid}/history")]
|
|
public async Task<ActionResult<IReadOnlyList<ProposalResponse>>> GetHistory(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.GetRevisionHistoryAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("{id:guid}/audit")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<ActionResult<IReadOnlyList<AuditLogResponse>>> GetAudit(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _proposalService.GetAuditTrailAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("{id:guid}/similar")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<ActionResult<IReadOnlyList<SimilarProposalResponse>>> GetSimilar(Guid id, CancellationToken ct)
|
|
{
|
|
var result = await _similarService.GetSimilarProposalsAsync(id, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("{id:guid}/generate-suggestions")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<IActionResult> GenerateSuggestions(Guid id, CancellationToken ct)
|
|
{
|
|
await _jobPublisher.PublishAsync("suggestions", new { proposalId = id, trigger = "generate" }, ct);
|
|
return Accepted();
|
|
}
|
|
|
|
[HttpPost("{id:guid}/regenerate")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<IActionResult> Regenerate(Guid id, CancellationToken ct)
|
|
{
|
|
await _jobPublisher.PublishAsync("suggestions", new { proposalId = id, trigger = "regenerate" }, ct);
|
|
return Accepted();
|
|
}
|
|
|
|
[HttpGet("stats")]
|
|
public async Task<ActionResult<ProposalStatsResponse>> GetStats(CancellationToken ct)
|
|
{
|
|
var stats = await _proposalService.GetStatsAsync(ct);
|
|
return Ok(stats);
|
|
}
|
|
|
|
[HttpPost("{id:guid}/similar-references")]
|
|
[Authorize(Roles = "admins,sysadmins")]
|
|
public async Task<IActionResult> AddSimilarReference(
|
|
Guid id,
|
|
[FromBody] CreateSimilarReferenceRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
await _similarService.AddReferenceAsync(id, request, ct);
|
|
return Created();
|
|
}
|
|
}
|