149 lines
6.3 KiB
C#
149 lines
6.3 KiB
C#
using Api.Clients.Api.Contracts;
|
|
using Api.Models.Requests;
|
|
using Api.Services.Contracts;
|
|
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 ICvMatcherApi _cvApi;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ICaptchaVerifier _captcha;
|
|
private readonly ILogger<CvMatcherController> _logger;
|
|
|
|
public CvMatcherController(
|
|
ICvMatcherApi cvApi,
|
|
IConfiguration configuration,
|
|
ICaptchaVerifier captcha,
|
|
ILogger<CvMatcherController> logger)
|
|
{
|
|
_cvApi = cvApi;
|
|
_configuration = configuration;
|
|
_captcha = captcha;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Upload a CV PDF to the cv-matcher-api.
|
|
/// </summary>
|
|
/// <param name="request">The uploaded CV request.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
[HttpPost("upload")]
|
|
[Consumes("multipart/form-data")]
|
|
[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] UploadCvRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
if (request.Cv is null)
|
|
{
|
|
return BadRequest(new { error = "Missing CV PDF." });
|
|
}
|
|
|
|
var cv = request.Cv;
|
|
var gdprConsent = request.GdprConsent;
|
|
|
|
try
|
|
{
|
|
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
|
|
cv.FileName, cv.Length, gdprConsent);
|
|
|
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
|
var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, ct);
|
|
if (!verdict.Success)
|
|
{
|
|
_logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp);
|
|
return BadRequest(new { error = "Captcha verification failed." });
|
|
}
|
|
|
|
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)
|
|
{
|
|
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));
|
|
|
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
|
var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, ct);
|
|
if (!verdict.Success)
|
|
{
|
|
_logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp);
|
|
return BadRequest(new { error = "Captcha verification failed." });
|
|
}
|
|
|
|
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." });
|
|
}
|
|
}
|
|
// 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"
|
|
};
|
|
}
|
|
}
|