mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-04 01:48:21 +00:00
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
This commit is contained in:
parent
9073a8266f
commit
f6a7fe6283
53 changed files with 973 additions and 118 deletions
|
@ -124,7 +124,7 @@ public static class LocalizationManager
|
|||
.Where(r => r != "resources")
|
||||
.ToList();
|
||||
|
||||
languages.Add(DefaultLang);
|
||||
languages.Insert(0, DefaultLang);
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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!";
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 *@
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -26,6 +26,9 @@
|
|||
{
|
||||
"Language", Model.GetLanguage()
|
||||
},
|
||||
{
|
||||
"TimeZone", Model.GetTimeZone()
|
||||
},
|
||||
})
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
@page "/moderation/newCase"
|
||||
@using LBPUnion.ProjectLighthouse.Administration
|
||||
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation.NewCasePage
|
||||
|
||||
@{
|
||||
|
|
|
@ -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")
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
@ -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")
|
||||
</i>
|
||||
</p>
|
||||
|
||||
|
@ -62,7 +64,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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
<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="~/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 class="cardIcon slotCardIcon" src="/gameAssets/@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="cardStats">
|
||||
|
@ -90,12 +92,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)
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
@{
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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("/");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
this.Success = SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body);
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("failed to send email");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
168
ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml
Normal file
168
ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml
Normal 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>
|
|
@ -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) 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<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();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
|
148
ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml
Normal file
148
ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
<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;
|
||||
if (lang == "en-PT") langName = "Pirate Speak (The Seven Seas)";
|
||||
<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>
|
|
@ -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) 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();
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ public class WebsiteStartup
|
|||
|
||||
app.UseMiddleware<HandlePageErrorMiddleware>();
|
||||
app.UseMiddleware<RequestLogMiddleware>();
|
||||
app.UseMiddleware<UserRequiredRedirectMiddleware>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</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>
|
||||
|
|
|
@ -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<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 +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<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);
|
||||
|
||||
|
|
|
@ -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<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_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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
21
ProjectLighthouse/Middlewares/MiddlewareDBContext.cs
Normal file
21
ProjectLighthouse/Middlewares/MiddlewareDBContext.cs
Normal 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);
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
@ -180,10 +181,9 @@ public class User
|
|||
public string? ApprovedIPAddress { get; set; }
|
||||
#nullable disable
|
||||
|
||||
/// <summary>
|
||||
/// ARRR! Forces the user to see Pirate English translations on the website.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="DDSReader" Version="1.0.8-pre" />
|
||||
<PackageReference Include="Pfim" Version="0.11.1"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3"/>
|
||||
<PackageReference Include="Discord.Net.Webhook" Version="3.8.0" />
|
||||
<PackageReference Include="InfluxDB.Client" Version="4.5.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -160,3 +160,26 @@ div.cardStatsUnderTitle > span {
|
|||
}
|
||||
|
||||
/*#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 */
|
Loading…
Add table
Add a link
Reference in a new issue