diff --git a/api-models/Requests/UploadCvRequest.cs b/api-models/Requests/UploadCvRequest.cs index 70c6b7a..18bd5db 100644 --- a/api-models/Requests/UploadCvRequest.cs +++ b/api-models/Requests/UploadCvRequest.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Http; -using System.ComponentModel.DataAnnotations; +using Shared.Models.Requests; namespace Models.Requests { - public sealed class UploadCvRequest + public sealed class UploadCvRequest : UploadFileRequest { - [Required] - public IFormFile Cv { get; set; } = default!; - public bool GdprConsent { get; set; } public string? CaptchaToken { get; set; } + } } diff --git a/api-models/api-models.csproj b/api-models/api-models.csproj index d7c7a21..f9c95a3 100644 --- a/api-models/api-models.csproj +++ b/api-models/api-models.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/api/Clients/Api/Contracts/ICvMatcherApi.cs b/api/Clients/Api/Contracts/ICvMatcherApi.cs index f7bb53e..e3e3a21 100644 --- a/api/Clients/Api/Contracts/ICvMatcherApi.cs +++ b/api/Clients/Api/Contracts/ICvMatcherApi.cs @@ -1,6 +1,6 @@ -using Refit; -using Models.Requests; using CvMatcher.Models.Responses; +using Models.Requests; +using Refit; namespace Api.Clients.Api.Contracts; @@ -8,8 +8,8 @@ public interface ICvMatcherApi { [Multipart] [Post("/api/cv/upload")] - Task Upload([AliasAs("cv")] StreamPart cv, [AliasAs("gdprConsent")] bool gdprConsent); + Task Upload([AliasAs("file")] StreamPart file, CancellationToken ct); [Post("/api/cv/match-job")] - Task MatchJob([Body] JobMatchRequest request); + Task MatchJob([Body] JobMatchRequest request, CancellationToken ct); } diff --git a/api/Controllers/CvMatcherController.cs b/api/Controllers/CvMatcherController.cs index 6a8c3a3..c9f63c5 100644 --- a/api/Controllers/CvMatcherController.cs +++ b/api/Controllers/CvMatcherController.cs @@ -47,12 +47,12 @@ public sealed class CvMatcherController : ControllerBase [FromForm] UploadCvRequest request, CancellationToken ct) { - if (request.Cv is null) + if (request.File is null) { return BadRequest(new { error = "Missing CV PDF." }); } - var cv = request.Cv; + var cv = request.File; var gdprConsent = request.GdprConsent; try @@ -65,12 +65,14 @@ public sealed class CvMatcherController : ControllerBase return BadRequest(new { error = "Captcha verification failed." }); } + if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required."); + _logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}", cv.FileName, cv.Length, gdprConsent); var stream = cv.OpenReadStream(); var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf"); - var res = await _cvApi.Upload(part, gdprConsent); + var res = await _cvApi.Upload(part, ct); return Ok(res); } catch (OperationCanceledException) when (ct.IsCancellationRequested) @@ -114,7 +116,7 @@ public sealed class CvMatcherController : ControllerBase request.CvDocumentId, !string.IsNullOrWhiteSpace(request.JobUrl), !string.IsNullOrWhiteSpace(request.JobDescription)); - var res = await _cvApi.MatchJob(request); + var res = await _cvApi.MatchJob(request, ct); return Ok(res); } catch (OperationCanceledException) when (ct.IsCancellationRequested) diff --git a/cv-matcher-api/Controllers/CvController.cs b/cv-matcher-api/Controllers/CvController.cs index edc8d95..0eb3a93 100644 --- a/cv-matcher-api/Controllers/CvController.cs +++ b/cv-matcher-api/Controllers/CvController.cs @@ -2,6 +2,7 @@ using CvMatcher.Models.Requests; using Api.Services.Contracts; using Microsoft.AspNetCore.Mvc; using CvMatcher.Models.Responses; +using Shared.Models.Requests; namespace Api.Controllers; @@ -20,13 +21,13 @@ public sealed class CvController : ControllerBase [HttpPost("upload")] [RequestSizeLimit(10 * 1024 * 1024)] - public async Task> Upload([FromForm(Name = "cv")] IFormFile? cv, [FromForm] bool gdprConsent, CancellationToken ct) + public async Task> Upload([FromForm] UploadFileRequest request, CancellationToken ct) { try { - if (cv is null) return BadRequest(new { error = "Missing CV PDF." }); - _logger.LogInformation("CV upload received. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}", cv.FileName, cv.Length, gdprConsent); - var result = await _service.UploadCvAsync(cv, gdprConsent, ct); + if (request.File is null) return BadRequest(new { error = "Missing CV PDF." }); + _logger.LogInformation("CV upload received. FileName={FileName}, Size={SizeBytes}", request.File.FileName, request.File.Length); + var result = await _service.UploadCvAsync(request.File, ct); _logger.LogInformation("CV upload processed. CvDocumentId={CvDocumentId}, Cached={Cached}", result.DocumentId, result.Cached); return Ok(result); } diff --git a/cv-matcher-api/Services/Contracts/ICvMatcherService.cs b/cv-matcher-api/Services/Contracts/ICvMatcherService.cs index 86ac599..9c483a5 100644 --- a/cv-matcher-api/Services/Contracts/ICvMatcherService.cs +++ b/cv-matcher-api/Services/Contracts/ICvMatcherService.cs @@ -5,7 +5,7 @@ namespace Api.Services.Contracts; public interface ICvMatcherService { - Task UploadCvAsync(IFormFile file, bool gdprConsent, CancellationToken ct); + Task UploadCvAsync(IFormFile file, CancellationToken ct); Task MatchJobAsync(MatchJobRequest request, CancellationToken ct); Task FindJobsAsync(FindJobsRequest request, CancellationToken ct); } diff --git a/cv-matcher-api/Services/CvMatcherService.cs b/cv-matcher-api/Services/CvMatcherService.cs index a853256..43d5bff 100644 --- a/cv-matcher-api/Services/CvMatcherService.cs +++ b/cv-matcher-api/Services/CvMatcherService.cs @@ -32,9 +32,8 @@ public sealed class CvMatcherService : ICvMatcherService _settings = options.Value; } - public async Task UploadCvAsync(IFormFile file, bool gdprConsent, CancellationToken ct) + public async Task UploadCvAsync(IFormFile file, CancellationToken ct) { - if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required."); var response = await _rag.IndexCvPdfAsync(file, ct); return new CvUploadResponse { diff --git a/rag-api-models/Requests/IndexDocumentRequest.cs b/rag-api-models/Requests/IndexDocumentRequest.cs index 422ca95..4c01e96 100644 --- a/rag-api-models/Requests/IndexDocumentRequest.cs +++ b/rag-api-models/Requests/IndexDocumentRequest.cs @@ -1,6 +1,6 @@ namespace Rag.Models.Requests { - public sealed class IndexDocumentRequest + public class IndexDocumentRequest { public string? Text { get; set; } public string? SourceUrl { get; set; } diff --git a/rag-api-models/Requests/IndexDocumentUploadRequest.cs b/rag-api-models/Requests/IndexDocumentUploadRequest.cs new file mode 100644 index 0000000..e5578b9 --- /dev/null +++ b/rag-api-models/Requests/IndexDocumentUploadRequest.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Http; + +namespace Rag.Models.Requests +{ + public sealed class IndexDocumentUploadRequest: IndexDocumentRequest + { + public IFormFile? File { get; set; } + } +} diff --git a/rag-api/Controllers/RagController.cs b/rag-api/Controllers/RagController.cs index e6ceafd..1027b93 100644 --- a/rag-api/Controllers/RagController.cs +++ b/rag-api/Controllers/RagController.cs @@ -21,21 +21,17 @@ public sealed class RagController : ControllerBase [HttpPost("documents")] [RequestSizeLimit(10 * 1024 * 1024)] public async Task> IndexDocument( - [FromForm] IFormFile? file, - [FromForm] string? text, - [FromForm] string? documentType, - [FromForm] string? title, - [FromForm] string? sourceUrl, + [FromForm] IndexDocumentUploadRequest request, CancellationToken ct) { try { _logger.LogInformation("Index document request received. HasFile={HasFile}, DocumentType={DocumentType}, Title={Title}, SourceUrl={SourceUrl}", - file is not null, documentType, title, sourceUrl); + request.File is not null, request.DocumentType, request.Title, request.SourceUrl); - if (file is not null) + if (request.File is not null) { - var result = await _ragService.IndexPdfAsync(file, documentType, title, sourceUrl, ct); + var result = await _ragService.IndexPdfAsync(request.File, request.DocumentType, request.Title, request.SourceUrl, ct); _logger.LogInformation("Indexed PDF document. DocumentId={DocumentId}, DocumentType={DocumentType}, Chunks={Chunks}, Cached={Cached}", result.DocumentId, result.DocumentType, result.Chunks, result.Cached); return Ok(result); @@ -43,10 +39,10 @@ public sealed class RagController : ControllerBase var textResult = await _ragService.IndexTextAsync(new IndexDocumentRequest { - Text = text, - DocumentType = documentType, - Title = title, - SourceUrl = sourceUrl + Text = request.Text, + DocumentType = request.DocumentType, + Title = request.Title, + SourceUrl = request.SourceUrl }, ct); _logger.LogInformation("Indexed text document. DocumentId={DocumentId}, DocumentType={DocumentType}, Chunks={Chunks}, Cached={Cached}", textResult.DocumentId, textResult.DocumentType, textResult.Chunks, textResult.Cached); diff --git a/shared-models/Requests/UploadFileRequest.cs b/shared-models/Requests/UploadFileRequest.cs new file mode 100644 index 0000000..c9ed0b2 --- /dev/null +++ b/shared-models/Requests/UploadFileRequest.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; +using System.ComponentModel.DataAnnotations; + +namespace Shared.Models.Requests +{ + public class UploadFileRequest + { + [Required] + public IFormFile File { get; set; } = default!; + } +} diff --git a/shared-models/shared-models.csproj b/shared-models/shared-models.csproj index 9e05988..0cea718 100644 --- a/shared-models/shared-models.csproj +++ b/shared-models/shared-models.csproj @@ -7,4 +7,8 @@ enable + + + +