Files
myAi/api/Controllers/CvMatcherController.cs
T
claude f1e8a9f8da
Build and Push Docker Images / build (push) Successful in 36s
Changes
2026-05-06 14:19:33 +03:00

143 lines
6.1 KiB
C#

using Api.Models.Requests;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers;
/// <summary>
/// Proxy endpoints for the CV matcher API. These endpoints forward requests to the internal cv-matcher-api.
/// </summary>
[ApiController]
[Route("api/cv-matcher")]
[EnableRateLimiting("cv-matcher")]
public sealed class CvMatcherController : ControllerBase
{
private readonly Api.Clients.Api.Contracts.ICvMatcherApi _cvApi;
private readonly IConfiguration _configuration;
private readonly ILogger<CvMatcherController> _logger;
public CvMatcherController(
Api.Clients.Api.Contracts.ICvMatcherApi cvApi,
IConfiguration configuration,
ILogger<CvMatcherController> logger)
{
_cvApi = cvApi;
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// Upload a CV PDF to the cv-matcher-api.
/// </summary>
/// <param name="cv">The uploaded CV PDF file.</param>
/// <param name="gdprConsent">Whether the user consented to GDPR processing.</param>
/// <param name="ct">Cancellation token.</param>
[HttpPost("upload")]
[RequestSizeLimit(8 * 1024 * 1024)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status502BadGateway)]
[SwaggerOperation(Summary = "Upload CV", Description = "Proxy upload of a CV PDF to the internal cv-matcher-api.")]
[SwaggerResponse(StatusCodes.Status200OK, "Upload succeeded")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Missing or invalid input")]
[SwaggerResponse(StatusCodes.Status502BadGateway, "Upstream cv-matcher-api error")]
public async Task<IActionResult> UploadCv(
[FromForm(Name = "cv")] IFormFile? cv,
[FromForm] bool gdprConsent,
CancellationToken ct)
{
if (cv is null)
{
return BadRequest(new { error = "Missing CV PDF." });
}
var baseUrl = GetCvMatcherBaseUrl();
if (string.IsNullOrWhiteSpace(baseUrl))
{
_logger.LogError("CvMatcherApi:BaseUrl is not configured. The public API cannot proxy CV upload requests.");
return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API is not configured." });
}
try
{
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
cv.FileName, cv.Length, gdprConsent);
var stream = cv.OpenReadStream();
var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf");
using var response = await _cvApi.Upload(part, gdprConsent);
return await ProxyResponseAsync(response, ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_logger.LogWarning("CV upload proxy request was cancelled by the client.");
return StatusCode(499, new { error = "Request cancelled." });
}
catch (Exception ex)
{
_logger.LogError(ex, "CV upload proxy request failed.");
return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." });
}
}
/// <summary>
/// Proxy a job matching request to the cv-matcher-api.
/// </summary>
/// <param name="request">Job match request payload containing CV document id or job description/url.</param>
/// <param name="ct">Cancellation token.</param>
[HttpPost("match-job")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status502BadGateway)]
[SwaggerOperation(Summary = "Match job", Description = "Proxy job matching request to the internal cv-matcher-api.")]
[SwaggerResponse(StatusCodes.Status200OK, "Match succeeded")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request")]
[SwaggerResponse(StatusCodes.Status502BadGateway, "Upstream cv-matcher-api error")]
public async Task<IActionResult> MatchJob([FromBody] JobMatchRequest request, CancellationToken ct)
{
var baseUrl = GetCvMatcherBaseUrl();
if (string.IsNullOrWhiteSpace(baseUrl))
{
_logger.LogError("CvMatcherApi:BaseUrl is not configured. The public API cannot proxy job matching requests.");
return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API is not configured." });
}
try
{
_logger.LogInformation("Proxying job match request to cv-matcher-api. CvDocumentId={CvDocumentId}, HasJobUrl={HasJobUrl}, HasJobDescription={HasJobDescription}",
request.CvDocumentId,
!string.IsNullOrWhiteSpace(request.JobUrl),
!string.IsNullOrWhiteSpace(request.JobDescription));
using var response = await _cvApi.MatchJob(request);
return await ProxyResponseAsync(response, ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_logger.LogWarning("Job match proxy request was cancelled by the client.");
return StatusCode(499, new { error = "Request cancelled." });
}
catch (Exception ex)
{
_logger.LogError(ex, "Job match proxy request failed.");
return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." });
}
}
private string GetCvMatcherBaseUrl() => _configuration["CvMatcherApi:BaseUrl"] ?? string.Empty;
// Refit client is configured in Program.cs; this helper only reads config for diagnostics
private static async Task<ContentResult> ProxyResponseAsync(HttpResponseMessage response, CancellationToken ct)
{
var body = await response.Content.ReadAsStringAsync(ct);
return new ContentResult
{
StatusCode = (int)response.StatusCode,
Content = body,
ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json"
};
}
}