CaptchaCallbackService
Documentație pentru serviciul CaptchaCallbackService
1. Descriere Generală
CaptchaCallbackService
este un serviciu care gestionează callback-urile pentru componentele reCAPTCHA, permițând altor componente să se aboneze la evenimentele de succes sau expirare ale validării captcha. Deși înregistrat în DI, implementarea actuală folosește RecaptchaService
pentru validare directă.
Caracteristici principale: - Gestionare evenimente captcha succes/expirare - Pattern Observer pentru notificări globale - Integrare cu Google reCAPTCHA v2 - Suport pentru multiple componente captcha - Decuplare între componente și logica captcha
2. Configurare și Înregistrare
// Program.cs sau Startup.cs
// Înregistrare automată prin AddFodComponents
builder.Services.AddFodComponents(configuration);
// Sau înregistrare manuală
builder.Services.AddScoped<ICaptchaCallBackService, CaptchaCallbackService>();
3. Interfața ICaptchaCallBackService
namespace FOD.Components.Services
{
public interface ICaptchaCallBackService
{
EventHandler<CaptchaSuccessEventArgs> SuccessCallBack { get; set; }
EventHandler<CaptchaTimeOutEventArgs> TimeOutCallBack { get; set; }
}
}
4. Evenimente și Modele
CaptchaSuccessEventArgs
public class CaptchaSuccessEventArgs : EventArgs
{
public string Response { get; set; }
public CaptchaSuccessEventArgs(string response)
{
Response = response;
}
}
CaptchaTimeOutEventArgs
public class CaptchaTimeOutEventArgs : EventArgs
{
public DateTime ExpiredAt { get; set; }
public CaptchaTimeOutEventArgs()
{
ExpiredAt = DateTime.Now;
}
}
5. Exemple de Utilizare
Utilizare basic cu callback
@page "/formular-cu-captcha"
@inject ICaptchaCallBackService CaptchaCallbackService
@inject IFodNotificationService NotificationService
@implements IDisposable
<FodContainer>
<FodCard>
<FodCardContent>
<FodText Typo="Typo.h6" GutterBottom="true">
Formular cu Protecție Captcha
</FodText>
<form @onsubmit="HandleSubmit" @onsubmit:preventDefault="true">
<!-- Câmpuri formular -->
<FodGrid Container="true" Spacing="3">
<FodGrid Item="true" xs="12">
<FodInput @bind-Value="model.Name"
Label="Nume complet"
Required="true" />
</FodGrid>
<FodGrid Item="true" xs="12">
<FodInput @bind-Value="model.Email"
Label="Email"
Type="email"
Required="true" />
</FodGrid>
<FodGrid Item="true" xs="12">
<FodTextArea @bind-Value="model.Message"
Label="Mesaj"
Rows="4"
Required="true" />
</FodGrid>
</FodGrid>
<!-- Componenta reCAPTCHA -->
<div class="mt-3">
<FodRecaptcha @ref="recaptcha" />
</div>
<!-- Buton submit -->
<FodButton Type="submit"
Color="FodColor.Primary"
Class="mt-3"
Disabled="@(!isCaptchaValid || isSubmitting)">
@if (isSubmitting)
{
<FodLoadingButton />
}
else
{
<text>Trimite</text>
}
</FodButton>
</form>
@if (!string.IsNullOrEmpty(captchaToken))
{
<FodAlert Severity="FodSeverity.Success" Class="mt-3">
Captcha validat cu succes!
</FodAlert>
}
</FodCardContent>
</FodCard>
</FodContainer>
@code {
private ContactFormModel model = new();
private FodRecaptcha recaptcha;
private string captchaToken;
private bool isCaptchaValid;
private bool isSubmitting;
protected override void OnInitialized()
{
// Abonare la evenimente captcha
CaptchaCallbackService.SuccessCallBack += OnCaptchaSuccess;
CaptchaCallbackService.TimeOutCallBack += OnCaptchaTimeout;
}
private void OnCaptchaSuccess(object sender, CaptchaSuccessEventArgs e)
{
captchaToken = e.Response;
isCaptchaValid = true;
NotificationService.Success("Captcha validat cu succes!");
StateHasChanged();
}
private void OnCaptchaTimeout(object sender, CaptchaTimeOutEventArgs e)
{
captchaToken = null;
isCaptchaValid = false;
NotificationService.Warning("Captcha a expirat. Vă rugăm să reîncercați.");
StateHasChanged();
}
private async Task HandleSubmit()
{
if (!isCaptchaValid || string.IsNullOrEmpty(captchaToken))
{
NotificationService.Error("Vă rugăm completați captcha!");
return;
}
isSubmitting = true;
try
{
// Trimite formularul cu token captcha
await ContactService.SendMessage(model, captchaToken);
NotificationService.Success("Mesaj trimis cu succes!");
// Reset formular
model = new();
captchaToken = null;
isCaptchaValid = false;
await recaptcha.ResetAsync();
}
catch (Exception ex)
{
NotificationService.Error($"Eroare: {ex.Message}");
}
finally
{
isSubmitting = false;
}
}
public void Dispose()
{
// Dezabonare de la evenimente
CaptchaCallbackService.SuccessCallBack -= OnCaptchaSuccess;
CaptchaCallbackService.TimeOutCallBack -= OnCaptchaTimeout;
}
}
Manager global pentru captcha
public class CaptchaManager
{
private readonly ICaptchaCallBackService _captchaCallbackService;
private readonly ILogger<CaptchaManager> _logger;
private readonly List<CaptchaSubscriber> _subscribers = new();
private string _currentToken;
private DateTime? _tokenExpiry;
public CaptchaManager(ICaptchaCallBackService captchaCallbackService,
ILogger<CaptchaManager> logger)
{
_captchaCallbackService = captchaCallbackService;
_logger = logger;
// Configurare handlers globali
_captchaCallbackService.SuccessCallBack += OnGlobalCaptchaSuccess;
_captchaCallbackService.TimeOutCallBack += OnGlobalCaptchaTimeout;
}
public string CurrentToken => _currentToken;
public bool IsTokenValid => !string.IsNullOrEmpty(_currentToken) &&
_tokenExpiry > DateTime.Now;
public void Subscribe(string componentId, Action<string> onSuccess,
Action onTimeout = null)
{
_subscribers.Add(new CaptchaSubscriber
{
ComponentId = componentId,
OnSuccess = onSuccess,
OnTimeout = onTimeout
});
}
public void Unsubscribe(string componentId)
{
_subscribers.RemoveAll(s => s.ComponentId == componentId);
}
private void OnGlobalCaptchaSuccess(object sender, CaptchaSuccessEventArgs e)
{
_currentToken = e.Response;
_tokenExpiry = DateTime.Now.AddMinutes(2); // reCAPTCHA token valid 2 minute
_logger.LogInformation("Captcha token received and cached");
// Notifică toți subscriberii
foreach (var subscriber in _subscribers)
{
try
{
subscriber.OnSuccess?.Invoke(e.Response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error notifying subscriber {ComponentId}",
subscriber.ComponentId);
}
}
}
private void OnGlobalCaptchaTimeout(object sender, CaptchaTimeOutEventArgs e)
{
_currentToken = null;
_tokenExpiry = null;
_logger.LogWarning("Captcha token expired at {ExpiredAt}", e.ExpiredAt);
// Notifică toți subscriberii
foreach (var subscriber in _subscribers)
{
try
{
subscriber.OnTimeout?.Invoke();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error notifying subscriber {ComponentId} of timeout",
subscriber.ComponentId);
}
}
}
private class CaptchaSubscriber
{
public string ComponentId { get; set; }
public Action<string> OnSuccess { get; set; }
public Action OnTimeout { get; set; }
}
}
Implementare alternativă cu RecaptchaService
@* Implementarea actuală folosește RecaptchaService direct *@
@inject IRecaptchaService RecaptchaService
<FodCard>
<FodCardContent>
<FodRecaptcha @ref="recaptcha" />
<FodButton OnClick="ValidateCaptcha"
Color="FodColor.Primary"
Class="mt-3">
Validează
</FodButton>
@if (!string.IsNullOrEmpty(token))
{
<FodAlert Severity="FodSeverity.Success" Class="mt-3">
Token obținut: @token.Substring(0, 20)...
</FodAlert>
}
</FodCardContent>
</FodCard>
@code {
private FodRecaptcha recaptcha;
private string token;
private async Task ValidateCaptcha()
{
try
{
// Metodă directă fără callback
token = await RecaptchaService.Execute();
if (!string.IsNullOrEmpty(token))
{
NotificationService.Success("Captcha validat!");
// Procesare token
}
}
catch (Exception ex)
{
NotificationService.Error($"Eroare captcha: {ex.Message}");
}
}
}
6. Integrare cu componente multiple
@* Pagină cu multiple formulare protejate *@
@inject ICaptchaCallBackService CaptchaCallbackService
@inject CaptchaManager CaptchaManager
<FodContainer>
<FodGrid Container="true" Spacing="3">
<!-- Formular 1 -->
<FodGrid Item="true" xs="12" md="6">
<ContactForm Id="contact-form" />
</FodGrid>
<!-- Formular 2 -->
<FodGrid Item="true" xs="12" md="6">
<FeedbackForm Id="feedback-form" />
</FodGrid>
</FodGrid>
<!-- Captcha centralizat -->
<FodCard Class="mt-4">
<FodCardContent>
<FodText Typo="Typo.h6" GutterBottom="true">
Verificare Securitate
</FodText>
<FodText Typo="Typo.body2" GutterBottom="true">
Completați captcha pentru a activa formularele
</FodText>
<FodRecaptcha />
@if (CaptchaManager.IsTokenValid)
{
<FodChip Color="FodColor.Success"
Icon="@FodIcons.Material.Filled.CheckCircle"
Class="mt-2">
Verificare completă - formularele sunt active
</FodChip>
}
</FodCardContent>
</FodCard>
</FodContainer>
7. Wrapper pentru captcha invisible
public class InvisibleCaptchaService
{
private readonly ICaptchaCallBackService _callbackService;
private readonly IJSRuntime _jsRuntime;
private TaskCompletionSource<string> _pendingValidation;
public InvisibleCaptchaService(ICaptchaCallBackService callbackService,
IJSRuntime jsRuntime)
{
_callbackService = callbackService;
_jsRuntime = jsRuntime;
_callbackService.SuccessCallBack += OnCaptchaSuccess;
_callbackService.TimeOutCallBack += OnCaptchaTimeout;
}
public async Task<string> ExecuteAsync(string action = "submit")
{
_pendingValidation = new TaskCompletionSource<string>();
// Declanșează captcha invisible
await _jsRuntime.InvokeVoidAsync("executeInvisibleCaptcha", action);
// Așteaptă callback cu timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
cts.Token.Register(() => _pendingValidation.TrySetCanceled());
return await _pendingValidation.Task;
}
private void OnCaptchaSuccess(object sender, CaptchaSuccessEventArgs e)
{
_pendingValidation?.TrySetResult(e.Response);
}
private void OnCaptchaTimeout(object sender, CaptchaTimeOutEventArgs e)
{
_pendingValidation?.TrySetException(
new TimeoutException("Captcha validation timed out"));
}
}
8. Monitorizare și metrici
public class MonitoredCaptchaCallbackService : ICaptchaCallBackService
{
private readonly CaptchaCallbackService _innerService;
private readonly IMetricsCollector _metrics;
private readonly ILogger<MonitoredCaptchaCallbackService> _logger;
public EventHandler<CaptchaSuccessEventArgs> SuccessCallBack
{
get => _innerService.SuccessCallBack;
set
{
_innerService.SuccessCallBack = value;
_innerService.SuccessCallBack += OnSuccessCallback;
}
}
public EventHandler<CaptchaTimeOutEventArgs> TimeOutCallBack
{
get => _innerService.TimeOutCallBack;
set
{
_innerService.TimeOutCallBack = value;
_innerService.TimeOutCallBack += OnTimeoutCallback;
}
}
private void OnSuccessCallback(object sender, CaptchaSuccessEventArgs e)
{
_metrics.RecordCounter("captcha_success_total", 1);
_logger.LogInformation("Captcha validation successful");
}
private void OnTimeoutCallback(object sender, CaptchaTimeOutEventArgs e)
{
_metrics.RecordCounter("captcha_timeout_total", 1);
_logger.LogWarning("Captcha timeout at {ExpiredAt}", e.ExpiredAt);
}
}
9. Testare
[TestClass]
public class CaptchaCallbackServiceTests
{
private ICaptchaCallBackService _service;
private bool _successCallbackInvoked;
private bool _timeoutCallbackInvoked;
private string _receivedToken;
[TestInitialize]
public void Setup()
{
_service = new CaptchaCallbackService();
_successCallbackInvoked = false;
_timeoutCallbackInvoked = false;
_receivedToken = null;
_service.SuccessCallBack += (sender, e) =>
{
_successCallbackInvoked = true;
_receivedToken = e.Response;
};
_service.TimeOutCallBack += (sender, e) =>
{
_timeoutCallbackInvoked = true;
};
}
[TestMethod]
public void SuccessCallback_WhenInvoked_NotifiesSubscribers()
{
// Arrange
var token = "test-captcha-token";
var args = new CaptchaSuccessEventArgs(token);
// Act
_service.SuccessCallBack?.Invoke(this, args);
// Assert
Assert.IsTrue(_successCallbackInvoked);
Assert.AreEqual(token, _receivedToken);
}
[TestMethod]
public void TimeoutCallback_WhenInvoked_NotifiesSubscribers()
{
// Arrange
var args = new CaptchaTimeOutEventArgs();
// Act
_service.TimeOutCallBack?.Invoke(this, args);
// Assert
Assert.IsTrue(_timeoutCallbackInvoked);
Assert.IsFalse(_successCallbackInvoked);
}
}
10. JavaScript Integration
// ReCaptcha.js - Integrare cu callback service
window.fodCaptcha = {
dotNetRef: null,
init: function(dotNetReference) {
this.dotNetRef = dotNetReference;
},
onSuccess: function(token) {
if (this.dotNetRef) {
this.dotNetRef.invokeMethodAsync('CallbackOnSuccess', token)
.catch(err => console.error('Error invoking success callback:', err));
}
},
onExpired: function() {
if (this.dotNetRef) {
this.dotNetRef.invokeMethodAsync('CallbackOnExpired')
.catch(err => console.error('Error invoking timeout callback:', err));
}
},
executeInvisible: function(action) {
grecaptcha.execute(window.fodRecaptchaSiteKey, { action: action });
}
};
11. Best Practices
- Dezabonare obligatorie - Întotdeauna dezabonați-vă în Dispose()
- Validare token - Verificați întotdeauna validitatea token-ului
- Timeout handling - Gestionați expirarea token-ului (2 minute)
- Error handling - Tratați erorile de callback
- Centralizare - Folosiți un manager pentru multiple componente
- Logging - Înregistrați toate evenimentele captcha
- Fallback - Aveți strategie alternativă la eșec
12. Concluzie
CaptchaCallbackService
oferă un mecanism flexibil pentru gestionarea evenimentelor reCAPTCHA în aplicațiile Blazor. Deși implementarea curentă folosește RecaptchaService
pentru validare directă, serviciul de callback permite scenarii mai complexe cu notificări globale și gestionare centralizată a validării captcha pentru multiple componente.