From 51e668bf1d113f8f1694f037f2aa5e5e36153d93 Mon Sep 17 00:00:00 2001 From: Gelu Mihes Date: Fri, 8 May 2026 13:46:25 +0300 Subject: [PATCH] Changes --- api/Controllers/CaptchaController.cs | 16 +++++++++++--- api/Controllers/ContactController.cs | 23 +++++++++++++------- api/Controllers/CvMatcherController.cs | 25 ++++++++++++---------- api/Controllers/FileDownloadController.cs | 13 +++++------ api/api.csproj | 1 + cv-matcher-api/Controllers/CvController.cs | 15 +++++++------ rag-api/Controllers/RagController.cs | 17 ++++++++------- shared-models/Responses/ErrorResponse.cs | 9 ++++++++ web/wwwroot/cv-matcher/index.html | 7 ++++++ web/wwwroot/index.html | 2 +- 10 files changed, 84 insertions(+), 44 deletions(-) create mode 100644 shared-models/Responses/ErrorResponse.cs diff --git a/api/Controllers/CaptchaController.cs b/api/Controllers/CaptchaController.cs index 5e22a2e..949d69a 100644 --- a/api/Controllers/CaptchaController.cs +++ b/api/Controllers/CaptchaController.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Models.Settings; using Swashbuckle.AspNetCore.Annotations; using Models.Requests; +using Shared.Models.Responses; namespace Api.Controllers { @@ -42,17 +43,26 @@ namespace Api.Controllers [HttpPost("verify")] [SwaggerOperation(Summary = "Verify captcha token")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [SwaggerResponse(StatusCodes.Status400BadRequest, "Captcha verification failed or token missing", typeof(ErrorResponse))] public async Task Verify([FromBody] CaptchaVerifyRequest req, CancellationToken ct) { - if (req is null || string.IsNullOrWhiteSpace(req.Token)) return BadRequest(new { error = "Missing token" }); + if (req is null || string.IsNullOrWhiteSpace(req.Token)) + { + return BadRequest(new ErrorResponse { Error = "Missing token", Code = "captcha_token_missing" }); + } var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); var verdict = await _captcha.VerifyAsync(req.Token, userIp, req.ExpectedAction, ct); if (!verdict.Success) { _log.LogWarning("Captcha failed. ip={Ip} score={Score} err={Err}", userIp, verdict.Score, verdict.Error); - return BadRequest(new { error = "Captcha verification failed.", score = verdict.Score }); + return BadRequest(new ErrorResponse + { + Error = "Captcha verification failed.", + Code = "captcha_verification_failed", + Score = verdict.Score + }); } return Ok(verdict); diff --git a/api/Controllers/ContactController.cs b/api/Controllers/ContactController.cs index 9ccf289..5672f0c 100644 --- a/api/Controllers/ContactController.cs +++ b/api/Controllers/ContactController.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Options; using Models.Settings; using Models.Requests; using Swashbuckle.AspNetCore.Annotations; +using Shared.Models.Responses; namespace Api.Controllers { @@ -48,8 +49,8 @@ namespace Api.Controllers [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request or captcha verification failed")] [SwaggerResponse(StatusCodes.Status500InternalServerError, "Contact message could not be sent due to server error")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)] public async Task Send([FromBody] ContactRequest req, CancellationToken ct) { if (!ModelState.IsValid) @@ -57,7 +58,10 @@ namespace Api.Controllers var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, "contact", ct); - if (!verdict.Success) return BadRequest("Captcha verification failed."); + if (!verdict.Success) + { + return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" }); + } try { @@ -67,7 +71,7 @@ namespace Api.Controllers catch (Exception ex) { _log.LogError(ex, "Contact send failed. ip={Ip} from={From}", userIp, req.Email); - return StatusCode(500, "Could not send message."); + return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "Could not send message.", Code = "contact_send_failed" }); } } @@ -89,8 +93,8 @@ namespace Api.Controllers [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request or captcha verification failed")] [SwaggerResponse(StatusCodes.Status500InternalServerError, "Subscription failed due to server error")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)] public async Task Subscribe([FromBody] SubscribeRequest req, CancellationToken ct) { if (!ModelState.IsValid) @@ -98,7 +102,10 @@ namespace Api.Controllers var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, "contact", ct); - if (!verdict.Success) return BadRequest("Captcha verification failed."); + if (!verdict.Success) + { + return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" }); + } try { @@ -108,7 +115,7 @@ namespace Api.Controllers catch (Exception ex) { _log.LogError(ex, "Subscription failed. ip={Ip} eMail={eMail}", userIp, req.Email); - return StatusCode(500, "Failed."); + return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "Failed.", Code = "subscription_failed" }); } } diff --git a/api/Controllers/CvMatcherController.cs b/api/Controllers/CvMatcherController.cs index c9f63c5..b96ceb1 100644 --- a/api/Controllers/CvMatcherController.cs +++ b/api/Controllers/CvMatcherController.cs @@ -4,6 +4,7 @@ using Api.Services.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Swashbuckle.AspNetCore.Annotations; +using Shared.Models.Responses; namespace Api.Controllers; @@ -37,8 +38,9 @@ public sealed class CvMatcherController : ControllerBase [HttpPost("upload")] [RequestSizeLimit(8 * 1024 * 1024)] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status502BadGateway)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status502BadGateway)] + [ProducesResponseType(typeof(ErrorResponse), 499)] [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")] @@ -49,7 +51,7 @@ public sealed class CvMatcherController : ControllerBase { if (request.File is null) { - return BadRequest(new { error = "Missing CV PDF." }); + return BadRequest(new ErrorResponse { Error = "Missing CV PDF.", Code = "cv_file_missing" }); } var cv = request.File; @@ -62,7 +64,7 @@ public sealed class CvMatcherController : ControllerBase if (!verdict.Success) { _logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp); - return BadRequest(new { error = "Captcha verification failed." }); + return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" }); } if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required."); @@ -78,12 +80,12 @@ public sealed class CvMatcherController : ControllerBase catch (OperationCanceledException) when (ct.IsCancellationRequested) { _logger.LogWarning("CV upload proxy request was cancelled by the client."); - return StatusCode(499, new { error = "Request cancelled." }); + return StatusCode(499, new ErrorResponse { Error = "Request cancelled.", Code = "request_cancelled" }); } catch (Exception ex) { _logger.LogError(ex, "CV upload proxy request failed."); - return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." }); + return StatusCode(StatusCodes.Status502BadGateway, new ErrorResponse { Error = "CV matcher API request failed.", Code = "upstream_request_failed" }); } } @@ -94,8 +96,9 @@ public sealed class CvMatcherController : ControllerBase /// Cancellation token. [HttpPost("match-job")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status502BadGateway)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status502BadGateway)] + [ProducesResponseType(typeof(ErrorResponse), 499)] [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")] @@ -109,7 +112,7 @@ public sealed class CvMatcherController : ControllerBase if (!verdict.Success) { _logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp); - return BadRequest(new { error = "Captcha verification failed." }); + return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" }); } _logger.LogInformation("Proxying job match request to cv-matcher-api. CvDocumentId={CvDocumentId}, HasJobUrl={HasJobUrl}, HasJobDescription={HasJobDescription}", @@ -122,12 +125,12 @@ public sealed class CvMatcherController : ControllerBase catch (OperationCanceledException) when (ct.IsCancellationRequested) { _logger.LogWarning("Job match proxy request was cancelled by the client."); - return StatusCode(499, new { error = "Request cancelled." }); + return StatusCode(499, new ErrorResponse { Error = "Request cancelled.", Code = "request_cancelled" }); } catch (Exception ex) { _logger.LogError(ex, "Job match proxy request failed."); - return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." }); + return StatusCode(StatusCodes.Status502BadGateway, new ErrorResponse { Error = "CV matcher API request failed.", Code = "upstream_request_failed" }); } } } diff --git a/api/Controllers/FileDownloadController.cs b/api/Controllers/FileDownloadController.cs index 2601843..6adf28f 100644 --- a/api/Controllers/FileDownloadController.cs +++ b/api/Controllers/FileDownloadController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Swashbuckle.AspNetCore.Annotations; +using Shared.Models.Responses; namespace Api.Controllers { @@ -57,10 +58,10 @@ namespace Api.Controllers [SwaggerResponse(StatusCodes.Status500InternalServerError, "Unexpected server error while downloading")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status206PartialContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)] public async Task DownloadFile(string? fileName = null) { try @@ -73,7 +74,7 @@ namespace Api.Controllers if (string.IsNullOrWhiteSpace(fileName)) { _logger.LogWarning("No file name provided and no default file name configured"); - return BadRequest(new { error = "File name is required" }); + return BadRequest(new ErrorResponse { Error = "File name is required", Code = "file_name_required" }); } _logger.LogInformation("Using default file name from settings: {FileName}", fileName); @@ -102,7 +103,7 @@ namespace Api.Controllers if (!System.IO.File.Exists(filePath)) { _logger.LogWarning("File not found: {FilePath}", filePath); - return NotFound(new { error = "File not found" }); + return NotFound(new ErrorResponse { Error = "File not found", Code = "file_not_found" }); } var fileInfo = new FileInfo(filePath); @@ -150,7 +151,7 @@ namespace Api.Controllers catch (Exception ex) { _logger.LogError(ex, "Error downloading file: {FileName}", fileName); - return StatusCode(StatusCodes.Status500InternalServerError, new { error = "An error occurred while downloading the file" }); + return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "An error occurred while downloading the file", Code = "download_failed" }); } } diff --git a/api/api.csproj b/api/api.csproj index 642cfe6..c5cf142 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -37,6 +37,7 @@ + diff --git a/cv-matcher-api/Controllers/CvController.cs b/cv-matcher-api/Controllers/CvController.cs index edd343d..87804aa 100644 --- a/cv-matcher-api/Controllers/CvController.cs +++ b/cv-matcher-api/Controllers/CvController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using CvMatcher.Models.Responses; using Shared.Models.Requests; using Swashbuckle.AspNetCore.Annotations; +using Shared.Models.Responses; namespace Api.Controllers; @@ -26,12 +27,12 @@ public sealed class CvController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "CV uploaded and indexed successfully")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid upload request")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] public async Task> Upload([FromForm] UploadFileRequest request, CancellationToken ct) { try { - if (request.File is null) return BadRequest(new { error = "Missing CV PDF." }); + if (request.File is null) return BadRequest(new ErrorResponse { Error = "Missing CV PDF.", Code = "cv_file_missing" }); _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); @@ -40,7 +41,7 @@ public sealed class CvController : ControllerBase catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid CV upload request."); - return BadRequest(new { error = ex.Message }); + return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" }); } } @@ -49,7 +50,7 @@ public sealed class CvController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "Matching jobs returned")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid find jobs request")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] public async Task> FindJobs([FromBody] FindJobsRequest request, CancellationToken ct) { try @@ -62,7 +63,7 @@ public sealed class CvController : ControllerBase catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid find jobs request."); - return BadRequest(new { error = ex.Message }); + return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" }); } } @@ -71,7 +72,7 @@ public sealed class CvController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "Job match computed successfully")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid match job request")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] public async Task> MatchJob([FromBody] MatchJobRequest request, CancellationToken ct) { try @@ -85,7 +86,7 @@ public sealed class CvController : ControllerBase catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid match job request."); - return BadRequest(new { error = ex.Message }); + return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" }); } } } diff --git a/rag-api/Controllers/RagController.cs b/rag-api/Controllers/RagController.cs index c0e07f7..0354752 100644 --- a/rag-api/Controllers/RagController.cs +++ b/rag-api/Controllers/RagController.cs @@ -3,6 +3,7 @@ using Api.Services.Contracts; using Rag.Models.Requests; using Rag.Models.Responses; using Swashbuckle.AspNetCore.Annotations; +using Shared.Models.Responses; namespace Api.Controllers; @@ -25,7 +26,7 @@ public sealed class RagController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "Document indexed successfully")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid indexing request")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] public async Task> IndexDocument( [FromForm] IndexDocumentUploadRequest request, CancellationToken ct) @@ -57,7 +58,7 @@ public sealed class RagController : ControllerBase catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid document indexing request."); - return BadRequest(new { error = ex.Message }); + return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" }); } } @@ -66,7 +67,7 @@ public sealed class RagController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "JSON document indexed successfully")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid JSON indexing request")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] public async Task> IndexJsonDocument([FromBody] IndexDocumentRequest request, CancellationToken ct) { try @@ -81,7 +82,7 @@ public sealed class RagController : ControllerBase catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid JSON document indexing request."); - return BadRequest(new { error = ex.Message }); + return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" }); } } @@ -90,7 +91,7 @@ public sealed class RagController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "Search results returned")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid search request")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] public async Task> Search([FromBody] SearchRequest request, CancellationToken ct) { try @@ -104,7 +105,7 @@ public sealed class RagController : ControllerBase catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Invalid semantic search request."); - return BadRequest(new { error = ex.Message }); + return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" }); } } @@ -113,7 +114,7 @@ public sealed class RagController : ControllerBase [SwaggerResponse(StatusCodes.Status200OK, "Document details returned")] [SwaggerResponse(StatusCodes.Status404NotFound, "Document was not found")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] public async Task> GetDocument(string id, CancellationToken ct) { _logger.LogInformation("Get document request received. DocumentId={DocumentId}", id); @@ -121,7 +122,7 @@ public sealed class RagController : ControllerBase if (document is null) { _logger.LogWarning("Document not found. DocumentId={DocumentId}", id); - return NotFound(new { error = "Document not found." }); + return NotFound(new ErrorResponse { Error = "Document not found.", Code = "document_not_found" }); } return Ok(document); } diff --git a/shared-models/Responses/ErrorResponse.cs b/shared-models/Responses/ErrorResponse.cs new file mode 100644 index 0000000..c688715 --- /dev/null +++ b/shared-models/Responses/ErrorResponse.cs @@ -0,0 +1,9 @@ +namespace Shared.Models.Responses; + +public sealed class ErrorResponse +{ + public string Error { get; init; } = string.Empty; + public string? Code { get; init; } + public string? Detail { get; init; } + public double? Score { get; init; } +} diff --git a/web/wwwroot/cv-matcher/index.html b/web/wwwroot/cv-matcher/index.html index 5b5a784..bb952b9 100644 --- a/web/wwwroot/cv-matcher/index.html +++ b/web/wwwroot/cv-matcher/index.html @@ -136,6 +136,13 @@ +40 722-523-764 +
+ WhatsApp + + +40 722-523-764 + +
+
diff --git a/web/wwwroot/index.html b/web/wwwroot/index.html index 50c6ee4..66a2229 100644 --- a/web/wwwroot/index.html +++ b/web/wwwroot/index.html @@ -133,7 +133,7 @@