diff --git a/Moonlight/App/Helpers/MustBeTrueAttribute.cs b/Moonlight/App/Helpers/MustBeTrueAttribute.cs new file mode 100644 index 00000000..72abde24 --- /dev/null +++ b/Moonlight/App/Helpers/MustBeTrueAttribute.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Logging.Net; + +namespace Moonlight.App.Helpers; + +[AttributeUsage(AttributeTargets.Property)] +public class MustBeTrueAttribute : ValidationAttribute +{ + public override bool IsValid(object value) + { + if (value is bool boolValue) + { + return boolValue; + } + + return false; + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/UserRegisterModel.cs b/Moonlight/App/Models/Forms/UserRegisterModel.cs index 399ddb7d..b8ee21d4 100644 --- a/Moonlight/App/Models/Forms/UserRegisterModel.cs +++ b/Moonlight/App/Models/Forms/UserRegisterModel.cs @@ -1,21 +1,31 @@ using System.ComponentModel.DataAnnotations; +using Moonlight.App.Helpers; namespace Moonlight.App.Models.Forms; public class UserRegisterModel { - [Required, EmailAddress] + [Required(ErrorMessage = "You need to specify a email address")] + [EmailAddress(ErrorMessage = "You need to specify a valid email address")] public string Email { get; set; } - [Required, MinLength(3)] + [Required(ErrorMessage = "You need to specify a first name")] + [MinLength(3, ErrorMessage = "You need to specify a valid first name")] + [MaxLength(30, ErrorMessage = "You need to specify a valid first name")] public string FirstName { get; set; } - [Required, MinLength(3)] + + [Required(ErrorMessage = "You need to specify a last name")] + [MinLength(3, ErrorMessage = "You need to specify a valid last name")] + [MaxLength(30, ErrorMessage = "You need to specify a valid last name")] public string LastName { get; set; } - [Required] + [Required(ErrorMessage = "You need to specify a password")] public string Password { get; set; } - [Required] + [Required(ErrorMessage = "You need to specify a password")] public string ConfirmPassword { get; set; } + + [MustBeTrue(ErrorMessage = "Please solve the captcha")] + public bool Captcha { get; set; } } \ No newline at end of file diff --git a/Moonlight/App/Services/Interop/ReCaptchaService.cs b/Moonlight/App/Services/Interop/ReCaptchaService.cs new file mode 100644 index 00000000..86c9af37 --- /dev/null +++ b/Moonlight/App/Services/Interop/ReCaptchaService.cs @@ -0,0 +1,91 @@ +using System.ComponentModel; +using System.Text; +using Microsoft.JSInterop; +using RestSharp; + +namespace Moonlight.App.Services.Interop; + +public class ReCaptchaService +{ + private readonly IJSRuntime JsRuntime; + private readonly ConfigService ConfigService; + + private readonly string SiteKey; + private readonly string SecretKey; + private readonly bool Enable = false; + + public Func? OnResponse { get; set; } + public Func? OnValidResponse { get; set; } + + public ReCaptchaService( + IJSRuntime jsRuntime, + ConfigService configService) + { + JsRuntime = jsRuntime; + ConfigService = configService; + + var recaptchaConfig = ConfigService + .GetSection("Moonlight") + .GetSection("Security") + .GetSection("ReCaptcha"); + + Enable = recaptchaConfig.GetValue("Enable"); + + if (Enable) + { + SiteKey = recaptchaConfig.GetValue("SiteKey"); + SecretKey = recaptchaConfig.GetValue("SecretKey"); + } + } + + public Task IsEnabled() + { + return Task.FromResult(Enable); + } + + public async Task Create(string elementId) + { + var page = DotNetObjectReference.Create(this); + var res = await JsRuntime.InvokeAsync("moonlight.recaptcha.render", elementId, SiteKey, page); + + return res.ToString() ?? ""; + } + + [JSInvokable, EditorBrowsable(EditorBrowsableState.Never)] + public async void CallbackOnSuccess(string res) + { + if(OnResponse != null) + await OnResponse.Invoke(res); + + var b = await Validate(res); + + if (b) + { + if(OnValidResponse != null) + await OnValidResponse.Invoke(); + } + } + + [JSInvokable, EditorBrowsable(EditorBrowsableState.Never)] + public void CallbackOnExpired() + { + + } + + private async Task Validate(string res) + { + var url = "https://www.google.com/recaptcha/api/siteverify"; + + var client = new RestClient(); + var request = new RestRequest(url); + request.AddParameter("secret", SecretKey); + request.AddParameter("response", res); + + var resp = await client.PostAsync(request); + + var data = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(resp.Content)) + ).Build(); + return data.GetValue("success"); + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index aba2a203..9c1b164a 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -117,6 +117,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Components/Auth/Register.razor b/Moonlight/Shared/Components/Auth/Register.razor index 0b284a2d..714bed6d 100644 --- a/Moonlight/Shared/Components/Auth/Register.razor +++ b/Moonlight/Shared/Components/Auth/Register.razor @@ -57,39 +57,44 @@ Or with email - +
- +
- +
- +
- +
- +
- +
- + +
+ + +
+
- +
Already registered? @@ -106,13 +111,13 @@ @code { private UserRegisterModel UserRegisterModel = new(); - + private async Task DoGoogle() { var url = await GoogleOAuth2Service.GetUrl(); NavigationManager.NavigateTo(url, true); } - + private async Task DoDiscord() { var url = await DiscordOAuth2Service.GetUrl(); @@ -126,7 +131,7 @@ await AlertService.Error(SmartTranslateService.Translate("Passwords need to match")); return; } - + var token = await UserService.Register(UserRegisterModel.Email, UserRegisterModel.Password, UserRegisterModel.FirstName, UserRegisterModel.LastName); await CookieService.SetValue("token", token, 10); @@ -135,4 +140,4 @@ else NavigationManager.NavigateTo(NavigationManager.Uri, true); } -} +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Forms/SmartReCaptcha.razor b/Moonlight/Shared/Components/Forms/SmartReCaptcha.razor new file mode 100644 index 00000000..5fd5d660 --- /dev/null +++ b/Moonlight/Shared/Components/Forms/SmartReCaptcha.razor @@ -0,0 +1,49 @@ +@using Moonlight.App.Services.Interop +@using Logging.Net + +@inject ReCaptchaService ReCaptchaService + +@inherits InputBase + +
+ +@code +{ + private string UniqueId = Guid.NewGuid().ToString(); + private string Id; + + private async Task OnValid() + { + CurrentValue = true; + await ValueChanged.InvokeAsync(CurrentValue); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + ReCaptchaService.OnValidResponse += OnValid; + + if(await ReCaptchaService.IsEnabled()) + Id = await ReCaptchaService.Create(UniqueId); + else + { + await OnValid(); + } + } + } + + protected override bool TryParseValueFromString(string? value, out bool result, out string? validationErrorMessage) + { + if (bool.TryParse(value, out result)) + { + validationErrorMessage = null; + return true; + } + else + { + validationErrorMessage = "Invalid value."; + return false; + } + } +} \ No newline at end of file