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 ICaptchaVerifier _captcha; private readonly IEmailSender _email; private readonly ILogger _log; public ContactController(ICaptchaVerifier captcha, IEmailSender email, ILogger log) { _captcha = captcha; _email = email; _log = log; } /// /// 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."); } } } }