diff --git a/ProjectLighthouse.Localization/General.lang-ar-SA.resx b/ProjectLighthouse.Localization/General.lang-ar-SA.resx index 16a1a80d..ca5e4960 100644 --- a/ProjectLighthouse.Localization/General.lang-ar-SA.resx +++ b/ProjectLighthouse.Localization/General.lang-ar-SA.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-da-DK.resx b/ProjectLighthouse.Localization/General.lang-da-DK.resx index fb9cc03a..6111fc81 100644 --- a/ProjectLighthouse.Localization/General.lang-da-DK.resx +++ b/ProjectLighthouse.Localization/General.lang-da-DK.resx @@ -53,4 +53,7 @@ De seneste billeder + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-de-DE.resx b/ProjectLighthouse.Localization/General.lang-de-DE.resx index a4ed9594..360d7f10 100644 --- a/ProjectLighthouse.Localization/General.lang-de-DE.resx +++ b/ProjectLighthouse.Localization/General.lang-de-DE.resx @@ -53,4 +53,7 @@ Neueste Fotos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-en-PT.resx b/ProjectLighthouse.Localization/General.lang-en-PT.resx index 8c46dc33..555d884e 100644 --- a/ProjectLighthouse.Localization/General.lang-en-PT.resx +++ b/ProjectLighthouse.Localization/General.lang-en-PT.resx @@ -53,4 +53,7 @@ Most recent maps + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-eo-UY.resx b/ProjectLighthouse.Localization/General.lang-eo-UY.resx index 1179db9d..f11fd9b1 100644 --- a/ProjectLighthouse.Localization/General.lang-eo-UY.resx +++ b/ProjectLighthouse.Localization/General.lang-eo-UY.resx @@ -53,4 +53,7 @@ Plej lastatempaj bildoj + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-es-ES.resx b/ProjectLighthouse.Localization/General.lang-es-ES.resx index aa1692e4..206928e5 100644 --- a/ProjectLighthouse.Localization/General.lang-es-ES.resx +++ b/ProjectLighthouse.Localization/General.lang-es-ES.resx @@ -53,4 +53,7 @@ Fotos más recientes + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-es-MX.resx b/ProjectLighthouse.Localization/General.lang-es-MX.resx index adbbea3f..24a9545e 100644 --- a/ProjectLighthouse.Localization/General.lang-es-MX.resx +++ b/ProjectLighthouse.Localization/General.lang-es-MX.resx @@ -53,4 +53,7 @@ Fotos más recientes + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-et-EE.resx b/ProjectLighthouse.Localization/General.lang-et-EE.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-et-EE.resx +++ b/ProjectLighthouse.Localization/General.lang-et-EE.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-fi-FI.resx b/ProjectLighthouse.Localization/General.lang-fi-FI.resx index 49b80ec7..5c99c43b 100644 --- a/ProjectLighthouse.Localization/General.lang-fi-FI.resx +++ b/ProjectLighthouse.Localization/General.lang-fi-FI.resx @@ -53,4 +53,7 @@ Viimeisimmät kuvat + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-fil-PH.resx b/ProjectLighthouse.Localization/General.lang-fil-PH.resx index 88945423..8110303c 100644 --- a/ProjectLighthouse.Localization/General.lang-fil-PH.resx +++ b/ProjectLighthouse.Localization/General.lang-fil-PH.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-fr-FR.resx b/ProjectLighthouse.Localization/General.lang-fr-FR.resx index 65c30c8a..8bb50d3a 100644 --- a/ProjectLighthouse.Localization/General.lang-fr-FR.resx +++ b/ProjectLighthouse.Localization/General.lang-fr-FR.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-ga-IE.resx b/ProjectLighthouse.Localization/General.lang-ga-IE.resx index bbd6553d..5007e301 100644 --- a/ProjectLighthouse.Localization/General.lang-ga-IE.resx +++ b/ProjectLighthouse.Localization/General.lang-ga-IE.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-he-IL.resx b/ProjectLighthouse.Localization/General.lang-he-IL.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-he-IL.resx +++ b/ProjectLighthouse.Localization/General.lang-he-IL.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-hi-IN.resx b/ProjectLighthouse.Localization/General.lang-hi-IN.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-hi-IN.resx +++ b/ProjectLighthouse.Localization/General.lang-hi-IN.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-id-ID.resx b/ProjectLighthouse.Localization/General.lang-id-ID.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-id-ID.resx +++ b/ProjectLighthouse.Localization/General.lang-id-ID.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-it-IT.resx b/ProjectLighthouse.Localization/General.lang-it-IT.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-it-IT.resx +++ b/ProjectLighthouse.Localization/General.lang-it-IT.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-ja-JP.resx b/ProjectLighthouse.Localization/General.lang-ja-JP.resx index b164cd2f..bcd980f2 100644 --- a/ProjectLighthouse.Localization/General.lang-ja-JP.resx +++ b/ProjectLighthouse.Localization/General.lang-ja-JP.resx @@ -53,4 +53,7 @@ 最近の写真 + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-nl-NL.resx b/ProjectLighthouse.Localization/General.lang-nl-NL.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-nl-NL.resx +++ b/ProjectLighthouse.Localization/General.lang-nl-NL.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-no-NO.resx b/ProjectLighthouse.Localization/General.lang-no-NO.resx index fafe11cf..6b7248a5 100644 --- a/ProjectLighthouse.Localization/General.lang-no-NO.resx +++ b/ProjectLighthouse.Localization/General.lang-no-NO.resx @@ -53,4 +53,7 @@ Siste bilder + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-pl-PL.resx b/ProjectLighthouse.Localization/General.lang-pl-PL.resx index 9079146a..072b9ded 100644 --- a/ProjectLighthouse.Localization/General.lang-pl-PL.resx +++ b/ProjectLighthouse.Localization/General.lang-pl-PL.resx @@ -53,4 +53,7 @@ Ostatnie zdjęcia + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-pt-PT.resx b/ProjectLighthouse.Localization/General.lang-pt-PT.resx index 4ec21051..d107092d 100644 --- a/ProjectLighthouse.Localization/General.lang-pt-PT.resx +++ b/ProjectLighthouse.Localization/General.lang-pt-PT.resx @@ -53,4 +53,7 @@ Fotos mais recentes + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-ru-RU.resx b/ProjectLighthouse.Localization/General.lang-ru-RU.resx index 4717a55d..81646fa3 100644 --- a/ProjectLighthouse.Localization/General.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/General.lang-ru-RU.resx @@ -53,4 +53,7 @@ Самые последние фото + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-sv-SE.resx b/ProjectLighthouse.Localization/General.lang-sv-SE.resx index b543cc11..00202f04 100644 --- a/ProjectLighthouse.Localization/General.lang-sv-SE.resx +++ b/ProjectLighthouse.Localization/General.lang-sv-SE.resx @@ -53,4 +53,7 @@ Senaste foton + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-th-TH.resx b/ProjectLighthouse.Localization/General.lang-th-TH.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-th-TH.resx +++ b/ProjectLighthouse.Localization/General.lang-th-TH.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-tr-TR.resx b/ProjectLighthouse.Localization/General.lang-tr-TR.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-tr-TR.resx +++ b/ProjectLighthouse.Localization/General.lang-tr-TR.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-uk-UA.resx b/ProjectLighthouse.Localization/General.lang-uk-UA.resx index e78d2849..ae31a0b4 100644 --- a/ProjectLighthouse.Localization/General.lang-uk-UA.resx +++ b/ProjectLighthouse.Localization/General.lang-uk-UA.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-zh-CN.resx b/ProjectLighthouse.Localization/General.lang-zh-CN.resx index c47dce25..f2af7010 100644 --- a/ProjectLighthouse.Localization/General.lang-zh-CN.resx +++ b/ProjectLighthouse.Localization/General.lang-zh-CN.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.lang-zh-TW.resx b/ProjectLighthouse.Localization/General.lang-zh-TW.resx index 601772cf..f57e4ae1 100644 --- a/ProjectLighthouse.Localization/General.lang-zh-TW.resx +++ b/ProjectLighthouse.Localization/General.lang-zh-TW.resx @@ -53,4 +53,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.resx b/ProjectLighthouse.Localization/General.resx index f3bd12b5..138736e8 100644 --- a/ProjectLighthouse.Localization/General.resx +++ b/ProjectLighthouse.Localization/General.resx @@ -54,4 +54,7 @@ Most recent photos + + Email + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs b/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs index 1b30afc9..edc3eb5f 100644 --- a/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs +++ b/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs @@ -4,6 +4,7 @@ public static class GeneralStrings { public static readonly TranslatableString Username = create("username"); public static readonly TranslatableString Password = create("password"); + public static readonly TranslatableString Email = create("email"); public static readonly TranslatableString Register = create("register"); public static readonly TranslatableString ResetPassword = create("reset_password"); public static readonly TranslatableString ForgotPassword = create("forgot_password"); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs index 504f8e80..1184a603 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.Levels.Categories; @@ -6,8 +8,8 @@ using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Serialization; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; @@ -23,8 +25,128 @@ public class CollectionController : ControllerBase this.database = database; } + [HttpGet("playlists/{playlistId:int}/slots")] + public async Task GetPlaylistSlots(int playlistId) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + Playlist? targetPlaylist = await this.database.Playlists.FirstOrDefaultAsync(p => p.PlaylistId == playlistId); + if (targetPlaylist == null) return this.BadRequest(); + + IQueryable slots = this.database.Slots.Include(s => s.Creator) + .Include(s => s.Location) + .Where(s => targetPlaylist.SlotIds.Contains(s.SlotId)); + + string response = Enumerable.Aggregate(slots, string.Empty, (current, slot) => current + slot.Serialize()); + int total = targetPlaylist.SlotIds.Length; + + return this.Ok(LbpSerializer.TaggedStringElement("slots", response, "total", total)); + } + + [HttpPost("playlists/{playlistId:int}")] + [HttpPost("playlists/{playlistId:int}/slots")] + [HttpPost("playlists/{playlistId:int}/slots/{slotId:int}/delete")] + [HttpPost("playlists/{playlistId:int}/order_slots")] + public async Task UpdatePlaylist(int playlistId, int slotId) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + Playlist? targetPlaylist = await this.database.Playlists.FirstOrDefaultAsync(p => p.PlaylistId == playlistId); + if (targetPlaylist == null) return this.BadRequest(); + + if (token.UserId != targetPlaylist.CreatorId) return this.BadRequest(); + + // Delete a slot from playlist + if (slotId != 0) + { + targetPlaylist.SlotIds = targetPlaylist.SlotIds.Where(s => s != slotId).ToArray(); + await this.database.SaveChangesAsync(); + return this.Ok(this.GetUserPlaylists(token.UserId)); + } + + Playlist? newPlaylist = await this.getPlaylistFromBody(); + + if (newPlaylist == null) return this.BadRequest(); + + if (newPlaylist.LevelIds != null) + { + HashSet slotIds = new(targetPlaylist.SlotIds); + + // Reorder + if (slotIds.SetEquals(newPlaylist.LevelIds)) + { + targetPlaylist.SlotIds = newPlaylist.LevelIds; + } + // Add a level + else + { + foreach (int id in newPlaylist.LevelIds) + { + targetPlaylist.SlotIds = targetPlaylist.SlotIds.Append(id).ToArray(); + } + } + } + + if (!string.IsNullOrWhiteSpace(newPlaylist.Name)) targetPlaylist.Name = newPlaylist.Name; + + if (!string.IsNullOrWhiteSpace(newPlaylist.Description)) targetPlaylist.Description = newPlaylist.Description; + + await this.database.SaveChangesAsync(); + + return this.Ok(this.GetUserPlaylists(token.UserId)); + } + + private string GetUserPlaylists(int userId) + { + string response = Enumerable.Aggregate( + this.database.Playlists.Include(p => p.Creator).Where(p => p.CreatorId == userId), + string.Empty, + (current, slot) => current + slot.Serialize()); + int total = this.database.Playlists.Count(p => p.CreatorId == userId); + + return LbpSerializer.TaggedStringElement("playlists", response, new Dictionary + { + {"total", total}, + {"hint_start", total+1}, + }); + } + + [HttpPost("playlists")] + public async Task CreatePlaylist() + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + int playlistCount = await this.database.Playlists.CountAsync(p => p.CreatorId == token.UserId); + + if (playlistCount > ServerConfiguration.Instance.UserGeneratedContentLimits.ListsQuota) return this.BadRequest(); + + Playlist? playlist = await this.getPlaylistFromBody(); + + if (playlist == null) return this.BadRequest(); + + playlist.CreatorId = token.UserId; + + this.database.Playlists.Add(playlist); + + await this.database.SaveChangesAsync(); + + return this.Ok(this.GetUserPlaylists(token.UserId)); + } + [HttpGet("user/{username}/playlists")] - public IActionResult GetUserPlaylists(string username) => this.Ok(""); + public async Task GetUserPlaylists(string username) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + int targetUserId = await this.database.Users.Where(u => u.Username == username).Select(u => u.UserId).FirstOrDefaultAsync(); + if (targetUserId == 0) return this.BadRequest(); + + return this.Ok(this.GetUserPlaylists(targetUserId)); + } [HttpGet("searches")] [HttpGet("genres")] @@ -121,4 +243,19 @@ public class CollectionController : ControllerBase ) ); } + + private async Task getPlaylistFromBody() + { + this.Request.Body.Position = 0; + string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); + + string rootElement = bodyString.StartsWith("") ? "playlist" : "levels"; + XmlSerializer serializer = new(typeof(Playlist), new XmlRootAttribute(rootElement)); + Playlist? playlist = (Playlist?)serializer.Deserialize(new StringReader(bodyString)); + + SanitizationHelper.SanitizeStringsInClass(playlist); + + return playlist; + } + } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 3e1d051b..f9783862 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -197,6 +197,64 @@ public class ListController : ControllerBase #endregion + #region Hearted Playlists + + [HttpGet("favouritePlaylists/{username}")] + public async Task GetFavouritePlaylists(string username, [FromQuery] int pageStart, [FromQuery] int pageSize) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + if (pageSize <= 0) return this.BadRequest(); + + int targetUserId = await this.database.Users.Where(u => u.Username == username).Select(u => u.UserId).FirstOrDefaultAsync(); + if (targetUserId == 0) return this.StatusCode(403, ""); + + IEnumerable heartedPlaylists = this.database.HeartedPlaylists.Where(p => p.UserId == targetUserId) + .Include(p => p.Playlist).Include(p => p.Playlist.Creator).OrderByDescending(p => p.HeartedPlaylistId).Select(p => p.Playlist); + + string response = heartedPlaylists.Aggregate(string.Empty, (current, p) => current + p.Serialize()); + + return this.Ok + ( + LbpSerializer.TaggedStringElement("favouritePlaylists", response, new Dictionary + { + { "total", this.database.HeartedPlaylists.Count(p => p.UserId == targetUserId) }, + { "hint_start", pageStart + Math.Min(pageSize, 30) }, + }) + ); + } + + [HttpPost("favourite/playlist/{playlistId:int}")] + public async Task AddFavouritePlaylist(int playlistId) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + Playlist? playlist = await this.database.Playlists.FirstOrDefaultAsync(s => s.PlaylistId == playlistId); + if (playlist == null) return this.NotFound(); + + await this.database.HeartPlaylist(token.UserId, playlist); + + return this.Ok(); + } + + [HttpPost("unfavourite/playlist/{playlistId:int}")] + public async Task RemoveFavouritePlaylist(int playlistId) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + Playlist? playlist = await this.database.Playlists.FirstOrDefaultAsync(s => s.PlaylistId == playlistId); + if (playlist == null) return this.NotFound(); + + await this.database.UnheartPlaylist(token.UserId, playlist); + + return this.Ok(); + } + + #endregion + #endregion Levels #region Users diff --git a/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml b/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml index cedbbdc7..401ec447 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml @@ -41,9 +41,12 @@ - @Model.Translate(GeneralStrings.Username) + @{ + string username = ServerConfiguration.Instance.Mail.MailEnabled ? Model.Translate(GeneralStrings.Email) : Model.Translate(GeneralStrings.Username); + } + @username - + @@ -75,9 +78,9 @@ } - - - @Model.Translate(GeneralStrings.ForgotPassword) - - + + + @Model.Translate(GeneralStrings.ForgotPassword) + + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml.cs index c751290e..8e9454b6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/LoginForm.cshtml.cs @@ -27,7 +27,7 @@ public class LoginForm : BaseLayout { if (string.IsNullOrWhiteSpace(username)) { - this.Error = this.Translate(ErrorStrings.UsernameInvalid); + this.Error = ServerConfiguration.Instance.Mail.MailEnabled ? this.Translate(ErrorStrings.UsernameInvalid) : this.Translate(ErrorStrings.EmailInvalid); return this.Page(); } @@ -43,7 +43,23 @@ public class LoginForm : BaseLayout return this.Page(); } - User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username); + User? user; + + if (!ServerConfiguration.Instance.Mail.MailEnabled) + { + user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username); + } + else + { + user = await this.Database.Users.FirstOrDefaultAsync(u => u.EmailAddress == username); + if (user == null) + { + User? noEmailUser = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username); + if (noEmailUser != null && noEmailUser.EmailAddress == null) user = noEmailUser; + + } + } + if (user == null) { Logger.Warn($"User {username} failed to login on web due to invalid username", LogArea.Login); diff --git a/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs index a65827f4..3d88a60e 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs @@ -12,7 +12,7 @@ public class PirateSignupPage : BaseLayout public IActionResult OnGet() { User? user = this.Database.UserFromWebRequest(this.Request); - if (user == null) return this.RedirectToPage("/login"); + if (user == null) return this.Redirect("/login"); return this.Page(); } diff --git a/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml b/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml index 46ebd31a..56ccafa6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml @@ -74,6 +74,12 @@ + + + + I confirm that I am above 13 years old + + @if (ServerConfiguration.Instance.Captcha.CaptchaEnabled) { diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml index 5fe1bfc3..7acab753 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml @@ -67,7 +67,7 @@ function onSubmit(e){ @if (ServerConfiguration.Instance.Mail.MailEnabled && (Model.User == Model.ProfileUser || Model.User!.IsAdmin)) { - Email + @Model.Translate(GeneralStrings.Email) } diff --git a/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj b/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj index 660e2cb4..55d4d04c 100644 --- a/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj +++ b/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj index 33af481e..5b8c545c 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj +++ b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj @@ -14,8 +14,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs b/ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs index 7624ca83..be2561ca 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs +++ b/ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs @@ -28,6 +28,8 @@ public class RegisterTests : LighthouseWebTest this.Driver.FindElement(By.Id("password")).SendKeys(password); this.Driver.FindElement(By.Id("confirmPassword")).SendKeys(password); + this.Driver.FindElement(By.Id("age-checkbox")).Click(); + this.Driver.FindElement(By.Id("submit")).Click(); User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == username); @@ -51,6 +53,8 @@ public class RegisterTests : LighthouseWebTest this.Driver.FindElement(By.Id("password")).SendKeys(password); this.Driver.FindElement(By.Id("confirmPassword")).SendKeys(password + "a"); + this.Driver.FindElement(By.Id("age-checkbox")).Click(); + this.Driver.FindElement(By.Id("submit")).Click(); User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == username); @@ -76,6 +80,8 @@ public class RegisterTests : LighthouseWebTest this.Driver.FindElement(By.Id("password")).SendKeys(password); this.Driver.FindElement(By.Id("confirmPassword")).SendKeys(password); + this.Driver.FindElement(By.Id("age-checkbox")).Click(); + this.Driver.FindElement(By.Id("submit")).Click(); Assert.Contains("The username you've chosen is already taken.", this.Driver.PageSource); diff --git a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj index b8c96bab..23cd2d59 100644 --- a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj +++ b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj @@ -19,7 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 86d1a38a..c87263fe 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -30,6 +30,7 @@ public class Database : DbContext public DbSet QueuedLevels { get; set; } public DbSet HeartedLevels { get; set; } public DbSet HeartedProfiles { get; set; } + public DbSet HeartedPlaylists { get; set; } public DbSet Comments { get; set; } public DbSet GameTokens { get; set; } public DbSet WebTokens { get; set; } @@ -51,6 +52,7 @@ public class Database : DbContext public DbSet PasswordResetTokens { get; set; } public DbSet RegistrationTokens { get; set; } public DbSet APIKeys { get; set; } + public DbSet Playlists { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseMySql(ServerConfiguration.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); @@ -231,6 +233,28 @@ public class Database : DbContext await this.SaveChangesAsync(); } + public async Task HeartPlaylist(int userId, Playlist heartedPlaylist) + { + HeartedPlaylist? heartedList = await this.HeartedPlaylists.FirstOrDefaultAsync(p => p.UserId == userId && p.PlaylistId == heartedPlaylist.PlaylistId); + if (heartedList != null) return; + + this.HeartedPlaylists.Add(new HeartedPlaylist() + { + PlaylistId = heartedPlaylist.PlaylistId, + UserId = userId, + }); + + await this.SaveChangesAsync(); + } + + public async Task UnheartPlaylist(int userId, Playlist heartedPlaylist) + { + HeartedPlaylist? heartedList = await this.HeartedPlaylists.FirstOrDefaultAsync(p => p.UserId == userId && p.PlaylistId == heartedPlaylist.PlaylistId); + if (heartedList != null) this.HeartedPlaylists.Remove(heartedList); + + await this.SaveChangesAsync(); + } + public async Task HeartLevel(int userId, Slot heartedSlot) { HeartedLevel? heartedLevel = await this.HeartedLevels.FirstOrDefaultAsync(q => q.UserId == userId && q.SlotId == heartedSlot.SlotId); diff --git a/ProjectLighthouse/Levels/HeartedPlaylist.cs b/ProjectLighthouse/Levels/HeartedPlaylist.cs new file mode 100644 index 00000000..f17e8c30 --- /dev/null +++ b/ProjectLighthouse/Levels/HeartedPlaylist.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; + +namespace LBPUnion.ProjectLighthouse.Levels; + +public class HeartedPlaylist +{ + [Key] + public int HeartedPlaylistId { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + public int PlaylistId { get; set; } + + [ForeignKey(nameof(PlaylistId))] + public Playlist Playlist { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Levels/Playlist.cs b/ProjectLighthouse/Levels/Playlist.cs new file mode 100644 index 00000000..deb2a108 --- /dev/null +++ b/ProjectLighthouse/Levels/Playlist.cs @@ -0,0 +1,70 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; +using LBPUnion.ProjectLighthouse.Serialization; + +namespace LBPUnion.ProjectLighthouse.Levels; + +public class Playlist +{ + [Key] + public int PlaylistId { get; set; } + + [XmlElement("name")] + public string Name { get; set; } = ""; + + [XmlElement("description")] + public string Description { get; set; } = ""; + + public int CreatorId { get; set; } + + [ForeignKey(nameof(CreatorId))] + [JsonIgnore] + public User? Creator { get; set; } + + public int Hearts(Database database) => database.HeartedPlaylists.Count(p => p.HeartedPlaylistId == this.PlaylistId); + + public string SlotCollection { get; set; } = ""; + + [JsonIgnore] + [NotMapped] + [XmlElement("level_id")] + public int[]? LevelIds { get; set; } + + [NotMapped] + public int[] SlotIds + { + get => this.SlotCollection.Split(",").Where(x => int.TryParse(x, out _)).Select(int.Parse).ToArray(); + set => this.SlotCollection = string.Join(",", value); + } + + public string Serialize() + { + using Database database = new(); + string playlist = LbpSerializer.StringElement("id", this.PlaylistId) + + LbpSerializer.StringElement("author", + LbpSerializer.StringElement("npHandle", this.Creator?.Username)) + + LbpSerializer.StringElement("name", this.Name) + + LbpSerializer.StringElement("description", this.Description) + + LbpSerializer.StringElement("levels", this.SlotIds.Length) + + LbpSerializer.StringElement("hearts", this.Hearts(database)) + + LbpSerializer.StringElement("levels_quota", ServerConfiguration.Instance.UserGeneratedContentLimits.ListsQuota) + + this.SerializeIcons(database); + + return LbpSerializer.StringElement("playlist", playlist); + } + + private string SerializeIcons(Database database) + { + string iconList = this.SlotIds.Select(id => database.Slots.FirstOrDefault(s => s.SlotId == id)) + .Where(slot => slot != null && slot.IconHash.Length > 0) + .Aggregate(string.Empty, (current, slot) => current + LbpSerializer.StringElement("icon", slot?.IconHash)); + return LbpSerializer.StringElement("icons", iconList); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse/Migrations/20220923042831_AddPlaylists.cs b/ProjectLighthouse/Migrations/20220923042831_AddPlaylists.cs new file mode 100644 index 00000000..f7023a6d --- /dev/null +++ b/ProjectLighthouse/Migrations/20220923042831_AddPlaylists.cs @@ -0,0 +1,94 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220923042831_AddPlaylists")] + public partial class AddPlaylists : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Playlists", + columns: table => new + { + PlaylistId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatorId = table.Column(type: "int", nullable: false), + SlotCollection = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Playlists", x => x.PlaylistId); + table.ForeignKey( + name: "FK_Playlists_Users_CreatorId", + column: x => x.CreatorId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "HeartedPlaylists", + columns: table => new + { + HeartedPlaylistId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + PlaylistId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HeartedPlaylists", x => x.HeartedPlaylistId); + table.ForeignKey( + name: "FK_HeartedPlaylists_Playlists_PlaylistId", + column: x => x.PlaylistId, + principalTable: "Playlists", + principalColumn: "PlaylistId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_HeartedPlaylists_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_HeartedPlaylists_PlaylistId", + table: "HeartedPlaylists", + column: "PlaylistId"); + + migrationBuilder.CreateIndex( + name: "IX_HeartedPlaylists_UserId", + table: "HeartedPlaylists", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Playlists_CreatorId", + table: "Playlists", + column: "CreatorId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HeartedPlaylists"); + + migrationBuilder.DropTable( + name: "Playlists"); + } + } +} diff --git a/ProjectLighthouse/PlayerData/Profiles/User.cs b/ProjectLighthouse/PlayerData/Profiles/User.cs index 73d9d3bb..46983b08 100644 --- a/ProjectLighthouse/PlayerData/Profiles/User.cs +++ b/ProjectLighthouse/PlayerData/Profiles/User.cs @@ -50,7 +50,7 @@ public class User [NotMapped] [JsonIgnore] - public int Lists => 0; + public int Lists => this.database.Playlists.Count(p => p.CreatorId == this.UserId); /// /// A user-customizable biography shown on the profile card @@ -116,6 +116,10 @@ public class User [JsonIgnore] public int HeartedUsers => this.database.HeartedProfiles.Count(p => p.UserId == this.UserId); + [NotMapped] + [JsonIgnore] + public int HeartedPlaylists => this.database.HeartedPlaylists.Count(p => p.UserId == this.UserId); + [NotMapped] [JsonIgnore] public int QueuedLevels => this.database.QueuedLevels.Count(p => p.UserId == this.UserId); @@ -197,14 +201,14 @@ public class User string user = LbpSerializer.TaggedStringElement("npHandle", this.Username, "icon", this.IconHash) + LbpSerializer.StringElement("game", (int)gameVersion) + this.serializeSlots(gameVersion) + - LbpSerializer.StringElement("lists", this.Lists, true) + - LbpSerializer.StringElement + LbpSerializer.StringElement("lists", this.Lists, true) + + LbpSerializer.StringElement ( "lists_quota", ServerConfiguration.Instance.UserGeneratedContentLimits.ListsQuota, true ) + // technically not a part of the user but LBP expects it - LbpSerializer.StringElement("heartCount", this.Hearts, true) + + LbpSerializer.StringElement("heartCount", this.Hearts, true) + this.serializeEarth(gameVersion) + LbpSerializer.StringElement("yay2", this.YayHash, true) + LbpSerializer.StringElement("boo2", this.BooHash, true) + @@ -218,6 +222,7 @@ public class User LbpSerializer.StringElement("location", this.Location.Serialize()) + LbpSerializer.StringElement("favouriteSlotCount", this.HeartedLevels, true) + LbpSerializer.StringElement("favouriteUserCount", this.HeartedUsers, true) + + LbpSerializer.StringElement("favouritePlaylistCount", this.HeartedPlaylists, true) + LbpSerializer.StringElement("lolcatftwCount", this.QueuedLevels, true) + LbpSerializer.StringElement("pins", this.Pins, true); diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 8f670e49..2254c57d 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -13,7 +13,7 @@ - + diff --git a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 1ce63adf..e4084263 100644 --- a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -16,7 +16,7 @@ namespace ProjectLighthouse.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("ProductVersion", "6.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b => @@ -172,6 +172,55 @@ namespace ProjectLighthouse.Migrations b.ToTable("HeartedLevels"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Levels.HeartedPlaylist", b => + { + b.Property("HeartedPlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("HeartedPlaylistId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("UserId"); + + b.ToTable("HeartedPlaylists"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Levels.Playlist", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SlotCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("PlaylistId"); + + b.HasIndex("CreatorId"); + + b.ToTable("Playlists"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Levels.QueuedLevel", b => { b.Property("QueuedLevelId") @@ -273,6 +322,9 @@ namespace ProjectLighthouse.Migrations b.Property("InternalSlotId") .HasColumnType("int"); + b.Property("IsAdventurePlanet") + .HasColumnType("tinyint(1)"); + b.Property("LastUpdated") .HasColumnType("bigint"); @@ -903,6 +955,9 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + b.Property("ChildSlotId") + .HasColumnType("int"); + b.Property("PlayerIdCollection") .HasColumnType("longtext"); @@ -989,6 +1044,36 @@ namespace ProjectLighthouse.Migrations b.Navigation("User"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Levels.HeartedPlaylist", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Levels.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Profiles.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playlist"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Levels.Playlist", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Profiles.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Levels.QueuedLevel", b => { b.HasOne("LBPUnion.ProjectLighthouse.Levels.Slot", "Slot")