From f6a7fe6283e52c32940dec51761924b59c91f4f2 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 17 Sep 2022 14:02:46 -0500 Subject: [PATCH] User settings, level settings, language and timezone selection and more. (#471) * Initial work for user settings page * Finish user setting and slot setting pages * Don't show slot upload date on home page and fix team pick redirection * Fix upload image button alignment on mobile * Fix image upload on iPhone * Remove unused css and add selected button color * Fix login email check and bump ChromeDriver to 105 * Remove duplicated code and allow users to leave fields empty * Add unpublish button on level settings and move settings button position * Don't show edit button on mini card * Self review bug fixes and users can no longer use an in-use email --- .../LocalizationManager.cs | 2 +- .../Controllers/LoginController.cs | 9 +- .../Admin/ModerationSlotController.cs | 8 +- .../Controllers/SlotPageController.cs | 21 +++ .../Extensions/PartialExtensions.cs | 21 ++- .../Middlewares/HandlePageErrorMiddleware.cs | 3 +- .../UserRequiredRedirectMiddleware.cs | 48 +++++ .../CompleteEmailVerificationPage.cshtml.cs | 7 +- .../ExternalAuth/AuthenticationPage.cshtml | 6 +- .../Pages/LandingPage.cshtml | 5 +- .../Pages/Layouts/BaseLayout.cshtml.cs | 17 +- .../Pages/Moderation/BannedUsersPage.cshtml | 3 + .../Pages/Moderation/CasePage.cshtml | 5 +- .../Pages/Moderation/NewCasePage.cshtml | 1 - .../Pages/Moderation/ReportPage.cshtml | 4 +- .../Pages/Moderation/ReportsPage.cshtml | 4 +- .../Pages/Partials/CommentsPartial.cshtml | 4 +- .../Partials/Links/UserLinkPartial.cshtml | 5 +- .../Partials/ModerationCasePartial.cshtml | 12 +- .../Pages/Partials/PhotoPartial.cshtml | 8 +- .../Pages/Partials/ReportPartial.cshtml | 9 +- .../Pages/Partials/SlotCardPartial.cshtml | 22 ++- .../Pages/Partials/UserCardPartial.cshtml | 5 +- .../Pages/PasswordResetRequiredPage.cshtml | 2 - .../Pages/PhotosPage.cshtml | 4 +- .../Pages/PirateSignupPage.cshtml | 2 +- .../Pages/PirateSignupPage.cshtml.cs | 4 +- .../Pages/RegisterForm.cshtml.cs | 4 +- .../Pages/SendVerificationEmailPage.cshtml | 11 +- .../Pages/SendVerificationEmailPage.cshtml.cs | 36 ++-- .../Pages/SetEmailForm.cshtml | 11 ++ .../Pages/SetEmailForm.cshtml.cs | 13 +- .../Pages/SlotPage.cshtml | 5 +- .../Pages/SlotSettingsPage.cshtml | 168 ++++++++++++++++++ .../Pages/SlotSettingsPage.cshtml.cs | 61 +++++++ .../Pages/UserPage.cshtml | 25 +-- .../Pages/UserSettingsPage.cshtml | 148 +++++++++++++++ .../Pages/UserSettingsPage.cshtml.cs | 86 +++++++++ .../Pages/UsersPage.cshtml | 4 +- .../Startup/WebsiteStartup.cs | 1 + ...rojectLighthouse.Tests.WebsiteTests.csproj | 2 +- ProjectLighthouse/Files/FileHelper.cs | 50 +++++- ProjectLighthouse/Helpers/LabelHelper.cs | 67 ++++++- .../Helpers/SanitizationHelper.cs | 3 +- ProjectLighthouse/Levels/LevelLabels.cs | 2 +- .../Middlewares/MiddlewareDBContext.cs | 21 +++ ...220910190711_AddUserLanguageAndTimezone.cs | 45 +++++ .../20220910190824_RemoveUserIsAPirate.cs | 33 ++++ ProjectLighthouse/PlayerData/Profiles/User.cs | 10 +- .../PlayerData/Profiles/UserStatus.cs | 9 +- ProjectLighthouse/ProjectLighthouse.csproj | 3 +- .../Migrations/DatabaseModelSnapshot.cs | 7 +- ProjectLighthouse/StaticFiles/css/styles.css | 25 ++- 53 files changed, 973 insertions(+), 118 deletions(-) create mode 100644 ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs create mode 100644 ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml create mode 100644 ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs create mode 100644 ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml create mode 100644 ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs create mode 100644 ProjectLighthouse/Middlewares/MiddlewareDBContext.cs create mode 100644 ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs create mode 100644 ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs diff --git a/ProjectLighthouse.Localization/LocalizationManager.cs b/ProjectLighthouse.Localization/LocalizationManager.cs index a29fdf16..c3ef79c0 100644 --- a/ProjectLighthouse.Localization/LocalizationManager.cs +++ b/ProjectLighthouse.Localization/LocalizationManager.cs @@ -124,7 +124,7 @@ public static class LocalizationManager .Where(r => r != "resources") .ToList(); - languages.Add(DefaultLang); + languages.Insert(0, DefaultLang); return languages; } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs index b41adee2..bd7c0019 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs @@ -67,8 +67,7 @@ public class LoginController : ControllerBase token = await this.database.AuthenticateUser(npTicket, ipAddress); if (token == null) { - Logger.Warn($"Unable to " + - $"find/generate a token for username {npTicket.Username}", LogArea.Login); + Logger.Warn($"Unable to find/generate a token for username {npTicket.Username}", LogArea.Login); return this.StatusCode(403, ""); // If not, then 403. } } @@ -81,6 +80,12 @@ public class LoginController : ControllerBase return this.StatusCode(403, ""); } + if (ServerConfiguration.Instance.Mail.MailEnabled && (user.EmailAddress == null || !user.EmailAddressVerified)) + { + Logger.Error($"Email address unverified for user {user.Username}", LogArea.Login); + return this.StatusCode(403, ""); + } + if (ServerConfiguration.Instance.Authentication.UseExternalAuth) { if (user.ApprovedIPAddress == ipAddress) diff --git a/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs b/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs index e67e7bf3..f7cf2ec6 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Admin/ModerationSlotController.cs @@ -1,8 +1,6 @@ #nullable enable -using LBPUnion.ProjectLighthouse.Administration; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -30,7 +28,7 @@ public class ModerationSlotController : ControllerBase slot.TeamPick = true; await this.database.SaveChangesAsync(); - return this.Ok(); + return this.Redirect("~/slot/" + id); } [HttpGet("removeTeamPick")] @@ -44,7 +42,7 @@ public class ModerationSlotController : ControllerBase slot.TeamPick = false; await this.database.SaveChangesAsync(); - return this.Ok(); + return this.Redirect("~/slot/" + id); } [HttpGet("delete")] @@ -58,6 +56,6 @@ public class ModerationSlotController : ControllerBase await this.database.RemoveSlot(slot); - return this.Ok(); + return this.Redirect("~/slots/0"); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs b/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs index a2843e46..a57b5d35 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs @@ -25,6 +25,27 @@ public class SlotPageController : ControllerBase this.database = database; } + [HttpGet("unpublish")] + public async Task UnpublishSlot([FromRoute] int id) + { + WebToken? token = this.database.WebTokenFromRequest(this.Request); + if (token == null) return this.Redirect("~/login"); + + Slot? targetSlot = await this.database.Slots.Include(s => s.Location).FirstOrDefaultAsync(s => s.SlotId == id); + if (targetSlot == null) return this.Redirect("~/slots/0"); + + if (targetSlot.Location == null) throw new ArgumentNullException(); + + if (targetSlot.CreatorId != token.UserId) return this.Redirect("~/slot/" + id); + + this.database.Locations.Remove(targetSlot.Location); + this.database.Slots.Remove(targetSlot); + + await this.database.SaveChangesAsync(); + + return this.Redirect("~/slots/0"); + } + [HttpGet("rateComment")] public async Task RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating) { diff --git a/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs b/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs index 78306838..6b6ca5ac 100644 --- a/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs +++ b/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs @@ -1,4 +1,3 @@ -using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using Microsoft.AspNetCore.Html; @@ -9,15 +8,19 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Extensions; public static class PartialExtensions { - // ReSharper disable once SuggestBaseTypeForParameter - public static ViewDataDictionary WithLang(this ViewDataDictionary viewData, string language) + + public static ViewDataDictionary WithLang(this ViewDataDictionary viewData, string language) => WithKeyValue(viewData, "Language", language); + + public static ViewDataDictionary WithTime(this ViewDataDictionary viewData, string timeZone) => WithKeyValue(viewData, "TimeZone", timeZone); + + private static ViewDataDictionary WithKeyValue(this ViewDataDictionary viewData, string key, object value) { try { - return new(viewData) + return new ViewDataDictionary(viewData) { { - "Language", language + key, value }, }; } @@ -27,9 +30,9 @@ public static class PartialExtensions } } - public static Task ToLink(this User user, IHtmlHelper helper, ViewDataDictionary viewData, string language) - => helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language)); + public static Task ToLink(this User user, IHtmlHelper helper, ViewDataDictionary viewData, string language, string timeZone = "", bool includeStatus = false) + => helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language).WithTime(timeZone).WithKeyValue("IncludeStatus", includeStatus)); - public static Task ToHtml(this Photo photo, IHtmlHelper helper, ViewDataDictionary viewData, string language) - => helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language)); + public static Task ToHtml(this Photo photo, IHtmlHelper helper, ViewDataDictionary viewData, string language, string timeZone) + => helper.PartialAsync("Partials/PhotoPartial", photo, viewData.WithLang(language).WithTime(timeZone)); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs b/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs index 61405d7a..6e8177bc 100644 --- a/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs +++ b/ProjectLighthouse.Servers.Website/Middlewares/HandlePageErrorMiddleware.cs @@ -10,7 +10,6 @@ public class HandlePageErrorMiddleware : Middleware public override async Task InvokeAsync(HttpContext ctx) { await this.next(ctx); -// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract if (ctx.Response.StatusCode == 404 && !ctx.Request.Path.StartsWithSegments("/gameAssets")) { try @@ -20,7 +19,7 @@ public class HandlePageErrorMiddleware : Middleware finally { // not much we can do to save us, carry on anyways - await next(ctx); + await this.next(ctx); } } } diff --git a/ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs b/ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs new file mode 100644 index 00000000..db7cd058 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Middlewares/UserRequiredRedirectMiddleware.cs @@ -0,0 +1,48 @@ +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; + } + + if (user.PasswordResetRequired && !ctx.Request.Path.StartsWithSegments("/passwordResetRequired") && + !ctx.Request.Path.StartsWithSegments("/passwordReset")) + { + ctx.Response.Redirect("/passwordResetRequired"); + return; + } + + if (ServerConfiguration.Instance.Mail.MailEnabled) + { + // The normal flow is for users to set their email during login so just force them to log out + if (user.EmailAddress == null) + { + ctx.Response.Redirect("/logout"); + return; + } + + if (!user.EmailAddressVerified && + !ctx.Request.Path.StartsWithSegments("/login/sendVerificationEmail") && + !ctx.Request.Path.StartsWithSegments("/verifyEmail")) + { + ctx.Response.Redirect("/login/sendVerificationEmail"); + return; + } + } + + await this.next(ctx); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs index 5f7e8752..0da6a3e2 100644 --- a/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/CompleteEmailVerificationPage.cshtml.cs @@ -3,7 +3,6 @@ using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -30,6 +29,12 @@ public class CompleteEmailVerificationPage : BaseLayout return this.Page(); } + if (DateTime.Now > emailVerifyToken.ExpiresAt) + { + this.Error = "This token has expired"; + return this.Page(); + } + if (emailVerifyToken.UserId != user.UserId) { this.Error = "This token doesn't belong to you!"; diff --git a/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml index 083792b4..ab33c246 100644 --- a/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/ExternalAuth/AuthenticationPage.cshtml @@ -5,6 +5,8 @@ @{ Layout = "Layouts/BaseLayout"; Model.Title = "Authentication"; + string timeZone = Model.GetTimeZone(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); } @if (Model.AuthenticationAttempts.Count == 0) @@ -41,9 +43,9 @@ else @foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts) { - DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp).ToLocalTime(); + DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp), timeZoneInfo);
-

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

+

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

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

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

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

There are @Model.ReportCount total reports.

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

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

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

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

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

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

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

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

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

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

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

There are @Model.PhotoCount total photos!

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

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

Just click this 'ere button, and welcome aboard!

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

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

-

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

+@if (Model.Success) +{ +

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

+

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

+} +else +{ +

Failed to send email, please try again later

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

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

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

@Model.Error

+
+} +
@Html.AntiForgeryToken() diff --git a/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs index 01ca1262..d16eb5a9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SetEmailForm.cshtml.cs @@ -1,12 +1,13 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Localization.StringLists; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -19,6 +20,8 @@ public class SetEmailForm : BaseLayout public EmailSetToken? EmailToken; + public string? Error { get; private set; } + public async Task OnGet(string? token = null) { if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); @@ -32,6 +35,7 @@ public class SetEmailForm : BaseLayout return this.Page(); } + [SuppressMessage("ReSharper", "SpecifyStringComparison")] public async Task OnPost(string emailAddress, string token) { if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound(); @@ -39,6 +43,13 @@ public class SetEmailForm : BaseLayout EmailSetToken? emailToken = await this.Database.EmailSetTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.EmailToken == token); if (emailToken == null) return this.Redirect("/login"); + if (await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower())) + { + this.Error = this.Translate(ErrorStrings.EmailTaken); + this.EmailToken = emailToken; + return this.Page(); + } + emailToken.User.EmailAddress = emailAddress; this.Database.EmailSetTokens.Remove(emailToken); diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml index e25df416..83e3dd77 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml @@ -19,6 +19,7 @@ bool isMobile = this.Request.IsMobile(); string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); } @if (Model.Slot!.Hidden) @@ -96,7 +97,7 @@
}
- @await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language)) + @await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone))
@if (isMobile) { @@ -216,7 +217,7 @@ @foreach (Photo photo in Model.Photos) {
- @await photo.ToHtml(Html, ViewData, language) + @await photo.ToHtml(Html, ViewData, language, timeZone)
}
diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml new file mode 100644 index 00000000..08fec3e4 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml @@ -0,0 +1,168 @@ +@page "/slot/{slotId:int}/settings" +@using System.Web +@using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Extensions +@using LBPUnion.ProjectLighthouse.Helpers +@using LBPUnion.ProjectLighthouse.Levels +@using LBPUnion.ProjectLighthouse.Localization.StringLists +@using LBPUnion.ProjectLighthouse.PlayerData +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotSettingsPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.ShowTitleInPage = false; + + Model.Title = HttpUtility.HtmlDecode(Model.Slot?.Name ?? ""); + + bool isMobile = Request.IsMobile(); + + int size = isMobile ? 100 : 200; +} + + + + + + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs new file mode 100644 index 00000000..744d0389 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml.cs @@ -0,0 +1,61 @@ +#nullable enable +using LBPUnion.ProjectLighthouse.Files; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Levels; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; + +public class SlotSettingsPage : BaseLayout +{ + + public Slot? Slot; + public SlotSettingsPage(Database database) : base(database) + {} + + public async Task OnPost([FromRoute] int slotId, [FromForm] string? avatar, [FromForm] string? name, [FromForm] string? description, string? labels) + { + this.Slot = await this.Database.Slots.FirstOrDefaultAsync(u => u.SlotId == slotId); + if (this.Slot == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/slot/" + slotId); + + if (!this.User.IsModerator && this.User != this.Slot.Creator) return this.Redirect("~/slot/" + slotId); + + string? avatarHash = await FileHelper.ParseBase64Image(avatar); + + if (avatarHash != null) this.Slot.IconHash = avatarHash; + + name = SanitizationHelper.SanitizeString(name); + if (this.Slot.Name != name) this.Slot.Name = name; + + description = SanitizationHelper.SanitizeString(description); + if (this.Slot.Description != description) this.Slot.Description = description; + + labels = LabelHelper.RemoveInvalidLabels(SanitizationHelper.SanitizeString(labels)); + if (this.Slot.AuthorLabels != labels) this.Slot.AuthorLabels = labels; + + // ReSharper disable once InvertIf + if (this.Database.ChangeTracker.HasChanges()) + { + this.Slot.LastUpdated = TimeHelper.TimestampMillis; + await this.Database.SaveChangesAsync(); + } + + return this.Redirect("~/slot/" + slotId); + } + + public async Task OnGet([FromRoute] int slotId) + { + this.Slot = await this.Database.Slots.FirstOrDefaultAsync(s => s.SlotId == slotId); + if (this.Slot == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/slot/" + slotId); + + if(!this.User.IsModerator && this.User.UserId != this.Slot.CreatorId) return this.Redirect("~/slot/" + slotId); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml index f2085172..7612b8bd 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml @@ -5,7 +5,6 @@ @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions -@using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage @{ @@ -15,8 +14,9 @@ Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username); Model.Description = Model.ProfileUser!.Biography; - bool isMobile = this.Request.IsMobile(); + bool isMobile = Request.IsMobile(); string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); } @if (Model.ProfileUser.IsBanned) @@ -50,7 +50,10 @@ }, { "Language", Model.GetLanguage() - } + }, + { + "TimeZone", Model.GetTimeZone() + }, })
@@ -140,7 +145,7 @@ } } -@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language)) +@await Html.PartialAsync("Partials/CommentsPartial", ViewData.WithLang(language).WithTime(timeZone)) @if (Model.User != null && Model.User.IsModerator) { diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml new file mode 100644 index 00000000..a81f2d37 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml @@ -0,0 +1,148 @@ +@page "/user/{userId:int}/settings" +@using System.Globalization +@using System.Web +@using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Extensions +@using LBPUnion.ProjectLighthouse.Localization +@using LBPUnion.ProjectLighthouse.Localization.StringLists +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserSettingsPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.ShowTitleInPage = false; + + Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username); + + bool isMobile = Request.IsMobile(); + + int size = isMobile ? 100 : 200; +} + + + + + + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs new file mode 100644 index 00000000..16ab05fe --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs @@ -0,0 +1,86 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Files; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Localization; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; + +public class UserSettingsPage : BaseLayout +{ + + public User? ProfileUser; + public UserSettingsPage(Database database) : base(database) + {} + + private static bool IsValidEmail(string? email) => !string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email); + + [SuppressMessage("ReSharper", "SpecifyStringComparison")] + public async Task OnPost([FromRoute] int userId, [FromForm] string? avatar, [FromForm] string? username, [FromForm] string? email, [FromForm] string? biography, [FromForm] string? timeZone, [FromForm] string? language) + { + this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); + if (this.ProfileUser == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/user/" + userId); + + if (!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId); + + string? avatarHash = await FileHelper.ParseBase64Image(avatar); + + if (avatarHash != null) this.ProfileUser.IconHash = avatarHash; + + biography = SanitizationHelper.SanitizeString(biography); + + if (this.ProfileUser.Biography != biography) this.ProfileUser.Biography = biography; + + if (ServerConfiguration.Instance.Mail.MailEnabled && IsValidEmail(email) && (this.User == this.ProfileUser || this.User.IsAdmin)) + { + // if email hasn't already been used + if (!await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email!.ToLower())) + { + if (this.ProfileUser.EmailAddress != email) + { + this.ProfileUser.EmailAddress = email; + this.ProfileUser.EmailAddressVerified = false; + } + } + } + + if (this.ProfileUser == this.User) + { + if (!string.IsNullOrWhiteSpace(language) && this.ProfileUser.Language != language) + { + if (LocalizationManager.GetAvailableLanguages().Contains(language)) + this.ProfileUser.Language = language; + } + + if (!string.IsNullOrWhiteSpace(timeZone) && this.ProfileUser.TimeZone != timeZone) + { + HashSet timeZoneIds = TimeZoneInfo.GetSystemTimeZones().Select(t => t.Id).ToHashSet(); + if (timeZoneIds.Contains(timeZone)) this.ProfileUser.TimeZone = timeZone; + } + } + + + await this.Database.SaveChangesAsync(); + return this.Redirect("~/user/" + userId); + } + + public async Task OnGet([FromRoute] int userId) + { + this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); + if (this.ProfileUser == null) return this.NotFound(); + + if (this.User == null) return this.Redirect("~/user/" + userId); + + if(!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml index e2f083e9..97c0940f 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UsersPage.cshtml @@ -2,7 +2,6 @@ @using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.PlayerData.Profiles -@using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UsersPage @{ @@ -35,6 +34,9 @@ { "Language", Model.GetLanguage() }, + { + "TimeZone", Model.GetTimeZone() + }, })
} diff --git a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs index 6e757fa7..159b4abb 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -80,6 +80,7 @@ public class WebsiteStartup app.UseMiddleware(); app.UseMiddleware(); + app.UseMiddleware(); app.UseRouting(); diff --git a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj index 9495735a..03535d62 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj +++ b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj @@ -16,7 +16,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ProjectLighthouse/Files/FileHelper.cs b/ProjectLighthouse/Files/FileHelper.cs index a922b25c..de4c0b98 100644 --- a/ProjectLighthouse/Files/FileHelper.cs +++ b/ProjectLighthouse/Files/FileHelper.cs @@ -6,14 +6,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using DDSReader; using ICSharpCode.SharpZipLib.Zip.Compression; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; +using Pfim; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -240,6 +241,34 @@ public static class FileHelper if (!Directory.Exists(path)) Directory.CreateDirectory(path ?? throw new ArgumentNullException(nameof(path))); } + private static readonly Regex base64Regex = new(@"data:([^\/]+)\/([^;]+);base64,(.*)", RegexOptions.Compiled); + + public static async Task ParseBase64Image(string? image) + { + if (string.IsNullOrWhiteSpace(image)) return null; + + System.Text.RegularExpressions.Match match = base64Regex.Match(image); + + if (!match.Success) return null; + + if (match.Groups.Count != 4) return null; + + byte[] data = Convert.FromBase64String(match.Groups[3].Value); + + LbpFile file = new(data); + + if (file.FileType is not (LbpFileType.Jpeg or LbpFileType.Png)) return null; + + if (ResourceExists(file.Hash)) return file.Hash; + + string assetsDirectory = ResourcePath; + string path = GetResourcePath(file.Hash); + + EnsureDirectoryCreated(assetsDirectory); + await File.WriteAllBytesAsync(path, file.Data); + return file.Hash; + } + public static string[] ResourcesNotUploaded(params string[] hashes) => hashes.Where(hash => !ResourceExists(hash)).ToArray(); public static void ConvertAllTexturesToPng() @@ -284,7 +313,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 +371,7 @@ public static class FileHelper if (compressed[i] == decompressed[i]) { writer.Write(deflatedData); + continue; } Inflater inflater = new(); @@ -357,19 +387,25 @@ public static class FileHelper private static bool DDSToPNG(string hash, byte[] data) { - using MemoryStream stream = new(); - DDSImage image = new(data); + Dds ddsImage = Dds.Create(data, new PfimConfig()); + if(ddsImage.Compressed) + ddsImage.Decompress(); - image.SaveAsPng(stream); + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + Image image = ddsImage.Format switch + { + ImageFormat.Rgba32 => Image.LoadPixelData(ddsImage.Data, ddsImage.Width, ddsImage.Height), + _ => throw new ArgumentOutOfRangeException($"ddsImage.Format is not supported: {ddsImage.Format}") + }; Directory.CreateDirectory("png"); - File.WriteAllBytes($"png/{hash}.png", stream.ToArray()); + image.SaveAsPngAsync($"png/{hash}.png"); return true; } private static bool JPGToPNG(string hash, byte[] data) { - using Image image = Image.Load(data); + using Image image = Image.Load(data); using MemoryStream ms = new(); image.SaveAsPng(ms); diff --git a/ProjectLighthouse/Helpers/LabelHelper.cs b/ProjectLighthouse/Helpers/LabelHelper.cs index 5bbb7d13..b88dddc9 100644 --- a/ProjectLighthouse/Helpers/LabelHelper.cs +++ b/ProjectLighthouse/Helpers/LabelHelper.cs @@ -1,32 +1,84 @@ 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 lbp3Labels = new() + { + "LABEL_SINGLE_PLAYER", + "LABEL_RPG", + "LABEL_TOP_DOWN", + "LABEL_CO_OP", + "LABEL_1st_Person", + "LABEL_3rd_Person", + "LABEL_Sci_Fi", + "LABEL_Social", + "LABEL_Arcade_Game", + "LABEL_Board_Game", + "LABEL_Card_Game", + "LABEL_Mini_Game", + "LABEL_Party_Game", + "LABEL_Defence", + "LABEL_Driving", + "LABEL_Hangout", + "LABEL_Hide_And_Seek", + "LABEL_Prop_Hunt", + "LABEL_Music_Gallery", + "LABEL_Costume_Gallery", + "LABEL_Sticker_Gallery", + "LABEL_Movie", + "LABEL_Pinball", + "LABEL_Technology", + "LABEL_Homage", + "LABEL_8_Bit", + "LABEL_16_Bit", + "LABEL_Seasonal", + "LABEL_Time_Trial", + "LABEL_INTERACTIVE_STREAM", + "LABEL_QUESTS", + "LABEL_SACKPOCKET", + "LABEL_SPRINGINATOR", + "LABEL_HOVERBOARD_NAME", + "LABEL_FLOATY_FLUID_NAME", + "LABEL_ODDSOCK", + "LABEL_TOGGLE", + "LABEL_SWOOP", + "LABEL_SACKBOY", + "LABEL_CREATED_CHARACTERS", + }; + private static readonly Dictionary translationTable = new() { + {"Label_SinglePlayer", "Single Player"}, + {"LABEL_Quick", "Short"}, + {"LABEL_Competitive", "Versus"}, + {"LABEL_Puzzle", "Puzzler"}, + {"LABEL_Platform", "Platformer"}, + {"LABEL_Race", "Racer"}, + {"LABEL_SurvivalChallenge", "Survival Challenge"}, {"LABEL_DirectControl", "Controllinator"}, {"LABEL_GrapplingHook", "Grappling Hook"}, {"LABEL_JumpPads", "Bounce Pads"}, {"LABEL_MagicBag", "Creatinator"}, {"LABEL_LowGravity", "Low Gravity"}, - {"LABEL_PowerGlove", "Grabinator"}, + {"LABEL_PowerGlove", "Grabinators"}, {"LABEL_ATTRACT_GEL", "Attract-o-Gel"}, {"LABEL_ATTRACT_TWEAK", "Attract-o-Tweaker"}, {"LABEL_HEROCAPE", "Hero Cape"}, {"LABEL_MEMORISER", "Memorizer"}, {"LABEL_WALLJUMP", "Wall Jump"}, {"LABEL_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,6 +92,17 @@ 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), + GameVersion.LittleBigPlanet3 => IsValidLabel(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); diff --git a/ProjectLighthouse/Helpers/SanitizationHelper.cs b/ProjectLighthouse/Helpers/SanitizationHelper.cs index 06882f02..3df81756 100644 --- a/ProjectLighthouse/Helpers/SanitizationHelper.cs +++ b/ProjectLighthouse/Helpers/SanitizationHelper.cs @@ -30,8 +30,9 @@ public static class SanitizationHelper } } - public static string SanitizeString(string input) + public static string SanitizeString(string? input) { + if (input == null) return ""; foreach ((string? key, string? value) in charsToReplace) { diff --git a/ProjectLighthouse/Levels/LevelLabels.cs b/ProjectLighthouse/Levels/LevelLabels.cs index 8d19d8f7..3fd15517 100644 --- a/ProjectLighthouse/Levels/LevelLabels.cs +++ b/ProjectLighthouse/Levels/LevelLabels.cs @@ -31,6 +31,7 @@ public enum LevelLabels LABEL_Strategy, LABEL_SurvivalChallenge, LABEL_Tutorial, + LABEL_Retro, LABEL_Collectables, LABEL_DirectControl, LABEL_Explosives, @@ -52,7 +53,6 @@ public enum LevelLabels LABEL_HEROCAPE, LABEL_MEMORISER, LABEL_WALLJUMP, - LABEL_Retro, LABEL_SINGLE_PLAYER, LABEL_RPG, LABEL_TOP_DOWN, diff --git a/ProjectLighthouse/Middlewares/MiddlewareDBContext.cs b/ProjectLighthouse/Middlewares/MiddlewareDBContext.cs new file mode 100644 index 00000000..77de227c --- /dev/null +++ b/ProjectLighthouse/Middlewares/MiddlewareDBContext.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; + +namespace LBPUnion.ProjectLighthouse.Middlewares; + +public abstract class MiddlewareDBContext +{ + // this makes it consistent with typical middleware usage + [SuppressMessage("ReSharper", "InconsistentNaming")] + protected RequestDelegate next { get; } + + protected MiddlewareDBContext(RequestDelegate next) + { + this.next = next; + } + + [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] + public abstract Task InvokeAsync(HttpContext ctx, Database db); +} \ No newline at end of file diff --git a/ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs b/ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs new file mode 100644 index 00000000..a574128a --- /dev/null +++ b/ProjectLighthouse/Migrations/20220910190711_AddUserLanguageAndTimezone.cs @@ -0,0 +1,45 @@ +using System; +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + + [DbContext(typeof(Database))] + [Migration("20220910190711_AddUserLanguageAndTimezone")] + public partial class AddUserLanguageAndTimezone : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Language", + table: "Users", + type: "longtext", + defaultValue: "en", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "TimeZone", + table: "Users", + type: "longtext", + defaultValue: TimeZoneInfo.Local.Id, + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Language", + table: "Users"); + + migrationBuilder.DropColumn( + name: "TimeZone", + table: "Users"); + } + } +} diff --git a/ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs b/ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs new file mode 100644 index 00000000..11f81031 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220910190824_RemoveUserIsAPirate.cs @@ -0,0 +1,33 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + + [DbContext(typeof(Database))] + [Migration("20220910190824_RemoveUserIsAPirate")] + public partial class RemoveUserIsAPirate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("UPDATE Users SET Language = \"en-PT\" WHERE isAPirate"); + + migrationBuilder.DropColumn( + name: "IsAPirate", + table: "Users"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAPirate", + table: "Users", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + } +} diff --git a/ProjectLighthouse/PlayerData/Profiles/User.cs b/ProjectLighthouse/PlayerData/Profiles/User.cs index cfaaa6c3..73d9d3bb 100644 --- a/ProjectLighthouse/PlayerData/Profiles/User.cs +++ b/ProjectLighthouse/PlayerData/Profiles/User.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; @@ -179,11 +180,10 @@ public class User [JsonIgnore] public string? ApprovedIPAddress { get; set; } #nullable disable - - /// - /// ARRR! Forces the user to see Pirate English translations on the website. - /// - public bool IsAPirate { get; set; } + + public string Language { get; set; } = "en"; + + public string TimeZone { get; set; } = TimeZoneInfo.Local.Id; public PrivacyType LevelVisibility { get; set; } = PrivacyType.All; diff --git a/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs b/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs index cfac696d..af718db3 100644 --- a/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs +++ b/ProjectLighthouse/PlayerData/Profiles/UserStatus.cs @@ -47,7 +47,7 @@ public class UserStatus this.CurrentRoom = RoomHelper.FindRoomByUserId(userId); } - private string FormatOfflineTimestamp(string language) + private string FormatOfflineTimestamp(string language, string timeZone) { if (this.LastLogout <= 0 && this.LastLogin <= 0) { @@ -56,11 +56,12 @@ public class UserStatus long timestamp = this.LastLogout; if (timestamp <= 0) timestamp = this.LastLogin; - string formattedTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToLocalTime().ToString("M/d/yyyy h:mm:ss tt"); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); + string formattedTime = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(timestamp), timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt"); return StatusStrings.LastOnline.Translate(language, formattedTime); } - public string ToTranslatedString(string language) + public string ToTranslatedString(string language, string timeZone) { this.CurrentVersion ??= GameVersion.Unknown; this.CurrentPlatform ??= Platform.Unknown; @@ -69,7 +70,7 @@ public class UserStatus { StatusType.Online => StatusStrings.CurrentlyOnline.Translate(language, ((GameVersion)this.CurrentVersion).ToPrettyString(), (Platform)this.CurrentPlatform), - StatusType.Offline => this.FormatOfflineTimestamp(language), + StatusType.Offline => this.FormatOfflineTimestamp(language, timeZone), _ => GeneralStrings.Unknown.Translate(language), }; } diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 01203ae1..1854fc34 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -10,7 +10,8 @@ - + + diff --git a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 1071afdc..1ce63adf 100644 --- a/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -730,8 +730,8 @@ namespace ProjectLighthouse.Migrations b.Property("IconHash") .HasColumnType("longtext"); - b.Property("IsAPirate") - .HasColumnType("tinyint(1)"); + b.Property("Language") + .HasColumnType("longtext"); b.Property("LastLogin") .HasColumnType("bigint"); @@ -772,6 +772,9 @@ namespace ProjectLighthouse.Migrations b.Property("ProfileVisibility") .HasColumnType("int"); + b.Property("TimeZone") + .HasColumnType("longtext"); + b.Property("Username") .IsRequired() .HasColumnType("longtext"); diff --git a/ProjectLighthouse/StaticFiles/css/styles.css b/ProjectLighthouse/StaticFiles/css/styles.css index c50eeac6..f9f7f285 100644 --- a/ProjectLighthouse/StaticFiles/css/styles.css +++ b/ProjectLighthouse/StaticFiles/css/styles.css @@ -159,4 +159,27 @@ div.cardStatsUnderTitle > span { margin-right: 0.5em; } -/*#endregion Comments*/ \ No newline at end of file +/*#endregion Comments*/ + +/*#region Slot labels */ + +.selected { + color: #fff !important; + background-color: #0e91f5 !important; +} + +.selected:hover { + background-color: #0084ea !important; +} + +.skew { + --scale: scale(1, 1); + --skew: rotate(0deg); + transition: transform 0.25s !important; + transform: var(--skew) var(--scale); +} +.skew:hover { + --scale: scale(1.2, 1.2); +} + +/*#endregion Slot labels */ \ No newline at end of file