Changes
Build and Push Docker Images / build (push) Successful in 36s

This commit is contained in:
2026-05-06 13:59:59 +03:00
parent 91cdb536b1
commit cd0e32513f
8 changed files with 99 additions and 39 deletions
@@ -0,0 +1,15 @@
using System.Net.Http;
using Refit;
using Api.Models.Requests;
namespace Api.Clients.Api.Contracts;
public interface ICvMatcherApi
{
[Multipart]
[Post("/api/cv/upload")]
Task<HttpResponseMessage> Upload([AliasAs("cv")] StreamPart cv, [AliasAs("gdprConsent")] bool gdprConsent);
[Post("/api/cv/match-job")]
Task<HttpResponseMessage> MatchJob([Body] JobMatchRequest request);
}
+31 -35
View File
@@ -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)
{
+26 -2
View File
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Serilog;
using System.Reflection;
using System.Threading.RateLimiting;
using Refit;
// Load .env file if it exists (for local development)
@@ -80,11 +81,34 @@ try
builder.Services.AddHttpClient<ICaptchaVerifier, RecaptchaVerifier>();
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
builder.Services.AddSingleton<Microsoft.AspNetCore.StaticFiles.IContentTypeProvider, Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider>();
builder.Services.AddHttpClient("CvMatcherApi");
// Refit client for CvMatcher API
builder.Services.AddRefitClient<Api.Clients.Api.Contracts.ICvMatcherApi>()
.ConfigureHttpClient((sp, client) =>
{
var config = sp.GetRequiredService<IConfiguration>();
var baseUrl = config["CvMatcherApi:BaseUrl"] ?? string.Empty;
if (!string.IsNullOrWhiteSpace(baseUrl)) client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
var key = config["CvMatcherApi:InternalApiKey"];
if (!string.IsNullOrWhiteSpace(key) && !client.DefaultRequestHeaders.Contains("X-Internal-Api-Key"))
{
client.DefaultRequestHeaders.Add("X-Internal-Api-Key", key);
}
});
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
// Include XML comments (enable <GenerateDocumentationFile> in csproj)
var xmlFile = (Assembly.GetExecutingAssembly().GetName().Name ?? "Api") + ".xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath)) options.IncludeXmlComments(xmlPath);
// Enable annotations like [SwaggerOperation], [SwaggerResponse]
options.EnableAnnotations();
});
// If you're behind Caddy / reverse proxy
builder.Services.Configure<ForwardedHeadersOptions>(options =>
+4
View File
@@ -11,6 +11,8 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DisableStaticWebAssets>true</DisableStaticWebAssets>
<RootNamespace>Api</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -24,6 +26,8 @@
<PackageReference Include="Serilog.Sinks.Email" Version="4.2.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="10.1.6" />
</ItemGroup>
<ItemGroup>