7908dad181
- UseJsonExceptionHandler now maps InvalidOperationException to 400 (was 500), so upstream business-rule rejections reach the browser as actionable messages. - CvMatcherController forwards Refit 4xx bodies from cv-matcher-api instead of swallowing them in a generic 502. - ErrorResponse.Score removed; CaptchaController puts the score in Detail. - Frontend extractApiError helper reads the server Error/error/title field for 4xx responses and falls back to a generic i18n string for 5xx / missing body. - All four failure handlers (CV upload, CV match, contact form, subscribe form) updated to use extractApiError with the correct rate-limit i18n key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
3.6 KiB
C#
81 lines
3.6 KiB
C#
using Api.Services.Contracts;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Options;
|
|
using Models.Settings;
|
|
using Swashbuckle.AspNetCore.Annotations;
|
|
using Models.Requests;
|
|
using Common.Responses;
|
|
|
|
namespace Api.Controllers
|
|
{
|
|
/// <summary>
|
|
/// Endpoints that expose captcha configuration and verification.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public sealed class CaptchaController : ControllerBase
|
|
{
|
|
private readonly CaptchaSettings _captchaSettings;
|
|
private readonly ICaptchaVerifier _captcha;
|
|
private readonly ILogger<CaptchaController> _log;
|
|
|
|
public CaptchaController(IOptions<CaptchaSettings> options, ICaptchaVerifier captcha, ILogger<CaptchaController> log)
|
|
{
|
|
_captchaSettings = options.Value;
|
|
_captcha = captcha;
|
|
_log = log;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the public reCAPTCHA site key used by the client to render the widget.
|
|
/// </summary>
|
|
/// <returns>200 OK with the configured public site key as a plain string.</returns>
|
|
[HttpGet]
|
|
[SwaggerOperation(Summary = "Get captcha public key", Description = "Returns the public reCAPTCHA site key required by the frontend to render the challenge widget.")]
|
|
[SwaggerResponse(StatusCodes.Status200OK, "Public site key returned")]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
public IActionResult GetSiteKey()
|
|
{
|
|
return Ok(_captchaSettings.PublicKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies a reCAPTCHA token submitted by the client and returns the full verification verdict.
|
|
/// </summary>
|
|
/// <param name="req">The verification request containing the token and optional expected action name.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>
|
|
/// 200 OK with the full captcha verdict when verification passes;
|
|
/// 400 Bad Request with an <see cref="ErrorResponse"/> if the token is missing or verification fails.
|
|
/// </returns>
|
|
[HttpPost("verify")]
|
|
[SwaggerOperation(Summary = "Verify captcha token", Description = "Verifies a reCAPTCHA token and returns the provider verdict including the score.")]
|
|
[SwaggerResponse(StatusCodes.Status200OK, "Token verified successfully")]
|
|
[SwaggerResponse(StatusCodes.Status400BadRequest, "Token missing or verification failed", typeof(ErrorResponse))]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> Verify([FromBody] CaptchaVerifyRequest req, CancellationToken ct)
|
|
{
|
|
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 ErrorResponse
|
|
{
|
|
Error = "Captcha verification failed.",
|
|
Code = "captcha_verification_failed",
|
|
Detail = verdict.Score.HasValue ? $"Score: {verdict.Score:0.00}" : null
|
|
});
|
|
}
|
|
|
|
return Ok(verdict);
|
|
}
|
|
}
|
|
}
|