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 Models.Settings; using Models.Requests; using Swashbuckle.AspNetCore.Annotations; using Shared.Models.Responses; 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")] [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(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)] 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, "contact", ct); if (!verdict.Success) { return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "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(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "Could not send message.", Code = "contact_send_failed" }); } } /// /// 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")] [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(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)] 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, "contact", ct); if (!verdict.Success) { return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "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(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "Failed.", Code = "subscription_failed" }); } } } }