@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using Models.Settings;
|
using Models.Settings;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using Models.Requests;
|
using Models.Requests;
|
||||||
|
using Shared.Models.Responses;
|
||||||
|
|
||||||
namespace Api.Controllers
|
namespace Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -42,17 +43,26 @@ namespace Api.Controllers
|
|||||||
[HttpPost("verify")]
|
[HttpPost("verify")]
|
||||||
[SwaggerOperation(Summary = "Verify captcha token")]
|
[SwaggerOperation(Summary = "Verify captcha token")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[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<IActionResult> Verify([FromBody] CaptchaVerifyRequest req, CancellationToken ct)
|
public async Task<IActionResult> 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 userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
var verdict = await _captcha.VerifyAsync(req.Token, userIp, req.ExpectedAction, ct);
|
var verdict = await _captcha.VerifyAsync(req.Token, userIp, req.ExpectedAction, ct);
|
||||||
if (!verdict.Success)
|
if (!verdict.Success)
|
||||||
{
|
{
|
||||||
_log.LogWarning("Captcha failed. ip={Ip} score={Score} err={Err}", userIp, verdict.Score, verdict.Error);
|
_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);
|
return Ok(verdict);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using Models.Settings;
|
using Models.Settings;
|
||||||
using Models.Requests;
|
using Models.Requests;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using Shared.Models.Responses;
|
||||||
|
|
||||||
namespace Api.Controllers
|
namespace Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -48,8 +49,8 @@ namespace Api.Controllers
|
|||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request or captcha verification failed")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request or captcha verification failed")]
|
||||||
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Contact message could not be sent due to server error")]
|
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Contact message could not be sent due to server error")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<IActionResult> Send([FromBody] ContactRequest req, CancellationToken ct)
|
public async Task<IActionResult> Send([FromBody] ContactRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@@ -57,7 +58,10 @@ namespace Api.Controllers
|
|||||||
|
|
||||||
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, "contact", ct);
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -67,7 +71,7 @@ namespace Api.Controllers
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_log.LogError(ex, "Contact send failed. ip={Ip} from={From}", userIp, req.Email);
|
_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.Status400BadRequest, "Invalid request or captcha verification failed")]
|
||||||
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Subscription failed due to server error")]
|
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Subscription failed due to server error")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<IActionResult> Subscribe([FromBody] SubscribeRequest req, CancellationToken ct)
|
public async Task<IActionResult> Subscribe([FromBody] SubscribeRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@@ -98,7 +102,10 @@ namespace Api.Controllers
|
|||||||
|
|
||||||
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, "contact", ct);
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -108,7 +115,7 @@ namespace Api.Controllers
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_log.LogError(ex, "Subscription failed. ip={Ip} eMail={eMail}", userIp, req.Email);
|
_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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Api.Services.Contracts;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using Shared.Models.Responses;
|
||||||
|
|
||||||
namespace Api.Controllers;
|
namespace Api.Controllers;
|
||||||
|
|
||||||
@@ -37,8 +38,9 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
[RequestSizeLimit(8 * 1024 * 1024)]
|
[RequestSizeLimit(8 * 1024 * 1024)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
[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.")]
|
[SwaggerOperation(Summary = "Upload CV", Description = "Proxy upload of a CV PDF to the internal cv-matcher-api.")]
|
||||||
[SwaggerResponse(StatusCodes.Status200OK, "Upload succeeded")]
|
[SwaggerResponse(StatusCodes.Status200OK, "Upload succeeded")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Missing or invalid input")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Missing or invalid input")]
|
||||||
@@ -49,7 +51,7 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (request.File is null)
|
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;
|
var cv = request.File;
|
||||||
@@ -62,7 +64,7 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
if (!verdict.Success)
|
if (!verdict.Success)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp);
|
_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.");
|
if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required.");
|
||||||
@@ -78,12 +80,12 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("CV upload proxy request was cancelled by the client.");
|
_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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "CV upload proxy request failed.");
|
_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
|
|||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
[HttpPost("match-job")]
|
[HttpPost("match-job")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status502BadGateway)]
|
||||||
|
[ProducesResponseType(typeof(ErrorResponse), 499)]
|
||||||
[SwaggerOperation(Summary = "Match job", Description = "Proxy job matching request to the internal cv-matcher-api.")]
|
[SwaggerOperation(Summary = "Match job", Description = "Proxy job matching request to the internal cv-matcher-api.")]
|
||||||
[SwaggerResponse(StatusCodes.Status200OK, "Match succeeded")]
|
[SwaggerResponse(StatusCodes.Status200OK, "Match succeeded")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request")]
|
||||||
@@ -109,7 +112,7 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
if (!verdict.Success)
|
if (!verdict.Success)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp);
|
_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}",
|
_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)
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Job match proxy request was cancelled by the client.");
|
_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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Job match proxy request failed.");
|
_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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.StaticFiles;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using Shared.Models.Responses;
|
||||||
|
|
||||||
namespace Api.Controllers
|
namespace Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -57,10 +58,10 @@ namespace Api.Controllers
|
|||||||
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Unexpected server error while downloading")]
|
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Unexpected server error while downloading")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status206PartialContent)]
|
[ProducesResponseType(StatusCodes.Status206PartialContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)]
|
[ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<IActionResult> DownloadFile(string? fileName = null)
|
public async Task<IActionResult> DownloadFile(string? fileName = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -73,7 +74,7 @@ namespace Api.Controllers
|
|||||||
if (string.IsNullOrWhiteSpace(fileName))
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No file name provided and no default file name configured");
|
_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);
|
_logger.LogInformation("Using default file name from settings: {FileName}", fileName);
|
||||||
@@ -102,7 +103,7 @@ namespace Api.Controllers
|
|||||||
if (!System.IO.File.Exists(filePath))
|
if (!System.IO.File.Exists(filePath))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("File not found: {FilePath}", 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);
|
var fileInfo = new FileInfo(filePath);
|
||||||
@@ -150,7 +151,7 @@ namespace Api.Controllers
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error downloading file: {FileName}", fileName);
|
_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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\api-models\api-models.csproj" />
|
<ProjectReference Include="..\api-models\api-models.csproj" />
|
||||||
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
|
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
|
||||||
|
<ProjectReference Include="..\shared-models\shared-models.csproj" />
|
||||||
<ProjectReference Include="..\startup-helpers\startup-helpers.csproj" />
|
<ProjectReference Include="..\startup-helpers\startup-helpers.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using CvMatcher.Models.Responses;
|
using CvMatcher.Models.Responses;
|
||||||
using Shared.Models.Requests;
|
using Shared.Models.Requests;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using Shared.Models.Responses;
|
||||||
|
|
||||||
namespace Api.Controllers;
|
namespace Api.Controllers;
|
||||||
|
|
||||||
@@ -26,12 +27,12 @@ public sealed class CvController : ControllerBase
|
|||||||
[SwaggerResponse(StatusCodes.Status200OK, "CV uploaded and indexed successfully")]
|
[SwaggerResponse(StatusCodes.Status200OK, "CV uploaded and indexed successfully")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid upload request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid upload request")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<CvUploadResponse>> Upload([FromForm] UploadFileRequest request, CancellationToken ct)
|
public async Task<ActionResult<CvUploadResponse>> Upload([FromForm] UploadFileRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
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);
|
_logger.LogInformation("CV upload received. FileName={FileName}, Size={SizeBytes}", request.File.FileName, request.File.Length);
|
||||||
var result = await _service.UploadCvAsync(request.File, ct);
|
var result = await _service.UploadCvAsync(request.File, ct);
|
||||||
_logger.LogInformation("CV upload processed. CvDocumentId={CvDocumentId}, Cached={Cached}", result.DocumentId, result.Cached);
|
_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)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Invalid CV upload request.");
|
_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.Status200OK, "Matching jobs returned")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid find jobs request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid find jobs request")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<FindJobsResponse>> FindJobs([FromBody] FindJobsRequest request, CancellationToken ct)
|
public async Task<ActionResult<FindJobsResponse>> FindJobs([FromBody] FindJobsRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -62,7 +63,7 @@ public sealed class CvController : ControllerBase
|
|||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Invalid find jobs request.");
|
_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.Status200OK, "Job match computed successfully")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid match job request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid match job request")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<JobMatchResponse>> MatchJob([FromBody] MatchJobRequest request, CancellationToken ct)
|
public async Task<ActionResult<JobMatchResponse>> MatchJob([FromBody] MatchJobRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -85,7 +86,7 @@ public sealed class CvController : ControllerBase
|
|||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Invalid match job request.");
|
_logger.LogWarning(ex, "Invalid match job request.");
|
||||||
return BadRequest(new { error = ex.Message });
|
return BadRequest(new ErrorResponse { Error = ex.Message, Code = "invalid_request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Api.Services.Contracts;
|
|||||||
using Rag.Models.Requests;
|
using Rag.Models.Requests;
|
||||||
using Rag.Models.Responses;
|
using Rag.Models.Responses;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using Shared.Models.Responses;
|
||||||
|
|
||||||
namespace Api.Controllers;
|
namespace Api.Controllers;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public sealed class RagController : ControllerBase
|
|||||||
[SwaggerResponse(StatusCodes.Status200OK, "Document indexed successfully")]
|
[SwaggerResponse(StatusCodes.Status200OK, "Document indexed successfully")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid indexing request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid indexing request")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<IndexDocumentResponse>> IndexDocument(
|
public async Task<ActionResult<IndexDocumentResponse>> IndexDocument(
|
||||||
[FromForm] IndexDocumentUploadRequest request,
|
[FromForm] IndexDocumentUploadRequest request,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -57,7 +58,7 @@ public sealed class RagController : ControllerBase
|
|||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Invalid document indexing request.");
|
_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.Status200OK, "JSON document indexed successfully")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid JSON indexing request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid JSON indexing request")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<IndexDocumentResponse>> IndexJsonDocument([FromBody] IndexDocumentRequest request, CancellationToken ct)
|
public async Task<ActionResult<IndexDocumentResponse>> IndexJsonDocument([FromBody] IndexDocumentRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -81,7 +82,7 @@ public sealed class RagController : ControllerBase
|
|||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Invalid JSON document indexing request.");
|
_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.Status200OK, "Search results returned")]
|
||||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid search request")]
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid search request")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<SearchResponse>> Search([FromBody] SearchRequest request, CancellationToken ct)
|
public async Task<ActionResult<SearchResponse>> Search([FromBody] SearchRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -104,7 +105,7 @@ public sealed class RagController : ControllerBase
|
|||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Invalid semantic search request.");
|
_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.Status200OK, "Document details returned")]
|
||||||
[SwaggerResponse(StatusCodes.Status404NotFound, "Document was not found")]
|
[SwaggerResponse(StatusCodes.Status404NotFound, "Document was not found")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<RagDocumentDetailsResponse>> GetDocument(string id, CancellationToken ct)
|
public async Task<ActionResult<RagDocumentDetailsResponse>> GetDocument(string id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Get document request received. DocumentId={DocumentId}", id);
|
_logger.LogInformation("Get document request received. DocumentId={DocumentId}", id);
|
||||||
@@ -121,7 +122,7 @@ public sealed class RagController : ControllerBase
|
|||||||
if (document is null)
|
if (document is null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Document not found. DocumentId={DocumentId}", id);
|
_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);
|
return Ok(document);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -136,6 +136,13 @@
|
|||||||
<a href="tel:+40722523764">+40 722-523-764</a>
|
<a href="tel:+40722523764">+40 722-523-764</a>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>WhatsApp</span>
|
||||||
|
<strong>
|
||||||
|
<a href="https://wa.me/40722523764" target="_blank" rel="noreferrer">+40 722-523-764</a>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="contact-form" id="contactForm">
|
<form class="contact-form" id="contactForm">
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span>WhatsApp</span>
|
<span>WhatsApp</span>
|
||||||
<strong>
|
<strong>
|
||||||
<a href="https://wa.me/40744564177" target="_blank" rel="noreferrer">+40 744-564-177</a>
|
<a href="https://wa.me/40722523764" target="_blank" rel="noreferrer">+40 722-523-764</a>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user