124 lines
5.7 KiB
C#
124 lines
5.7 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[EnableCors("FrontendOnly")]
|
|
public sealed class ContactController : ControllerBase
|
|
{
|
|
private readonly ICaptchaVerifier _captcha;
|
|
private readonly IEmailSender _email;
|
|
private readonly ILogger<ContactController> _log;
|
|
|
|
public ContactController(ICaptchaVerifier captcha, IEmailSender email, ILogger<ContactController> log)
|
|
{
|
|
_captcha = captcha;
|
|
_email = email;
|
|
_log = log;
|
|
}
|
|
/// <summary>
|
|
/// Validates the provided reCAPTCHA token and sends a contact message
|
|
/// via the configured email sender.
|
|
/// </summary>
|
|
/// <param name="req">Contact request containing name, email, subject,
|
|
/// and message. The <c>CaptchaToken</c> field is required for verification.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>
|
|
/// 200 OK when the message was queued/sent; 400 Bad Request when
|
|
/// captcha verification fails; 500 on internal errors.
|
|
/// </returns>
|
|
[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<IActionResult> 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" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the provided reCAPTCHA token and subscribes the given
|
|
/// email address to the newsletter or mailing list.
|
|
/// </summary>
|
|
/// <param name="req">Subscription request containing the email and
|
|
/// the <c>CaptchaToken</c>.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>
|
|
/// 200 OK when subscription succeeded; 400 when captcha verification
|
|
/// fails; 500 on internal errors.
|
|
/// </returns>
|
|
[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<IActionResult> 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, "subscribe", 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" });
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|