134 lines
5.2 KiB
C#
134 lines
5.2 KiB
C#
using Api.Models;
|
|
using Api.Services.Contracts;
|
|
using Api.Settings;
|
|
using Microsoft.AspNetCore.Cors;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
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 CaptchaSettings _captchaSettings;
|
|
private readonly ICaptchaVerifier _captcha;
|
|
private readonly IEmailSender _email;
|
|
private readonly ILogger<ContactController> _log;
|
|
|
|
public ContactController(IOptions<CaptchaSettings> options, ICaptchaVerifier captcha, IEmailSender email, ILogger<ContactController> log)
|
|
{
|
|
_captchaSettings = options.Value;
|
|
_captcha = captcha;
|
|
_email = email;
|
|
_log = log;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the public reCAPTCHA site key used by the client to render
|
|
/// the reCAPTCHA widget and obtain client-side tokens.
|
|
/// </summary>
|
|
/// <returns>200 OK with the public site key as a string.</returns>
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetReCaptchaSiteKey(CancellationToken ct)
|
|
{
|
|
return Ok(_captchaSettings.PublicKey);
|
|
}
|
|
|
|
/// <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")]
|
|
public async Task<IActionResult> Send([FromBody] ContactRequest req, CancellationToken ct)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
return ValidationProblem(ModelState);
|
|
|
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
|
|
|
var res = await ValidateCaptcha(req.CaptchaToken, ct);
|
|
if (!res.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}", res.UserIp, req.Email);
|
|
return StatusCode(500, "Could not send message.");
|
|
}
|
|
}
|
|
|
|
/// <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")]
|
|
public async Task<IActionResult> Subscribe([FromBody] SubscribeRequest req, CancellationToken ct)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
return ValidationProblem(ModelState);
|
|
|
|
var res = await ValidateCaptcha(req.CaptchaToken, ct);
|
|
if (!res.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}", res.UserIp, req.Email);
|
|
return StatusCode(500, "Failed.");
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper that runs reCAPTCHA verification for the supplied token and
|
|
/// returns the verdict along with the resolved user IP address.
|
|
/// </summary>
|
|
/// <param name="token">Client-provided reCAPTCHA token.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>Tuple containing the verification verdict and user IP.</returns>
|
|
private async Task<(CaptchaVerdict Verdict, string? UserIp)> ValidateCaptcha(string token, CancellationToken ct)
|
|
{
|
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
|
var verdict = await _captcha.VerifyAsync(token, userIp, ct);
|
|
if (!verdict.Success)
|
|
{
|
|
_log.LogWarning("Captcha failed. ip={Ip} score={Score} err={Err}",
|
|
userIp, verdict.Score, verdict.Error);
|
|
}
|
|
return (verdict, userIp);
|
|
}
|
|
}
|
|
}
|