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:
Josh 2022-09-17 14:02:46 -05:00 committed by GitHub
commit f6a7fe6283
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 973 additions and 118 deletions

View file

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

View file

@ -25,6 +25,27 @@ public class SlotPageController : ControllerBase
this.database = database;
}
[HttpGet("unpublish")]
public async Task<IActionResult> UnpublishSlot([FromRoute] int id)
{
WebToken? token = this.database.WebTokenFromRequest(this.Request);
if (token == null) return this.Redirect("~/login");
Slot? targetSlot = await this.database.Slots.Include(s => s.Location).FirstOrDefaultAsync(s => s.SlotId == id);
if (targetSlot == null) return this.Redirect("~/slots/0");
if (targetSlot.Location == null) throw new ArgumentNullException();
if (targetSlot.CreatorId != token.UserId) return this.Redirect("~/slot/" + id);
this.database.Locations.Remove(targetSlot.Location);
this.database.Slots.Remove(targetSlot);
await this.database.SaveChangesAsync();
return this.Redirect("~/slots/0");
}
[HttpGet("rateComment")]
public async Task<IActionResult> RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating)
{

View file

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

View file

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

View file

@ -0,0 +1,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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,12 @@
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
bool includeStatus = (bool?)ViewData["IncludeStatus"] ?? false;
string userStatus = includeStatus ? Model.Status.ToTranslatedString(language, timeZone) : "";
}
<a href="/user/@Model.UserId" title="@Model.Status.ToTranslatedString(language)" class="user-link">
<a href="/user/@Model.UserId" title="@userStatus" class="user-link">
<img src="/gameAssets/@Model.WebsiteAvatarHash" alt=""/>
@Model.Username
</a>

View file

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

View file

@ -8,6 +8,8 @@
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
<div style="position: relative">
@ -26,7 +28,7 @@
{
<span>by</span>
<b>
@await Model.Creator.ToLink(Html, ViewData, language)
@await Model.Creator.ToLink(Html, ViewData, language, timeZone)
</b>
}
@if (Model.Slot != null)
@ -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>

View file

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

View file

@ -18,6 +18,8 @@
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool mini = (bool?)ViewData["IsMini"] ?? false;
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
bool isQueued = false;
bool isHearted = false;
@ -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)
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
@page "/login/setEmail"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm
@{
@ -9,6 +10,16 @@
<p>This instance requires email verification. As your account was created before this was a requirement, you must now set an email for your account before continuing.</p>
@if (!string.IsNullOrWhiteSpace(Model.Error))
{
<div class="ui negative message">
<div class="header">
@Model.Translate(GeneralStrings.Error)
</div>
<p style="white-space: pre-line">@Model.Error</p>
</div>
}
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()

View file

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

View file

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

View file

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

View file

@ -0,0 +1,61 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class SlotSettingsPage : BaseLayout
{
public Slot? Slot;
public SlotSettingsPage(Database database) : base(database)
{}
public async Task<IActionResult> OnPost([FromRoute] int slotId, [FromForm] string? avatar, [FromForm] string? name, [FromForm] string? description, string? labels)
{
this.Slot = await this.Database.Slots.FirstOrDefaultAsync(u => u.SlotId == slotId);
if (this.Slot == null) return this.NotFound();
if (this.User == null) return this.Redirect("~/slot/" + slotId);
if (!this.User.IsModerator && this.User != this.Slot.Creator) return this.Redirect("~/slot/" + slotId);
string? avatarHash = await FileHelper.ParseBase64Image(avatar);
if (avatarHash != null) this.Slot.IconHash = avatarHash;
name = SanitizationHelper.SanitizeString(name);
if (this.Slot.Name != name) 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();
}
}

View file

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

View file

@ -0,0 +1,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>

View file

@ -0,0 +1,86 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Localization;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class UserSettingsPage : BaseLayout
{
public User? ProfileUser;
public UserSettingsPage(Database database) : base(database)
{}
private static bool IsValidEmail(string? email) => !string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email);
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
public async Task<IActionResult> OnPost([FromRoute] int userId, [FromForm] string? avatar, [FromForm] string? username, [FromForm] string? email, [FromForm] string? biography, [FromForm] string? timeZone, [FromForm] string? language)
{
this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId);
if (this.ProfileUser == null) return this.NotFound();
if (this.User == null) return this.Redirect("~/user/" + userId);
if (!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId);
string? avatarHash = await FileHelper.ParseBase64Image(avatar);
if (avatarHash != null) this.ProfileUser.IconHash = avatarHash;
biography = SanitizationHelper.SanitizeString(biography);
if (this.ProfileUser.Biography != biography) this.ProfileUser.Biography = biography;
if (ServerConfiguration.Instance.Mail.MailEnabled && IsValidEmail(email) && (this.User == this.ProfileUser || this.User.IsAdmin))
{
// if email hasn't already been used
if (!await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email!.ToLower()))
{
if (this.ProfileUser.EmailAddress != email)
{
this.ProfileUser.EmailAddress = email;
this.ProfileUser.EmailAddressVerified = false;
}
}
}
if (this.ProfileUser == this.User)
{
if (!string.IsNullOrWhiteSpace(language) && this.ProfileUser.Language != language)
{
if (LocalizationManager.GetAvailableLanguages().Contains(language))
this.ProfileUser.Language = language;
}
if (!string.IsNullOrWhiteSpace(timeZone) && this.ProfileUser.TimeZone != timeZone)
{
HashSet<string> timeZoneIds = TimeZoneInfo.GetSystemTimeZones().Select(t => t.Id).ToHashSet();
if (timeZoneIds.Contains(timeZone)) this.ProfileUser.TimeZone = timeZone;
}
}
await this.Database.SaveChangesAsync();
return this.Redirect("~/user/" + userId);
}
public async Task<IActionResult> OnGet([FromRoute] int userId)
{
this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId);
if (this.ProfileUser == null) return this.NotFound();
if (this.User == null) return this.Redirect("~/user/" + userId);
if(!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId);
return this.Page();
}
}

View file

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

View file

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