diff --git a/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml b/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml
index e0f60e14..6f9a9516 100644
--- a/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml
+++ b/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/ProjectLighthouse/Helpers/CaptchaHelper.cs b/ProjectLighthouse/Helpers/CaptchaHelper.cs
new file mode 100644
index 00000000..3008b084
--- /dev/null
+++ b/ProjectLighthouse/Helpers/CaptchaHelper.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using LBPUnion.ProjectLighthouse.Types.Settings;
+using Newtonsoft.Json.Linq;
+
+namespace LBPUnion.ProjectLighthouse.Helpers;
+
+public static class CaptchaHelper
+{
+ private static readonly HttpClient client = new()
+ {
+ BaseAddress = new Uri("https://hcaptcha.com"),
+ };
+
+ public static async Task Verify(string token)
+ {
+ if (!ServerSettings.Instance.HCaptchaEnabled) return true;
+
+ List> payload = new()
+ {
+ new("secret", ServerSettings.Instance.HCaptchaSecret),
+ new("response", token),
+ };
+
+ HttpResponseMessage response = await client.PostAsync("/siteverify", new FormUrlEncodedContent(payload));
+
+ response.EnsureSuccessStatusCode();
+
+ string responseBody = await response.Content.ReadAsStringAsync();
+
+ // We only really care about the success result, nothing else that hcaptcha sends us, so lets only parse that.
+ bool success = bool.Parse(JObject.Parse(responseBody)["success"]?.ToString() ?? "false");
+ return success;
+ }
+}
\ No newline at end of file
diff --git a/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs b/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs
index 05208124..b8738c4d 100644
--- a/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs
+++ b/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs
@@ -1,5 +1,9 @@
+#nullable enable
using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace LBPUnion.ProjectLighthouse.Helpers.Extensions;
@@ -11,4 +15,17 @@ public static class RequestExtensions
("Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
public static bool IsMobile(this HttpRequest request) => mobileCheck.IsMatch(request.Headers[HeaderNames.UserAgent].ToString());
+
+ public static async Task CheckCaptchaValidity(this HttpRequest request)
+ {
+ if (ServerSettings.Instance.HCaptchaEnabled)
+ {
+ bool gotCaptcha = request.Form.TryGetValue("h-captcha-response", out StringValues values);
+ if (!gotCaptcha) return false;
+
+ if (!await CaptchaHelper.Verify(values[0])) return false;
+ }
+
+ return true;
+ }
}
\ No newline at end of file
diff --git a/ProjectLighthouse/Pages/LoginForm.cshtml b/ProjectLighthouse/Pages/LoginForm.cshtml
index 2e5494a4..f07daa9f 100644
--- a/ProjectLighthouse/Pages/LoginForm.cshtml
+++ b/ProjectLighthouse/Pages/LoginForm.cshtml
@@ -50,6 +50,11 @@
+ @if (ServerSettings.Instance.HCaptchaEnabled)
+ {
+ @await Html.PartialAsync("Partials/CaptchaPartial")
+ }
+
@if (ServerSettings.Instance.RegistrationEnabled)
{
diff --git a/ProjectLighthouse/Pages/LoginForm.cshtml.cs b/ProjectLighthouse/Pages/LoginForm.cshtml.cs
index 5e19c5a4..7ceb63d3 100644
--- a/ProjectLighthouse/Pages/LoginForm.cshtml.cs
+++ b/ProjectLighthouse/Pages/LoginForm.cshtml.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
+using LBPUnion.ProjectLighthouse.Helpers.Extensions;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
@@ -18,8 +19,6 @@ public class LoginForm : BaseLayout
public string Error { get; private set; }
- public bool WasLoginRequest { get; private set; }
-
[UsedImplicitly]
public async Task OnPost(string username, string password)
{
@@ -35,6 +34,12 @@ public class LoginForm : BaseLayout
return this.Page();
}
+ if (!await Request.CheckCaptchaValidity())
+ {
+ this.Error = "You must complete the captcha correctly.";
+ return this.Page();
+ }
+
User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null)
{
@@ -68,6 +73,8 @@ public class LoginForm : BaseLayout
this.Response.Cookies.Append("LighthouseToken", webToken.UserToken);
+ Logger.Log($"User {user.Username} (id: {user.UserId}) successfully logged in on web", LoggerLevelLogin.Instance);
+
if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
return this.RedirectToPage(nameof(LandingPage));
diff --git a/ProjectLighthouse/Pages/Partials/CaptchaPartial.cshtml b/ProjectLighthouse/Pages/Partials/CaptchaPartial.cshtml
new file mode 100644
index 00000000..e1891722
--- /dev/null
+++ b/ProjectLighthouse/Pages/Partials/CaptchaPartial.cshtml
@@ -0,0 +1,6 @@
+@using LBPUnion.ProjectLighthouse.Types.Settings
+@if (ServerSettings.Instance.HCaptchaEnabled)
+{
+
+
+}
\ No newline at end of file
diff --git a/ProjectLighthouse/Pages/RegisterForm.cshtml b/ProjectLighthouse/Pages/RegisterForm.cshtml
index 67b3a05a..54775d85 100644
--- a/ProjectLighthouse/Pages/RegisterForm.cshtml
+++ b/ProjectLighthouse/Pages/RegisterForm.cshtml
@@ -1,4 +1,5 @@
@page "/register"
+@using LBPUnion.ProjectLighthouse.Types.Settings
@model LBPUnion.ProjectLighthouse.Pages.RegisterForm
@{
@@ -60,5 +61,10 @@
+ @if (ServerSettings.Instance.HCaptchaEnabled)
+ {
+ @await Html.PartialAsync("Partials/CaptchaPartial")
+ }
+
\ No newline at end of file
diff --git a/ProjectLighthouse/Pages/RegisterForm.cshtml.cs b/ProjectLighthouse/Pages/RegisterForm.cshtml.cs
index 310884d5..a7945459 100644
--- a/ProjectLighthouse/Pages/RegisterForm.cshtml.cs
+++ b/ProjectLighthouse/Pages/RegisterForm.cshtml.cs
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
+using LBPUnion.ProjectLighthouse.Helpers.Extensions;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
@@ -42,13 +43,18 @@ public class RegisterForm : BaseLayout
return this.Page();
}
- bool userExists = await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null;
- if (userExists)
+ if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null)
{
this.Error = "The username you've chosen is already taken.";
return this.Page();
}
+ if (!await Request.CheckCaptchaValidity())
+ {
+ this.Error = "You must complete the captcha correctly.";
+ return this.Page();
+ }
+
User user = await this.Database.CreateUser(username, HashHelper.BCryptHash(password));
WebToken webToken = new()
diff --git a/ProjectLighthouse/Types/Settings/ServerSettings.cs b/ProjectLighthouse/Types/Settings/ServerSettings.cs
index 8948ca69..f6142538 100644
--- a/ProjectLighthouse/Types/Settings/ServerSettings.cs
+++ b/ProjectLighthouse/Types/Settings/ServerSettings.cs
@@ -5,7 +5,6 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Kettu;
-using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
namespace LBPUnion.ProjectLighthouse.Types.Settings;
@@ -13,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings;
[Serializable]
public class ServerSettings
{
- public const int CurrentConfigVersion = 19; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
+ public const int CurrentConfigVersion = 20; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
private static FileSystemWatcher fileWatcher;
static ServerSettings()
{
@@ -156,6 +155,12 @@ public class ServerSettings
public string MissingIconHash { get; set; } = "";
+ public bool HCaptchaEnabled { get; set; }
+
+ public string HCaptchaSiteKey { get; set; } = "";
+
+ public string HCaptchaSecret { get; set; } = "";
+
#region Meta
[NotNull]