diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b42ef99b..5660a96c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "6.0.8", + "version": "6.0.9", "commands": [ "dotnet-ef" ] diff --git a/ProjectLighthouse.Localization/BaseLayout.lang-de-DE.resx b/ProjectLighthouse.Localization/BaseLayout.lang-de-DE.resx index c505621d..c14abc8f 100644 --- a/ProjectLighthouse.Localization/BaseLayout.lang-de-DE.resx +++ b/ProjectLighthouse.Localization/BaseLayout.lang-de-DE.resx @@ -50,7 +50,7 @@ A quick shortcut on the header to take you to your profile if logged in. - Admin + Administrationsmenü A header link that takes you to the admin panel if available. diff --git a/ProjectLighthouse.Localization/BaseLayout.lang-es-ES.resx b/ProjectLighthouse.Localization/BaseLayout.lang-es-ES.resx index 9d5bce4d..91d0115f 100644 --- a/ProjectLighthouse.Localization/BaseLayout.lang-es-ES.resx +++ b/ProjectLighthouse.Localization/BaseLayout.lang-es-ES.resx @@ -64,7 +64,7 @@ Página generada por {0}. - Esta página fue generada usando una versión modificada de Project Lighthouse. Por favor asegúrese de que está revelando correctamente el código principal a los usuarios que están usando está instancia + Esta página fue generada usando una versión modificada de Project Lighthouse. Por favor, asegúrese de que está revelando correctamente el código fuente a los usuarios que están usando está instancia Aunque intentemos tener el mínimo de JavaScript posible, no podemos garantizar que todo funcione sin él. Le recomendamos que actives JavaScript para Project Lighthouse. diff --git a/ProjectLighthouse.Localization/BaseLayout.lang-ru-RU.resx b/ProjectLighthouse.Localization/BaseLayout.lang-ru-RU.resx index 54a2c94b..0a804fcf 100644 --- a/ProjectLighthouse.Localization/BaseLayout.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/BaseLayout.lang-ru-RU.resx @@ -50,7 +50,7 @@ A quick shortcut on the header to take you to your profile if logged in. - Admin + Администратор A header link that takes you to the admin panel if available. @@ -58,30 +58,30 @@ A shortcut to log you out of your account. - Mod Panel + Панель модов - Page generated by {0}. + Страница создана {0} - This page was generated using a modified version of Project Lighthouse. Please make sure you are properly disclosing the source code to any users who may be using this instance. + Эта страница была создана с помощью модифицированной версии Project Lighthouse. Пожалуйста, убедитесь, что вы правильно раскрываете исходный код всем пользователям, которые могут использовать этот экземпляр. - While we intend to have as little JavaScript as possible, we can not guarantee everything will work without it. We recommend that you whitelist JavaScript for Project Lighthouse. + Хотя мы намерены использовать как можно меньше JavaScript, мы не можем гарантировать, что все будет работать без него. Мы рекомендуем включить JavaScript в белый список для освещения проекта Lighthouse. JavaScript не включен - Potential License Violation + Потенциальное нарушение лицензии - This instance is a public-facing instance that has been modified without the changes published. You may be in violation of the {0}. + Этот экземпляр является общедоступным экземпляром, который был изменен без публикации изменений. Вы могли нарушить {0}. - If you believe this is an error, please create an issue with the output of {0} ran from the root of the server source code in the description on our {1}issue tracker{2}. + Если вы считаете, что это ошибка, пожалуйста, создайте проблему с выводом {0} выполненным из корня исходного кода сервера в описании на нашем {1}трекера задач{2}. - If not, please publish the source code somewhere accessible to your users. + Если нет, пожалуйста, опубликуйте исходный код в доступном для ваших пользователей месте. \ No newline at end of file diff --git a/ProjectLighthouse.Localization/Error.lang-ru-RU.resx b/ProjectLighthouse.Localization/Error.lang-ru-RU.resx index da414456..10eec104 100644 --- a/ProjectLighthouse.Localization/Error.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/Error.lang-ru-RU.resx @@ -18,13 +18,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The username field is blank. + Поле имени пользователя пустое. Выбранное имя пользователя уже занято. - Password field is required. + Требуется ввести пароль. Пароли не совпадают! @@ -36,10 +36,10 @@ Вы должны правильно завершить капчу. - The email address you've chosen is already taken. + Выбранный адрес электронной почты уже занят. - Email address field is required. + Введите адрес электронной почты. Вы были заблокированы. Пожалуйста, свяжитесь с администратором для получения дополнительной информации.\nПричина: {0} diff --git a/ProjectLighthouse.Localization/General.lang-es-ES.resx b/ProjectLighthouse.Localization/General.lang-es-ES.resx index bfb2f43f..aa1692e4 100644 --- a/ProjectLighthouse.Localization/General.lang-es-ES.resx +++ b/ProjectLighthouse.Localization/General.lang-es-ES.resx @@ -18,7 +18,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Nombre de usuario + Usuario Contraseña diff --git a/ProjectLighthouse.Localization/General.lang-ru-RU.resx b/ProjectLighthouse.Localization/General.lang-ru-RU.resx index 8c79526c..4717a55d 100644 --- a/ProjectLighthouse.Localization/General.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/General.lang-ru-RU.resx @@ -45,12 +45,12 @@ Сбросить пароль - Recent Activity + Недавняя активность Скоро будет! - Most recent photos + Самые последние фото \ No newline at end of file diff --git a/ProjectLighthouse.Localization/LandingPage.lang-de-DE.resx b/ProjectLighthouse.Localization/LandingPage.lang-de-DE.resx index d9a2625c..65c8b3b2 100644 --- a/ProjectLighthouse.Localization/LandingPage.lang-de-DE.resx +++ b/ProjectLighthouse.Localization/LandingPage.lang-de-DE.resx @@ -26,19 +26,19 @@ A greeting on the main page of the website. - Greetings, {0}. + Willkommen, {0}. A greeting on the main page of the website. - There are no people online. Why not hop on? + Es ist niemand online. Lust auf ein Spiel? A greeting on the main page of the website. - There is 1 person currently online: + Gerade ist ein Spieler online: A greeting on the main page of the website. - There are currently {0} people online: + Gerade sind {0} Spieler online: A greeting on the main page of the website. diff --git a/ProjectLighthouse.Localization/LandingPage.lang-ru-RU.resx b/ProjectLighthouse.Localization/LandingPage.lang-ru-RU.resx index b72e6b71..6ebf741d 100644 --- a/ProjectLighthouse.Localization/LandingPage.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/LandingPage.lang-ru-RU.resx @@ -26,19 +26,19 @@ A greeting on the main page of the website. - Greetings, {0}. + Приветствуем, {0}. A greeting on the main page of the website. - There are no people online. Why not hop on? + Нет людей в сети. Почему бы не продолжить играть? A greeting on the main page of the website. - There is 1 person currently online: + Сейчас в сети 1 пользователь: A greeting on the main page of the website. - There are currently {0} people online: + В настоящее время {0} человек в сети: A greeting on the main page of the website. diff --git a/ProjectLighthouse.Localization/LocalizationManager.cs b/ProjectLighthouse.Localization/LocalizationManager.cs index a29fdf16..c3ef79c0 100644 --- a/ProjectLighthouse.Localization/LocalizationManager.cs +++ b/ProjectLighthouse.Localization/LocalizationManager.cs @@ -124,7 +124,7 @@ public static class LocalizationManager .Where(r => r != "resources") .ToList(); - languages.Add(DefaultLang); + languages.Insert(0, DefaultLang); return languages; } diff --git a/ProjectLighthouse.Localization/ModPanel.lang-ru-RU.resx b/ProjectLighthouse.Localization/ModPanel.lang-ru-RU.resx index 0471ed47..fc305894 100644 --- a/ProjectLighthouse.Localization/ModPanel.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/ModPanel.lang-ru-RU.resx @@ -18,15 +18,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Moderation Panel + Панель модерации - Welcome to the moderation panel, {0}! + Добро пожаловать на панель модерации, {0}! - Banned Users + Заблокированные пользователи - Hidden Levels + Скрытые уровни \ No newline at end of file diff --git a/ProjectLighthouse.Localization/Profile.lang-ru-RU.resx b/ProjectLighthouse.Localization/Profile.lang-ru-RU.resx index e3252b0b..83d6633d 100644 --- a/ProjectLighthouse.Localization/Profile.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/Profile.lang-ru-RU.resx @@ -18,12 +18,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Biography + Биография - {0} hasn't introduced themselves yet. + {0} Ещё не представился. - {0}'s user page + Страница пользователя {0} \ No newline at end of file diff --git a/ProjectLighthouse.Localization/Register.lang-ru-RU.resx b/ProjectLighthouse.Localization/Register.lang-ru-RU.resx index abd741d2..d19ca775 100644 --- a/ProjectLighthouse.Localization/Register.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/Register.lang-ru-RU.resx @@ -18,6 +18,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Caution: Your username MUST match your PSN/RPCN username in order to be able to sign in from in-game. + Внимание: Ваше имя пользователя ДОЛЖНО соответствует вашему логину PSN/RPCN для того, чтобы иметь возможность войти в игру. \ No newline at end of file diff --git a/ProjectLighthouse.Localization/Status.lang-de-DE.resx b/ProjectLighthouse.Localization/Status.lang-de-DE.resx index abf5a0ae..6bb4b095 100644 --- a/ProjectLighthouse.Localization/Status.lang-de-DE.resx +++ b/ProjectLighthouse.Localization/Status.lang-de-DE.resx @@ -18,7 +18,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Currently playing {0} on {1} + Spielt gerade {0} auf {1} Offline diff --git a/ProjectLighthouse.Localization/Status.lang-es-ES.resx b/ProjectLighthouse.Localization/Status.lang-es-ES.resx index 55669b55..1c1858fe 100644 --- a/ProjectLighthouse.Localization/Status.lang-es-ES.resx +++ b/ProjectLighthouse.Localization/Status.lang-es-ES.resx @@ -24,6 +24,6 @@ Desconectado - Desconectado desde hace {0} + Desconectado desde el {0} \ No newline at end of file diff --git a/ProjectLighthouse.Localization/Status.lang-es-MX.resx b/ProjectLighthouse.Localization/Status.lang-es-MX.resx index 94d4150f..1c1858fe 100644 --- a/ProjectLighthouse.Localization/Status.lang-es-MX.resx +++ b/ProjectLighthouse.Localization/Status.lang-es-MX.resx @@ -24,6 +24,6 @@ Desconectado - Desconectado desde {0} + Desconectado desde el {0} \ No newline at end of file diff --git a/ProjectLighthouse.Localization/Status.lang-pl-PL.resx b/ProjectLighthouse.Localization/Status.lang-pl-PL.resx index d06737e7..d04e261c 100644 --- a/ProjectLighthouse.Localization/Status.lang-pl-PL.resx +++ b/ProjectLighthouse.Localization/Status.lang-pl-PL.resx @@ -18,7 +18,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Currently playing {0} on {1} + Online w {0} na {1} Offline diff --git a/ProjectLighthouse.Localization/Status.lang-ru-RU.resx b/ProjectLighthouse.Localization/Status.lang-ru-RU.resx index cd181c0b..f08be300 100644 --- a/ProjectLighthouse.Localization/Status.lang-ru-RU.resx +++ b/ProjectLighthouse.Localization/Status.lang-ru-RU.resx @@ -18,12 +18,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Currently playing {0} on {1} + Сейчас играет {0} на {1} Не в сети - Offline since {0} + Последний раз был(а) в сети {0} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.API/Responses/MinimalSlot.cs b/ProjectLighthouse.Servers.API/Responses/MinimalSlot.cs index f1d39c66..326b9265 100644 --- a/ProjectLighthouse.Servers.API/Responses/MinimalSlot.cs +++ b/ProjectLighthouse.Servers.API/Responses/MinimalSlot.cs @@ -10,6 +10,7 @@ public struct MinimalSlot public string Name { get; set; } public string IconHash { get; set; } public bool TeamPick { get; set; } + public bool IsAdventure { get; set; } public GameVersion GameVersion { get; set; } #if DEBUG public long FirstUploaded { get; set; } @@ -22,6 +23,7 @@ public struct MinimalSlot Name = slot.Name, IconHash = slot.IconHash, TeamPick = slot.TeamPick, + IsAdventure = slot.IsAdventurePlanet, GameVersion = slot.GameVersion, #if DEBUG FirstUploaded = slot.FirstUploaded, diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs index b41adee2..bd7c0019 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs @@ -67,8 +67,7 @@ public class LoginController : ControllerBase token = await this.database.AuthenticateUser(npTicket, ipAddress); if (token == null) { - Logger.Warn($"Unable to " + - $"find/generate a token for username {npTicket.Username}", LogArea.Login); + Logger.Warn($"Unable to find/generate a token for username {npTicket.Username}", LogArea.Login); return this.StatusCode(403, ""); // If not, then 403. } } @@ -81,6 +80,12 @@ public class LoginController : ControllerBase return this.StatusCode(403, ""); } + if (ServerConfiguration.Instance.Mail.MailEnabled && (user.EmailAddress == null || !user.EmailAddressVerified)) + { + Logger.Error($"Email address unverified for user {user.Username}", LogArea.Login); + return this.StatusCode(403, ""); + } + if (ServerConfiguration.Instance.Authentication.UseExternalAuth) { if (user.ApprovedIPAddress == ipAddress) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index e19c432c..044236ee 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -63,8 +63,12 @@ public class PhotosController : ControllerBase { case SlotType.User: { - Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.SlotId == photoSlot.SlotId); - if (slot != null && !string.IsNullOrEmpty(slot.RootLevel)) validLevel = true; + // We'll grab the slot by the RootLevel and see what happens from here. + Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.ResourceCollection.Contains(photoSlot.RootLevel)); + if(slot == null) break; + + if (!string.IsNullOrEmpty(slot!.RootLevel)) validLevel = true; + if (slot.IsAdventurePlanet) photoSlot.SlotId = slot.SlotId; break; } case SlotType.Pod: diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs index 43ec1d48..d1caa841 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs @@ -56,6 +56,12 @@ public class ResourcesController : ControllerBase string path = FileHelper.GetResourcePath(hash); + string fullPath = Path.GetFullPath(path); + string basePath = Path.GetFullPath(FileHelper.ResourcePath); + + // Prevent directory traversal attacks + if (!fullPath.StartsWith(basePath)) return this.BadRequest(); + if (FileHelper.ResourceExists(hash)) return this.File(IOFile.OpenRead(path), "application/octet-stream"); return this.NotFound(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 84744a7c..3e1d051b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -53,8 +53,11 @@ public class ListController : ControllerBase return this.Ok ( - LbpSerializer.TaggedStringElement - ("slots", response, "total", this.database.QueuedLevels.Include(q => q.User).Count(q => q.User.Username == username)) + LbpSerializer.TaggedStringElement("slots", response, new Dictionary + { + { "total", await this.database.QueuedLevels.CountAsync(q => q.UserId == token.UserId) }, + { "hint_start", pageStart + Math.Min(pageSize, 30) }, + }) ); } @@ -136,7 +139,7 @@ public class ListController : ControllerBase ( LbpSerializer.TaggedStringElement("favouriteSlots", response, new Dictionary { - { "total", this.database.HeartedLevels.Count(q => q.UserId == targetUser.UserId) }, + { "total", await this.database.HeartedLevels.CountAsync(q => q.UserId == targetUser.UserId) }, { "hint_start", pageStart + Math.Min(pageSize, 30) }, }) ); @@ -225,7 +228,7 @@ public class ListController : ControllerBase ( LbpSerializer.TaggedStringElement("favouriteUsers", response, new Dictionary { - { "total", this.database.HeartedProfiles.Count(q => q.UserId == targetUser.UserId) }, + { "total", await this.database.HeartedProfiles.CountAsync(q => q.UserId == targetUser.UserId) }, { "hint_start", pageStart + Math.Min(pageSize, 30) }, }) ); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index d462cfdd..f8f91830 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -40,7 +40,8 @@ public class PublishController : ControllerBase GameToken gameToken = userAndToken.Value.Item2; Slot? slot = await this.getSlotFromBody(); - if (slot == null) { + if (slot == null) + { Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish); return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded } @@ -135,10 +136,21 @@ public class PublishController : ControllerBase return this.BadRequest(); } - if (rootLevel.FileType != LbpFileType.Level) + if (!slot.IsAdventurePlanet) { - Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish); - return this.BadRequest(); + if (rootLevel.FileType != LbpFileType.Level) + { + Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish); + return this.BadRequest(); + } + } + else + { + if (rootLevel.FileType != LbpFileType.Adventure) + { + Logger.Warn("Rejecting level upload, rootLevel is not a LBP 3 Adventure", LogArea.Publish); + return this.BadRequest(); + } } GameVersion slotVersion = FileHelper.ParseLevelVersion(rootLevel); @@ -232,7 +244,7 @@ public class PublishController : ControllerBase this.database.Slots.Add(slot); await this.database.SaveChangesAsync(); - + if (user.LevelVisibility == PrivacyType.All) { await WebhookHelper.SendWebhook("New level published!", diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index f25f4d57..78f1305a 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -26,7 +26,8 @@ public class ScoreController : ControllerBase } [HttpPost("scoreboard/{slotType}/{id:int}")] - public async Task SubmitScore(string slotType, int id, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false) + [HttpPost("scoreboard/{slotType}/{id:int}/{childId:int}")] + public async Task SubmitScore(string slotType, int id, int childId, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false) { GameToken? token = await this.database.GameTokenFromRequest(this.Request); if (token == null) return this.StatusCode(403, ""); @@ -78,6 +79,7 @@ public class ScoreController : ControllerBase if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); score.SlotId = id; + score.ChildSlotId = childId; Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId); if (slot == null) @@ -116,12 +118,13 @@ public class ScoreController : ControllerBase Type = score.Type, Points = score.Points, SlotId = score.SlotId, + ChildSlotId = score.ChildSlotId, }; IQueryable existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId) - .Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection) - .Where(s => s.Type == playerScore.Type); - + .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId) + .Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection) + .Where(s => s.Type == playerScore.Type); if (existingScore.Any()) { Score first = existingScore.First(s => s.SlotId == playerScore.SlotId); @@ -137,13 +140,14 @@ public class ScoreController : ControllerBase await this.database.SaveChangesAsync(); - string myRanking = this.getScores(score.SlotId, score.Type, username, -1, 5, "scoreboardSegment"); + string myRanking = this.getScores(score.SlotId, score.Type, username, -1, 5, "scoreboardSegment", childId: score.ChildSlotId); return this.Ok(myRanking); } [HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")] - public async Task FriendScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) + [HttpGet("friendscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")] + public async Task FriendScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) { GameToken? token = await this.database.GameTokenFromRequest(this.Request); if (token == null) return this.StatusCode(403, ""); @@ -169,12 +173,13 @@ public class ScoreController : ControllerBase if (friendUsername != null) friendNames.Add(friendUsername); } - return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, "scores", friendNames.ToArray())); + return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, "scores", friendNames.ToArray(), childId)); } [HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")] + [HttpGet("topscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - public async Task TopScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) + public async Task TopScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) { GameToken? token = await this.database.GameTokenFromRequest(this.Request); if (token == null) return this.StatusCode(403, ""); @@ -187,7 +192,7 @@ public class ScoreController : ControllerBase if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); - return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize)); + return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, childId: childId)); } [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] @@ -199,7 +204,8 @@ public class ScoreController : ControllerBase int pageStart = -1, int pageSize = 5, string rootName = "scores", - string[]? playerIds = null + string[]? playerIds = null, + int? childId = 0 ) { @@ -207,8 +213,9 @@ public class ScoreController : ControllerBase // var needed for Anonymous type returned from SELECT var rankedScores = this.database.Scores .Where(s => s.SlotId == slotId && s.Type == type) - .AsEnumerable() + .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId) .Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Contains(id))) + .AsEnumerable() .OrderByDescending(s => s.Points) .ThenBy(s => s.ScoreId) .ToList() diff --git a/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs b/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs index e67e7bf3..f7cf2ec6 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs @@ -1,8 +1,6 @@ #nullable enable -using LBPUnion.ProjectLighthouse.Administration; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -30,7 +28,7 @@ public class ModerationSlotController : ControllerBase slot.TeamPick = true; await this.database.SaveChangesAsync(); - return this.Ok(); + return this.Redirect("~/slot/" + id); } [HttpGet("removeTeamPick")] @@ -44,7 +42,7 @@ public class ModerationSlotController : ControllerBase slot.TeamPick = false; await this.database.SaveChangesAsync(); - return this.Ok(); + return this.Redirect("~/slot/" + id); } [HttpGet("delete")] @@ -58,6 +56,6 @@ public class ModerationSlotController : ControllerBase await this.database.RemoveSlot(slot); - return this.Ok(); + return this.Redirect("~/slots/0"); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Controllers/ResourcesController.cs b/ProjectLighthouse.Servers.Website/Controllers/ResourcesController.cs index 3edafd84..cdef6071 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/ResourcesController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/ResourcesController.cs @@ -11,18 +11,19 @@ public class ResourcesController : ControllerBase [HttpGet("/gameAssets/{hash}")] public IActionResult GetGameImage(string hash) { - string path = Path.Combine("png", $"{hash}.png"); + string path = FileHelper.GetImagePath($"{hash}.png"); - if (IOFile.Exists(path)) - { - return this.File(IOFile.OpenRead(path), "image/png"); - } + string fullPath = Path.GetFullPath(path); + string basePath = Path.GetFullPath(FileHelper.ImagePath); + + // Prevent directory traversal attacks + if (!fullPath.StartsWith(basePath)) return this.BadRequest(); + + if (IOFile.Exists(path)) return this.File(IOFile.OpenRead(path), "image/png"); LbpFile? file = LbpFile.FromHash(hash); - if (file != null && FileHelper.LbpFileToPNG(file)) - { - return this.File(IOFile.OpenRead(path), "image/png"); - } + if (file != null && FileHelper.LbpFileToPNG(file)) return this.File(IOFile.OpenRead(path), "image/png"); + return this.NotFound(); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs b/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs index a2843e46..a57b5d35 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs @@ -25,6 +25,27 @@ public class SlotPageController : ControllerBase this.database = database; } + [HttpGet("unpublish")] + public async Task UnpublishSlot([FromRoute] int id) + { + WebToken? token = this.database.WebTokenFromRequest(this.Request); + if (token == null) return this.Redirect("~/login"); + + Slot? targetSlot = await this.database.Slots.Include(s => s.Location).FirstOrDefaultAsync(s => s.SlotId == id); + if (targetSlot == null) return this.Redirect("~/slots/0"); + + if (targetSlot.Location == null) throw new ArgumentNullException(); + + if (targetSlot.CreatorId != token.UserId) return this.Redirect("~/slot/" + id); + + this.database.Locations.Remove(targetSlot.Location); + this.database.Slots.Remove(targetSlot); + + await this.database.SaveChangesAsync(); + + return this.Redirect("~/slots/0"); + } + [HttpGet("rateComment")] public async Task RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating) { diff --git a/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs b/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs index 78306838..6b6ca5ac 100644 --- a/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs +++ b/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs @@ -1,4 +1,3 @@ -using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using Microsoft.AspNetCore.Html; @@ -9,15 +8,19 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Extensions; public static class PartialExtensions { - // ReSharper disable once SuggestBaseTypeForParameter - public static ViewDataDictionary WithLang(this ViewDataDictionary viewData, string language) + + public static ViewDataDictionary WithLang(this ViewDataDictionary viewData, string language) => WithKeyValue(viewData, "Language", language); + + public static ViewDataDictionary WithTime(this ViewDataDictionary viewData, string timeZone) => WithKeyValue(viewData, "TimeZone", timeZone); + + private static ViewDataDictionary WithKeyValue(this ViewDataDictionary viewData, string key, object value) { try { - return new(viewData) + return new ViewDataDictionary(viewData) { { - "Language", language + key, value }, }; } @@ -27,9 +30,9 @@ public static class PartialExtensions } } - public static Task ToLink(this User user, IHtmlHelper helper, ViewDataDictionary viewData, string language) - => helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language)); + public static Task ToLink(this User user, IHtmlHelper helper, ViewDataDictionary viewData, string language, string timeZone = "", bool includeStatus = false) + => helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language).WithTime(timeZone).WithKeyValue("IncludeStatus", includeStatus)); - public static Task ToHtml(this Photo photo, IHtmlHelper helper, ViewDataDictionary viewData, string language) - => helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language)); + public static Task ToHtml(this Photo photo, IHtmlHelper helper, ViewDataDictionary viewData, string language, string timeZone) + => helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language).WithTime(timeZone)); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs b/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs index 61405d7a..6e8177bc 100644 --- a/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs +++ b/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs @@ -10,7 +10,6 @@ public class HandlePageErrorMiddleware : Middleware public override async Task InvokeAsync(HttpContext ctx) { await this.next(ctx); -// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract if (ctx.Response.StatusCode == 404 && !ctx.Request.Path.StartsWithSegments("/gameAssets")) { try @@ -20,7 +19,7 @@ public class HandlePageErrorMiddleware : Middleware finally { // not much we can do to save us, carry on anyways - await next(ctx); + await this.next(ctx); } } } diff --git a/ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs b/ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs new file mode 100644 index 00000000..92d81793 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs @@ -0,0 +1,61 @@ +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Middlewares; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Middlewares; + +public class UserRequiredRedirectMiddleware : MiddlewareDBContext +{ + public UserRequiredRedirectMiddleware(RequestDelegate next) : base(next) + { } + + public override async Task InvokeAsync(HttpContext ctx, Database database) + { + User? user = database.UserFromWebRequest(ctx.Request); + if (user == null || ctx.Request.Path.StartsWithSegments("/logout")) + { + await this.next(ctx); + return; + } + + // Request ends with a path (e.g. /css/style.css) + if (!string.IsNullOrEmpty(Path.GetExtension(ctx.Request.Path)) || ctx.Request.Path.StartsWithSegments("/gameAssets")) + { + await this.next(ctx); + return; + } + + if (user.PasswordResetRequired) + { + if (!ctx.Request.Path.StartsWithSegments("/passwordResetRequired") && + !ctx.Request.Path.StartsWithSegments("/passwordReset")) + { + ctx.Response.Redirect("/passwordResetRequired"); + return; + } + + await this.next(ctx); + return; + } + + if (ServerConfiguration.Instance.Mail.MailEnabled) + { + // The normal flow is for users to set their email during login so just force them to log out + if (user.EmailAddress == null) + { + ctx.Response.Redirect("/logout"); + return; + } + + if (!user.EmailAddressVerified && + !ctx.Request.Path.StartsWithSegments("/login/sendVerificationEmail") && + !ctx.Request.Path.StartsWithSegments("/verifyEmail")) + { + ctx.Response.Redirect("/login/sendVerificationEmail"); + return; + } + } + + await this.next(ctx); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs index 5f7e8752..0da6a3e2 100644 --- a/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs @@ -3,7 +3,6 @@ using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -30,6 +29,12 @@ public class CompleteEmailVerificationPage : BaseLayout return this.Page(); } + if (DateTime.Now > emailVerifyToken.ExpiresAt) + { + this.Error = "This token has expired"; + return this.Page(); + } + if (emailVerifyToken.UserId != user.UserId) { this.Error = "This token doesn't belong to you!"; diff --git a/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml index 083792b4..ab33c246 100644 --- a/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml @@ -5,6 +5,8 @@ @{ Layout = "Layouts/BaseLayout"; Model.Title = "Authentication"; + string timeZone = Model.GetTimeZone(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); } @if (Model.AuthenticationAttempts.Count == 0) @@ -41,9 +43,9 @@ else @foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts) { - DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp).ToLocalTime(); + DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp), timeZoneInfo);
-

A @authAttempt.Platform authentication request was logged at @timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC from the IP address @authAttempt.IPAddress.

+

A @authAttempt.Platform authentication request was logged at @timestamp.ToString("M/d/yyyy @ h:mm tt") from the IP address @authAttempt.IPAddress.

} diff --git a/ProjectLighthouse.Servers.Website/Pages/Moderation/CasePage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Moderation/CasePage.cshtml index 77ced934..635b4fcb 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Moderation/CasePage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Moderation/CasePage.cshtml @@ -1,10 +1,13 @@ @page "/moderation/cases/{pageNumber:int}" @using LBPUnion.ProjectLighthouse.Administration +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.CasePage @{ Layout = "Layouts/BaseLayout"; Model.Title = "Cases"; + + string timeZone = Model.GetTimeZone(); }

There are @Model.CaseCount total cases, @Model.DismissedCaseCount of which have been dismissed.

@@ -20,5 +23,5 @@ @foreach (ModerationCase @case in Model.Cases) { - @(await Html.PartialAsync("Partials/ModerationCasePartial", @case)) + @(await Html.PartialAsync("Partials/ModerationCasePartial", @case, ViewData.WithTime(timeZone))) } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Moderation/NewCasePage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Moderation/NewCasePage.cshtml index 6985ebad..ec666361 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Moderation/NewCasePage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Moderation/NewCasePage.cshtml @@ -1,5 +1,4 @@ @page "/moderation/newCase" -@using LBPUnion.ProjectLighthouse.Administration @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.NewCasePage @{ diff --git a/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportPage.cshtml index 53b0fe32..492c5a59 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportPage.cshtml @@ -1,9 +1,11 @@ @page "/moderation/report/{reportId:int}" +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.ReportPage @{ Layout = "Layouts/BaseLayout"; Model.Title = $"Report {Model.Report.ReportId}"; + string timeZone = Model.GetTimeZone(); } -@await Html.PartialAsync("Partials/ReportPartial", Model.Report) +@await Html.PartialAsync("Partials/ReportPartial", Model.Report, ViewData.WithTime(timeZone)) @await Html.PartialAsync("Partials/RenderReportBoundsPartial") \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportsPage.cshtml index 0efde1ed..806ba3f6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportsPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Moderation/ReportsPage.cshtml @@ -1,10 +1,12 @@ @page "/moderation/reports/{pageNumber:int}" @using LBPUnion.ProjectLighthouse.Administration.Reports +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.ReportsPage @{ Layout = "Layouts/BaseLayout"; Model.Title = "Reports"; + string timeZone = Model.GetTimeZone(); }

There are @Model.ReportCount total reports.

@@ -28,7 +30,7 @@ @foreach (GriefReport report in Model.Reports) { - @await Html.PartialAsync("Partials/ReportPartial", report) + @await Html.PartialAsync("Partials/ReportPartial", report, ViewData.WithTime(timeZone)) } @await Html.PartialAsync("Partials/RenderReportBoundsPartial") diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml index 4cb549c4..0a8c24a3 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml @@ -6,6 +6,8 @@ @{ string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); }
@@ -85,7 +87,7 @@ @decodedMessage }

- @timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC + @TimeZoneInfo.ConvertTime(timestamp, timeZoneInfo).ToString("M/d/yyyy @ h:mm:ss tt")

@if (i != Model.Comments.Count - 1) { diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml index 42d4a332..7aed4e31 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml @@ -3,9 +3,12 @@ @{ string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + bool includeStatus = (bool?)ViewData["IncludeStatus"] ?? false; + string userStatus = includeStatus ? Model.Status.ToTranslatedString(language, timeZone) : ""; } -
+ @Model.Username \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml index b34d1b95..09b3773f 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml @@ -1,7 +1,6 @@ @using System.Diagnostics @using LBPUnion.ProjectLighthouse @using LBPUnion.ProjectLighthouse.Administration -@using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Levels @using LBPUnion.ProjectLighthouse.PlayerData.Profiles @model LBPUnion.ProjectLighthouse.Administration.ModerationCase @@ -10,6 +9,9 @@ Database database = new(); string color = "blue"; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); + if (Model.Expired) color = "yellow"; if (Model.Dismissed) color = "green"; } @@ -23,19 +25,19 @@ Debug.Assert(Model.DismissedAt != null);

- This case was dismissed by @Model.Dismisser.Username on @Model.DismissedAt.Value.ToString("MM/dd/yyyy @ h:mm tt"). + This case was dismissed by @Model.Dismisser.Username on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").

} - else if (Model.Expired) + else if (Model.Expired && Model.ExpiresAt != null) {

- This case expired on @Model.ExpiresAt!.Value.ToString("MM/dd/yyyy @ h:mm tt"). + This case expired on @TimeZoneInfo.ConvertTime(Model.ExpiresAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").

} Case created by @Model.Creator.Username - on @Model.CreatedAt.ToString("MM/dd/yyyy @ h:mm tt") + on @TimeZoneInfo.ConvertTime(Model.CreatedAt, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt")
@if (Model.Type.AffectsLevel()) diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index 5e0252e8..89de6ad1 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -8,6 +8,8 @@ @{ string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); }
@@ -26,7 +28,7 @@ { by - @await Model.Creator.ToLink(Html, ViewData, language) + @await Model.Creator.ToLink(Html, ViewData, language, timeZone) } @if (Model.Slot != null) @@ -35,7 +37,8 @@ { case SlotType.User: - in level @HttpUtility.HtmlDecode(Model.Slot.Name) + @(Model.Slot.IsAdventurePlanet ? "on an adventure in" : "in level") + @HttpUtility.HtmlDecode(Model.Slot.Name) break; case SlotType.Developer: @@ -49,7 +52,7 @@ break; } } - at @DateTime.UnixEpoch.AddSeconds(Model.Timestamp).ToString(CultureInfo.CurrentCulture) + at @TimeZoneInfo.ConvertTime(DateTime.UnixEpoch.AddSeconds(Model.Timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt")

@@ -62,7 +65,7 @@
@foreach (PhotoSubject subject in Model.Subjects) { - @await subject.User.ToLink(Html, ViewData, language) + @await subject.User.ToLink(Html, ViewData, language, timeZone) }
diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ReportPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ReportPartial.cshtml index b8ad6a90..a07343ef 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ReportPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ReportPartial.cshtml @@ -1,7 +1,12 @@ @using LBPUnion.ProjectLighthouse.Administration.Reports @model LBPUnion.ProjectLighthouse.Administration.Reports.GriefReport -
+@{ + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); +} + +
@@ -26,7 +31,7 @@
- Report time: @(DateTimeOffset.FromUnixTimeMilliseconds(Model.Timestamp).ToLocalTime().ToString("R")) + Report time: @(TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(Model.Timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt"))
Report reason: @Model.Type diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml index 843f2b03..aa166760 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml @@ -18,6 +18,8 @@ bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool mini = (bool?)ViewData["IsMini"] ?? false; string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); bool isQueued = false; bool isHearted = false; @@ -37,11 +39,13 @@
@{ int size = isMobile || mini ? 50 : 100; + bool isAdventure = Model.IsAdventurePlanet; + string advenStyleExt = isAdventure ? "-webkit-mask-image: url(/assets/advSlotCardMask.png); -webkit-mask-size: contain; border-radius: 0%;" : ""; }
- - - + +
@@ -90,12 +94,24 @@
@if (Model.Creator != null) { + string date = ""; + if(!mini) + date = " on " + TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(Model.FirstUploaded), timeZoneInfo).DateTime.ToShortDateString();

- Created by @await Model.Creator.ToLink(Html, ViewData, language) on @Model.GameVersion.ToPrettyString() + Created by @await Model.Creator.ToLink(Html, ViewData, language) in @Model.GameVersion.ToPrettyString()@date

}
+
+ @if (user != null && !mini && (user.IsModerator || user.UserId == Model.CreatorId)) + { + + + + } +
+

@if (user != null && !mini) { diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml index 32311fb6..99f9c265 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml @@ -6,13 +6,14 @@ bool showLink = (bool?)ViewData["ShowLink"] ?? false; bool isMobile = (bool?)ViewData["IsMobile"] ?? false; string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; }
@{ int size = isMobile ? 50 : 100; } -
+
@if (showLink) @@ -28,7 +29,7 @@ } - @Model.Status.ToTranslatedString(language) + @Model.Status.ToTranslatedString(language, timeZone)
@Model.Hearts diff --git a/ProjectLighthouse.Servers.Website/Pages/PasswordResetRequiredPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/PasswordResetRequiredPage.cshtml index e62de0c3..6724cc96 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PasswordResetRequiredPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/PasswordResetRequiredPage.cshtml @@ -1,6 +1,4 @@ @page "/passwordResetRequired" -@using LBPUnion.ProjectLighthouse.Localization.StringLists -@using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequiredPage @{ diff --git a/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml index 1a189049..0af93f44 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml @@ -2,7 +2,6 @@ @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions -@using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PhotosPage @{ @@ -10,6 +9,7 @@ Model.Title = Model.Translate(BaseLayoutStrings.HeaderPhotos); string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); }

There are @Model.PhotoCount total photos!

@@ -25,7 +25,7 @@ @foreach (Photo photo in Model.Photos) {
- @await photo.ToHtml(Html, ViewData, language) + @await photo.ToHtml(Html, ViewData, language, timeZone)
} diff --git a/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml index 9d9b183e..f2b3e9b9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml @@ -7,7 +7,7 @@ } -@if (!Model.User!.IsAPirate) +@if (Model.User!.Language != "en-PT") {

So, ye wanna be a pirate? Well, ye came to the right place!

Just click this 'ere button, and welcome aboard!

diff --git a/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs index 17dff379..a65827f4 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/PirateSignupPage.cshtml.cs @@ -1,8 +1,6 @@ using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; @@ -24,7 +22,7 @@ public class PirateSignupPage : BaseLayout User? user = this.Database.UserFromWebRequest(this.Request); if (user == null) return this.Redirect("/login"); - user.IsAPirate = !user.IsAPirate; + user.Language = user.Language == "en-PT" ? "en" : "en-PT"; await this.Database.SaveChangesAsync(); return this.Redirect("/"); diff --git a/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml.cs index 6f6a7619..3f8260d0 100644 --- a/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/RegisterForm.cshtml.cs @@ -72,7 +72,7 @@ public class RegisterForm : BaseLayout } if (ServerConfiguration.Instance.Mail.MailEnabled && - await this.Database.Users.FirstOrDefaultAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower()) != null) + await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower())) { this.Error = this.Translate(ErrorStrings.EmailTaken); return this.Page(); @@ -86,7 +86,7 @@ public class RegisterForm : BaseLayout if (this.Request.Query.ContainsKey("token")) { - await Database.RemoveRegistrationToken(this.Request.Query["token"]); + await this.Database.RemoveRegistrationToken(this.Request.Query["token"]); } User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress); diff --git a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml index cae13524..0a3fa0f6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml @@ -6,8 +6,15 @@ Model.Title = "Verify Email Address"; } -

An email address on your account has been set, but hasn't been verified yet.

-

To verify it, check the email sent to @Model.User?.EmailAddress and click the link in the email.

+@if (Model.Success) +{ +

An email address on your account has been set, but hasn't been verified yet.

+

To verify it, check the email sent to @Model.User?.EmailAddress and click the link in the email.

+} +else +{ +

Failed to send email, please try again later

+}
Resend email
diff --git a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs index fae0a1d4..7431acc4 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs @@ -4,8 +4,8 @@ using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; @@ -14,6 +14,8 @@ public class SendVerificationEmailPage : BaseLayout public SendVerificationEmailPage(Database database) : base(database) {} + public bool Success { get; set; } + public async Task OnGet() { if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); @@ -33,29 +35,29 @@ public class SendVerificationEmailPage : BaseLayout } #endif - EmailVerificationToken verifyToken = new() + EmailVerificationToken? verifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(v => v.UserId == user.UserId); + // If user doesn't have a token or it is expired then regenerate + if (verifyToken == null || DateTime.Now > verifyToken.ExpiresAt) { - UserId = user.UserId, - User = user, - EmailToken = CryptoHelper.GenerateAuthToken(), - }; + verifyToken = new EmailVerificationToken + { + UserId = user.UserId, + User = user, + EmailToken = CryptoHelper.GenerateAuthToken(), + ExpiresAt = DateTime.Now.AddHours(6), + }; - this.Database.EmailVerificationTokens.Add(verifyToken); - - await this.Database.SaveChangesAsync(); + this.Database.EmailVerificationTokens.Add(verifyToken); + await this.Database.SaveChangesAsync(); + } string body = "Hello,\n\n" + $"This email is a request to verify this email for your (likely new!) Project Lighthouse account ({user.Username}).\n\n" + $"To verify your account, click the following link: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" + "If this wasn't you, feel free to ignore this email."; - if (SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body)) - { - return this.Page(); - } - else - { - throw new Exception("failed to send email"); - } + this.Success = SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body); + + return this.Page(); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml b/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml index 38782a30..0bf88c98 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml @@ -1,5 +1,6 @@ @page "/login/setEmail" @using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Localization.StringLists @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm @{ @@ -9,6 +10,16 @@

This instance requires email verification. As your account was created before this was a requirement, you must now set an email for your account before continuing.

+@if (!string.IsNullOrWhiteSpace(Model.Error)) +{ +
+
+ @Model.Translate(GeneralStrings.Error) +
+

@Model.Error

+
+} +
@Html.AntiForgeryToken() diff --git a/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs index 01ca1262..d16eb5a9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs @@ -1,12 +1,13 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Localization.StringLists; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -19,6 +20,8 @@ public class SetEmailForm : BaseLayout public EmailSetToken? EmailToken; + public string? Error { get; private set; } + public async Task OnGet(string? token = null) { if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); @@ -32,6 +35,7 @@ public class SetEmailForm : BaseLayout return this.Page(); } + [SuppressMessage("ReSharper", "SpecifyStringComparison")] public async Task OnPost(string emailAddress, string token) { if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); @@ -39,6 +43,13 @@ public class SetEmailForm : BaseLayout EmailSetToken? emailToken = await this.Database.EmailSetTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.EmailToken == token); if (emailToken == null) return this.Redirect("/login"); + if (await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower())) + { + this.Error = this.Translate(ErrorStrings.EmailTaken); + this.EmailToken = emailToken; + return this.Page(); + } + emailToken.User.EmailAddress = emailAddress; this.Database.EmailSetTokens.Remove(emailToken); diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml index e25df416..83e3dd77 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml @@ -19,6 +19,7 @@ bool isMobile = this.Request.IsMobile(); string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); } @if (Model.Slot!.Hidden) @@ -96,7 +97,7 @@
}
- @await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language)) + @await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
@if (isMobile) { @@ -216,7 +217,7 @@ @foreach (Photo photo in Model.Photos) {
- @await photo.ToHtml(Html, ViewData, language) + @await photo.ToHtml(Html, ViewData, language, timeZone)
}
diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml new file mode 100644 index 00000000..08fec3e4 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml @@ -0,0 +1,168 @@ +@page "/slot/{slotId:int}/settings" +@using System.Web +@using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Extensions +@using LBPUnion.ProjectLighthouse.Helpers +@using LBPUnion.ProjectLighthouse.Levels +@using LBPUnion.ProjectLighthouse.Localization.StringLists +@using LBPUnion.ProjectLighthouse.PlayerData +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotSettingsPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.ShowTitleInPage = false; + + Model.Title = HttpUtility.HtmlDecode(Model.Slot?.Name ?? ""); + + bool isMobile = Request.IsMobile(); + + int size = isMobile ? 100 : 200; +} + + + + + + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs new file mode 100644 index 00000000..a2ceedf4 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs @@ -0,0 +1,61 @@ +#nullable enable +using LBPUnion.ProjectLighthouse.Files; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Levels; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; + +public class SlotSettingsPage : BaseLayout +{ + + public Slot? Slot; + public SlotSettingsPage(Database database) : base(database) + {} + + public async Task OnPost([FromRoute] int slotId, [FromForm] string? avatar, [FromForm] string? name, [FromForm] string? description, string? labels) + { + this.Slot = await this.Database.Slots.FirstOrDefaultAsync(u => u.SlotId == slotId); + if (this.Slot == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/slot/" + slotId); + + if (!this.User.IsModerator && this.User != this.Slot.Creator) return this.Redirect("~/slot/" + slotId); + + string? avatarHash = await FileHelper.ParseBase64Image(avatar); + + if (avatarHash != null) this.Slot.IconHash = avatarHash; + + name = SanitizationHelper.SanitizeString(name); + if (this.Slot.Name != name && name.Length <= 64) this.Slot.Name = name; + + description = SanitizationHelper.SanitizeString(description); + if (this.Slot.Description != description && description.Length <= 512) this.Slot.Description = description; + + labels = LabelHelper.RemoveInvalidLabels(SanitizationHelper.SanitizeString(labels)); + if (this.Slot.AuthorLabels != labels) this.Slot.AuthorLabels = labels; + + // ReSharper disable once InvertIf + if (this.Database.ChangeTracker.HasChanges()) + { + this.Slot.LastUpdated = TimeHelper.TimestampMillis; + await this.Database.SaveChangesAsync(); + } + + return this.Redirect("~/slot/" + slotId); + } + + public async Task OnGet([FromRoute] int slotId) + { + this.Slot = await this.Database.Slots.FirstOrDefaultAsync(s => s.SlotId == slotId); + if (this.Slot == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/slot/" + slotId); + + if(!this.User.IsModerator && this.User.UserId != this.Slot.CreatorId) return this.Redirect("~/slot/" + slotId); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml index f2085172..47e4fd90 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml @@ -5,7 +5,6 @@ @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions -@using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage @{ @@ -15,8 +14,9 @@ Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username); Model.Description = Model.ProfileUser!.Biography; - bool isMobile = this.Request.IsMobile(); + bool isMobile = Request.IsMobile(); string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); } @if (Model.ProfileUser.IsBanned) @@ -50,7 +50,10 @@ }, { "Language", Model.GetLanguage() - } + }, + { + "TimeZone", Model.GetTimeZone() + }, })
@@ -129,7 +134,7 @@ { string width = isMobile ? "sixteen" : "eight";
- @await photo.ToHtml(Html, ViewData, language) + @await photo.ToHtml(Html, ViewData, language, timeZone)
}
@@ -140,7 +145,7 @@ } } -@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language)) +@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone)) @if (Model.User != null && Model.User.IsModerator) { diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml new file mode 100644 index 00000000..5fe1bfc3 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml @@ -0,0 +1,154 @@ +@page "/user/{userId:int}/settings" +@using System.Globalization +@using System.Web +@using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Extensions +@using LBPUnion.ProjectLighthouse.Localization +@using LBPUnion.ProjectLighthouse.Localization.StringLists +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserSettingsPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.ShowTitleInPage = false; + + Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username); + + bool isMobile = Request.IsMobile(); + + int size = isMobile ? 100 : 200; +} + + + + + + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs new file mode 100644 index 00000000..78796aef --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs @@ -0,0 +1,86 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Files; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Localization; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; + +public class UserSettingsPage : BaseLayout +{ + + public User? ProfileUser; + public UserSettingsPage(Database database) : base(database) + {} + + private static bool IsValidEmail(string? email) => !string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email); + + [SuppressMessage("ReSharper", "SpecifyStringComparison")] + public async Task OnPost([FromRoute] int userId, [FromForm] string? avatar, [FromForm] string? username, [FromForm] string? email, [FromForm] string? biography, [FromForm] string? timeZone, [FromForm] string? language) + { + this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); + if (this.ProfileUser == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/user/" + userId); + + if (!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId); + + string? avatarHash = await FileHelper.ParseBase64Image(avatar); + + if (avatarHash != null) this.ProfileUser.IconHash = avatarHash; + + biography = SanitizationHelper.SanitizeString(biography); + + if (this.ProfileUser.Biography != biography && biography.Length <= 512) this.ProfileUser.Biography = biography; + + if (ServerConfiguration.Instance.Mail.MailEnabled && IsValidEmail(email) && (this.User == this.ProfileUser || this.User.IsAdmin)) + { + // if email hasn't already been used + if (!await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email!.ToLower())) + { + if (this.ProfileUser.EmailAddress != email) + { + this.ProfileUser.EmailAddress = email; + this.ProfileUser.EmailAddressVerified = false; + } + } + } + + if (this.ProfileUser == this.User) + { + if (!string.IsNullOrWhiteSpace(language) && this.ProfileUser.Language != language) + { + if (LocalizationManager.GetAvailableLanguages().Contains(language)) + this.ProfileUser.Language = language; + } + + if (!string.IsNullOrWhiteSpace(timeZone) && this.ProfileUser.TimeZone != timeZone) + { + HashSet timeZoneIds = TimeZoneInfo.GetSystemTimeZones().Select(t => t.Id).ToHashSet(); + if (timeZoneIds.Contains(timeZone)) this.ProfileUser.TimeZone = timeZone; + } + } + + + await this.Database.SaveChangesAsync(); + return this.Redirect("~/user/" + userId); + } + + public async Task OnGet([FromRoute] int userId) + { + this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); + if (this.ProfileUser == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/user/" + userId); + + if(!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml index e2f083e9..97c0940f 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml @@ -2,7 +2,6 @@ @using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.PlayerData.Profiles -@using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UsersPage @{ @@ -35,6 +34,9 @@ { "Language", Model.GetLanguage() }, + { + "TimeZone", Model.GetTimeZone() + }, })
} diff --git a/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj b/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj index cf72bd1f..4c421f7b 100644 --- a/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj +++ b/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj @@ -33,7 +33,7 @@ - + diff --git a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs index 6e757fa7..159b4abb 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -80,6 +80,7 @@ public class WebsiteStartup app.UseMiddleware(); app.UseMiddleware(); + app.UseMiddleware(); app.UseRouting(); diff --git a/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj b/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj index 3ba83d83..660e2cb4 100644 --- a/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj +++ b/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj @@ -9,8 +9,8 @@ - - + + all 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 9495735a..33af481e 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj +++ b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj @@ -9,14 +9,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj index 9886d57e..b8c96bab 100644 --- a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj +++ b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj @@ -14,8 +14,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ProjectLighthouse/Files/FileHelper.cs b/ProjectLighthouse/Files/FileHelper.cs index a922b25c..9e2d9452 100644 --- a/ProjectLighthouse/Files/FileHelper.cs +++ b/ProjectLighthouse/Files/FileHelper.cs @@ -6,14 +6,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using DDSReader; using ICSharpCode.SharpZipLib.Zip.Compression; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; +using Pfim; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -23,8 +24,12 @@ public static class FileHelper { public static readonly string ResourcePath = Path.Combine(Environment.CurrentDirectory, "r"); + public static readonly string ImagePath = Path.Combine(Environment.CurrentDirectory, "png"); + public static string GetResourcePath(string hash) => Path.Combine(ResourcePath, hash); + public static string GetImagePath(string hash) => Path.Combine(ImagePath, hash); + public static bool AreDependenciesSafe(LbpFile file) { // recursively check if dependencies are safe @@ -63,6 +68,7 @@ public static class FileHelper LbpFileType.Texture => true, LbpFileType.Script => false, LbpFileType.Level => true, + LbpFileType.Adventure => true, LbpFileType.Voice => true, LbpFileType.Quest => true, LbpFileType.Plan => true, @@ -189,6 +195,8 @@ public static class FileHelper "FSHb" => LbpFileType.Script, "VOPb" => LbpFileType.Voice, "LVLb" => LbpFileType.Level, + "ADCb" => LbpFileType.Adventure, + "ADSb" => LbpFileType.Adventure, "PLNb" => LbpFileType.Plan, "QSTb" => LbpFileType.Quest, _ => readAlternateHeader(reader), @@ -240,6 +248,34 @@ public static class FileHelper if (!Directory.Exists(path)) Directory.CreateDirectory(path ?? throw new ArgumentNullException(nameof(path))); } + private static readonly Regex base64Regex = new(@"data:([^\/]+)\/([^;]+);base64,(.*)", RegexOptions.Compiled); + + public static async Task ParseBase64Image(string? image) + { + if (string.IsNullOrWhiteSpace(image)) return null; + + System.Text.RegularExpressions.Match match = base64Regex.Match(image); + + if (!match.Success) return null; + + if (match.Groups.Count != 4) return null; + + byte[] data = Convert.FromBase64String(match.Groups[3].Value); + + LbpFile file = new(data); + + if (file.FileType is not (LbpFileType.Jpeg or LbpFileType.Png)) return null; + + if (ResourceExists(file.Hash)) return file.Hash; + + string assetsDirectory = ResourcePath; + string path = GetResourcePath(file.Hash); + + EnsureDirectoryCreated(assetsDirectory); + await File.WriteAllBytesAsync(path, file.Data); + return file.Hash; + } + public static string[] ResourcesNotUploaded(params string[] hashes) => hashes.Where(hash => !ResourceExists(hash)).ToArray(); public static void ConvertAllTexturesToPng() @@ -284,7 +320,7 @@ public static class FileHelper public static bool LbpFileToPNG(LbpFile file) => LbpFileToPNG(file.Data, file.Hash, file.FileType); - public static bool LbpFileToPNG(byte[] data, string hash, LbpFileType type) + private static bool LbpFileToPNG(byte[] data, string hash, LbpFileType type) { if (type != LbpFileType.Jpeg && type != LbpFileType.Png && type != LbpFileType.Texture) return false; @@ -342,6 +378,7 @@ public static class FileHelper if (compressed[i] == decompressed[i]) { writer.Write(deflatedData); + continue; } Inflater inflater = new(); @@ -357,19 +394,25 @@ public static class FileHelper private static bool DDSToPNG(string hash, byte[] data) { - using MemoryStream stream = new(); - DDSImage image = new(data); + Dds ddsImage = Dds.Create(data, new PfimConfig()); + if(ddsImage.Compressed) + ddsImage.Decompress(); - image.SaveAsPng(stream); + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + Image image = ddsImage.Format switch + { + ImageFormat.Rgba32 => Image.LoadPixelData(ddsImage.Data, ddsImage.Width, ddsImage.Height), + _ => throw new ArgumentOutOfRangeException($"ddsImage.Format is not supported: {ddsImage.Format}") + }; Directory.CreateDirectory("png"); - File.WriteAllBytes($"png/{hash}.png", stream.ToArray()); + image.SaveAsPngAsync($"png/{hash}.png"); return true; } private static bool JPGToPNG(string hash, byte[] data) { - using Image image = Image.Load(data); + using Image image = Image.Load(data); using MemoryStream ms = new(); image.SaveAsPng(ms); diff --git a/ProjectLighthouse/Files/LbpFileType.cs b/ProjectLighthouse/Files/LbpFileType.cs index 7e4bdf03..4f9a3c2a 100644 --- a/ProjectLighthouse/Files/LbpFileType.cs +++ b/ProjectLighthouse/Files/LbpFileType.cs @@ -5,6 +5,7 @@ public enum LbpFileType Script, // .ff, FSH Texture, // TEX Level, // LVL + Adventure, // ADC, ADS CrossLevel, // PRF, Cross controller level FileArchive, // .farc, (ends with FARC) Plan, // PLN, uploaded with levels diff --git a/ProjectLighthouse/Helpers/LabelHelper.cs b/ProjectLighthouse/Helpers/LabelHelper.cs index 5bbb7d13..8718f9ca 100644 --- a/ProjectLighthouse/Helpers/LabelHelper.cs +++ b/ProjectLighthouse/Helpers/LabelHelper.cs @@ -1,32 +1,107 @@ using System; using System.Collections.Generic; using LBPUnion.ProjectLighthouse.Levels; +using LBPUnion.ProjectLighthouse.PlayerData; namespace LBPUnion.ProjectLighthouse.Helpers; public static class LabelHelper { + private static readonly List lbpVitaLabels = new() + { + "LABEL_Arcade", + "LABEL_Co_op", + "LABEL_Precision", + "LABEL_Controlinator", + "LABEL_Flick", + "LABEL_Memoriser", + "LABEL_MultiLevel", + "LABEL_Portrait", + "LABEL_RearTouch", + "LABEL_SharedScreen", + "LABEL_Swipe", + "LABEL_Tap", + "LABEL_Tilt", + "LABEL_Touch", + }; + + private static readonly List lbp3Labels = new() + { + "LABEL_SINGLE_PLAYER", + "LABEL_RPG", + "LABEL_TOP_DOWN", + "LABEL_CO_OP", + "LABEL_1st_Person", + "LABEL_3rd_Person", + "LABEL_Sci_Fi", + "LABEL_Social", + "LABEL_Arcade_Game", + "LABEL_Board_Game", + "LABEL_Card_Game", + "LABEL_Mini_Game", + "LABEL_Party_Game", + "LABEL_Defence", + "LABEL_Driving", + "LABEL_Hangout", + "LABEL_Hide_And_Seek", + "LABEL_Prop_Hunt", + "LABEL_Music_Gallery", + "LABEL_Costume_Gallery", + "LABEL_Sticker_Gallery", + "LABEL_Movie", + "LABEL_Pinball", + "LABEL_Technology", + "LABEL_Homage", + "LABEL_8_Bit", + "LABEL_16_Bit", + "LABEL_Seasonal", + "LABEL_Time_Trial", + "LABEL_INTERACTIVE_STREAM", + "LABEL_QUESTS", + "LABEL_SACKPOCKET", + "LABEL_SPRINGINATOR", + "LABEL_HOVERBOARD_NAME", + "LABEL_FLOATY_FLUID_NAME", + "LABEL_ODDSOCK", + "LABEL_TOGGLE", + "LABEL_SWOOP", + "LABEL_SACKBOY", + "LABEL_CREATED_CHARACTERS", + }; + private static readonly Dictionary translationTable = new() { + {"Label_SinglePlayer", "Single Player"}, + {"LABEL_Quick", "Short"}, + {"LABEL_Competitive", "Versus"}, + {"LABEL_Puzzle", "Puzzler"}, + {"LABEL_Platform", "Platformer"}, + {"LABEL_Race", "Racer"}, + {"LABEL_SurvivalChallenge", "Survival Challenge"}, {"LABEL_DirectControl", "Controllinator"}, {"LABEL_GrapplingHook", "Grappling Hook"}, {"LABEL_JumpPads", "Bounce Pads"}, {"LABEL_MagicBag", "Creatinator"}, {"LABEL_LowGravity", "Low Gravity"}, - {"LABEL_PowerGlove", "Grabinator"}, + {"LABEL_PowerGlove", "Grabinators"}, {"LABEL_ATTRACT_GEL", "Attract-o-Gel"}, {"LABEL_ATTRACT_TWEAK", "Attract-o-Tweaker"}, {"LABEL_HEROCAPE", "Hero Cape"}, {"LABEL_MEMORISER", "Memorizer"}, {"LABEL_WALLJUMP", "Wall Jump"}, + {"Label_Controlinator", "Controllinator"}, + {"LABEL_MultiLevel", "Multi Level"}, + {"LABEL_Portrait", "Portrait View"}, + {"LABEL_RearTouch", "Rear touch pad"}, + {"LABEL_SharedScreen", "Shared Screen"}, {"LABEL_SINGLE_PLAYER", "Single Player"}, - {"LABEL_SurvivalChallenge", "Survival Challenge"}, {"LABEL_TOP_DOWN", "Top Down"}, {"LABEL_CO_OP", "Co-Op"}, {"LABEL_Sci_Fi", "Sci-Fi"}, {"LABEL_INTERACTIVE_STREAM", "Interactive Stream"}, {"LABEL_QUESTS", "Quests"}, + {"LABEL_Mini_Game", "Mini-Game"}, {"8_Bit", "8-bit"}, {"16_Bit", "16-bit"}, {"LABEL_SACKPOCKET", "Sackpocket"}, @@ -40,13 +115,27 @@ public static class LabelHelper {"LABEL_CREATED_CHARACTERS", "Created Characters"}, }; + public static bool isValidForGame(string label, GameVersion gameVersion) + { + return gameVersion switch + { + GameVersion.LittleBigPlanet1 => IsValidTag(label), + GameVersion.LittleBigPlanet2 => IsValidLabel(label) && !lbp3Labels.Contains(label) && !lbpVitaLabels.Contains(label), + GameVersion.LittleBigPlanetVita => IsValidLabel(label) && !lbp3Labels.Contains(label), + GameVersion.LittleBigPlanet3 => IsValidLabel(label) && !lbpVitaLabels.Contains(label), + _ => false, + }; + } + public static bool IsValidTag(string tag) => Enum.IsDefined(typeof(LevelTags), tag.Replace("TAG_", "").Replace("-", "_")); - public static bool IsValidLabel(string label) => Enum.IsDefined(typeof(LevelLabels), label); + private static bool IsValidLabel(string label) => Enum.IsDefined(typeof(LevelLabels), label.Replace("-", "_")); public static string RemoveInvalidLabels(string authorLabels) { List labels = new(authorLabels.Split(",")); + if (labels.Count > 5) labels = labels.GetRange(0, 5); + for (int i = labels.Count - 1; i >= 0; i--) { if (!IsValidLabel(labels[i])) labels.Remove(labels[i]); diff --git a/ProjectLighthouse/Helpers/SanitizationHelper.cs b/ProjectLighthouse/Helpers/SanitizationHelper.cs index 06882f02..3df81756 100644 --- a/ProjectLighthouse/Helpers/SanitizationHelper.cs +++ b/ProjectLighthouse/Helpers/SanitizationHelper.cs @@ -30,8 +30,9 @@ public static class SanitizationHelper } } - public static string SanitizeString(string input) + public static string SanitizeString(string? input) { + if (input == null) return ""; foreach ((string? key, string? value) in charsToReplace) { diff --git a/ProjectLighthouse/Levels/LevelLabels.cs b/ProjectLighthouse/Levels/LevelLabels.cs index 8d19d8f7..186743f6 100644 --- a/ProjectLighthouse/Levels/LevelLabels.cs +++ b/ProjectLighthouse/Levels/LevelLabels.cs @@ -7,6 +7,7 @@ namespace LBPUnion.ProjectLighthouse.Levels; // I would remove the LABEL prefix, but some of the tags start with numbers and won't compile public enum LevelLabels { + // Start LBP2 Labels LABEL_SinglePlayer, LABEL_Multiplayer, LABEL_Quick, @@ -31,6 +32,7 @@ public enum LevelLabels LABEL_Strategy, LABEL_SurvivalChallenge, LABEL_Tutorial, + LABEL_Retro, LABEL_Collectables, LABEL_DirectControl, LABEL_Explosives, @@ -52,7 +54,22 @@ public enum LevelLabels LABEL_HEROCAPE, LABEL_MEMORISER, LABEL_WALLJUMP, - LABEL_Retro, + // Start LBP Vita Labels + LABEL_Arcade, + LABEL_Co_op, + LABEL_Precision, + LABEL_Controlinator, + LABEL_Flick, + LABEL_Memoriser, + LABEL_MultiLevel, + LABEL_Portrait, + LABEL_RearTouch, + LABEL_SharedScreen, + LABEL_Swipe, + LABEL_Tap, + LABEL_Tilt, + LABEL_Touch, + // Start LBP3 Labels LABEL_SINGLE_PLAYER, LABEL_RPG, LABEL_TOP_DOWN, diff --git a/ProjectLighthouse/Levels/Slot.cs b/ProjectLighthouse/Levels/Slot.cs index 7171f473..cf981518 100644 --- a/ProjectLighthouse/Levels/Slot.cs +++ b/ProjectLighthouse/Levels/Slot.cs @@ -60,6 +60,9 @@ public class Slot [XmlElement("icon")] public string IconHash { get; set; } = ""; + [XmlElement("isAdventurePlanet")] + public bool IsAdventurePlanet { get; set; } + [XmlElement("rootLevel")] [JsonIgnore] public string RootLevel { get; set; } = ""; @@ -301,6 +304,7 @@ public class Slot LbpSerializer.StringElement("initiallyLocked", this.InitiallyLocked) + LbpSerializer.StringElement("isSubLevel", this.SubLevel) + LbpSerializer.StringElement("isLBP1Only", this.Lbp1Only) + + LbpSerializer.StringElement("isAdventurePlanet", this.IsAdventurePlanet) + LbpSerializer.StringElement("background", this.BackgroundHash) + LbpSerializer.StringElement("shareable", this.Shareable) + LbpSerializer.StringElement("authorLabels", this.AuthorLabels) + diff --git a/ProjectLighthouse/Middlewares/MiddlewareDBContext.cs b/ProjectLighthouse/Middlewares/MiddlewareDBContext.cs new file mode 100644 index 00000000..77de227c --- /dev/null +++ b/ProjectLighthouse/Middlewares/MiddlewareDBContext.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; + +namespace LBPUnion.ProjectLighthouse.Middlewares; + +public abstract class MiddlewareDBContext +{ + // this makes it consistent with typical middleware usage + [SuppressMessage("ReSharper", "InconsistentNaming")] + protected RequestDelegate next { get; } + + protected MiddlewareDBContext(RequestDelegate next) + { + this.next = next; + } + + [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] + public abstract Task InvokeAsync(HttpContext ctx, Database db); +} \ No newline at end of file diff --git a/ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs b/ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs new file mode 100644 index 00000000..a574128a --- /dev/null +++ b/ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs @@ -0,0 +1,45 @@ +using System; +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + + [DbContext(typeof(Database))] + [Migration("20220910190711_AddUserLanguageAndTimezone")] + public partial class AddUserLanguageAndTimezone : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Language", + table: "Users", + type: "longtext", + defaultValue: "en", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "TimeZone", + table: "Users", + type: "longtext", + defaultValue: TimeZoneInfo.Local.Id, + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Language", + table: "Users"); + + migrationBuilder.DropColumn( + name: "TimeZone", + table: "Users"); + } + } +} diff --git a/ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs b/ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs new file mode 100644 index 00000000..11f81031 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs @@ -0,0 +1,33 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + + [DbContext(typeof(Database))] + [Migration("20220910190824_RemoveUserIsAPirate")] + public partial class RemoveUserIsAPirate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("UPDATE Users SET Language = \"en-PT\" WHERE isAPirate"); + + migrationBuilder.DropColumn( + name: "IsAPirate", + table: "Users"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAPirate", + table: "Users", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + } +} diff --git a/ProjectLighthouse/Migrations/20220916141401_ScoreboardAdvSlot.cs b/ProjectLighthouse/Migrations/20220916141401_ScoreboardAdvSlot.cs new file mode 100644 index 00000000..9c5cb567 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220916141401_ScoreboardAdvSlot.cs @@ -0,0 +1,23 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220916141401_ScoreboardAdvSlot")] + public partial class CreateScoreboardAdvSlot : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ChildSlotId", + table: "Scores", + type: "int", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/ProjectLighthouse/Migrations/20220918154500_AddIsAdventureColumn.cs b/ProjectLighthouse/Migrations/20220918154500_AddIsAdventureColumn.cs new file mode 100644 index 00000000..aecbffb1 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220918154500_AddIsAdventureColumn.cs @@ -0,0 +1,23 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220918154500_AddIsAdventureColumn")] + public partial class AddisAdventureColumn : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAdventurePlanet", + table: "Slots", + type: "bool", + nullable: false, + defaultValue: false); + } + } +} diff --git a/ProjectLighthouse/PlayerData/Profiles/User.cs b/ProjectLighthouse/PlayerData/Profiles/User.cs index 259eb573..534fc9bd 100644 --- a/ProjectLighthouse/PlayerData/Profiles/User.cs +++ b/ProjectLighthouse/PlayerData/Profiles/User.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; @@ -179,12 +180,10 @@ public class User [JsonIgnore] public string? ApprovedIPAddress { get; set; } #nullable disable - - /// - /// ARRR! Forces the user to see Pirate English translations on the website. - /// - [NotMapped] - public bool IsAPirate { get; set; } + + public string Language { get; set; } = "en"; + + public string TimeZone { get; set; } = TimeZoneInfo.Local.Id; public PrivacyType LevelVisibility { get; set; } = PrivacyType.All; diff --git a/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs b/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs index cfac696d..af718db3 100644 --- a/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs +++ b/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs @@ -47,7 +47,7 @@ public class UserStatus this.CurrentRoom = RoomHelper.FindRoomByUserId(userId); } - private string FormatOfflineTimestamp(string language) + private string FormatOfflineTimestamp(string language, string timeZone) { if (this.LastLogout <= 0 && this.LastLogin <= 0) { @@ -56,11 +56,12 @@ public class UserStatus long timestamp = this.LastLogout; if (timestamp <= 0) timestamp = this.LastLogin; - string formattedTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToLocalTime().ToString("M/d/yyyy h:mm:ss tt"); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); + string formattedTime = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt"); return StatusStrings.LastOnline.Translate(language, formattedTime); } - public string ToTranslatedString(string language) + public string ToTranslatedString(string language, string timeZone) { this.CurrentVersion ??= GameVersion.Unknown; this.CurrentPlatform ??= Platform.Unknown; @@ -69,7 +70,7 @@ public class UserStatus { StatusType.Online => StatusStrings.CurrentlyOnline.Translate(language, ((GameVersion)this.CurrentVersion).ToPrettyString(), (Platform)this.CurrentPlatform), - StatusType.Offline => this.FormatOfflineTimestamp(language), + StatusType.Offline => this.FormatOfflineTimestamp(language, timeZone), _ => GeneralStrings.Unknown.Translate(language), }; } diff --git a/ProjectLighthouse/PlayerData/Score.cs b/ProjectLighthouse/PlayerData/Score.cs index 67ad2601..b584b43f 100644 --- a/ProjectLighthouse/PlayerData/Score.cs +++ b/ProjectLighthouse/PlayerData/Score.cs @@ -21,6 +21,9 @@ public class Score [ForeignKey(nameof(SlotId))] public Slot Slot { get; set; } + [XmlIgnore] + public int? ChildSlotId { get; set; } + [XmlElement("type")] public int Type { get; set; } diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 01203ae1..fd7a2b22 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -10,21 +10,22 @@ - - + + + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + diff --git a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 1071afdc..1ce63adf 100644 --- a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -730,8 +730,8 @@ namespace ProjectLighthouse.Migrations b.Property("IconHash") .HasColumnType("longtext"); - b.Property("IsAPirate") - .HasColumnType("tinyint(1)"); + b.Property("Language") + .HasColumnType("longtext"); b.Property("LastLogin") .HasColumnType("bigint"); @@ -772,6 +772,9 @@ namespace ProjectLighthouse.Migrations b.Property("ProfileVisibility") .HasColumnType("int"); + b.Property("TimeZone") + .HasColumnType("longtext"); + b.Property("Username") .IsRequired() .HasColumnType("longtext"); diff --git a/ProjectLighthouse/StaticFiles/assets/advSlotCardMask.png b/ProjectLighthouse/StaticFiles/assets/advSlotCardMask.png new file mode 100644 index 00000000..1a2e85bb Binary files /dev/null and b/ProjectLighthouse/StaticFiles/assets/advSlotCardMask.png differ diff --git a/ProjectLighthouse/StaticFiles/assets/advSlotCardOverlay.png b/ProjectLighthouse/StaticFiles/assets/advSlotCardOverlay.png new file mode 100644 index 00000000..f4223d5a Binary files /dev/null and b/ProjectLighthouse/StaticFiles/assets/advSlotCardOverlay.png differ diff --git a/ProjectLighthouse/StaticFiles/css/styles.css b/ProjectLighthouse/StaticFiles/css/styles.css index c50eeac6..f9f7f285 100644 --- a/ProjectLighthouse/StaticFiles/css/styles.css +++ b/ProjectLighthouse/StaticFiles/css/styles.css @@ -159,4 +159,27 @@ div.cardStatsUnderTitle > span { margin-right: 0.5em; } -/*#endregion Comments*/ \ No newline at end of file +/*#endregion Comments*/ + +/*#region Slot labels */ + +.selected { + color: #fff !important; + background-color: #0e91f5 !important; +} + +.selected:hover { + background-color: #0084ea !important; +} + +.skew { + --scale: scale(1, 1); + --skew: rotate(0deg); + transition: transform 0.25s !important; + transform: var(--skew) var(--scale); +} +.skew:hover { + --scale: scale(1.2, 1.2); +} + +/*#endregion Slot labels */ \ No newline at end of file