using Api.Services.Contracts.Models;
using Api.Services.Contracts;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Api.Models.Settings;
using Api.Models.Requests;
namespace Api.Controllers
{
///
/// Exposes endpoints used by the frontend to send contact messages and to
/// subscribe to newsletters. All endpoints are protected by reCAPTCHA
/// verification and rate limiting.
///
[ApiController]
[Route("api/[controller]")]
[EnableCors("FrontendOnly")]
public sealed class ContactController : ControllerBase
{
private readonly CaptchaSettings _captchaSettings;
private readonly ICaptchaVerifier _captcha;
private readonly IEmailSender _email;
private readonly ILogger _log;
public ContactController(IOptions options, ICaptchaVerifier captcha, IEmailSender email, ILogger log)
{
_captchaSettings = options.Value;
_captcha = captcha;
_email = email;
_log = log;
}
///
/// Returns the public reCAPTCHA site key used by the client to render
/// the reCAPTCHA widget and obtain client-side tokens.
///
/// 200 OK with the public site key as a string.
// ReCaptcha endpoints have been extracted to CaptchaController
///
/// Validates the provided reCAPTCHA token and sends a contact message
/// via the configured email sender.
///
/// Contact request containing name, email, subject,
/// and message. The CaptchaToken field is required for verification.
/// Cancellation token.
///
/// 200 OK when the message was queued/sent; 400 Bad Request when
/// captcha verification fails; 500 on internal errors.
///
[HttpPost]
[EnableRateLimiting("contact")]
public async Task Send([FromBody] ContactRequest req, CancellationToken ct)
{
if (!ModelState.IsValid)
return ValidationProblem(ModelState);
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, ct);
if (!verdict.Success) return BadRequest("Captcha verification failed.");
try
{
await _email.SendContactAsync(req, ct);
return Ok(new { ok = true });
}
catch (Exception ex)
{
_log.LogError(ex, "Contact send failed. ip={Ip} from={From}", userIp, req.Email);
return StatusCode(500, "Could not send message.");
}
}
///
/// Validates the provided reCAPTCHA token and subscribes the given
/// email address to the newsletter or mailing list.
///
/// Subscription request containing the email and
/// the CaptchaToken.
/// Cancellation token.
///
/// 200 OK when subscription succeeded; 400 when captcha verification
/// fails; 500 on internal errors.
///
[HttpPost("subscribe")]
[EnableRateLimiting("contact")]
public async Task Subscribe([FromBody] SubscribeRequest req, CancellationToken ct)
{
if (!ModelState.IsValid)
return ValidationProblem(ModelState);
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, ct);
if (!verdict.Success) return BadRequest("Captcha verification failed.");
try
{
await _email.SendSubscribeAsync(req, ct);
return Ok(new { ok = true });
}
catch (Exception ex)
{
_log.LogError(ex, "Subscription failed. ip={Ip} eMail={eMail}", userIp, req.Email);
return StatusCode(500, "Failed.");
}
}
// Captcha verification helper was moved to CaptchaController; ContactController calls _captcha.VerifyAsync directly.
}
}