This commit is contained in:
2026-05-08 13:31:41 +03:00
parent 845f41255f
commit 86d4d2af06
8 changed files with 114 additions and 0 deletions
+15
View File
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Models.Settings; using Models.Settings;
using Models.Requests; using Models.Requests;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers namespace Api.Controllers
{ {
@@ -42,6 +43,13 @@ namespace Api.Controllers
/// </returns> /// </returns>
[HttpPost] [HttpPost]
[EnableRateLimiting("contact")] [EnableRateLimiting("contact")]
[SwaggerOperation(Summary = "Send contact message", Description = "Validates captcha and sends a contact message using the configured email sender.")]
[SwaggerResponse(StatusCodes.Status200OK, "Contact message sent")]
[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)]
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)
@@ -76,6 +84,13 @@ namespace Api.Controllers
/// </returns> /// </returns>
[HttpPost("subscribe")] [HttpPost("subscribe")]
[EnableRateLimiting("contact")] [EnableRateLimiting("contact")]
[SwaggerOperation(Summary = "Subscribe email", Description = "Validates captcha and subscribes an email address to the mailing list.")]
[SwaggerResponse(StatusCodes.Status200OK, "Subscription succeeded")]
[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)]
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)
+10
View File
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles; 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;
namespace Api.Controllers namespace Api.Controllers
{ {
@@ -47,10 +48,19 @@ namespace Api.Controllers
/// <response code="404">File not found</response> /// <response code="404">File not found</response>
/// <response code="416">Requested range not satisfiable</response> /// <response code="416">Requested range not satisfiable</response>
[HttpGet("{fileName?}")] [HttpGet("{fileName?}")]
[SwaggerOperation(Summary = "Download file", Description = "Downloads a file with support for full and ranged (resumable) transfers.")]
[SwaggerResponse(StatusCodes.Status200OK, "Full file content returned")]
[SwaggerResponse(StatusCodes.Status206PartialContent, "Partial file content returned for a range request")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "No file name provided and no default configured")]
[SwaggerResponse(StatusCodes.Status404NotFound, "Requested file was not found")]
[SwaggerResponse(StatusCodes.Status416RangeNotSatisfiable, "Requested byte range is invalid")]
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Unexpected server error while downloading")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status206PartialContent)] [ProducesResponseType(StatusCodes.Status206PartialContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)] [ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DownloadFile(string? fileName = null) public async Task<IActionResult> DownloadFile(string? fileName = null)
{ {
try try
+7
View File
@@ -2,6 +2,7 @@ using Models.Settings;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers namespace Api.Controllers
{ {
@@ -29,6 +30,9 @@ namespace Api.Controllers
/// </summary> /// </summary>
/// <returns>200 OK with the Tag Manager ID as a string.</returns> /// <returns>200 OK with the Tag Manager ID as a string.</returns>
[HttpGet("tagmanager")] [HttpGet("tagmanager")]
[SwaggerOperation(Summary = "Get Google Tag Manager ID", Description = "Returns the Google Tag Manager ID configured for frontend analytics.")]
[SwaggerResponse(StatusCodes.Status200OK, "Tag Manager ID returned")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetTagManagerId(CancellationToken ct) public async Task<IActionResult> GetTagManagerId(CancellationToken ct)
{ {
return Ok(_googleSettings.TagManagerId); return Ok(_googleSettings.TagManagerId);
@@ -41,6 +45,9 @@ namespace Api.Controllers
/// </summary> /// </summary>
/// <returns>200 OK with the maps API key as a string.</returns> /// <returns>200 OK with the maps API key as a string.</returns>
[HttpGet("maps")] [HttpGet("maps")]
[SwaggerOperation(Summary = "Get Google Maps key", Description = "Returns the Google Maps API key configured for frontend map features.")]
[SwaggerResponse(StatusCodes.Status200OK, "Maps API key returned")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetMapKey(CancellationToken ct) public async Task<IActionResult> GetMapKey(CancellationToken ct)
{ {
return Ok(_googleSettings.MapKey); return Ok(_googleSettings.MapKey);
+15
View File
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers namespace Api.Controllers
{ {
@@ -22,6 +23,9 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health/live // GET api/health/live
[HttpGet("live")] [HttpGet("live")]
[SwaggerOperation(Summary = "Liveness probe", Description = "Returns whether the API process is alive.")]
[SwaggerResponse(StatusCodes.Status200OK, "Service is alive")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Live() => Ok(new { status = "alive" }); public IActionResult Live() => Ok(new { status = "alive" });
/// <summary> /// <summary>
@@ -33,6 +37,9 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health // GET api/health
[HttpGet] [HttpGet]
[SwaggerOperation(Summary = "Health check", Description = "Returns overall health status and current UTC time.")]
[SwaggerResponse(StatusCodes.Status200OK, "Health check succeeded")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Health() => Ok(new { status = "ok", time = DateTimeOffset.UtcNow }); public IActionResult Health() => Ok(new { status = "ok", time = DateTimeOffset.UtcNow });
/// <summary> /// <summary>
@@ -43,6 +50,9 @@ namespace Api.Controllers
/// <returns>200 OK with the same JSON payload provided in the request body.</returns> /// <returns>200 OK with the same JSON payload provided in the request body.</returns>
// POST api/health/echo // POST api/health/echo
[HttpPost("echo")] [HttpPost("echo")]
[SwaggerOperation(Summary = "Echo payload", Description = "Returns the same JSON payload received in the request body.")]
[SwaggerResponse(StatusCodes.Status200OK, "Payload echoed successfully")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Echo(object payload) => Ok(payload); public IActionResult Echo(object payload) => Ok(payload);
/// <summary> /// <summary>
@@ -55,6 +65,11 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health/ready // GET api/health/ready
[HttpGet("ready")] [HttpGet("ready")]
[SwaggerOperation(Summary = "Readiness probe", Description = "Returns whether the service is ready to accept traffic.")]
[SwaggerResponse(StatusCodes.Status200OK, "Service is ready")]
[SwaggerResponse(StatusCodes.Status503ServiceUnavailable, "Service is not ready")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public IActionResult Ready() public IActionResult Ready()
{ {
var ready = true; var ready = true;
@@ -3,6 +3,7 @@ using Api.Services.Contracts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using CvMatcher.Models.Responses; using CvMatcher.Models.Responses;
using Shared.Models.Requests; using Shared.Models.Requests;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers; namespace Api.Controllers;
@@ -21,6 +22,11 @@ public sealed class CvController : ControllerBase
[HttpPost("upload")] [HttpPost("upload")]
[RequestSizeLimit(10 * 1024 * 1024)] [RequestSizeLimit(10 * 1024 * 1024)]
[SwaggerOperation(Summary = "Upload CV document", Description = "Uploads a CV PDF and indexes it for matching.")]
[SwaggerResponse(StatusCodes.Status200OK, "CV uploaded and indexed successfully")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid upload request")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(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
@@ -39,6 +45,11 @@ public sealed class CvController : ControllerBase
} }
[HttpPost("find-jobs")] [HttpPost("find-jobs")]
[SwaggerOperation(Summary = "Find matching jobs", Description = "Finds top matching jobs for a previously uploaded CV document.")]
[SwaggerResponse(StatusCodes.Status200OK, "Matching jobs returned")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid find jobs request")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(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
@@ -56,6 +67,11 @@ public sealed class CvController : ControllerBase
} }
[HttpPost("match-job")] [HttpPost("match-job")]
[SwaggerOperation(Summary = "Match CV to one job", Description = "Computes detailed match analysis between a CV and a single job description or URL.")]
[SwaggerResponse(StatusCodes.Status200OK, "Job match computed successfully")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid match job request")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(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
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers namespace Api.Controllers
{ {
@@ -19,6 +20,9 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health/live // GET api/health/live
[HttpGet("live")] [HttpGet("live")]
[SwaggerOperation(Summary = "Liveness probe", Description = "Returns whether the API process is alive.")]
[SwaggerResponse(StatusCodes.Status200OK, "Service is alive")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Live() => Ok(new { status = "alive" }); public IActionResult Live() => Ok(new { status = "alive" });
/// <summary> /// <summary>
@@ -30,6 +34,9 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health // GET api/health
[HttpGet] [HttpGet]
[SwaggerOperation(Summary = "Health check", Description = "Returns overall health status and current UTC time.")]
[SwaggerResponse(StatusCodes.Status200OK, "Health check succeeded")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Health() => Ok(new { status = "ok", time = DateTimeOffset.UtcNow }); public IActionResult Health() => Ok(new { status = "ok", time = DateTimeOffset.UtcNow });
/// <summary> /// <summary>
@@ -40,6 +47,9 @@ namespace Api.Controllers
/// <returns>200 OK with the same JSON payload provided in the request body.</returns> /// <returns>200 OK with the same JSON payload provided in the request body.</returns>
// POST api/health/echo // POST api/health/echo
[HttpPost("echo")] [HttpPost("echo")]
[SwaggerOperation(Summary = "Echo payload", Description = "Returns the same JSON payload received in the request body.")]
[SwaggerResponse(StatusCodes.Status200OK, "Payload echoed successfully")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Echo(object payload) => Ok(payload); public IActionResult Echo(object payload) => Ok(payload);
/// <summary> /// <summary>
@@ -52,6 +62,11 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health/ready // GET api/health/ready
[HttpGet("ready")] [HttpGet("ready")]
[SwaggerOperation(Summary = "Readiness probe", Description = "Returns whether the service is ready to accept traffic.")]
[SwaggerResponse(StatusCodes.Status200OK, "Service is ready")]
[SwaggerResponse(StatusCodes.Status503ServiceUnavailable, "Service is not ready")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public IActionResult Ready() public IActionResult Ready()
{ {
var ready = true; var ready = true;
+15
View File
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers namespace Api.Controllers
{ {
@@ -19,6 +20,9 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health/live // GET api/health/live
[HttpGet("live")] [HttpGet("live")]
[SwaggerOperation(Summary = "Liveness probe", Description = "Returns whether the API process is alive.")]
[SwaggerResponse(StatusCodes.Status200OK, "Service is alive")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Live() => Ok(new { status = "alive" }); public IActionResult Live() => Ok(new { status = "alive" });
/// <summary> /// <summary>
@@ -30,6 +34,9 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health // GET api/health
[HttpGet] [HttpGet]
[SwaggerOperation(Summary = "Health check", Description = "Returns overall health status and current UTC time.")]
[SwaggerResponse(StatusCodes.Status200OK, "Health check succeeded")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Health() => Ok(new { status = "ok", time = DateTimeOffset.UtcNow }); public IActionResult Health() => Ok(new { status = "ok", time = DateTimeOffset.UtcNow });
/// <summary> /// <summary>
@@ -40,6 +47,9 @@ namespace Api.Controllers
/// <returns>200 OK with the same JSON payload provided in the request body.</returns> /// <returns>200 OK with the same JSON payload provided in the request body.</returns>
// POST api/health/echo // POST api/health/echo
[HttpPost("echo")] [HttpPost("echo")]
[SwaggerOperation(Summary = "Echo payload", Description = "Returns the same JSON payload received in the request body.")]
[SwaggerResponse(StatusCodes.Status200OK, "Payload echoed successfully")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Echo(object payload) => Ok(payload); public IActionResult Echo(object payload) => Ok(payload);
/// <summary> /// <summary>
@@ -52,6 +62,11 @@ namespace Api.Controllers
/// </returns> /// </returns>
// GET api/health/ready // GET api/health/ready
[HttpGet("ready")] [HttpGet("ready")]
[SwaggerOperation(Summary = "Readiness probe", Description = "Returns whether the service is ready to accept traffic.")]
[SwaggerResponse(StatusCodes.Status200OK, "Service is ready")]
[SwaggerResponse(StatusCodes.Status503ServiceUnavailable, "Service is not ready")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public IActionResult Ready() public IActionResult Ready()
{ {
var ready = true; var ready = true;
+21
View File
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using Api.Services.Contracts; using Api.Services.Contracts;
using Rag.Models.Requests; using Rag.Models.Requests;
using Rag.Models.Responses; using Rag.Models.Responses;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers; namespace Api.Controllers;
@@ -20,6 +21,11 @@ public sealed class RagController : ControllerBase
[HttpPost("documents")] [HttpPost("documents")]
[RequestSizeLimit(10 * 1024 * 1024)] [RequestSizeLimit(10 * 1024 * 1024)]
[SwaggerOperation(Summary = "Index document (multipart)", Description = "Indexes a PDF file or raw text document using multipart/form-data payload.")]
[SwaggerResponse(StatusCodes.Status200OK, "Document indexed successfully")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid indexing request")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IndexDocumentResponse>> IndexDocument( public async Task<ActionResult<IndexDocumentResponse>> IndexDocument(
[FromForm] IndexDocumentUploadRequest request, [FromForm] IndexDocumentUploadRequest request,
CancellationToken ct) CancellationToken ct)
@@ -56,6 +62,11 @@ public sealed class RagController : ControllerBase
} }
[HttpPost("documents/json")] [HttpPost("documents/json")]
[SwaggerOperation(Summary = "Index document (JSON)", Description = "Indexes a text document sent as JSON.")]
[SwaggerResponse(StatusCodes.Status200OK, "JSON document indexed successfully")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid JSON indexing request")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(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
@@ -75,6 +86,11 @@ public sealed class RagController : ControllerBase
} }
[HttpPost("search")] [HttpPost("search")]
[SwaggerOperation(Summary = "Semantic search", Description = "Performs semantic retrieval over indexed documents.")]
[SwaggerResponse(StatusCodes.Status200OK, "Search results returned")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid search request")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(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
@@ -93,6 +109,11 @@ public sealed class RagController : ControllerBase
} }
[HttpGet("documents/{id}")] [HttpGet("documents/{id}")]
[SwaggerOperation(Summary = "Get document details", Description = "Returns indexed document details for the provided document id.")]
[SwaggerResponse(StatusCodes.Status200OK, "Document details returned")]
[SwaggerResponse(StatusCodes.Status404NotFound, "Document was not found")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(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);