Initial commit
Build and Push Docker Images / build (push) Successful in 29s

This commit is contained in:
2026-05-02 21:31:31 +03:00
commit fc2dd721e4
78 changed files with 5002 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
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);
}
}
}