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; [ApiController] [Route("api/rag")] [EnableRateLimiting("rag")] public sealed class RagController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; public RagController( IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger) { _httpClientFactory = httpClientFactory; _configuration = configuration; _logger = logger; } [HttpPost("cv")] [RequestSizeLimit(8 * 1024 * 1024)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status502BadGateway)] public async Task 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); 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 (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." }); } } [HttpPost("match-job")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status502BadGateway)] public async Task 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 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 (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; 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 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" }; } }