@@ -1,36 +1,47 @@
|
||||
using Api.Models.Requests;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
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 RagController : ControllerBase
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly Api.Clients.Api.Contracts.ICvMatcherApi _cvApi;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<RagController> _logger;
|
||||
|
||||
public RagController(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
Api.Clients.Api.Contracts.ICvMatcherApi cvApi,
|
||||
IConfiguration configuration,
|
||||
ILogger<RagController> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_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,
|
||||
@@ -53,15 +64,9 @@ public sealed class RagController : ControllerBase
|
||||
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
|
||||
cv.FileName, cv.Length, gdprConsent);
|
||||
|
||||
using var client = CreateCvMatcherClient(baseUrl);
|
||||
using var form = new MultipartFormDataContent();
|
||||
await using var stream = cv.OpenReadStream();
|
||||
using var fileContent = new StreamContent(stream);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
|
||||
form.Add(fileContent, "cv", cv.FileName);
|
||||
form.Add(new StringContent(gdprConsent.ToString().ToLowerInvariant()), "gdprConsent");
|
||||
|
||||
using var response = await client.PostAsync("api/cv/upload", form, ct);
|
||||
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)
|
||||
@@ -76,10 +81,19 @@ public sealed class RagController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
@@ -96,13 +110,7 @@ public sealed class RagController : ControllerBase
|
||||
!string.IsNullOrWhiteSpace(request.JobUrl),
|
||||
!string.IsNullOrWhiteSpace(request.JobDescription));
|
||||
|
||||
using var client = CreateCvMatcherClient(baseUrl);
|
||||
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
using var response = await client.PostAsync(
|
||||
"api/cv/match-job",
|
||||
new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
ct);
|
||||
|
||||
using var response = await _cvApi.MatchJob(request);
|
||||
return await ProxyResponseAsync(response, ct);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
@@ -119,19 +127,7 @@ public sealed class RagController : ControllerBase
|
||||
|
||||
private string GetCvMatcherBaseUrl() => _configuration["CvMatcherApi:BaseUrl"] ?? string.Empty;
|
||||
|
||||
private HttpClient CreateCvMatcherClient(string baseUrl)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("CvMatcherApi");
|
||||
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
|
||||
|
||||
var key = _configuration["CvMatcherApi:InternalApiKey"];
|
||||
if (!string.IsNullOrWhiteSpace(key) && !client.DefaultRequestHeaders.Contains("X-Internal-Api-Key"))
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("X-Internal-Api-Key", key);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
// Refit client is configured in Program.cs; this helper only reads config for diagnostics
|
||||
|
||||
private static async Task<ContentResult> ProxyResponseAsync(HttpResponseMessage response, CancellationToken ct)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user