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,
"tools": {
"dotnet-ef": {
"version": "6.0.8",
"version": "6.0.9",
"commands": [
"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>
</data>
<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>
</data>
<data name="header_logout" xml:space="preserve">

View file

@ -64,7 +64,7 @@
<value>Página generada por {0}.</value>
</data>
<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 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>

View file

@ -50,7 +50,7 @@
<comment>A quick shortcut on the header to take you to your profile if logged in.</comment>
</data>
<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>
</data>
<data name="header_logout" xml:space="preserve">
@ -58,30 +58,30 @@
<comment>A shortcut to log you out of your account.</comment>
</data>
<data name="header_modPanel" xml:space="preserve">
<value>Mod Panel</value>
<value>Панель модов</value>
</data>
<data name="generated_by" xml:space="preserve">
<value>Page generated by {0}.</value>
<value>Страница создана {0}</value>
</data>
<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 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 name="js_warn_title" xml:space="preserve">
<value>JavaScript не включен</value>
</data>
<data name="license_warn_title" xml:space="preserve">
<value>Potential License Violation</value>
<value>Потенциальное нарушение лицензии</value>
</data>
<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 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 name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
<value>Если нет, пожалуйста, опубликуйте исходный код в доступном для ваших пользователей месте.</value>
</data>
</root>

View file

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

View file

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

View file

@ -26,19 +26,19 @@
<comment>A greeting on the main page of the website.</comment>
</data>
<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>
</data>
<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>
</data>
<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>
</data>
<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>
</data>
<data name="authAttemptsPending" xml:space="preserve">

View file

@ -26,19 +26,19 @@
<comment>A greeting on the main page of the website.</comment>
</data>
<data name="loggedInAs" xml:space="preserve">
<value>Greetings, {0}.</value>
<value>Приветствуем, {0}.</value>
<comment>A greeting on the main page of the website.</comment>
</data>
<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>
</data>
<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>
</data>
<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>
</data>
<data name="authAttemptsPending" xml:space="preserve">

View file

@ -124,7 +124,7 @@ public static class LocalizationManager
.Where(r => r != "resources")
.ToList();
languages.Add(DefaultLang);
languages.Insert(0, DefaultLang);
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>
</resheader>
<data name="mod_panel_title" xml:space="preserve">
<value>Moderation Panel</value>
<value>Панель модерации</value>
</data>
<data name="greeting" xml:space="preserve">
<value>Welcome to the moderation panel, {0}!</value>
<value>Добро пожаловать на панель модерации, {0}!</value>
</data>
<data name="banned_users" xml:space="preserve">
<value>Banned Users</value>
<value>Заблокированные пользователи</value>
</data>
<data name="hidden_levels" xml:space="preserve">
<value>Hidden Levels</value>
<value>Скрытые уровни</value>
</data>
</root>

View file

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

View file

@ -18,6 +18,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<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>
</root>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -56,6 +56,12 @@ public class ResourcesController : ControllerBase
string path = FileHelper.GetResourcePath(hash);
string fullPath = Path.GetFullPath(path);
string basePath = Path.GetFullPath(FileHelper.ResourcePath);
// Prevent directory traversal attacks
if (!fullPath.StartsWith(basePath)) return this.BadRequest();
if (FileHelper.ResourceExists(hash)) return this.File(IOFile.OpenRead(path), "application/octet-stream");
return this.NotFound();

View file

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

View file

@ -40,7 +40,8 @@ public class PublishController : ControllerBase
GameToken gameToken = userAndToken.Value.Item2;
Slot? slot = await this.getSlotFromBody();
if (slot == null) {
if (slot == null)
{
Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish);
return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded
}
@ -135,10 +136,21 @@ public class PublishController : ControllerBase
return this.BadRequest();
}
if (rootLevel.FileType != LbpFileType.Level)
if (!slot.IsAdventurePlanet)
{
Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish);
return this.BadRequest();
if (rootLevel.FileType != LbpFileType.Level)
{
Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish);
return this.BadRequest();
}
}
else
{
if (rootLevel.FileType != LbpFileType.Adventure)
{
Logger.Warn("Rejecting level upload, rootLevel is not a LBP 3 Adventure", LogArea.Publish);
return this.BadRequest();
}
}
GameVersion slotVersion = FileHelper.ParseLevelVersion(rootLevel);
@ -232,7 +244,7 @@ public class PublishController : ControllerBase
this.database.Slots.Add(slot);
await this.database.SaveChangesAsync();
if (user.LevelVisibility == PrivacyType.All)
{
await WebhookHelper.SendWebhook("New level published!",

View file

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

View file

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

View file

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

View file

@ -25,6 +25,27 @@ public class SlotPageController : ControllerBase
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")]
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.Profiles;
using Microsoft.AspNetCore.Html;
@ -9,15 +8,19 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
public static class PartialExtensions
{
// ReSharper disable once SuggestBaseTypeForParameter
public static ViewDataDictionary<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
{
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)
=> helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(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).WithTime(timeZone).WithKeyValue("IncludeStatus", includeStatus));
public static Task<IHtmlContent> ToHtml<T>(this Photo photo, IHtmlHelper<T> helper, ViewDataDictionary<T> viewData, string language)
=> helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(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).WithTime(timeZone));
}

View file

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

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

View file

@ -5,6 +5,8 @@
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Authentication";
string timeZone = Model.GetTimeZone();
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
@if (Model.AuthenticationAttempts.Count == 0)
@ -41,9 +43,9 @@ else
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp).ToLocalTime();
DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp), timeZoneInfo);
<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>
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
<button class="ui small green button">

View file

@ -11,8 +11,9 @@
Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false;
bool isMobile = this.Request.IsMobile();
bool isMobile = Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
}
<h1 class="lighthouse-welcome lighthouse-title">
@Model.Translate(LandingPageStrings.Welcome, ServerConfiguration.Instance.Customization.ServerName)
@ -48,7 +49,7 @@ else
foreach (User user in Model.PlayersOnline)
{
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
using System;
using System.Collections.Generic;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Localization;
using LBPUnion.ProjectLighthouse.Localization.StringLists;
@ -46,20 +44,31 @@ public class BaseLayout : PageModel
}
private string? language;
private string? timeZone;
public string GetLanguage()
{
if (ServerStatics.IsUnitTesting) return LocalizationManager.DefaultLang;
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;
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, params object?[] format) => translatableString.Translate(this.GetLanguage(), format);
}

View file

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

View file

@ -1,10 +1,13 @@
@page "/moderation/cases/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.CasePage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Cases";
string timeZone = Model.GetTimeZone();
}
<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)
{
@(await Html.PartialAsync("Partials/ModerationCasePartial", @case))
@(await Html.PartialAsync("Partials/ModerationCasePartial", @case, ViewData.WithTime(timeZone)))
}

View file

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

View file

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

View file

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

View file

@ -6,6 +6,8 @@
@{
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">
@ -85,7 +87,7 @@
<span>@decodedMessage</span>
}
<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>
@if (i != Model.Comments.Count - 1)
{

View file

@ -3,9 +3,12 @@
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
bool includeStatus = (bool?)ViewData["IncludeStatus"] ?? false;
string userStatus = includeStatus ? Model.Status.ToTranslatedString(language, timeZone) : "";
}
<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=""/>
@Model.Username
</a>

View file

@ -1,7 +1,6 @@
@using System.Diagnostics
@using LBPUnion.ProjectLighthouse
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@model LBPUnion.ProjectLighthouse.Administration.ModerationCase
@ -10,6 +9,9 @@
Database database = new();
string color = "blue";
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
if (Model.Expired) color = "yellow";
if (Model.Dismissed) color = "green";
}
@ -23,19 +25,19 @@
Debug.Assert(Model.DismissedAt != null);
<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>
}
else if (Model.Expired)
else if (Model.Expired && Model.ExpiresAt != null)
{
<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>
}
<span>
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>
@if (Model.Type.AffectsLevel())

View file

@ -8,6 +8,8 @@
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
<div style="position: relative">
@ -26,7 +28,7 @@
{
<span>by</span>
<b>
@await Model.Creator.ToLink(Html, ViewData, language)
@await Model.Creator.ToLink(Html, ViewData, language, timeZone)
</b>
}
@if (Model.Slot != null)
@ -35,7 +37,8 @@
{
case SlotType.User:
<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>
break;
case SlotType.Developer:
@ -49,7 +52,7 @@
break;
}
}
at @DateTime.UnixEpoch.AddSeconds(Model.Timestamp).ToString(CultureInfo.CurrentCulture)
at @TimeZoneInfo.ConvertTime(DateTime.UnixEpoch.AddSeconds(Model.Timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt")
</i>
</p>
@ -62,7 +65,7 @@
<div id="hover-subjects-@Model.PhotoId">
@foreach (PhotoSubject subject in Model.Subjects)
{
@await subject.User.ToLink(Html, ViewData, language)
@await subject.User.ToLink(Html, ViewData, language, timeZone)
}
</div>

View file

@ -1,7 +1,12 @@
@using LBPUnion.ProjectLighthouse.Administration.Reports
@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>
<canvas class="hide-subjects" id="canvas-subjects-@Model.ReportId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)">
@ -26,7 +31,7 @@
<br>
<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>
<b>Report reason: </b>@Model.Type

View file

@ -18,6 +18,8 @@
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool mini = (bool?)ViewData["IsMini"] ?? false;
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
bool isQueued = false;
bool isHearted = false;
@ -37,11 +39,13 @@
<div class="card">
@{
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>
<img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute">
<img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: -1;">
<img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px;"
<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; @(advenStyleExt)">
<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'">
</div>
<div class="cardStats">
@ -90,12 +94,24 @@
</div>
@if (Model.Creator != null)
{
string date = "";
if(!mini)
date = " on " + TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(Model.FirstUploaded), timeZoneInfo).DateTime.ToShortDateString();
<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>
}
</div>
<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>
@if (user != null && !mini)
{

View file

@ -6,13 +6,14 @@
bool showLink = (bool?)ViewData["ShowLink"] ?? false;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
}
<div class="card">
@{
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 class="cardStats">
@if (showLink)
@ -28,7 +29,7 @@
</h1>
}
<span>
<i>@Model.Status.ToTranslatedString(language)</i>
<i>@Model.Status.ToTranslatedString(language, timeZone)</i>
</span>
<div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>

View file

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

View file

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

View file

@ -7,7 +7,7 @@
}
<!--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>Just click this 'ere button, and welcome aboard!</p>

View file

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

View file

@ -72,7 +72,7 @@ public class RegisterForm : BaseLayout
}
if (ServerConfiguration.Instance.Mail.MailEnabled &&
await this.Database.Users.FirstOrDefaultAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower()) != null)
await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower()))
{
this.Error = this.Translate(ErrorStrings.EmailTaken);
return this.Page();
@ -86,7 +86,7 @@ public class RegisterForm : BaseLayout
if (this.Request.Query.ContainsKey("token"))
{
await Database.RemoveRegistrationToken(this.Request.Query["token"]);
await this.Database.RemoveRegistrationToken(this.Request.Query["token"]);
}
User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress);

View file

@ -6,8 +6,15 @@
Model.Title = "Verify Email Address";
}
<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>
@if (Model.Success)
{
<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">
<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.Email;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
@ -14,6 +14,8 @@ public class SendVerificationEmailPage : BaseLayout
public SendVerificationEmailPage(Database database) : base(database)
{}
public bool Success { get; set; }
public async Task<IActionResult> OnGet()
{
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
@ -33,29 +35,29 @@ public class SendVerificationEmailPage : BaseLayout
}
#endif
EmailVerificationToken verifyToken = new()
EmailVerificationToken? verifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(v => v.UserId == user.UserId);
// If user doesn't have a token or it is expired then regenerate
if (verifyToken == null || DateTime.Now > verifyToken.ExpiresAt)
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
};
verifyToken = new EmailVerificationToken
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now.AddHours(6),
};
this.Database.EmailVerificationTokens.Add(verifyToken);
await this.Database.SaveChangesAsync();
this.Database.EmailVerificationTokens.Add(verifyToken);
await this.Database.SaveChangesAsync();
}
string body = "Hello,\n\n" +
$"This email is a request to verify this email for your (likely new!) Project Lighthouse account ({user.Username}).\n\n" +
$"To verify your account, click the following link: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" +
"If this wasn't you, feel free to ignore this email.";
if (SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body))
{
return this.Page();
}
else
{
throw new Exception("failed to send email");
}
this.Success = SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body);
return this.Page();
}
}

View file

@ -1,5 +1,6 @@
@page "/login/setEmail"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm
@{
@ -9,6 +10,16 @@
<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">
@Html.AntiForgeryToken()

View file

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

View file

@ -19,6 +19,7 @@
bool isMobile = this.Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
}
@if (Model.Slot!.Hidden)
@ -96,7 +97,7 @@
<br/>
}
<div class="eight wide column">
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language))
@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
</div>
@if (isMobile)
{
@ -216,7 +217,7 @@
@foreach (Photo photo in Model.Photos)
{
<div class="eight wide column">
@await photo.ToHtml(Html, ViewData, language)
@await photo.ToHtml(Html, ViewData, language, timeZone)
</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.PlayerData
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage
@{
@ -15,8 +14,9 @@
Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username);
Model.Description = Model.ProfileUser!.Biography;
bool isMobile = this.Request.IsMobile();
bool isMobile = Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
}
@if (Model.ProfileUser.IsBanned)
@ -50,7 +50,10 @@
},
{
"Language", Model.GetLanguage()
}
},
{
"TimeZone", Model.GetTimeZone()
},
})
</div>
<div class="eight wide right aligned column">
@ -72,13 +75,15 @@
</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)
{
<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">
<i class="user slash icon"></i>
@Model.Translate(BaseLayoutStrings.HeaderLogout)
@ -98,7 +103,7 @@
}
else
{
<p>@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p>
<p style="overflow-wrap: anywhere;">@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p>
}
</div>
</div>
@ -129,7 +134,7 @@
{
string width = isMobile ? "sixteen" : "eight";
<div class="@width wide column">
@await photo.ToHtml(Html, ViewData, language)
@await photo.ToHtml(Html, ViewData, language, timeZone)
</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)
{

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

View file

@ -33,7 +33,7 @@
</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>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View file

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

View file

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

View file

@ -9,14 +9,14 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<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.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

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

View file

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

View file

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

View file

@ -1,32 +1,107 @@
using System;
using System.Collections.Generic;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
namespace LBPUnion.ProjectLighthouse.Helpers;
public static class LabelHelper
{
private static readonly List<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()
{
{"Label_SinglePlayer", "Single Player"},
{"LABEL_Quick", "Short"},
{"LABEL_Competitive", "Versus"},
{"LABEL_Puzzle", "Puzzler"},
{"LABEL_Platform", "Platformer"},
{"LABEL_Race", "Racer"},
{"LABEL_SurvivalChallenge", "Survival Challenge"},
{"LABEL_DirectControl", "Controllinator"},
{"LABEL_GrapplingHook", "Grappling Hook"},
{"LABEL_JumpPads", "Bounce Pads"},
{"LABEL_MagicBag", "Creatinator"},
{"LABEL_LowGravity", "Low Gravity"},
{"LABEL_PowerGlove", "Grabinator"},
{"LABEL_PowerGlove", "Grabinators"},
{"LABEL_ATTRACT_GEL", "Attract-o-Gel"},
{"LABEL_ATTRACT_TWEAK", "Attract-o-Tweaker"},
{"LABEL_HEROCAPE", "Hero Cape"},
{"LABEL_MEMORISER", "Memorizer"},
{"LABEL_WALLJUMP", "Wall Jump"},
{"Label_Controlinator", "Controllinator"},
{"LABEL_MultiLevel", "Multi Level"},
{"LABEL_Portrait", "Portrait View"},
{"LABEL_RearTouch", "Rear touch pad"},
{"LABEL_SharedScreen", "Shared Screen"},
{"LABEL_SINGLE_PLAYER", "Single Player"},
{"LABEL_SurvivalChallenge", "Survival Challenge"},
{"LABEL_TOP_DOWN", "Top Down"},
{"LABEL_CO_OP", "Co-Op"},
{"LABEL_Sci_Fi", "Sci-Fi"},
{"LABEL_INTERACTIVE_STREAM", "Interactive Stream"},
{"LABEL_QUESTS", "Quests"},
{"LABEL_Mini_Game", "Mini-Game"},
{"8_Bit", "8-bit"},
{"16_Bit", "16-bit"},
{"LABEL_SACKPOCKET", "Sackpocket"},
@ -40,13 +115,27 @@ public static class LabelHelper
{"LABEL_CREATED_CHARACTERS", "Created Characters"},
};
public static bool isValidForGame(string label, GameVersion gameVersion)
{
return gameVersion switch
{
GameVersion.LittleBigPlanet1 => IsValidTag(label),
GameVersion.LittleBigPlanet2 => IsValidLabel(label) && !lbp3Labels.Contains(label) && !lbpVitaLabels.Contains(label),
GameVersion.LittleBigPlanetVita => IsValidLabel(label) && !lbp3Labels.Contains(label),
GameVersion.LittleBigPlanet3 => IsValidLabel(label) && !lbpVitaLabels.Contains(label),
_ => false,
};
}
public static bool IsValidTag(string tag) => Enum.IsDefined(typeof(LevelTags), tag.Replace("TAG_", "").Replace("-", "_"));
public static bool IsValidLabel(string label) => Enum.IsDefined(typeof(LevelLabels), label);
private static bool IsValidLabel(string label) => Enum.IsDefined(typeof(LevelLabels), label.Replace("-", "_"));
public static string RemoveInvalidLabels(string authorLabels)
{
List<string> labels = new(authorLabels.Split(","));
if (labels.Count > 5) labels = labels.GetRange(0, 5);
for (int i = labels.Count - 1; i >= 0; i--)
{
if (!IsValidLabel(labels[i])) labels.Remove(labels[i]);

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)
{

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
public enum LevelLabels
{
// Start LBP2 Labels
LABEL_SinglePlayer,
LABEL_Multiplayer,
LABEL_Quick,
@ -31,6 +32,7 @@ public enum LevelLabels
LABEL_Strategy,
LABEL_SurvivalChallenge,
LABEL_Tutorial,
LABEL_Retro,
LABEL_Collectables,
LABEL_DirectControl,
LABEL_Explosives,
@ -52,7 +54,22 @@ public enum LevelLabels
LABEL_HEROCAPE,
LABEL_MEMORISER,
LABEL_WALLJUMP,
LABEL_Retro,
// Start LBP Vita Labels
LABEL_Arcade,
LABEL_Co_op,
LABEL_Precision,
LABEL_Controlinator,
LABEL_Flick,
LABEL_Memoriser,
LABEL_MultiLevel,
LABEL_Portrait,
LABEL_RearTouch,
LABEL_SharedScreen,
LABEL_Swipe,
LABEL_Tap,
LABEL_Tilt,
LABEL_Touch,
// Start LBP3 Labels
LABEL_SINGLE_PLAYER,
LABEL_RPG,
LABEL_TOP_DOWN,

View file

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

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

View file

@ -47,7 +47,7 @@ public class UserStatus
this.CurrentRoom = RoomHelper.FindRoomByUserId(userId);
}
private string FormatOfflineTimestamp(string language)
private string FormatOfflineTimestamp(string language, string timeZone)
{
if (this.LastLogout <= 0 && this.LastLogin <= 0)
{
@ -56,11 +56,12 @@ public class UserStatus
long timestamp = this.LastLogout;
if (timestamp <= 0) timestamp = this.LastLogin;
string formattedTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToLocalTime().ToString("M/d/yyyy h:mm:ss tt");
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
string formattedTime = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt");
return StatusStrings.LastOnline.Translate(language, formattedTime);
}
public string ToTranslatedString(string language)
public string ToTranslatedString(string language, string timeZone)
{
this.CurrentVersion ??= GameVersion.Unknown;
this.CurrentPlatform ??= Platform.Unknown;
@ -69,7 +70,7 @@ public class UserStatus
{
StatusType.Online => StatusStrings.CurrentlyOnline.Translate(language,
((GameVersion)this.CurrentVersion).ToPrettyString(), (Platform)this.CurrentPlatform),
StatusType.Offline => this.FormatOfflineTimestamp(language),
StatusType.Offline => this.FormatOfflineTimestamp(language, timeZone),
_ => GeneralStrings.Unknown.Translate(language),
};
}

View file

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

View file

@ -10,21 +10,22 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="DDSReader" Version="1.0.8-pre" />
<PackageReference Include="Discord.Net.Webhook" Version="3.8.0" />
<PackageReference Include="Pfim" Version="0.11.1"/>
<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="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
<PackageReference Include="Redis.OM" Version="0.2.1" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="Redis.OM" Version="0.2.3" />
<PackageReference Include="SharpZipLib" Version="1.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>

View file

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