diff --git a/ProjectLighthouse.Servers.Website/Extensions/SlugExtensions.cs b/ProjectLighthouse.Servers.Website/Extensions/SlugExtensions.cs
new file mode 100644
index 000000000..eaef5e257
--- /dev/null
+++ b/ProjectLighthouse.Servers.Website/Extensions/SlugExtensions.cs
@@ -0,0 +1,33 @@
+using System.Text.RegularExpressions;
+using System.Web;
+using LBPUnion.ProjectLighthouse.Types.Entities.Level;
+using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
+
+namespace LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
+
+public static partial class SlugExtensions
+{
+ [GeneratedRegex("[^a-zA-Z0-9 ]")]
+ private static partial Regex ValidSlugCharactersRegex();
+
+ [GeneratedRegex(@"[\s]{2,}")]
+ private static partial Regex WhitespaceRegex();
+
+ ///
+ /// Generates a URL slug that only contains alphanumeric characters
+ /// with spaces replaced with dashes
+ ///
+ /// The slot to generate the slug for
+ /// A string containing the url slug for this slot
+ public static string GenerateSlug(this SlotEntity slot) =>
+ slot.Name.Length == 0
+ ? "unnamed-level"
+ : WhitespaceRegex().Replace(ValidSlugCharactersRegex().Replace(HttpUtility.HtmlDecode(slot.Name), ""), " ").Replace(" ", "-").ToLower();
+
+ ///
+ /// Generates a URL slug for the given user
+ ///
+ /// The user to generate the slug for
+ /// A string containing the url slug for this user
+ public static string GenerateSlug(this UserEntity user) => user.Username.ToLower();
+}
\ No newline at end of file
diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml
index 4d889b336..d43791bed 100644
--- a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml
+++ b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml
@@ -12,7 +12,7 @@
string userStatus = includeStatus ? Model.GetStatus(Database).ToTranslatedString(language, timeZone) : "";
}
-
+
@if (Model.IsModerator)
diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml
index 43ac34237..0b6940cbe 100644
--- a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml
+++ b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml
@@ -53,7 +53,7 @@
@if (showLink)
{
}
else
@@ -68,7 +68,7 @@
@if (showLink)
{
}
else
diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml
index 683c69c3f..14c800a3d 100644
--- a/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml
+++ b/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml
@@ -22,7 +22,7 @@
@if (showLink)
{
- @Model.Username
+ @Model.Username
@if (Model.IsModerator)
{
diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml
index d6e6ca2d5..d42517e6a 100644
--- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml
+++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml
@@ -1,4 +1,4 @@
-@page "/slot/{id:int}"
+@page "/slot/{id:int}/{slug?}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Database
@using LBPUnion.ProjectLighthouse.Extensions
diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs
index e228c82bc..bed130780 100644
--- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs
+++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs
@@ -1,6 +1,7 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
+using LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
@@ -28,7 +29,7 @@ public class SlotPage : BaseLayout
public SlotPage(DatabaseContext database) : base(database)
{}
- public async Task OnGet([FromRoute] int id)
+ public async Task OnGet([FromRoute] int id, string? slug)
{
SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator)
.Where(s => s.Type == SlotType.User || (this.User != null && this.User.PermissionLevel >= PermissionLevel.Moderator))
@@ -45,6 +46,12 @@ public async Task OnGet([FromRoute] int id)
if ((slot.Hidden || slot.SubLevel && (this.User == null && this.User != slot.Creator)) && !(this.User?.IsModerator ?? false))
return this.NotFound();
+ string slotSlug = slot.GenerateSlug();
+ if (slug == null || slotSlug != slug)
+ {
+ return this.Redirect($"~/slot/{id}/{slotSlug}");
+ }
+
this.Slot = slot;
List blockedUsers = this.User == null
diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml
index 0e78f3d32..a24a3ddb8 100644
--- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml
+++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml
@@ -1,4 +1,4 @@
-@page "/user/{userId:int}"
+@page "/user/{userId:int}/{slug?}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Localization.StringLists
diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs
index ee85fc87b..670c8801e 100644
--- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs
+++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs
@@ -1,6 +1,7 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
+using LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
@@ -38,11 +39,17 @@ public class UserPage : BaseLayout
public UserPage(DatabaseContext database) : base(database)
{ }
- public async Task OnGet([FromRoute] int userId)
+ public async Task OnGet([FromRoute] int userId, string? slug)
{
this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId);
if (this.ProfileUser == null) return this.NotFound();
+ string userSlug = this.ProfileUser.GenerateSlug();
+ if (slug == null || userSlug != slug)
+ {
+ return this.Redirect($"~/user/{userId}/{userSlug}");
+ }
+
bool isAuthenticated = this.User != null;
bool isOwner = this.ProfileUser == this.User || this.User != null && this.User.IsModerator;