@@ -1,4 +1,5 @@
|
||||
using Api.Models;
|
||||
using Api.Services.Contracts.Models;
|
||||
using Api.Requests;
|
||||
using Api.Services.Contracts;
|
||||
using Api.Settings;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
@@ -118,7 +119,7 @@ namespace Api.Controllers
|
||||
/// <param name="token">Client-provided reCAPTCHA token.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Tuple containing the verification verdict and user IP.</returns>
|
||||
private async Task<(CaptchaVerdict Verdict, string? UserIp)> ValidateCaptcha(string token, CancellationToken ct)
|
||||
private async Task<(CaptchaVerdictModel Verdict, string? UserIp)> ValidateCaptcha(string token, CancellationToken ct)
|
||||
{
|
||||
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var verdict = await _captcha.VerifyAsync(token, userIp, ct);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using api.Services.Contracts.Rag;
|
||||
using Api.Models.Rag;
|
||||
using Api.Services.Rag;
|
||||
using Api.Requests;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Api.Controllers;
|
||||
|
||||
@@ -11,52 +12,135 @@ namespace Api.Controllers;
|
||||
[EnableRateLimiting("rag")]
|
||||
public sealed class RagController : ControllerBase
|
||||
{
|
||||
private readonly ICvRagService _cvRagService;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<RagController> _logger;
|
||||
|
||||
public RagController(ICvRagService cvRagService, ILogger<RagController> logger)
|
||||
public RagController(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
ILogger<RagController> logger)
|
||||
{
|
||||
_cvRagService = cvRagService;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("cv")]
|
||||
[RequestSizeLimit(8 * 1024 * 1024)]
|
||||
public async Task<IActionResult> UploadCv([FromForm(Name = "cv")] IFormFile? cv, [FromForm] bool gdprConsent, CancellationToken ct)
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||
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
|
||||
{
|
||||
if (cv is null) return BadRequest(new { error = "Missing CV PDF." });
|
||||
var result = await _cvRagService.IngestCvAsync(cv, gdprConsent, ct);
|
||||
return Ok(result);
|
||||
_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);
|
||||
return await ProxyResponseAsync(response, ct);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
_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 ingestion failed");
|
||||
return StatusCode(500, new { error = "CV ingestion failed." });
|
||||
_logger.LogError(ex, "CV upload proxy request failed.");
|
||||
return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("match-job")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||
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
|
||||
{
|
||||
var result = await _cvRagService.MatchJobAsync(request, ct);
|
||||
return Ok(result);
|
||||
_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 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);
|
||||
|
||||
return await ProxyResponseAsync(response, ct);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
_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 matching failed");
|
||||
return StatusCode(500, new { error = "Job matching failed." });
|
||||
_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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user