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 { /// /// Endpoints that expose captcha configuration and verification. /// [ApiController] [Route("api/[controller]")] public sealed class CaptchaController : ControllerBase { private readonly CaptchaSettings _captchaSettings; private readonly ICaptchaVerifier _captcha; private readonly ILogger _log; public CaptchaController(IOptions options, ICaptchaVerifier captcha, ILogger log) { _captchaSettings = options.Value; _captcha = captcha; _log = log; } /// /// Returns the public reCAPTCHA site key used by the client to render the widget. /// /// 200 OK with the configured public site key as a plain string. [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); } /// /// Verifies a reCAPTCHA token submitted by the client and returns the full verification verdict. /// /// The verification request containing the token and optional expected action name. /// Cancellation token. /// /// 200 OK with the full captcha verdict when verification passes; /// 400 Bad Request with an if the token is missing or verification fails. /// [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 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); } } }