Merge remote-tracking branch 'lighthouse/main' into playlists

# Conflicts:
#	ProjectLighthouse/PlayerData/Profiles/User.cs
This commit is contained in:
Slendy 2022-09-22 23:24:01 -05:00
commit 1d4d143519
No known key found for this signature in database
GPG key ID: 7288D68361B91428
87 changed files with 1223 additions and 208 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "6.0.8", "version": "6.0.9",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]

View file

@ -50,7 +50,7 @@
<comment>A quick shortcut on the header to take you to your profile if logged in.</comment> <comment>A quick shortcut on the header to take you to your profile if logged in.</comment>
</data> </data>
<data name="header_adminPanel" xml:space="preserve"> <data name="header_adminPanel" xml:space="preserve">
<value>Admin</value> <value>Administrationsmenü</value>
<comment>A header link that takes you to the admin panel if available.</comment> <comment>A header link that takes you to the admin panel if available.</comment>
</data> </data>
<data name="header_logout" xml:space="preserve"> <data name="header_logout" xml:space="preserve">

View file

@ -64,7 +64,7 @@
<value>Página generada por {0}.</value> <value>Página generada por {0}.</value>
</data> </data>
<data name="generated_modified" xml:space="preserve"> <data name="generated_modified" xml:space="preserve">
<value>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</value> <value>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</value>
</data> </data>
<data name="js_warn" xml:space="preserve"> <data name="js_warn" xml:space="preserve">
<value>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.</value> <value>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.</value>

View file

@ -50,7 +50,7 @@
<comment>A quick shortcut on the header to take you to your profile if logged in.</comment> <comment>A quick shortcut on the header to take you to your profile if logged in.</comment>
</data> </data>
<data name="header_adminPanel" xml:space="preserve"> <data name="header_adminPanel" xml:space="preserve">
<value>Admin</value> <value>Администратор</value>
<comment>A header link that takes you to the admin panel if available.</comment> <comment>A header link that takes you to the admin panel if available.</comment>
</data> </data>
<data name="header_logout" xml:space="preserve"> <data name="header_logout" xml:space="preserve">
@ -58,30 +58,30 @@
<comment>A shortcut to log you out of your account.</comment> <comment>A shortcut to log you out of your account.</comment>
</data> </data>
<data name="header_modPanel" xml:space="preserve"> <data name="header_modPanel" xml:space="preserve">
<value>Mod Panel</value> <value>Панель модов</value>
</data> </data>
<data name="generated_by" xml:space="preserve"> <data name="generated_by" xml:space="preserve">
<value>Page generated by {0}.</value> <value>Страница создана {0}</value>
</data> </data>
<data name="generated_modified" xml:space="preserve"> <data name="generated_modified" xml:space="preserve">
<value>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.</value> <value>Эта страница была создана с помощью модифицированной версии Project Lighthouse. Пожалуйста, убедитесь, что вы правильно раскрываете исходный код всем пользователям, которые могут использовать этот экземпляр.</value>
</data> </data>
<data name="js_warn" xml:space="preserve"> <data name="js_warn" xml:space="preserve">
<value>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.</value> <value>Хотя мы намерены использовать как можно меньше JavaScript, мы не можем гарантировать, что все будет работать без него. Мы рекомендуем включить JavaScript в белый список для освещения проекта Lighthouse.</value>
</data> </data>
<data name="js_warn_title" xml:space="preserve"> <data name="js_warn_title" xml:space="preserve">
<value>JavaScript не включен</value> <value>JavaScript не включен</value>
</data> </data>
<data name="license_warn_title" xml:space="preserve"> <data name="license_warn_title" xml:space="preserve">
<value>Potential License Violation</value> <value>Потенциальное нарушение лицензии</value>
</data> </data>
<data name="license_warn_1" xml:space="preserve"> <data name="license_warn_1" xml:space="preserve">
<value>This instance is a public-facing instance that has been modified without the changes published. You may be in violation of the {0}.</value> <value>Этот экземпляр является общедоступным экземпляром, который был изменен без публикации изменений. Вы могли нарушить {0}.</value>
</data> </data>
<data name="license_warn_2" xml:space="preserve"> <data name="license_warn_2" xml:space="preserve">
<value>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}.</value> <value>Если вы считаете, что это ошибка, пожалуйста, создайте проблему с выводом {0} выполненным из корня исходного кода сервера в описании на нашем {1}трекера задач{2}.</value>
</data> </data>
<data name="license_warn_3" xml:space="preserve"> <data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value> <value>Если нет, пожалуйста, опубликуйте исходный код в доступном для ваших пользователей месте.</value>
</data> </data>
</root> </root>

View file

@ -18,13 +18,13 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="username_invalid" xml:space="preserve"> <data name="username_invalid" xml:space="preserve">
<value>The username field is blank.</value> <value>Поле имени пользователя пустое.</value>
</data> </data>
<data name="username_taken" xml:space="preserve"> <data name="username_taken" xml:space="preserve">
<value>Выбранное имя пользователя уже занято.</value> <value>Выбранное имя пользователя уже занято.</value>
</data> </data>
<data name="password_invalid" xml:space="preserve"> <data name="password_invalid" xml:space="preserve">
<value>Password field is required.</value> <value>Требуется ввести пароль.</value>
</data> </data>
<data name="password_doesnt_match" xml:space="preserve"> <data name="password_doesnt_match" xml:space="preserve">
<value>Пароли не совпадают!</value> <value>Пароли не совпадают!</value>
@ -36,10 +36,10 @@
<value>Вы должны правильно завершить капчу.</value> <value>Вы должны правильно завершить капчу.</value>
</data> </data>
<data name="email_taken" xml:space="preserve"> <data name="email_taken" xml:space="preserve">
<value>The email address you've chosen is already taken.</value> <value>Выбранный адрес электронной почты уже занят.</value>
</data> </data>
<data name="email_invalid" xml:space="preserve"> <data name="email_invalid" xml:space="preserve">
<value>Email address field is required.</value> <value>Введите адрес электронной почты.</value>
</data> </data>
<data name="user_banned" xml:space="preserve"> <data name="user_banned" xml:space="preserve">
<value>Вы были заблокированы. Пожалуйста, свяжитесь с администратором для получения дополнительной информации.\nПричина: {0}</value> <value>Вы были заблокированы. Пожалуйста, свяжитесь с администратором для получения дополнительной информации.\nПричина: {0}</value>

View file

@ -18,7 +18,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="username" xml:space="preserve"> <data name="username" xml:space="preserve">
<value>Nombre de usuario</value> <value>Usuario</value>
</data> </data>
<data name="password" xml:space="preserve"> <data name="password" xml:space="preserve">
<value>Contraseña</value> <value>Contraseña</value>

View file

@ -45,12 +45,12 @@
<value>Сбросить пароль</value> <value>Сбросить пароль</value>
</data> </data>
<data name="recent_activity" xml:space="preserve"> <data name="recent_activity" xml:space="preserve">
<value>Recent Activity</value> <value>Недавняя активность</value>
</data> </data>
<data name="soon" xml:space="preserve"> <data name="soon" xml:space="preserve">
<value>Скоро будет!</value> <value>Скоро будет!</value>
</data> </data>
<data name="recent_photos" xml:space="preserve"> <data name="recent_photos" xml:space="preserve">
<value>Most recent photos</value> <value>Самые последние фото</value>
</data> </data>
</root> </root>

View file

@ -26,19 +26,19 @@
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="loggedInAs" xml:space="preserve"> <data name="loggedInAs" xml:space="preserve">
<value>Greetings, {0}.</value> <value>Willkommen, {0}.</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="users_none" xml:space="preserve"> <data name="users_none" xml:space="preserve">
<value>There are no people online. Why not hop on?</value> <value>Es ist niemand online. Lust auf ein Spiel?</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="users_single" xml:space="preserve"> <data name="users_single" xml:space="preserve">
<value>There is 1 person currently online:</value> <value>Gerade ist ein Spieler online:</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="users_multiple" xml:space="preserve"> <data name="users_multiple" xml:space="preserve">
<value>There are currently {0} people online:</value> <value>Gerade sind {0} Spieler online:</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="authAttemptsPending" xml:space="preserve"> <data name="authAttemptsPending" xml:space="preserve">

View file

@ -26,19 +26,19 @@
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="loggedInAs" xml:space="preserve"> <data name="loggedInAs" xml:space="preserve">
<value>Greetings, {0}.</value> <value>Приветствуем, {0}.</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="users_none" xml:space="preserve"> <data name="users_none" xml:space="preserve">
<value>There are no people online. Why not hop on?</value> <value>Нет людей в сети. Почему бы не продолжить играть?</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="users_single" xml:space="preserve"> <data name="users_single" xml:space="preserve">
<value>There is 1 person currently online:</value> <value>Сейчас в сети 1 пользователь:</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="users_multiple" xml:space="preserve"> <data name="users_multiple" xml:space="preserve">
<value>There are currently {0} people online:</value> <value>В настоящее время {0} человек в сети:</value>
<comment>A greeting on the main page of the website.</comment> <comment>A greeting on the main page of the website.</comment>
</data> </data>
<data name="authAttemptsPending" xml:space="preserve"> <data name="authAttemptsPending" xml:space="preserve">

View file

@ -124,7 +124,7 @@ public static class LocalizationManager
.Where(r => r != "resources") .Where(r => r != "resources")
.ToList(); .ToList();
languages.Add(DefaultLang); languages.Insert(0, DefaultLang);
return languages; return languages;
} }

View file

@ -18,15 +18,15 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="mod_panel_title" xml:space="preserve"> <data name="mod_panel_title" xml:space="preserve">
<value>Moderation Panel</value> <value>Панель модерации</value>
</data> </data>
<data name="greeting" xml:space="preserve"> <data name="greeting" xml:space="preserve">
<value>Welcome to the moderation panel, {0}!</value> <value>Добро пожаловать на панель модерации, {0}!</value>
</data> </data>
<data name="banned_users" xml:space="preserve"> <data name="banned_users" xml:space="preserve">
<value>Banned Users</value> <value>Заблокированные пользователи</value>
</data> </data>
<data name="hidden_levels" xml:space="preserve"> <data name="hidden_levels" xml:space="preserve">
<value>Hidden Levels</value> <value>Скрытые уровни</value>
</data> </data>
</root> </root>

View file

@ -18,12 +18,12 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="biography" xml:space="preserve"> <data name="biography" xml:space="preserve">
<value>Biography</value> <value>Биография</value>
</data> </data>
<data name="no_biography" xml:space="preserve"> <data name="no_biography" xml:space="preserve">
<value>{0} hasn't introduced themselves yet.</value> <value>{0} Ещё не представился.</value>
</data> </data>
<data name="title" xml:space="preserve"> <data name="title" xml:space="preserve">
<value>{0}'s user page</value> <value>Страница пользователя {0}</value>
</data> </data>
</root> </root>

View file

@ -18,6 +18,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="username_notice" xml:space="preserve"> <data name="username_notice" xml:space="preserve">
<value>Caution: Your username MUST match your PSN/RPCN username in order to be able to sign in from in-game.</value> <value>Внимание: Ваше имя пользователя ДОЛЖНО соответствует вашему логину PSN/RPCN для того, чтобы иметь возможность войти в игру.</value>
</data> </data>
</root> </root>

View file

@ -18,7 +18,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="currently_online" xml:space="preserve"> <data name="currently_online" xml:space="preserve">
<value>Currently playing {0} on {1}</value> <value>Spielt gerade {0} auf {1}</value>
</data> </data>
<data name="offline" xml:space="preserve"> <data name="offline" xml:space="preserve">
<value>Offline</value> <value>Offline</value>

View file

@ -24,6 +24,6 @@
<value>Desconectado</value> <value>Desconectado</value>
</data> </data>
<data name="last_online" xml:space="preserve"> <data name="last_online" xml:space="preserve">
<value>Desconectado desde hace {0}</value> <value>Desconectado desde el {0}</value>
</data> </data>
</root> </root>

View file

@ -24,6 +24,6 @@
<value>Desconectado</value> <value>Desconectado</value>
</data> </data>
<data name="last_online" xml:space="preserve"> <data name="last_online" xml:space="preserve">
<value>Desconectado desde {0}</value> <value>Desconectado desde el {0}</value>
</data> </data>
</root> </root>

View file

@ -18,7 +18,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="currently_online" xml:space="preserve"> <data name="currently_online" xml:space="preserve">
<value>Currently playing {0} on {1}</value> <value>Online w {0} na {1}</value>
</data> </data>
<data name="offline" xml:space="preserve"> <data name="offline" xml:space="preserve">
<value>Offline</value> <value>Offline</value>

View file

@ -18,12 +18,12 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="currently_online" xml:space="preserve"> <data name="currently_online" xml:space="preserve">
<value>Currently playing {0} on {1}</value> <value>Сейчас играет {0} на {1}</value>
</data> </data>
<data name="offline" xml:space="preserve"> <data name="offline" xml:space="preserve">
<value>Не в сети</value> <value>Не в сети</value>
</data> </data>
<data name="last_online" xml:space="preserve"> <data name="last_online" xml:space="preserve">
<value>Offline since {0}</value> <value>Последний раз был(а) в сети {0}</value>
</data> </data>
</root> </root>

View file

@ -10,6 +10,7 @@ public struct MinimalSlot
public string Name { get; set; } public string Name { get; set; }
public string IconHash { get; set; } public string IconHash { get; set; }
public bool TeamPick { get; set; } public bool TeamPick { get; set; }
public bool IsAdventure { get; set; }
public GameVersion GameVersion { get; set; } public GameVersion GameVersion { get; set; }
#if DEBUG #if DEBUG
public long FirstUploaded { get; set; } public long FirstUploaded { get; set; }
@ -22,6 +23,7 @@ public struct MinimalSlot
Name = slot.Name, Name = slot.Name,
IconHash = slot.IconHash, IconHash = slot.IconHash,
TeamPick = slot.TeamPick, TeamPick = slot.TeamPick,
IsAdventure = slot.IsAdventurePlanet,
GameVersion = slot.GameVersion, GameVersion = slot.GameVersion,
#if DEBUG #if DEBUG
FirstUploaded = slot.FirstUploaded, FirstUploaded = slot.FirstUploaded,

View file

@ -67,8 +67,7 @@ public class LoginController : ControllerBase
token = await this.database.AuthenticateUser(npTicket, ipAddress); token = await this.database.AuthenticateUser(npTicket, ipAddress);
if (token == null) if (token == null)
{ {
Logger.Warn($"Unable to " + Logger.Warn($"Unable to find/generate a token for username {npTicket.Username}", LogArea.Login);
$"find/generate a token for username {npTicket.Username}", LogArea.Login);
return this.StatusCode(403, ""); // If not, then 403. return this.StatusCode(403, ""); // If not, then 403.
} }
} }
@ -81,6 +80,12 @@ public class LoginController : ControllerBase
return this.StatusCode(403, ""); 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 (ServerConfiguration.Instance.Authentication.UseExternalAuth)
{ {
if (user.ApprovedIPAddress == ipAddress) if (user.ApprovedIPAddress == ipAddress)

View file

@ -63,8 +63,12 @@ public class PhotosController : ControllerBase
{ {
case SlotType.User: case SlotType.User:
{ {
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.SlotId == photoSlot.SlotId); // We'll grab the slot by the RootLevel and see what happens from here.
if (slot != null && !string.IsNullOrEmpty(slot.RootLevel)) validLevel = true; 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; break;
} }
case SlotType.Pod: case SlotType.Pod:

View file

@ -56,6 +56,12 @@ public class ResourcesController : ControllerBase
string path = FileHelper.GetResourcePath(hash); 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"); if (FileHelper.ResourceExists(hash)) return this.File(IOFile.OpenRead(path), "application/octet-stream");
return this.NotFound(); return this.NotFound();

View file

@ -53,8 +53,11 @@ public class ListController : ControllerBase
return this.Ok return this.Ok
( (
LbpSerializer.TaggedStringElement LbpSerializer.TaggedStringElement("slots", response, new Dictionary<string, object>
("slots", response, "total", this.database.QueuedLevels.Include(q => q.User).Count(q => q.User.Username == username)) {
{ "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<string, object> LbpSerializer.TaggedStringElement("favouriteSlots", response, new Dictionary<string, object>
{ {
{ "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) }, { "hint_start", pageStart + Math.Min(pageSize, 30) },
}) })
); );
@ -225,7 +228,7 @@ public class ListController : ControllerBase
( (
LbpSerializer.TaggedStringElement("favouriteUsers", response, new Dictionary<string, object> LbpSerializer.TaggedStringElement("favouriteUsers", response, new Dictionary<string, object>
{ {
{ "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) }, { "hint_start", pageStart + Math.Min(pageSize, 30) },
}) })
); );

View file

@ -40,7 +40,8 @@ public class PublishController : ControllerBase
GameToken gameToken = userAndToken.Value.Item2; GameToken gameToken = userAndToken.Value.Item2;
Slot? slot = await this.getSlotFromBody(); Slot? slot = await this.getSlotFromBody();
if (slot == null) { if (slot == null)
{
Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish); 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 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(); return this.BadRequest();
} }
if (rootLevel.FileType != LbpFileType.Level) if (!slot.IsAdventurePlanet)
{ {
Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish); if (rootLevel.FileType != LbpFileType.Level)
return this.BadRequest(); {
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); GameVersion slotVersion = FileHelper.ParseLevelVersion(rootLevel);
@ -232,7 +244,7 @@ public class PublishController : ControllerBase
this.database.Slots.Add(slot); this.database.Slots.Add(slot);
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
if (user.LevelVisibility == PrivacyType.All) if (user.LevelVisibility == PrivacyType.All)
{ {
await WebhookHelper.SendWebhook("New level published!", await WebhookHelper.SendWebhook("New level published!",

View file

@ -26,7 +26,8 @@ public class ScoreController : ControllerBase
} }
[HttpPost("scoreboard/{slotType}/{id:int}")] [HttpPost("scoreboard/{slotType}/{id:int}")]
public async Task<IActionResult> 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<IActionResult> 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); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); 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); if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
score.SlotId = id; score.SlotId = id;
score.ChildSlotId = childId;
Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId); Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId);
if (slot == null) if (slot == null)
@ -116,12 +118,13 @@ public class ScoreController : ControllerBase
Type = score.Type, Type = score.Type,
Points = score.Points, Points = score.Points,
SlotId = score.SlotId, SlotId = score.SlotId,
ChildSlotId = score.ChildSlotId,
}; };
IQueryable<Score> existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId) IQueryable<Score> existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId)
.Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection) .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId)
.Where(s => s.Type == playerScore.Type); .Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection)
.Where(s => s.Type == playerScore.Type);
if (existingScore.Any()) if (existingScore.Any())
{ {
Score first = existingScore.First(s => s.SlotId == playerScore.SlotId); Score first = existingScore.First(s => s.SlotId == playerScore.SlotId);
@ -137,13 +140,14 @@ public class ScoreController : ControllerBase
await this.database.SaveChangesAsync(); 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); return this.Ok(myRanking);
} }
[HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")] [HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")]
public async Task<IActionResult> 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<IActionResult> 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); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); if (token == null) return this.StatusCode(403, "");
@ -169,12 +173,13 @@ public class ScoreController : ControllerBase
if (friendUsername != null) friendNames.Add(friendUsername); 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}/{type:int}")]
[HttpGet("topscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")]
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public async Task<IActionResult> TopScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) public async Task<IActionResult> 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); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); 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); 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")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
@ -199,7 +204,8 @@ public class ScoreController : ControllerBase
int pageStart = -1, int pageStart = -1,
int pageSize = 5, int pageSize = 5,
string rootName = "scores", 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 needed for Anonymous type returned from SELECT
var rankedScores = this.database.Scores var rankedScores = this.database.Scores
.Where(s => s.SlotId == slotId && s.Type == type) .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))) .Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Contains(id)))
.AsEnumerable()
.OrderByDescending(s => s.Points) .OrderByDescending(s => s.Points)
.ThenBy(s => s.ScoreId) .ThenBy(s => s.ScoreId)
.ToList() .ToList()

View file

@ -1,8 +1,6 @@
#nullable enable #nullable enable
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -30,7 +28,7 @@ public class ModerationSlotController : ControllerBase
slot.TeamPick = true; slot.TeamPick = true;
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
return this.Ok(); return this.Redirect("~/slot/" + id);
} }
[HttpGet("removeTeamPick")] [HttpGet("removeTeamPick")]
@ -44,7 +42,7 @@ public class ModerationSlotController : ControllerBase
slot.TeamPick = false; slot.TeamPick = false;
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
return this.Ok(); return this.Redirect("~/slot/" + id);
} }
[HttpGet("delete")] [HttpGet("delete")]
@ -58,6 +56,6 @@ public class ModerationSlotController : ControllerBase
await this.database.RemoveSlot(slot); await this.database.RemoveSlot(slot);
return this.Ok(); return this.Redirect("~/slots/0");
} }
} }

View file

@ -11,18 +11,19 @@ public class ResourcesController : ControllerBase
[HttpGet("/gameAssets/{hash}")] [HttpGet("/gameAssets/{hash}")]
public IActionResult GetGameImage(string hash) public IActionResult GetGameImage(string hash)
{ {
string path = Path.Combine("png", $"{hash}.png"); string path = FileHelper.GetImagePath($"{hash}.png");
if (IOFile.Exists(path)) string fullPath = Path.GetFullPath(path);
{ string basePath = Path.GetFullPath(FileHelper.ImagePath);
return this.File(IOFile.OpenRead(path), "image/png");
} // 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); LbpFile? file = LbpFile.FromHash(hash);
if (file != null && FileHelper.LbpFileToPNG(file)) if (file != null && FileHelper.LbpFileToPNG(file)) return this.File(IOFile.OpenRead(path), "image/png");
{
return this.File(IOFile.OpenRead(path), "image/png");
}
return this.NotFound(); return this.NotFound();
} }
} }

View file

@ -25,6 +25,27 @@ public class SlotPageController : ControllerBase
this.database = database; this.database = database;
} }
[HttpGet("unpublish")]
public async Task<IActionResult> 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")] [HttpGet("rateComment")]
public async Task<IActionResult> RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating) public async Task<IActionResult> RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating)
{ {

View file

@ -1,4 +1,3 @@
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
@ -9,15 +8,19 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
public static class PartialExtensions public static class PartialExtensions
{ {
// ReSharper disable once SuggestBaseTypeForParameter
public static ViewDataDictionary<T> WithLang<T>(this ViewDataDictionary<T> viewData, string language) public static ViewDataDictionary<T> WithLang<T>(this ViewDataDictionary<T> viewData, string language) => WithKeyValue(viewData, "Language", language);
public static ViewDataDictionary<T> WithTime<T>(this ViewDataDictionary<T> viewData, string timeZone) => WithKeyValue(viewData, "TimeZone", timeZone);
private static ViewDataDictionary<T> WithKeyValue<T>(this ViewDataDictionary<T> viewData, string key, object value)
{ {
try try
{ {
return new(viewData) return new ViewDataDictionary<T>(viewData)
{ {
{ {
"Language", language key, value
}, },
}; };
} }
@ -27,9 +30,9 @@ public static class PartialExtensions
} }
} }
public static Task<IHtmlContent> ToLink<T>(this User user, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language) public static Task<IHtmlContent> ToLink<T>(this User user, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language, string timeZone = "", bool includeStatus = false)
=> helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language)); => helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language).WithTime(timeZone).WithKeyValue("IncludeStatus", includeStatus));
public static Task<IHtmlContent> ToHtml<T>(this Photo photo, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language) public static Task<IHtmlContent> ToHtml<T>(this Photo photo, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language, string timeZone)
=> helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language)); => helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language).WithTime(timeZone));
} }

View file

@ -10,7 +10,6 @@ public class HandlePageErrorMiddleware : Middleware
public override async Task InvokeAsync(HttpContext ctx) public override async Task InvokeAsync(HttpContext ctx)
{ {
await this.next(ctx); await this.next(ctx);
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (ctx.Response.StatusCode == 404 && !ctx.Request.Path.StartsWithSegments("/gameAssets")) if (ctx.Response.StatusCode == 404 && !ctx.Request.Path.StartsWithSegments("/gameAssets"))
{ {
try try
@ -20,7 +19,7 @@ public class HandlePageErrorMiddleware : Middleware
finally finally
{ {
// not much we can do to save us, carry on anyways // not much we can do to save us, carry on anyways
await next(ctx); await this.next(ctx);
} }
} }
} }

View file

@ -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);
}
}

View file

@ -3,7 +3,6 @@ using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -30,6 +29,12 @@ public class CompleteEmailVerificationPage : BaseLayout
return this.Page(); return this.Page();
} }
if (DateTime.Now > emailVerifyToken.ExpiresAt)
{
this.Error = "This token has expired";
return this.Page();
}
if (emailVerifyToken.UserId != user.UserId) if (emailVerifyToken.UserId != user.UserId)
{ {
this.Error = "This token doesn't belong to you!"; this.Error = "This token doesn't belong to you!";

View file

@ -5,6 +5,8 @@
@{ @{
Layout = "Layouts/BaseLayout"; Layout = "Layouts/BaseLayout";
Model.Title = "Authentication"; Model.Title = "Authentication";
string timeZone = Model.GetTimeZone();
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
} }
@if (Model.AuthenticationAttempts.Count == 0) @if (Model.AuthenticationAttempts.Count == 0)
@ -41,9 +43,9 @@ else
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts) @foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
{ {
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp).ToLocalTime(); DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp), timeZoneInfo);
<div class="ui red segment"> <div class="ui red segment">
<p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</b> from the IP address <b>@authAttempt.IPAddress</b>.</p> <p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("M/d/yyyy @ h:mm tt")</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
<div> <div>
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId"> <a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
<button class="ui small green button"> <button class="ui small green button">

View file

@ -11,8 +11,9 @@
Layout = "Layouts/BaseLayout"; Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false; Model.ShowTitleInPage = false;
bool isMobile = this.Request.IsMobile(); bool isMobile = Request.IsMobile();
string language = Model.GetLanguage(); string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
} }
<h1 class="lighthouse-welcome lighthouse-title"> <h1 class="lighthouse-welcome lighthouse-title">
@Model.Translate(LandingPageStrings.Welcome, ServerConfiguration.Instance.Customization.ServerName) @Model.Translate(LandingPageStrings.Welcome, ServerConfiguration.Instance.Customization.ServerName)
@ -48,7 +49,7 @@ else
foreach (User user in Model.PlayersOnline) foreach (User user in Model.PlayersOnline)
{ {
i++; i++;
@await user.ToLink(Html, ViewData, language)if (i != Model.PlayersOnline.Count){<span>,</span>} @* whitespace has forced my hand *@ @await user.ToLink(Html, ViewData, language, timeZone, true)if (i != Model.PlayersOnline.Count){<span>,</span>} @* whitespace has forced my hand *@
} }
} }

View file

@ -1,6 +1,4 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic;
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Localization; using LBPUnion.ProjectLighthouse.Localization;
using LBPUnion.ProjectLighthouse.Localization.StringLists; using LBPUnion.ProjectLighthouse.Localization.StringLists;
@ -46,20 +44,31 @@ public class BaseLayout : PageModel
} }
private string? language; private string? language;
private string? timeZone;
public string GetLanguage() public string GetLanguage()
{ {
if (ServerStatics.IsUnitTesting) return LocalizationManager.DefaultLang; if (ServerStatics.IsUnitTesting) return LocalizationManager.DefaultLang;
if (this.language != null) return this.language; if (this.language != null) return this.language;
if (this.User?.IsAPirate ?? false) return "en-PT"; if (this.User != null) return this.language = this.User.Language;
IRequestCultureFeature? requestCulture = Request.HttpContext.Features.Get<IRequestCultureFeature>(); IRequestCultureFeature? requestCulture = this.Request.HttpContext.Features.Get<IRequestCultureFeature>();
if (requestCulture == null) return this.language = LocalizationManager.DefaultLang; if (requestCulture == null) return this.language = LocalizationManager.DefaultLang;
return this.language = requestCulture.RequestCulture.UICulture.Name; return this.language = requestCulture.RequestCulture.UICulture.Name;
} }
public string GetTimeZone()
{
if (ServerStatics.IsUnitTesting) return TimeZoneInfo.Local.Id;
if (this.timeZone != null) return this.timeZone;
string userTimeZone = this.User?.TimeZone ?? TimeZoneInfo.Local.Id;
return this.timeZone = userTimeZone;
}
public string Translate(TranslatableString translatableString) => translatableString.Translate(this.GetLanguage()); public string Translate(TranslatableString translatableString) => translatableString.Translate(this.GetLanguage());
public string Translate(TranslatableString translatableString, params object?[] format) => translatableString.Translate(this.GetLanguage(), format); public string Translate(TranslatableString translatableString, params object?[] format) => translatableString.Translate(this.GetLanguage(), format);
} }

View file

@ -26,6 +26,9 @@
{ {
"Language", Model.GetLanguage() "Language", Model.GetLanguage()
}, },
{
"TimeZone", Model.GetTimeZone()
},
}) })
</div> </div>
} }

View file

@ -1,10 +1,13 @@
@page "/moderation/cases/{pageNumber:int}" @page "/moderation/cases/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Administration @using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.CasePage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.CasePage
@{ @{
Layout = "Layouts/BaseLayout"; Layout = "Layouts/BaseLayout";
Model.Title = "Cases"; Model.Title = "Cases";
string timeZone = Model.GetTimeZone();
} }
<p>There are @Model.CaseCount total cases, @Model.DismissedCaseCount of which have been dismissed.</p> <p>There are @Model.CaseCount total cases, @Model.DismissedCaseCount of which have been dismissed.</p>
@ -20,5 +23,5 @@
@foreach (ModerationCase @case in Model.Cases) @foreach (ModerationCase @case in Model.Cases)
{ {
@(await Html.PartialAsync("Partials/ModerationCasePartial", @case)) @(await Html.PartialAsync("Partials/ModerationCasePartial", @case, ViewData.WithTime(timeZone)))
} }

View file

@ -1,5 +1,4 @@
@page "/moderation/newCase" @page "/moderation/newCase"
@using LBPUnion.ProjectLighthouse.Administration
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.NewCasePage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.NewCasePage
@{ @{

View file

@ -1,9 +1,11 @@
@page "/moderation/report/{reportId:int}" @page "/moderation/report/{reportId:int}"
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.ReportPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.ReportPage
@{ @{
Layout = "Layouts/BaseLayout"; Layout = "Layouts/BaseLayout";
Model.Title = $"Report {Model.Report.ReportId}"; Model.Title = $"Report {Model.Report.ReportId}";
string timeZone = Model.GetTimeZone();
} }
<script> <script>
@ -14,5 +16,5 @@
let images = []; let images = [];
</script> </script>
@await Html.PartialAsync("Partials/ReportPartial", Model.Report) @await Html.PartialAsync("Partials/ReportPartial", Model.Report, ViewData.WithTime(timeZone))
@await Html.PartialAsync("Partials/RenderReportBoundsPartial") @await Html.PartialAsync("Partials/RenderReportBoundsPartial")

View file

@ -1,10 +1,12 @@
@page "/moderation/reports/{pageNumber:int}" @page "/moderation/reports/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Administration.Reports @using LBPUnion.ProjectLighthouse.Administration.Reports
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.ReportsPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.ReportsPage
@{ @{
Layout = "Layouts/BaseLayout"; Layout = "Layouts/BaseLayout";
Model.Title = "Reports"; Model.Title = "Reports";
string timeZone = Model.GetTimeZone();
} }
<p>There are @Model.ReportCount total reports.</p> <p>There are @Model.ReportCount total reports.</p>
@ -28,7 +30,7 @@
@foreach (GriefReport report in Model.Reports) @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") @await Html.PartialAsync("Partials/RenderReportBoundsPartial")

View file

@ -6,6 +6,8 @@
@{ @{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
} }
<div class="ui yellow segment" id="comments"> <div class="ui yellow segment" id="comments">
@ -85,7 +87,7 @@
<span>@decodedMessage</span> <span>@decodedMessage</span>
} }
<p> <p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i> <i>@TimeZoneInfo.ConvertTime(timestamp, timeZoneInfo).ToString("M/d/yyyy @ h:mm:ss tt")</i>
</p> </p>
@if (i != Model.Comments.Count - 1) @if (i != Model.Comments.Count - 1)
{ {

View file

@ -3,9 +3,12 @@
@{ @{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; 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) : "";
} }
<a href="/user/@Model.UserId" title="@Model.Status.ToTranslatedString(language)" class="user-link"> <a href="/user/@Model.UserId" title="@userStatus" class="user-link">
<img src="/gameAssets/@Model.WebsiteAvatarHash" alt=""/> <img src="/gameAssets/@Model.WebsiteAvatarHash" alt=""/>
@Model.Username @Model.Username
</a> </a>

View file

@ -1,7 +1,6 @@
@using System.Diagnostics @using System.Diagnostics
@using LBPUnion.ProjectLighthouse @using LBPUnion.ProjectLighthouse
@using LBPUnion.ProjectLighthouse.Administration @using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Levels @using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles @using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@model LBPUnion.ProjectLighthouse.Administration.ModerationCase @model LBPUnion.ProjectLighthouse.Administration.ModerationCase
@ -10,6 +9,9 @@
Database database = new(); Database database = new();
string color = "blue"; string color = "blue";
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
if (Model.Expired) color = "yellow"; if (Model.Expired) color = "yellow";
if (Model.Dismissed) color = "green"; if (Model.Dismissed) color = "green";
} }
@ -23,19 +25,19 @@
Debug.Assert(Model.DismissedAt != null); Debug.Assert(Model.DismissedAt != null);
<h3 class="ui @color header"> <h3 class="ui @color header">
This case was dismissed by <a href="/user/@Model.Dismisser.UserId">@Model.Dismisser.Username</a> on @Model.DismissedAt.Value.ToString("MM/dd/yyyy @ h:mm tt"). This case was dismissed by <a href="/user/@Model.Dismisser.UserId">@Model.Dismisser.Username</a> on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
</h3> </h3>
} }
else if (Model.Expired) else if (Model.Expired && Model.ExpiresAt != null)
{ {
<h3 class="ui @color header"> <h3 class="ui @color header">
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").
</h3> </h3>
} }
<span> <span>
Case created by <a href="/user/@Model.Creator!.UserId">@Model.Creator.Username</a> Case created by <a href="/user/@Model.Creator!.UserId">@Model.Creator.Username</a>
on @Model.CreatedAt.ToString("MM/dd/yyyy @ h:mm tt") on @TimeZoneInfo.ConvertTime(Model.CreatedAt, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt")
</span><br> </span><br>
@if (Model.Type.AffectsLevel()) @if (Model.Type.AffectsLevel())

View file

@ -8,6 +8,8 @@
@{ @{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
} }
<div style="position: relative"> <div style="position: relative">
@ -26,7 +28,7 @@
{ {
<span>by</span> <span>by</span>
<b> <b>
@await Model.Creator.ToLink(Html, ViewData, language) @await Model.Creator.ToLink(Html, ViewData, language, timeZone)
</b> </b>
} }
@if (Model.Slot != null) @if (Model.Slot != null)
@ -35,7 +37,8 @@
{ {
case SlotType.User: case SlotType.User:
<span> <span>
in level <b><a href="/slot/@Model.SlotId">@HttpUtility.HtmlDecode(Model.Slot.Name)</a></b> @(Model.Slot.IsAdventurePlanet ? "on an adventure in" : "in level")
<b><a href="/slot/@Model.SlotId">@HttpUtility.HtmlDecode(Model.Slot.Name)</a></b>
</span> </span>
break; break;
case SlotType.Developer: case SlotType.Developer:
@ -49,7 +52,7 @@
break; 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")
</i> </i>
</p> </p>
@ -62,7 +65,7 @@
<div id="hover-subjects-@Model.PhotoId"> <div id="hover-subjects-@Model.PhotoId">
@foreach (PhotoSubject subject in Model.Subjects) @foreach (PhotoSubject subject in Model.Subjects)
{ {
@await subject.User.ToLink(Html, ViewData, language) @await subject.User.ToLink(Html, ViewData, language, timeZone)
} }
</div> </div>

View file

@ -1,7 +1,12 @@
@using LBPUnion.ProjectLighthouse.Administration.Reports @using LBPUnion.ProjectLighthouse.Administration.Reports
@model LBPUnion.ProjectLighthouse.Administration.Reports.GriefReport @model LBPUnion.ProjectLighthouse.Administration.Reports.GriefReport
<div class="ui segment"> @{
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
<div class="ui segment">
<div> <div>
<canvas class="hide-subjects" id="canvas-subjects-@Model.ReportId" width="1920" height="1080" <canvas class="hide-subjects" id="canvas-subjects-@Model.ReportId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)"> style="position: absolute; transform: rotate(180deg)">
@ -26,7 +31,7 @@
<br> <br>
<div> <div>
<b>Report time: </b>@(DateTimeOffset.FromUnixTimeMilliseconds(Model.Timestamp).ToLocalTime().ToString("R")) <b>Report time: </b>@(TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(Model.Timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt"))
</div> </div>
<div> <div>
<b>Report reason: </b>@Model.Type <b>Report reason: </b>@Model.Type

View file

@ -18,6 +18,8 @@
bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool mini = (bool?)ViewData["IsMini"] ?? false; bool mini = (bool?)ViewData["IsMini"] ?? false;
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
bool isQueued = false; bool isQueued = false;
bool isHearted = false; bool isHearted = false;
@ -37,11 +39,13 @@
<div class="card"> <div class="card">
@{ @{
int size = isMobile || mini ? 50 : 100; 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%;" : "";
} }
<div> <div>
<img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute"> <img src=@(isAdventure ? "/assets/advSlotCardOverlay.png" : "/assets/slotCardOverlay.png") style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute; z-index: 3;">
<img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: -1;"> <img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: 1; @(advenStyleExt)">
<img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px;" <img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: relative; z-index: 2; @(advenStyleExt)"
onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'"> onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'">
</div> </div>
<div class="cardStats"> <div class="cardStats">
@ -90,12 +94,24 @@
</div> </div>
@if (Model.Creator != null) @if (Model.Creator != null)
{ {
string date = "";
if(!mini)
date = " on " + TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(Model.FirstUploaded), timeZoneInfo).DateTime.ToShortDateString();
<p> <p>
<i>Created by @await Model.Creator.ToLink(Html, ViewData, language) on @Model.GameVersion.ToPrettyString()</i> <i>Created by @await Model.Creator.ToLink(Html, ViewData, language) in @Model.GameVersion.ToPrettyString()@date</i>
</p> </p>
} }
</div> </div>
<div class="cardButtons"> <div class="cardButtons">
<br>
@if (user != null && !mini && (user.IsModerator || user.UserId == Model.CreatorId))
{
<a class="ui blue tiny button" href="/slot/@Model.SlotId/settings" title="Settings">
<i class="cog icon" style="margin: 0"></i>
</a>
}
</div>
<div class="cardButtons" style="margin-left: 0">
<br> <br>
@if (user != null && !mini) @if (user != null && !mini)
{ {

View file

@ -6,13 +6,14 @@
bool showLink = (bool?)ViewData["ShowLink"] ?? false; bool showLink = (bool?)ViewData["ShowLink"] ?? false;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
} }
<div class="card"> <div class="card">
@{ @{
int size = isMobile ? 50 : 100; int size = isMobile ? 50 : 100;
} }
<div class="cardIcon userCardIcon" style="background-image: url('/gameAssets/@Model.WebsiteAvatarHash'); min-width: @(size)px; width: @(size)px; height: @(size)px; background-position: center center; background-size: auto @(size)px;"> <div class="cardIcon userCardIcon" style="background-image: url('/gameAssets/@Model.WebsiteAvatarHash'); min-width: @(size)px; width: @(size)px; height: @(size)px; background-position: center center; background-size: cover; background-repeat: no-repeat">
</div> </div>
<div class="cardStats"> <div class="cardStats">
@if (showLink) @if (showLink)
@ -28,7 +29,7 @@
</h1> </h1>
} }
<span> <span>
<i>@Model.Status.ToTranslatedString(language)</i> <i>@Model.Status.ToTranslatedString(language, timeZone)</i>
</span> </span>
<div class="cardStatsUnderTitle"> <div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span> <i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>

View file

@ -1,6 +1,4 @@
@page "/passwordResetRequired" @page "/passwordResetRequired"
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequiredPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequiredPage
@{ @{

View file

@ -2,7 +2,6 @@
@using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PhotosPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PhotosPage
@{ @{
@ -10,6 +9,7 @@
Model.Title = Model.Translate(BaseLayoutStrings.HeaderPhotos); Model.Title = Model.Translate(BaseLayoutStrings.HeaderPhotos);
string language = Model.GetLanguage(); string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
} }
<p>There are @Model.PhotoCount total photos!</p> <p>There are @Model.PhotoCount total photos!</p>
@ -25,7 +25,7 @@
@foreach (Photo photo in Model.Photos) @foreach (Photo photo in Model.Photos)
{ {
<div class="ui segment"> <div class="ui segment">
@await photo.ToHtml(Html, ViewData, language) @await photo.ToHtml(Html, ViewData, language, timeZone)
</div> </div>
} }

View file

@ -7,7 +7,7 @@
} }
<!--suppress GrazieInspection --> <!--suppress GrazieInspection -->
@if (!Model.User!.IsAPirate) @if (Model.User!.Language != "en-PT")
{ {
<p>So, ye wanna be a pirate? Well, ye came to the right place!</p> <p>So, ye wanna be a pirate? Well, ye came to the right place!</p>
<p>Just click this 'ere button, and welcome aboard!</p> <p>Just click this 'ere button, and welcome aboard!</p>

View file

@ -1,8 +1,6 @@
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
@ -24,7 +22,7 @@ public class PirateSignupPage : BaseLayout
User? user = this.Database.UserFromWebRequest(this.Request); User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login"); if (user == null) return this.Redirect("/login");
user.IsAPirate = !user.IsAPirate; user.Language = user.Language == "en-PT" ? "en" : "en-PT";
await this.Database.SaveChangesAsync(); await this.Database.SaveChangesAsync();
return this.Redirect("/"); return this.Redirect("/");

View file

@ -72,7 +72,7 @@ public class RegisterForm : BaseLayout
} }
if (ServerConfiguration.Instance.Mail.MailEnabled && 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); this.Error = this.Translate(ErrorStrings.EmailTaken);
return this.Page(); return this.Page();
@ -86,7 +86,7 @@ public class RegisterForm : BaseLayout
if (this.Request.Query.ContainsKey("token")) 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); User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress);

View file

@ -6,8 +6,15 @@
Model.Title = "Verify Email Address"; Model.Title = "Verify Email Address";
} }
<p>An email address on your account has been set, but hasn't been verified yet.</p> @if (Model.Success)
<p>To verify it, check the email sent to <a href="mailto:@Model.User?.EmailAddress">@Model.User?.EmailAddress</a> and click the link in the email.</p> {
<p>An email address on your account has been set, but hasn't been verified yet.</p>
<p>To verify it, check the email sent to <a href="mailto:@Model.User?.EmailAddress">@Model.User?.EmailAddress</a> and click the link in the email.</p>
}
else
{
<p>Failed to send email, please try again later</p>
}
<a href="/login/sendVerificationEmail"> <a href="/login/sendVerificationEmail">
<div class="ui blue button">Resend email</div> <div class="ui blue button">Resend email</div>

View file

@ -4,8 +4,8 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
@ -14,6 +14,8 @@ public class SendVerificationEmailPage : BaseLayout
public SendVerificationEmailPage(Database database) : base(database) public SendVerificationEmailPage(Database database) : base(database)
{} {}
public bool Success { get; set; }
public async Task<IActionResult> OnGet() public async Task<IActionResult> OnGet()
{ {
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
@ -33,29 +35,29 @@ public class SendVerificationEmailPage : BaseLayout
} }
#endif #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, verifyToken = new EmailVerificationToken
User = user, {
EmailToken = CryptoHelper.GenerateAuthToken(), UserId = user.UserId,
}; User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now.AddHours(6),
};
this.Database.EmailVerificationTokens.Add(verifyToken); this.Database.EmailVerificationTokens.Add(verifyToken);
await this.Database.SaveChangesAsync();
await this.Database.SaveChangesAsync(); }
string body = "Hello,\n\n" + 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" + $"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" + $"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 this wasn't you, feel free to ignore this email.";
if (SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body)) this.Success = SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body);
{
return this.Page(); return this.Page();
}
else
{
throw new Exception("failed to send email");
}
} }
} }

View file

@ -1,5 +1,6 @@
@page "/login/setEmail" @page "/login/setEmail"
@using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm
@{ @{
@ -9,6 +10,16 @@
<p>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.</p> <p>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.</p>
@if (!string.IsNullOrWhiteSpace(Model.Error))
{
<div class="ui negative message">
<div class="header">
@Model.Translate(GeneralStrings.Error)
</div>
<p style="white-space: pre-line">@Model.Error</p>
</div>
}
<form class="ui form" onsubmit="return onSubmit(this)" method="post"> <form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()

View file

@ -1,12 +1,13 @@
#nullable enable #nullable enable
using System.Diagnostics.CodeAnalysis;
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Localization.StringLists;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -19,6 +20,8 @@ public class SetEmailForm : BaseLayout
public EmailSetToken? EmailToken; public EmailSetToken? EmailToken;
public string? Error { get; private set; }
public async Task<IActionResult> OnGet(string? token = null) public async Task<IActionResult> OnGet(string? token = null)
{ {
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
@ -32,6 +35,7 @@ public class SetEmailForm : BaseLayout
return this.Page(); return this.Page();
} }
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
public async Task<IActionResult> OnPost(string emailAddress, string token) public async Task<IActionResult> OnPost(string emailAddress, string token)
{ {
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); 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); EmailSetToken? emailToken = await this.Database.EmailSetTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.EmailToken == token);
if (emailToken == null) return this.Redirect("/login"); 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; emailToken.User.EmailAddress = emailAddress;
this.Database.EmailSetTokens.Remove(emailToken); this.Database.EmailSetTokens.Remove(emailToken);

View file

@ -19,6 +19,7 @@
bool isMobile = this.Request.IsMobile(); bool isMobile = this.Request.IsMobile();
string language = Model.GetLanguage(); string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
} }
@if (Model.Slot!.Hidden) @if (Model.Slot!.Hidden)
@ -96,7 +97,7 @@
<br/> <br/>
} }
<div class="eight wide column"> <div class="eight wide column">
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language)) @await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
</div> </div>
@if (isMobile) @if (isMobile)
{ {
@ -216,7 +217,7 @@
@foreach (Photo photo in Model.Photos) @foreach (Photo photo in Model.Photos)
{ {
<div class="eight wide column"> <div class="eight wide column">
@await photo.ToHtml(Html, ViewData, language) @await photo.ToHtml(Html, ViewData, language, timeZone)
</div> </div>
} }
</div> </div>

View file

@ -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;
}
<script>
function onSubmit(){
document.getElementById("avatar-encoded").value = selectedAvatar.toString();
document.getElementById("labels").value = serializeLabels();
return true;
}
</script>
<div class="@(isMobile ? "" : "ui center aligned grid")">
<div class="eight wide column">
<div class="ui blue segment">
<h1><i class="cog icon"></i>Slot Settings</h1>
<div class="ui divider"></div>
<form id="form" method="POST" class="ui form center aligned" action="/slot/@Model.Slot!.SlotId/settings" onsubmit="onSubmit()">
@Html.AntiForgeryToken()
<div class="field" style="display: flex; justify-content: center; align-items: center;">
<div>
<div>
<img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute; z-index: 3;">
<img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: 1;">
<img id="slotIcon" class="cardIcon slotCardIcon" src="/gameAssets/@Model.Slot.IconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: relative; z-index: 2"
onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'">
</div>
<div class="ui fitted divider hidden"></div>
<label for="avatar" class="ui blue button" style="color: white; max-width: @(size)px">
<i class="arrow circle up icon"></i>
<span>Upload file</span>
</label>
<input style="display: none" type="file" id="avatar" accept="image/png, image/jpeg">
<input type="hidden" name="avatar" id="avatar-encoded">
</div>
</div>
<div class="field">
<label style="text-align: left" for="name">@Model.Translate(GeneralStrings.Username)</label>
<input type="text" name="name" id="name" value="@HttpUtility.HtmlDecode(Model.Slot.Name)" placeholder="Name">
</div>
<div class="field">
<label style="text-align: left" for="description">Description</label>
<textarea name="description" id="description" spellcheck="false" placeholder="Description">@HttpUtility.HtmlDecode(Model.Slot.Description)</textarea>
</div>
@if (Model.Slot.GameVersion != GameVersion.LittleBigPlanet1)
{
<div class="field">
<label style="text-align: left">Labels</label>
@{
foreach (string s in Enum.GetNames(typeof(LevelLabels)))
{
if (!LabelHelper.isValidForGame(s, Model.Slot.GameVersion)) continue;
string color = "";
if (Model.Slot.AuthorLabels.Contains(s)) color += "selected";
<button type="button" onclick="labelButtonClick(event)" onmouseleave="onHoverStart(this)" onmouseenter="onHoverStart(this)" style="margin: .35em" class="ui button skew @color" id="@s">@LabelHelper.TranslateTag(s)</button>
}
}
<input type="hidden" name="labels" id="labels">
</div>
}
<button class="ui button green" tabindex="0">Save Changes</button>
<a class="ui button red" href="/slot/@Model.Slot.SlotId">Discard Changes</a>
<div class="ui divider fitted hidden"></div>
@if (Model.Slot.CreatorId == Model.User!.UserId)
{
<button type="button" class="ui button red" onclick="confirmUnpublish()">Unpublish level</button>
}
</form>
</div>
</div>
</div>
<script>
let selectedButtons = [];
@if (Model.Slot.CreatorId == Model.User.UserId)
{
<text>
function confirmUnpublish(){
if (window.confirm("Are you sure you want to unpublish this level?\nThis action cannot be undone")){
window.location.href = "/slot/@Model.Slot.SlotId/unpublish";
}
}
</text>
}
function onHoverStart(btn){
generateRandomSkew(btn);
}
function generateRandomSkew(element){
let rand = Math.random() * 6 - 3;
element.style.setProperty("--skew", "rotate(" + rand + "deg)");
}
function setupButtons(){
const elements = document.getElementsByClassName("skew");
for (let i = 0; i < elements.length; i++) {
generateRandomSkew(elements[i]);
if (elements[i].classList.contains("selected"))
selectedButtons.push(elements[i]);
}
}
function serializeLabels(){
let labels = "";
for (let i = 0; i < selectedButtons.length; i++) {
if (selectedButtons[i] == null) continue;
labels += selectedButtons[i].id;
if (i !== selectedButtons.length - 1) {
labels += ",";
}
}
return labels;
}
function labelButtonClick(e){
e.preventDefault();
const target = e.target;
target.blur();
if (target.classList.contains("selected")){
target.classList.remove("selected");
} else {
target.classList.add("selected");
}
if (selectedButtons.includes(target)){
let startIndex = selectedButtons.indexOf(target);
selectedButtons.splice(startIndex, 1);
} else {
selectedButtons.push(target);
if (selectedButtons.length > 5){
let removed = selectedButtons.shift();
removed.classList.remove("selected");
}
}
}
setupButtons();
let selectedAvatar = "";
document.getElementById("avatar").onchange = function (e){
const file = e.target.files.item(0);
if (file.type !== "image/jpeg" && file.type !== "image/png")
return;
const output = document.getElementById('slotIcon');
const reader = new FileReader();
reader.onload = function(){
output.src = reader.result;
selectedAvatar = reader.result;
};
reader.readAsDataURL(file);
}
</script>

View file

@ -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<IActionResult> 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<IActionResult> 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();
}
}

View file

@ -5,7 +5,6 @@
@using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage
@{ @{
@ -15,8 +14,9 @@
Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username); Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username);
Model.Description = Model.ProfileUser!.Biography; Model.Description = Model.ProfileUser!.Biography;
bool isMobile = this.Request.IsMobile(); bool isMobile = Request.IsMobile();
string language = Model.GetLanguage(); string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
} }
@if (Model.ProfileUser.IsBanned) @if (Model.ProfileUser.IsBanned)
@ -50,7 +50,10 @@
}, },
{ {
"Language", Model.GetLanguage() "Language", Model.GetLanguage()
} },
{
"TimeZone", Model.GetTimeZone()
},
}) })
</div> </div>
<div class="eight wide right aligned column"> <div class="eight wide right aligned column">
@ -72,13 +75,15 @@
</a> </a>
} }
} }
@if (Model.ProfileUser == Model.User || (Model.User?.IsModerator ?? false))
{
<a class="ui blue button" href="/user/@Model.ProfileUser.UserId/settings">
<i class="cog icon"></i>
<span>Settings</span>
</a>
}
@if (Model.ProfileUser == Model.User) @if (Model.ProfileUser == Model.User)
{ {
<a class="ui blue button" href="/passwordReset">
<i class="key icon"></i>
<span>@Model.Translate(GeneralStrings.ResetPassword)</span>
</a>
<a href="/logout" class="ui red button"> <a href="/logout" class="ui red button">
<i class="user slash icon"></i> <i class="user slash icon"></i>
@Model.Translate(BaseLayoutStrings.HeaderLogout) @Model.Translate(BaseLayoutStrings.HeaderLogout)
@ -98,7 +103,7 @@
} }
else else
{ {
<p>@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p> <p style="overflow-wrap: anywhere;">@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p>
} }
</div> </div>
</div> </div>
@ -129,7 +134,7 @@
{ {
string width = isMobile ? "sixteen" : "eight"; string width = isMobile ? "sixteen" : "eight";
<div class="@width wide column"> <div class="@width wide column">
@await photo.ToHtml(Html, ViewData, language) @await photo.ToHtml(Html, ViewData, language, timeZone)
</div> </div>
} }
</div> </div>
@ -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) @if (Model.User != null && Model.User.IsModerator)
{ {

View file

@ -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;
}
<script>
function onSubmit(e){
document.getElementById("avatar-encoded").value = selectedAvatar.toString();
@if(ServerConfiguration.Instance.Mail.MailEnabled){
<text>
let newEmail = document.getElementById("email").value;
if (newEmail.length === 0){
e.preventDefault();
return false;
}
if (newEmail !== email){
if (!window.confirm("This action will change your email to '" + newEmail + "'\nYour old email will be removed from your account if you continue")){
e.preventDefault();
return false;
}
}
</text>
}
return true;
}
</script>
<div class="@(isMobile ? "" : "ui center aligned grid")">
<div class="eight wide column">
<div class="ui blue segment">
<h1><i class="cog icon"></i>@Model.ProfileUser.Username's Settings</h1>
<div class="ui divider"></div>
<form id="form" method="POST" class="ui form center aligned" action="/user/@Model.ProfileUser.UserId/settings" onsubmit="onSubmit(event)">
@Html.AntiForgeryToken()
<div class="field" style="display: flex; justify-content: center; align-items: center">
<div>
<div class="cardIcon userCardIcon" id="userPicture" style="background-image: url('/gameAssets/@Model.ProfileUser.WebsiteAvatarHash'); min-width: @(size)px; width: @(size)px; height: @(size)px; background-position: center center; background-size: cover; background-repeat: no-repeat;"></div>
<div class="ui fitted divider hidden"></div>
<label for="avatar" class="ui blue button" style="color: white; max-width: @(size)px">
<i class="arrow circle up icon"></i>
<span>Upload file</span>
</label>
<input style="display: none" type="file" id="avatar" accept="image/png, image/jpeg">
<input type="hidden" name="avatar" id="avatar-encoded">
</div>
</div>
<div class="field">
<label style="text-align: left" for="username">@Model.Translate(GeneralStrings.Username)</label>
<input type="text" name="username" id="username" value="@Model.ProfileUser.Username" placeholder="Username" readonly>
</div>
@if (ServerConfiguration.Instance.Mail.MailEnabled && (Model.User == Model.ProfileUser || Model.User!.IsAdmin))
{
<div class="field">
<label style="text-align: left" for="email">Email</label>
<input type="text" name="email" id="email" required value="@Model.ProfileUser.EmailAddress" placeholder="Email Address">
</div>
}
<div class="field">
<label style="text-align: left" for="biography">@Model.Translate(ProfileStrings.Biography)</label>
<textarea name="biography" id="biography" spellcheck="false" placeholder="Biography">@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</textarea>
</div>
@if (Model.User == Model.ProfileUser)
{
<div class="field">
<label style="text-align: left">Language</label>
<select class="ui fluid dropdown" name="language">
@foreach (string lang in LocalizationManager.GetAvailableLanguages())
{
string selected = "";
if (lang.Equals(Model.ProfileUser.Language))
{
selected = " selected=\"selected\"";
}
string langName = new CultureInfo(lang).DisplayName;
langName = lang switch
{
"en-PT" => "Pirate Speak (The Seven Seas)",
"zh-CN" => "Simplified Chinese",
"zh-TW" => "Traditional Chinese",
_ => langName,
};
<option value="@lang"@selected>@langName</option>
}
</select>
</div>
<div class="field">
<label style="text-align: left">Timezone</label>
<select class="ui fluid dropdown" name="timeZone">
@foreach (TimeZoneInfo systemTimeZone in TimeZoneInfo.GetSystemTimeZones())
{
string selected = "";
if (systemTimeZone.Id.Equals(Model.ProfileUser.TimeZone))
{
selected = " selected=\"selected\"";
}
<option value="@systemTimeZone.Id"@selected>@systemTimeZone.DisplayName</option>
}
</select>
</div>
}
<button class="ui button green" tabindex="0">Save Changes</button>
<a class="ui button red" href="/user/@Model.ProfileUser.UserId">Discard Changes</a>
<div class="ui divider fitted hidden"></div>
@if (Model.User == Model.ProfileUser)
{
<a class="ui blue button" href="/passwordReset">
<i class="key icon"></i>
@Model.Translate(GeneralStrings.ResetPassword)
</a>
}
</form>
</div>
</div>
</div>
<script>
@if (ServerConfiguration.Instance.Mail.MailEnabled)
{
<text>
const email = document.getElementById("email").value;
</text>
}
let selectedAvatar = "";
document.getElementById("avatar").addEventListener("change", e => {
const file = e.target.files.item(0);
if (file.type !== "image/jpeg" && file.type !== "image/png")
return;
const output = document.getElementById('userPicture');
const reader = new FileReader();
reader.onload = function(){
output.style.backgroundImage = "url(" + reader.result + ")";
selectedAvatar = reader.result;
};
reader.readAsDataURL(file);
});
</script>

View file

@ -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<IActionResult> 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<string> 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<IActionResult> 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();
}
}

View file

@ -2,7 +2,6 @@
@using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles @using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UsersPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UsersPage
@{ @{
@ -35,6 +34,9 @@
{ {
"Language", Model.GetLanguage() "Language", Model.GetLanguage()
}, },
{
"TimeZone", Model.GetTimeZone()
},
}) })
</div> </div>
} }

View file

@ -33,7 +33,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View file

@ -80,6 +80,7 @@ public class WebsiteStartup
app.UseMiddleware<HandlePageErrorMiddleware>(); app.UseMiddleware<HandlePageErrorMiddleware>();
app.UseMiddleware<RequestLogMiddleware>(); app.UseMiddleware<RequestLogMiddleware>();
app.UseMiddleware<UserRequiredRedirectMiddleware>();
app.UseRouting(); app.UseRouting();

View file

@ -9,8 +9,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -9,14 +9,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.4.0" /> <PackageReference Include="Selenium.WebDriver" Version="4.4.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="103.0.5060.13400" /> <PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="105.0.5195.5200" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -14,8 +14,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -6,14 +6,15 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DDSReader;
using ICSharpCode.SharpZipLib.Zip.Compression; using ICSharpCode.SharpZipLib.Zip.Compression;
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using Pfim;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; 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 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 GetResourcePath(string hash) => Path.Combine(ResourcePath, hash);
public static string GetImagePath(string hash) => Path.Combine(ImagePath, hash);
public static bool AreDependenciesSafe(LbpFile file) public static bool AreDependenciesSafe(LbpFile file)
{ {
// recursively check if dependencies are safe // recursively check if dependencies are safe
@ -63,6 +68,7 @@ public static class FileHelper
LbpFileType.Texture => true, LbpFileType.Texture => true,
LbpFileType.Script => false, LbpFileType.Script => false,
LbpFileType.Level => true, LbpFileType.Level => true,
LbpFileType.Adventure => true,
LbpFileType.Voice => true, LbpFileType.Voice => true,
LbpFileType.Quest => true, LbpFileType.Quest => true,
LbpFileType.Plan => true, LbpFileType.Plan => true,
@ -189,6 +195,8 @@ public static class FileHelper
"FSHb" => LbpFileType.Script, "FSHb" => LbpFileType.Script,
"VOPb" => LbpFileType.Voice, "VOPb" => LbpFileType.Voice,
"LVLb" => LbpFileType.Level, "LVLb" => LbpFileType.Level,
"ADCb" => LbpFileType.Adventure,
"ADSb" => LbpFileType.Adventure,
"PLNb" => LbpFileType.Plan, "PLNb" => LbpFileType.Plan,
"QSTb" => LbpFileType.Quest, "QSTb" => LbpFileType.Quest,
_ => readAlternateHeader(reader), _ => readAlternateHeader(reader),
@ -240,6 +248,34 @@ public static class FileHelper
if (!Directory.Exists(path)) Directory.CreateDirectory(path ?? throw new ArgumentNullException(nameof(path))); 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<string?> 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 string[] ResourcesNotUploaded(params string[] hashes) => hashes.Where(hash => !ResourceExists(hash)).ToArray();
public static void ConvertAllTexturesToPng() 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(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; 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]) if (compressed[i] == decompressed[i])
{ {
writer.Write(deflatedData); writer.Write(deflatedData);
continue;
} }
Inflater inflater = new(); Inflater inflater = new();
@ -357,19 +394,25 @@ public static class FileHelper
private static bool DDSToPNG(string hash, byte[] data) private static bool DDSToPNG(string hash, byte[] data)
{ {
using MemoryStream stream = new(); Dds ddsImage = Dds.Create(data, new PfimConfig());
DDSImage image = new(data); if(ddsImage.Compressed)
ddsImage.Decompress();
image.SaveAsPng(stream); // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
Image image = ddsImage.Format switch
{
ImageFormat.Rgba32 => Image.LoadPixelData<Bgra32>(ddsImage.Data, ddsImage.Width, ddsImage.Height),
_ => throw new ArgumentOutOfRangeException($"ddsImage.Format is not supported: {ddsImage.Format}")
};
Directory.CreateDirectory("png"); Directory.CreateDirectory("png");
File.WriteAllBytes($"png/{hash}.png", stream.ToArray()); image.SaveAsPngAsync($"png/{hash}.png");
return true; return true;
} }
private static bool JPGToPNG(string hash, byte[] data) private static bool JPGToPNG(string hash, byte[] data)
{ {
using Image<Rgba32> image = Image.Load(data); using Image image = Image.Load(data);
using MemoryStream ms = new(); using MemoryStream ms = new();
image.SaveAsPng(ms); image.SaveAsPng(ms);

View file

@ -5,6 +5,7 @@ public enum LbpFileType
Script, // .ff, FSH Script, // .ff, FSH
Texture, // TEX Texture, // TEX
Level, // LVL Level, // LVL
Adventure, // ADC, ADS
CrossLevel, // PRF, Cross controller level CrossLevel, // PRF, Cross controller level
FileArchive, // .farc, (ends with FARC) FileArchive, // .farc, (ends with FARC)
Plan, // PLN, uploaded with levels Plan, // PLN, uploaded with levels

View file

@ -1,32 +1,107 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
namespace LBPUnion.ProjectLighthouse.Helpers; namespace LBPUnion.ProjectLighthouse.Helpers;
public static class LabelHelper public static class LabelHelper
{ {
private static readonly List<string> 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<string> 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<string, string> translationTable = new() private static readonly Dictionary<string, string> 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_DirectControl", "Controllinator"},
{"LABEL_GrapplingHook", "Grappling Hook"}, {"LABEL_GrapplingHook", "Grappling Hook"},
{"LABEL_JumpPads", "Bounce Pads"}, {"LABEL_JumpPads", "Bounce Pads"},
{"LABEL_MagicBag", "Creatinator"}, {"LABEL_MagicBag", "Creatinator"},
{"LABEL_LowGravity", "Low Gravity"}, {"LABEL_LowGravity", "Low Gravity"},
{"LABEL_PowerGlove", "Grabinator"}, {"LABEL_PowerGlove", "Grabinators"},
{"LABEL_ATTRACT_GEL", "Attract-o-Gel"}, {"LABEL_ATTRACT_GEL", "Attract-o-Gel"},
{"LABEL_ATTRACT_TWEAK", "Attract-o-Tweaker"}, {"LABEL_ATTRACT_TWEAK", "Attract-o-Tweaker"},
{"LABEL_HEROCAPE", "Hero Cape"}, {"LABEL_HEROCAPE", "Hero Cape"},
{"LABEL_MEMORISER", "Memorizer"}, {"LABEL_MEMORISER", "Memorizer"},
{"LABEL_WALLJUMP", "Wall Jump"}, {"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_SINGLE_PLAYER", "Single Player"},
{"LABEL_SurvivalChallenge", "Survival Challenge"},
{"LABEL_TOP_DOWN", "Top Down"}, {"LABEL_TOP_DOWN", "Top Down"},
{"LABEL_CO_OP", "Co-Op"}, {"LABEL_CO_OP", "Co-Op"},
{"LABEL_Sci_Fi", "Sci-Fi"}, {"LABEL_Sci_Fi", "Sci-Fi"},
{"LABEL_INTERACTIVE_STREAM", "Interactive Stream"}, {"LABEL_INTERACTIVE_STREAM", "Interactive Stream"},
{"LABEL_QUESTS", "Quests"}, {"LABEL_QUESTS", "Quests"},
{"LABEL_Mini_Game", "Mini-Game"},
{"8_Bit", "8-bit"}, {"8_Bit", "8-bit"},
{"16_Bit", "16-bit"}, {"16_Bit", "16-bit"},
{"LABEL_SACKPOCKET", "Sackpocket"}, {"LABEL_SACKPOCKET", "Sackpocket"},
@ -40,13 +115,27 @@ public static class LabelHelper
{"LABEL_CREATED_CHARACTERS", "Created Characters"}, {"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 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) public static string RemoveInvalidLabels(string authorLabels)
{ {
List<string> labels = new(authorLabels.Split(",")); List<string> labels = new(authorLabels.Split(","));
if (labels.Count > 5) labels = labels.GetRange(0, 5);
for (int i = labels.Count - 1; i >= 0; i--) for (int i = labels.Count - 1; i >= 0; i--)
{ {
if (!IsValidLabel(labels[i])) labels.Remove(labels[i]); if (!IsValidLabel(labels[i])) labels.Remove(labels[i]);

View file

@ -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) foreach ((string? key, string? value) in charsToReplace)
{ {

View file

@ -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 // I would remove the LABEL prefix, but some of the tags start with numbers and won't compile
public enum LevelLabels public enum LevelLabels
{ {
// Start LBP2 Labels
LABEL_SinglePlayer, LABEL_SinglePlayer,
LABEL_Multiplayer, LABEL_Multiplayer,
LABEL_Quick, LABEL_Quick,
@ -31,6 +32,7 @@ public enum LevelLabels
LABEL_Strategy, LABEL_Strategy,
LABEL_SurvivalChallenge, LABEL_SurvivalChallenge,
LABEL_Tutorial, LABEL_Tutorial,
LABEL_Retro,
LABEL_Collectables, LABEL_Collectables,
LABEL_DirectControl, LABEL_DirectControl,
LABEL_Explosives, LABEL_Explosives,
@ -52,7 +54,22 @@ public enum LevelLabels
LABEL_HEROCAPE, LABEL_HEROCAPE,
LABEL_MEMORISER, LABEL_MEMORISER,
LABEL_WALLJUMP, 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_SINGLE_PLAYER,
LABEL_RPG, LABEL_RPG,
LABEL_TOP_DOWN, LABEL_TOP_DOWN,

View file

@ -60,6 +60,9 @@ public class Slot
[XmlElement("icon")] [XmlElement("icon")]
public string IconHash { get; set; } = ""; public string IconHash { get; set; } = "";
[XmlElement("isAdventurePlanet")]
public bool IsAdventurePlanet { get; set; }
[XmlElement("rootLevel")] [XmlElement("rootLevel")]
[JsonIgnore] [JsonIgnore]
public string RootLevel { get; set; } = ""; public string RootLevel { get; set; } = "";
@ -301,6 +304,7 @@ public class Slot
LbpSerializer.StringElement("initiallyLocked", this.InitiallyLocked) + LbpSerializer.StringElement("initiallyLocked", this.InitiallyLocked) +
LbpSerializer.StringElement("isSubLevel", this.SubLevel) + LbpSerializer.StringElement("isSubLevel", this.SubLevel) +
LbpSerializer.StringElement("isLBP1Only", this.Lbp1Only) + LbpSerializer.StringElement("isLBP1Only", this.Lbp1Only) +
LbpSerializer.StringElement("isAdventurePlanet", this.IsAdventurePlanet) +
LbpSerializer.StringElement("background", this.BackgroundHash) + LbpSerializer.StringElement("background", this.BackgroundHash) +
LbpSerializer.StringElement("shareable", this.Shareable) + LbpSerializer.StringElement("shareable", this.Shareable) +
LbpSerializer.StringElement("authorLabels", this.AuthorLabels) + LbpSerializer.StringElement("authorLabels", this.AuthorLabels) +

View file

@ -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);
}

View file

@ -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<string>(
name: "Language",
table: "Users",
type: "longtext",
defaultValue: "en",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
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");
}
}
}

View file

@ -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<bool>(
name: "IsAPirate",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
}
}

View file

@ -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<int>(
name: "ChildSlotId",
table: "Scores",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View file

@ -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<long>(
name: "IsAdventurePlanet",
table: "Slots",
type: "bool",
nullable: false,
defaultValue: false);
}
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -179,12 +180,10 @@ public class User
[JsonIgnore] [JsonIgnore]
public string? ApprovedIPAddress { get; set; } public string? ApprovedIPAddress { get; set; }
#nullable disable #nullable disable
/// <summary> public string Language { get; set; } = "en";
/// ARRR! Forces the user to see Pirate English translations on the website.
/// </summary> public string TimeZone { get; set; } = TimeZoneInfo.Local.Id;
[NotMapped]
public bool IsAPirate { get; set; }
public PrivacyType LevelVisibility { get; set; } = PrivacyType.All; public PrivacyType LevelVisibility { get; set; } = PrivacyType.All;

View file

@ -47,7 +47,7 @@ public class UserStatus
this.CurrentRoom = RoomHelper.FindRoomByUserId(userId); this.CurrentRoom = RoomHelper.FindRoomByUserId(userId);
} }
private string FormatOfflineTimestamp(string language) private string FormatOfflineTimestamp(string language, string timeZone)
{ {
if (this.LastLogout <= 0 && this.LastLogin <= 0) if (this.LastLogout <= 0 && this.LastLogin <= 0)
{ {
@ -56,11 +56,12 @@ public class UserStatus
long timestamp = this.LastLogout; long timestamp = this.LastLogout;
if (timestamp <= 0) timestamp = this.LastLogin; 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); return StatusStrings.LastOnline.Translate(language, formattedTime);
} }
public string ToTranslatedString(string language) public string ToTranslatedString(string language, string timeZone)
{ {
this.CurrentVersion ??= GameVersion.Unknown; this.CurrentVersion ??= GameVersion.Unknown;
this.CurrentPlatform ??= Platform.Unknown; this.CurrentPlatform ??= Platform.Unknown;
@ -69,7 +70,7 @@ public class UserStatus
{ {
StatusType.Online => StatusStrings.CurrentlyOnline.Translate(language, StatusType.Online => StatusStrings.CurrentlyOnline.Translate(language,
((GameVersion)this.CurrentVersion).ToPrettyString(), (Platform)this.CurrentPlatform), ((GameVersion)this.CurrentVersion).ToPrettyString(), (Platform)this.CurrentPlatform),
StatusType.Offline => this.FormatOfflineTimestamp(language), StatusType.Offline => this.FormatOfflineTimestamp(language, timeZone),
_ => GeneralStrings.Unknown.Translate(language), _ => GeneralStrings.Unknown.Translate(language),
}; };
} }

View file

@ -21,6 +21,9 @@ public class Score
[ForeignKey(nameof(SlotId))] [ForeignKey(nameof(SlotId))]
public Slot Slot { get; set; } public Slot Slot { get; set; }
[XmlIgnore]
public int? ChildSlotId { get; set; }
[XmlElement("type")] [XmlElement("type")]
public int Type { get; set; } public int Type { get; set; }

View file

@ -10,21 +10,22 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="DDSReader" Version="1.0.8-pre" /> <PackageReference Include="Pfim" Version="0.11.1"/>
<PackageReference Include="Discord.Net.Webhook" Version="3.8.0" /> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3"/>
<PackageReference Include="Discord.Net.Webhook" Version="3.8.1" />
<PackageReference Include="InfluxDB.Client" Version="4.5.0" /> <PackageReference Include="InfluxDB.Client" Version="4.5.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
<PackageReference Include="Redis.OM" Version="0.2.1" /> <PackageReference Include="Redis.OM" Version="0.2.3" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="YamlDotNet" Version="12.0.0" /> <PackageReference Include="YamlDotNet" Version="12.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -730,8 +730,8 @@ namespace ProjectLighthouse.Migrations
b.Property<string>("IconHash") b.Property<string>("IconHash")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<bool>("IsAPirate") b.Property<string>("Language")
.HasColumnType("tinyint(1)"); .HasColumnType("longtext");
b.Property<long>("LastLogin") b.Property<long>("LastLogin")
.HasColumnType("bigint"); .HasColumnType("bigint");
@ -772,6 +772,9 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("ProfileVisibility") b.Property<int>("ProfileVisibility")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("TimeZone")
.HasColumnType("longtext");
b.Property<string>("Username") b.Property<string>("Username")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -159,4 +159,27 @@ div.cardStatsUnderTitle > span {
margin-right: 0.5em; margin-right: 0.5em;
} }
/*#endregion Comments*/ /*#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 */