Merge master into ..i'm not typing that.

This commit is contained in:
jvyden 2022-06-10 03:57:00 -04:00
commit b3b5354c68
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
334 changed files with 5045 additions and 2956 deletions

View file

@ -0,0 +1,20 @@
@page "/admin/user/{id:int}/ban"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminBanUserPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Ban " + Model.TargetedUser!.Username + "?";
}
<p>Are you sure you want to ban this user?</p>
<form method="post">
@Html.AntiForgeryToken()
<div class="ui left labeled input">
<label for="text" class="ui blue label">Reason: </label>
<input type="text" name="reason" id="text">
</div><br><br>
<input type="submit" value="Yes, ban @Model.TargetedUser.Username!" id="submit" class="ui red button"><br>
</form>

View file

@ -0,0 +1,48 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin;
public class AdminBanUserPage : BaseLayout
{
public User? TargetedUser;
public AdminBanUserPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet([FromRoute] int id)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (this.TargetedUser == null) return this.NotFound();
return this.Page();
}
public async Task<IActionResult> OnPost([FromRoute] int id, string reason)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (this.TargetedUser == null) return this.NotFound();
this.TargetedUser.Banned = true;
this.TargetedUser.BannedReason = reason;
// invalidate all currently active gametokens
this.Database.GameTokens.RemoveRange(this.Database.GameTokens.Where(t => t.UserId == this.TargetedUser.UserId));
// invalidate all currently active webtokens
this.Database.WebTokens.RemoveRange(this.Database.WebTokens.Where(t => t.UserId == this.TargetedUser.UserId));
await this.Database.SaveChangesAsync();
return this.Redirect($"/user/{this.TargetedUser.UserId}");
}
}

View file

@ -0,0 +1,89 @@
@page "/admin"
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Administration.Maintenance
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminPanelPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Admin Panel";
}
@if (Model.Log != null)
{
<div class="ui bottom attached message">
<h2>Command Output</h2>
@foreach (string line in Model.Log.Split("\n"))
{
<code>@line.TrimEnd()</code><br>
}
</div>
}
@if (!this.Request.IsMobile())
{
<div class="ui center aligned grid">
@foreach (AdminPanelStatistic statistic in Model.Statistics)
{
@await Html.PartialAsync("Partials/AdminPanelStatisticPartial", statistic)
}
</div>
<br>
}
else
{
@foreach (AdminPanelStatistic statistic in Model.Statistics)
{
@await Html.PartialAsync("Partials/AdminPanelStatisticPartial", statistic)
<br>
}
}
<h2>Commands</h2>
@foreach (ICommand command in MaintenanceHelper.Commands)
{
<div class="ui blue segment">
<h3>@command.Name()</h3>
<form>
<input type="text" name="command" style="display: none;" value="@command.FirstAlias">
@if (command.RequiredArgs() > 0)
{
<div class="ui left action input" style="width: 100%">
<button type="submit" class="ui green button">
<i class="play icon"></i>
Execute
</button>
<input type="text" name="args" placeholder="@command.Arguments()">
</div>
}
else
{
<button type="submit" class="ui green button">
<i class="play icon"></i>
Execute
</button>
}
</form>
</div>
}
<h2>Maintenance Jobs</h2>
<p>
<b>Warning: Interrupting Lighthouse during maintenance may leave the database in an unclean state.</b>
</p>
@foreach (IMaintenanceJob job in MaintenanceHelper.MaintenanceJobs)
{
<div class="ui red segment">
<h3>@job.Name()</h3>
<p>@job.Description()</p>
<form>
<input type="text" name="maintenanceJob" style="display: none;" value="@job.GetType().Name">
<button type="submit" class="ui green button">
<i class="play icon"></i>
Run Job
</button>
</form>
</div>
}

View file

@ -0,0 +1,58 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin;
public class AdminPanelPage : BaseLayout
{
public List<ICommand> Commands = MaintenanceHelper.Commands;
public AdminPanelPage(Database database) : base(database)
{}
public List<AdminPanelStatistic> Statistics = new();
public string? Log;
public async Task<IActionResult> OnGet([FromQuery] string? args, [FromQuery] string? command, [FromQuery] string? maintenanceJob, [FromQuery] string? log)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
if (!user.IsAdmin) return this.NotFound();
this.Statistics.Add(new AdminPanelStatistic("Users", await StatisticsHelper.UserCount(), "users"));
this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount()));
this.Statistics.Add(new AdminPanelStatistic("Photos", await StatisticsHelper.PhotoCount()));
this.Statistics.Add(new AdminPanelStatistic("Reports", await StatisticsHelper.ReportCount(), "reports/0"));
if (!string.IsNullOrEmpty(command))
{
args ??= "";
args = command + " " + args;
string[] split = args.Split(" ");
List<LogLine> runCommand = await MaintenanceHelper.RunCommand(split);
return this.Redirect($"~/admin?log={CryptoHelper.ToBase64(runCommand.ToLogString())}");
}
if (!string.IsNullOrEmpty(maintenanceJob))
{
await MaintenanceHelper.RunMaintenanceJob(maintenanceJob);
return this.Redirect("~/admin");
}
if (!string.IsNullOrEmpty(log))
{
this.Log = CryptoHelper.FromBase64(log);
}
return this.Page();
}
}

View file

@ -0,0 +1,41 @@
@page "/admin/users"
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminPanelUsersPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Users";
}
<p>There are currently @Model.UserCount users registered to your instance.</p>
<p><b>Note:</b> Users are ordered by most-recent-first.</p>
<div class="ui grid">
@foreach (User user in Model.Users)
{
string color = "blue";
string subtitle = "User";
if (user.IsAdmin)
{
color = "yellow";
subtitle = "Admin";
}
if (user.Banned)
{
color = "red";
subtitle = $"Banned user! Reason: {user.BannedReason}";
}
subtitle += $" (id: {user.UserId})";
<div class="eight wide column">
<div class="ui @color segment">
<h2>
<a href="/user/@user.UserId">@user.Username</a>
</h2>
<h3>@subtitle</h3>
</div>
</div>
}
</div>

View file

@ -0,0 +1,29 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin;
public class AdminPanelUsersPage : BaseLayout
{
public int UserCount;
public List<User> Users = new();
public AdminPanelUsersPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
if (!user.IsAdmin) return this.NotFound();
this.Users = await this.Database.Users.OrderByDescending(u => u.UserId).ToListAsync();
this.UserCount = this.Users.Count;
return this.Page();
}
}

View file

@ -0,0 +1,9 @@
@page "/admin/user/{id:int}/setGrantedSlots"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminSetGrantedSlotsPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Set granted slots for " + Model.TargetedUser!.Username;
}
@await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.TargetedUser)

View file

@ -0,0 +1,41 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin;
public class AdminSetGrantedSlotsPage : BaseLayout
{
public AdminSetGrantedSlotsPage(Database database) : base(database)
{}
public User? TargetedUser;
public async Task<IActionResult> OnGet([FromRoute] int id)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (this.TargetedUser == null) return this.NotFound();
return this.Page();
}
public async Task<IActionResult> OnPost([FromRoute] int id, int grantedSlotCount)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (this.TargetedUser == null) return this.NotFound();
this.TargetedUser.AdminGrantedSlots = grantedSlotCount;
await this.Database.SaveChangesAsync();
return this.Redirect($"/user/{this.TargetedUser.UserId}");
}
}

View file

@ -0,0 +1,30 @@
@page "/verifyEmail"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.CompleteEmailVerificationPage
@{
Layout = "Layouts/BaseLayout";
if (Model.Error == null)
{
Model.Title = "Email Address Verified";
}
else
{
Model.Title = "Couldn't verify email address";
}
}
@if (Model.Error != null)
{
<p>
<b>Reason:</b> @Model.Error
</p>
<p>Please try again. If the error persists, please contact the instance administrator.</p>
<a href="/login/sendVerificationEmail">
<div class="ui blue button">Resend email</div>
</a>
}
else
{
<p>Your email has been successfully verified. You may now close this tab.</p>
}

View file

@ -0,0 +1,47 @@
#nullable enable
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;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class CompleteEmailVerificationPage : BaseLayout
{
public CompleteEmailVerificationPage(Database database) : base(database)
{}
public string? Error;
public async Task<IActionResult> OnGet(string token)
{
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
EmailVerificationToken? emailVerifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(e => e.EmailToken == token);
if (emailVerifyToken == null)
{
this.Error = "Invalid verification token";
return this.Page();
}
if (emailVerifyToken.UserId != user.UserId)
{
this.Error = "This token doesn't belong to you!";
return this.Page();
}
this.Database.EmailVerificationTokens.Remove(emailVerifyToken);
user.EmailAddressVerified = true;
await this.Database.SaveChangesAsync();
return this.Page();
}
}

View file

@ -0,0 +1,22 @@
@page "/debug/filter"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.FilterTestPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Debug - Filter Test";
}
@if (Model.FilteredText != null)
{
<p>@Model.FilteredText</p>
}
<form>
<div class="ui right action input">
<input type="text" name="text" placeholder="Text to be filtered" value="@(Model.Text ?? "")">
<button type="submit" class="ui blue button">
<i class="pen icon"></i>
Filter
</button>
</div>
</form>

View file

@ -0,0 +1,27 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug;
public class FilterTestPage : BaseLayout
{
public FilterTestPage(Database database) : base(database)
{}
public string? FilteredText;
public string? Text;
public IActionResult OnGet(string? text = null)
{
#if !DEBUG
return this.NotFound();
#endif
if (text != null) this.FilteredText = CensorHelper.ScanMessage(text);
this.Text = text;
return this.Page();
}
}

View file

@ -0,0 +1,86 @@
@page "/debug/roomVisualizer"
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Match.Rooms
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.RoomVisualizerPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Debug - Room Visualizer";
const int refreshSeconds = 5;
}
<script>
let shouldRefresh = true;
setTimeout(() => {
if (shouldRefresh) window.location.reload();
}, @(refreshSeconds * 1000));
function stopRefreshing() {
shouldRefresh = false;
console.log("Stopped refresh");
const stopRefreshButton = document.getElementById("stop-refresh-button");
stopRefreshButton.parentElement.removeChild(stopRefreshButton);
console.log("Removed stop refresh button");
}
</script>
<p>This page will automatically refresh every @refreshSeconds seconds.</p>
@* workaround for users w/o js*@
<noscript>
<b>You will not be able to disable auto-refresh without JavaScript. Please enable JavaScript for this functionality.</b><br>
<meta http-equiv="refresh" content="@refreshSeconds">
</noscript>
<p>@RoomHelper.Rooms.Count() rooms</p>
<a href="/debug/roomVisualizer/createFakeRoom">
<div class="ui blue button">Create Fake Room</div>
</a>
<a href="/debug/roomVisualizer/deleteRooms">
<div class="ui red button">Nuke all rooms</div>
</a>
<button class="ui blue button" onclick="stopRefreshing()" id="stop-refresh-button">Stop refreshing</button>
<h2>Best rooms for each game version</h2>
@foreach (GameVersion version in Enum.GetValues<GameVersion>())
{
#nullable enable
if (version == GameVersion.LittleBigPlanet1 || version == GameVersion.LittleBigPlanetPSP || version == GameVersion.Unknown) continue;
FindBestRoomResponse? response = RoomHelper.FindBestRoom(null, version, null, null, null);
string text = response == null ? "No room found." : "Room " + response.RoomId;
<p><b>Best room for @version.ToPrettyString()</b>: @text</p>
}
<h2>Room display</h2>
@foreach (Room room in RoomHelper.Rooms)
{
bool userInRoom = room.PlayerIds.Contains(Model.User?.UserId ?? -1);
string color = userInRoom ? "green" : "blue";
<div class="ui @color inverted segment">
<h3>Room @room.RoomId</h3>
@if (userInRoom)
{
<p>
<b>You are currently in this room.</b>
</p>
}
<p>@room.PlayerIds.Count players, state is @room.State, version is @room.RoomVersion.ToPrettyString() on platform @room.RoomPlatform</p>
<p>Slot type: @room.Slot.SlotType, slot id: @room.Slot.SlotId</p>
@foreach (User player in room.GetPlayers(Model.Database))
{
<div class="ui segment">@player.Username</div>
}
</div>
}

View file

@ -0,0 +1,26 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
#if !DEBUG
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Types;
#endif
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug;
public class RoomVisualizerPage : BaseLayout
{
public RoomVisualizerPage(Database database) : base(database)
{}
public IActionResult OnGet()
{
#if !DEBUG
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
#endif
return this.Page();
}
}

View file

@ -0,0 +1,25 @@
@page "/debug/version"
@using LBPUnion.ProjectLighthouse.Helpers
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.VersionInfoPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Debug - Version Information";
Model.Description = VersionHelper.FullVersion;
}
<h2>Build specific information</h2>
<p><b>FullVersion</b>: @VersionHelper.FullVersion</p>
<br>
<p><b>Branch</b>: @VersionHelper.Branch</p>
<p><b>Build</b>: @VersionHelper.Build</p>
<p><b>CommitHash</b>: @VersionHelper.CommitHash</p>
<p><b>IsDirty</b>: @VersionHelper.IsDirty</p>
<p><b>CanCheckForUpdates</b>: @VersionHelper.CanCheckForUpdates</p>
<p><b>CommitsOutOfDate</b>: @VersionHelper.CommitsOutOfDate</p>
<h2>Remotes</h2>
@foreach (string remote in VersionHelper.Remotes)
{
<p>@remote</p>
}

View file

@ -0,0 +1,12 @@
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug;
public class VersionInfoPage : BaseLayout
{
public VersionInfoPage([NotNull] Database database) : base(database)
{}
public IActionResult OnGet() => this.Page();
}

View file

@ -0,0 +1,63 @@
@page "/authentication"
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth.AuthenticationPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Authentication";
}
@if (Model.AuthenticationAttempts.Count == 0)
{
<p>You have no pending authentication attempts.</p>
}
else
{
<p>You have @Model.AuthenticationAttempts.Count authentication attempts pending.</p>
@if (Model.IpAddress != null)
{
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. If this matches with an authentication attempt below, then it's safe to assume the authentication attempt came from the same network as this device.</p>
}
}
<a href="/authentication/autoApprovals">
<button class="ui small blue button">
<i class="cog icon"></i>
<span>Manage automatically approved IP addresses</span>
</button>
</a>
<a href="/authentication/denyAll">
<button class="ui small red button">
<i class="x icon"></i>
<span>Deny all</span>
</button>
</a>
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp);
<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>
<div>
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny green button">
<i class="check icon"></i>
<span>Automatically approve every time</span>
</button>
</a>
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny yellow button">
<i class="check icon"></i>
<span>Approve this time</span>
</button>
</a>
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny red button">
<i class="x icon"></i>
<span>Deny</span>
</button>
</a>
</div>
</div>
}

View file

@ -0,0 +1,36 @@
#nullable enable
using System.Net;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth;
public class AuthenticationPage : BaseLayout
{
public List<AuthenticationAttempt> AuthenticationAttempts = new();
public IPAddress? IpAddress;
public AuthenticationPage(Database database) : base(database)
{}
public IActionResult OnGet()
{
if (!ServerConfiguration.Instance.Authentication.UseExternalAuth) return this.NotFound();
if (this.User == null) return this.StatusCode(403, "");
this.IpAddress = this.HttpContext.Connection.RemoteIpAddress;
this.AuthenticationAttempts = this.Database.AuthenticationAttempts.Include
(a => a.GameToken)
.Where(a => a.GameToken.UserId == this.User.UserId)
.OrderByDescending(a => a.Timestamp)
.ToList();
return this.Page();
}
}

View file

@ -0,0 +1,23 @@
@page "/authentication/autoApprovals"
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth.ManageUserApprovedIpAddressesPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Automatically approved IP addresses";
}
@foreach (UserApprovedIpAddress approvedIpAddress in Model.ApprovedIpAddresses)
{
<div class="ui blue segment">
<p>@approvedIpAddress.IpAddress</p>
<a href="/authentication/revokeAutoApproval/@approvedIpAddress.UserApprovedIpAddressId">
<button class="ui red button">
<i class="trash icon"></i>
<span>Revoke</span>
</button>
</a>
</div>
}

View file

@ -0,0 +1,26 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth;
public class ManageUserApprovedIpAddressesPage : BaseLayout
{
public List<UserApprovedIpAddress> ApprovedIpAddresses = new();
public ManageUserApprovedIpAddressesPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login");
this.ApprovedIpAddresses = await this.Database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).ToListAsync();
return this.Page();
}
}

View file

@ -0,0 +1,81 @@
@page "/"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Levels
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LandingPage
@{
Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false;
bool isMobile = this.Request.IsMobile();
}
<h1>Welcome to <b>@ServerConfiguration.Instance.Customization.ServerName</b>!</h1>
@if (Model.User != null)
{
<p>You are currently logged in as <b>@Model.User.Username</b>.</p>
if (ServerConfiguration.Instance.Authentication.UseExternalAuth && Model.AuthenticationAttemptsCount > 0)
{
<p>
<b>You have @Model.AuthenticationAttemptsCount authentication attempts pending. Click <a href="/authentication">here</a> to view them.</b>
</p>
}
}
@if (Model.PlayersOnlineCount == 1)
{
<p>There is 1 user currently online:</p>
@foreach (User user in Model.PlayersOnline)
{
<a href="/user/@user.UserId" title="@user.Status.ToString()">@user.Username</a>
}
}
else if (Model.PlayersOnlineCount == 0)
{
<p>There are no users online. Why not hop on?</p>
}
else
{
<p>There are currently @Model.PlayersOnlineCount users online:</p>
@foreach (User user in Model.PlayersOnline)
{
<a href="/user/@user.UserId" title="@user.Status.ToString()">@user.Username</a>
}
}
<br>
<div class="@(isMobile ? "" : "ui center aligned grid")">
<div class="eight wide column">
<div class="ui pink segment">
<h1><i class="ribbon icon"></i>Latest Team Picks</h1>
<div class="ui divider"></div>
<div class="ui left aligned segment">
@foreach (Slot slot in Model.LatestTeamPicks!) @* Can't reach a point where this is null *@
{
@await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile))
<br>
}
</div>
</div>
</div>
@if (isMobile)
{
<br>
}
<div class="eight wide column">
<div class="ui blue segment">
<h1><i class="certificate icon"></i>Newest Levels</h1>
<div class="ui divider"></div>
<div class="ui left aligned segment">
@foreach (Slot slot in Model.NewestLevels!) @* Can't reach a point where this is null *@
{
@await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile))
<br>
}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,76 @@
#nullable enable
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class LandingPage : BaseLayout
{
public LandingPage(Database database) : base(database)
{}
public int AuthenticationAttemptsCount;
public List<User> PlayersOnline = new();
public int PlayersOnlineCount;
public List<Slot>? LatestTeamPicks;
public List<Slot>? NewestLevels;
[UsedImplicitly]
public async Task<IActionResult> OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user != null && user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
this.PlayersOnlineCount = await StatisticsHelper.RecentMatches();
if (user != null)
this.AuthenticationAttemptsCount = await this.Database.AuthenticationAttempts.Include
(a => a.GameToken)
.CountAsync(a => a.GameToken.UserId == user.UserId);
List<int> userIds = await this.Database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync();
this.PlayersOnline = await this.Database.Users.Where(u => userIds.Contains(u.UserId)).ToListAsync();
const int maxShownLevels = 5;
this.LatestTeamPicks = await this.Database.Slots.Where
(s => s.TeamPick)
.OrderByDescending(s => s.FirstUploaded)
.Take(maxShownLevels)
.Include(s => s.Creator)
.ToListAsync();
this.NewestLevels = await this.Database.Slots.OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync();
return this.Page();
}
public ViewDataDictionary GetSlotViewData(int slotId, bool isMobile = false)
=> new(ViewData)
{
{
"User", this.User
},
{
"CallbackUrl", $"~/slot/{slotId}"
},
{
"ShowLink", true
},
{
"IsMini", true
},
{
"IsMobile", isMobile
},
};
}

View file

@ -0,0 +1,209 @@
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts.BaseLayout
@{
if (Model!.User == null)
{
Model.NavigationItemsRight.Add(new PageNavigationItem("Login / Register", "/login", "sign in"));
}
else
{
if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
{
Model.NavigationItems.Add(new PageNavigationItem("Authentication", "/authentication", "key"));
}
Model.NavigationItemsRight.Add(new PageNavigationItem("Profile", "/user/" + Model.User.UserId, "user alternate"));
@if (Model.User.IsAdmin)
{
Model.NavigationItemsRight.Add(new PageNavigationItem("Admin Panel", "/admin", "cogs"));
}
Model.NavigationItemsRight.Add(new PageNavigationItem("Log out", "/logout", "user alternate slash")); // should always be last
}
Model.IsMobile = Model.Request.IsMobile();
}
<!DOCTYPE html>
<html lang="en">
<head>
@if (Model.Title == string.Empty)
{
<title>@ServerConfiguration.Instance.Customization.ServerName</title>
}
else
{
<title>@ServerConfiguration.Instance.Customization.ServerName - @Model.Title</title>
}
<link rel="stylesheet" type="text/css" href="~/css/styles.css">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.css">
@* Favicon *@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#008cff">
<meta name="msapplication-TileColor" content="#008cff">
@* Embed Stuff *@
<meta name="theme-color" data-react-helmet="true" content="#008cff">
<meta content="@ServerConfiguration.Instance.Customization.ServerName - @Model.Title" property="og:title">
@if (!string.IsNullOrEmpty(Model.Description))
{
<meta content="@Model.Description" property="og:description">
}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@* Google Analytics *@
@if (ServerConfiguration.Instance.GoogleAnalytics.AnalyticsEnabled)
{
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=@ServerConfiguration.Instance.GoogleAnalytics.Id"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '@ServerConfiguration.Instance.GoogleAnalytics.Id');
</script>
}
</head>
<body>
<div class="pageContainer">
<header class="lighthouse-header">
<div class="ui attached menu">
<div class="ui container">
@{
string mobileIconStyle = Model.IsMobile ? "margin-right: 0;" : "";
}
@foreach (PageNavigationItem navigationItem in Model!.NavigationItems)
{
<a class="item" href="@navigationItem.Url">
@if (navigationItem.Icon != null)
{
<i class="@navigationItem.Icon icon" style="@mobileIconStyle"></i>
}
@if (!Model.IsMobile)
{
@navigationItem.Name
}
</a>
}
<div class="right menu">
@foreach (PageNavigationItem navigationItem in Model!.NavigationItemsRight)
{
<a class="item" href="@navigationItem.Url">
@if (navigationItem.Icon != null)
{
<i class="@navigationItem.Icon icon" style="@mobileIconStyle"></i>
}
@if (!Model.IsMobile)
{
@navigationItem.Name
}
</a>
}
</div>
</div>
</div>
<noscript>
<div class="ui bottom attached yellow message small">
<div class="ui container">
<div style="display: flex; align-items: center; font-size: 1.2rem;">
<i class="warning icon"></i>
<span style="font-size: 1.2rem;">JavaScript not enabled</span>
</div>
<p>
While we intend to have as little JavaScript as possible, we can not
guarantee everything will work without it. We recommend that you whitelist JavaScript for Project Lighthouse.
It's not <i>too</i> bloated, we promise.
</p>
</div>
</div>
</noscript>
@if (!ServerStatics.IsDebug && VersionHelper.IsDirty)
{
<div class="ui bottom attached red message large">
<div class="ui container">
<i class="warning icon"></i>
<span style="font-size: 1.2rem;">Potential License Violation</span>
<p>This instance is a public-facing instance that has been modified without the changes published. You may be in violation of the <a href="https://github.com/LBPUnion/project-lighthouse/blob/main/LICENSE">GNU Affero General Public License v3.0</a>.</p>
<p>If you believe this is an error, please create an issue with the output of <code>git status</code> ran from the root of the server source code in the description on our <a href="https://github.com/LBPUnion/project-lighthouse/issues">issue tracker</a>.</p>
<p>If not, please publish the source code somewhere accessible to your users.</p>
</div>
</div>
}
</header>
<div class="main">
<div class="ui container">
<br>
@if (Model.ShowTitleInPage)
{
<h1>@Model.Title</h1>
}
@RenderBody()
<div style="height: 50px;"></div> @* makes it look nicer *@
</div>
</div>
<footer>
<div class="ui black attached inverted segment">
<div class="ui container">
<p>Page generated by @VersionHelper.FullVersion.</p>
@if (VersionHelper.IsDirty)
{
<p>This page was generated using a modified version of Project Lighthouse. Please make sure you are properly disclosing the source code to any users who may be using this instance.</p>
}
</div>
</div>
@if (ServerStatics.IsDebug)
{
<div class="ui red attached inverted segment">
<div class="ui container">
<button type="button" class="ui inverted button collapsible">
<b>Show/Hide Debug Info</b>
</button>
<div style="display:none" id="lighthouse-debug-info">
<br>
<p>Model.IsMobile: @Model.IsMobile</p>
<p>Model.Title: @Model.Title</p>
<p>Model.Description: @Model.Description</p>
<p>Model.User.UserId: @(Model.User?.UserId.ToString() ?? "(not logged in)")</p>
</div>
</div>
</div>
<script>
const collapsible = document.getElementsByClassName("collapsible");
for (let i = 0; i < collapsible.length; i++)
{
collapsible[i].addEventListener("click", function()
{
this.classList.toggle("active");
const content = this.nextElementSibling;
if (content.style.display === "block")
{
content.style.display = "none";
}
else
{
content.style.display = "block";
}
});
}
</script>
}
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,44 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
public class BaseLayout : PageModel
{
public readonly Database Database;
public readonly List<PageNavigationItem> NavigationItems = new()
{
new PageNavigationItem("Home", "/", "home"),
new PageNavigationItem("Users", "/users/0", "user friends"),
new PageNavigationItem("Photos", "/photos/0", "camera"),
new PageNavigationItem("Levels", "/slots/0", "certificate"),
};
public readonly List<PageNavigationItem> NavigationItemsRight = new();
public string Description = string.Empty;
public bool IsMobile;
public bool ShowTitleInPage = true;
public string Title = string.Empty;
private User? user;
public BaseLayout(Database database)
{
this.Database = database;
}
public new User? User {
get {
if (this.user != null) return this.user;
return this.user = this.Database.UserFromWebRequest(this.Request);
}
set => this.user = value;
}
}

View file

@ -0,0 +1,68 @@
@page "/login"
@using LBPUnion.ProjectLighthouse.Configuration
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LoginForm
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Log in";
}
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
<script>
function onSubmit(form) {
const passwordInput = document.getElementById("password");
const passwordSubmit = document.getElementById("password-submit");
passwordSubmit.value = sha256(passwordInput.value);
return true;
}
</script>
@if (!string.IsNullOrWhiteSpace(Model.Error))
{
<div class="ui negative message">
<div class="header">
Uh oh!
</div>
<p style="white-space: pre-line">@Model.Error</p>
</div>
}
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" name="username" id="text" placeholder="Username">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" id="password" placeholder="Password">
<input type="hidden" id="password-submit" name="password">
<i class="lock icon"></i>
</div>
</div>
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
@await Html.PartialAsync("Partials/CaptchaPartial")
}
<input type="submit" value="Log in" id="submit" class="ui blue button">
@if (ServerConfiguration.Instance.Authentication.RegistrationEnabled)
{
<a href="/register">
<div class="ui button">
<i class="user alternate add icon"></i>
Register
</div>
</a>
}
</form>

View file

@ -0,0 +1,113 @@
#nullable enable
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
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;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class LoginForm : BaseLayout
{
public LoginForm(Database database) : base(database)
{}
public string? Error { get; private set; }
[UsedImplicitly]
public async Task<IActionResult> OnPost(string username, string password)
{
if (string.IsNullOrWhiteSpace(username))
{
this.Error = "The username field is required.";
return this.Page();
}
if (string.IsNullOrWhiteSpace(password))
{
this.Error = "The password field is required.";
return this.Page();
}
if (!await this.Request.CheckCaptchaValidity())
{
this.Error = "You must complete the captcha correctly.";
return this.Page();
}
User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null)
{
Logger.Warn($"User {username} failed to login on web due to invalid username", LogArea.Login);
this.Error = "The username or password you entered is invalid.";
return this.Page();
}
if (!BCrypt.Net.BCrypt.Verify(password, user.Password))
{
Logger.Warn($"User {user.Username} (id: {user.UserId}) failed to login on web due to invalid password", LogArea.Login);
this.Error = "The username or password you entered is invalid.";
return this.Page();
}
if (user.Banned)
{
Logger.Warn($"User {user.Username} (id: {user.UserId}) failed to login on web due to being banned", LogArea.Login);
this.Error = "You have been banned. Please contact an administrator for more information.\nReason: " + user.BannedReason;
return this.Page();
}
if (user.EmailAddress == null && ServerConfiguration.Instance.Mail.MailEnabled)
{
Logger.Warn($"User {user.Username} (id: {user.UserId}) failed to login; email not set", LogArea.Login);
EmailSetToken emailSetToken = new()
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
};
this.Database.EmailSetTokens.Add(emailSetToken);
await this.Database.SaveChangesAsync();
return this.Redirect("/login/setEmail?token=" + emailSetToken.EmailToken);
}
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = CryptoHelper.GenerateAuthToken(),
};
this.Database.WebTokens.Add(webToken);
await this.Database.SaveChangesAsync();
this.Response.Cookies.Append
(
"LighthouseToken",
webToken.UserToken,
new CookieOptions
{
Expires = DateTimeOffset.Now.AddDays(7),
}
);
Logger.Success($"User {user.Username} (id: {user.UserId}) successfully logged in on web", LogArea.Login);
if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
if (ServerConfiguration.Instance.Mail.MailEnabled && !user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail");
return this.RedirectToPage(nameof(LandingPage));
}
[UsedImplicitly]
public IActionResult OnGet() => this.Page();
}

View file

@ -0,0 +1,10 @@
@page "/logout"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LogoutPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Logged out";
}
<p>You have been successfully logged out. You will be redirected in 5 seconds, or you may click <a href="/">here</a> to do so manually.</p>
<meta http-equiv="refresh" content="5; url=/"/>

View file

@ -0,0 +1,25 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class LogoutPage : BaseLayout
{
public LogoutPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet()
{
WebToken? token = this.Database.WebTokenFromRequest(this.Request);
if (token == null) return this.BadRequest();
this.Database.WebTokens.Remove(token);
await this.Database.SaveChangesAsync();
this.Response.Cookies.Delete("LighthouseToken");
return this.Page();
}
}

View file

@ -0,0 +1,17 @@
@model LBPUnion.ProjectLighthouse.Administration.AdminPanelStatistic
<div class="three wide column">
<div class="ui center aligned blue segment">
@if (Model.ViewAllEndpoint != null)
{
<h2>
<a href="/admin/@Model.ViewAllEndpoint">@Model.StatisticNamePlural</a>
</h2>
}
else
{
<h2>@Model.StatisticNamePlural</h2>
}
<h3>@Model.Count</h3>
</div>
</div>

View file

@ -0,0 +1,12 @@
@model LBPUnion.ProjectLighthouse.PlayerData.Profiles.User
<form method="post" action="/admin/user/@Model.UserId/setGrantedSlots">
@Html.AntiForgeryToken()
<div class="ui left action input">
<button type="submit" class="ui blue button">
<i class="pencil icon"></i>
<span>Set Granted Slots</span>
</button>
<input type="text" name="grantedSlotCount" placeholder="Granted Slots" value="@Model.AdminGrantedSlots">
</div>
</form>

View file

@ -0,0 +1,6 @@
@using LBPUnion.ProjectLighthouse.Configuration
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
<div class="h-captcha" data-sitekey="@ServerConfiguration.Instance.Captcha.SiteKey"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
}

View file

@ -0,0 +1,82 @@
@using System.Web
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
<div class="ui yellow segment" id="comments">
<h2>Comments</h2>
@if (Model.Comments.Count == 0 && Model.CommentsEnabled)
{
<p>There are no comments.</p>
}
else if (!Model.CommentsEnabled)
{
<b>
<i>Comments are disabled.</i>
</b>
}
else
{
int count = Model.Comments.Count;
<p>There @(count == 1 ? "is" : "are") @count comment@(count == 1 ? "" : "s").</p>
}
@if (Model.CommentsEnabled && Model.User != null)
{
<div class="ui divider"></div>
<form class="ui reply form" action="@Url.RouteUrl(ViewContext.RouteData.Values)/postComment" method="post">
<div class="field">
<textarea style="min-height: 70px; height: 70px; max-height:120px" name="msg"></textarea>
</div>
<input type="submit" class="ui blue button">
</form>
@if (Model.Comments.Count > 0)
{
<div class="ui divider"></div>
}
}
@for(int i = 0; i < Model.Comments.Count; i++)
{
Comment comment = Model.Comments[i];
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
string? url = Url.RouteUrl(ViewContext.RouteData.Values);
if (url == null) continue;
int rating = comment.ThumbsUp - comment.ThumbsDown;
<div style="display: flex" id="@comment.CommentId">
<div class="voting">
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == 1 ? 0 : 1)">
<i class="fitted @(comment.YourThumb == 1 ? "green" : "grey") arrow up link icon" style="display: block"></i>
</a>
<span style="text-align: center; margin: auto; @(rating < 0 ? "margin-left: -5px" : "")">@(rating)</span>
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == -1 ? 0 : -1)">
<i class="fitted @(comment.YourThumb == -1 ? "red" : "grey") arrow down link icon" style="display: block"></i>
</a>
</div>
<div class="comment">
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
@if (comment.Deleted)
{
<i>
<span>@decodedMessage</span>
</i>
}
else
{
<span>@decodedMessage</span>
}
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>
@if (i != Model.Comments.Count - 1)
{
<div class="ui divider"></div>
}
</div>
</div>
}
</div>

View file

@ -0,0 +1,127 @@
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.PlayerData.Photo
<div style="position: relative">
<canvas class="hide-subjects" id="canvas-subjects-@Model.PhotoId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)">
</canvas>
<img id="game-image-@Model.PhotoId" src="/gameAssets/@Model.LargeHash"
style="width: 100%; height: auto; border-radius: .28571429rem;">
</div>
<br>
<p>
<i>
Taken by
<b>
<a href="/user/@Model.Creator?.UserId">@Model.Creator?.Username</a>
</b>
</i>
</p>
<p>
<b>Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"):</b>
</p>
<div id="hover-subjects-@Model.PhotoId">
@foreach (PhotoSubject subject in Model.Subjects)
{
<a href="/user/@subject.UserId">@subject.User.Username</a>
}
</div>
@{
PhotoSubject[] subjects = Model.Subjects.ToArray();
foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username;
}
<script>
// render the page first so that image heights have been calculated
window.addEventListener("load", function () {
const canvas = document.getElementById("canvas-subjects-@Model.PhotoId");
const hoverer = document.getElementById("hover-subjects-@Model.PhotoId");
const image = document.getElementById("game-image-@Model.PhotoId");
hoverer.addEventListener('mouseenter', function () {
canvas.className = "photo-subjects";
});
hoverer.addEventListener('mouseleave', function () {
canvas.className = "photo-subjects hide-subjects";
});
const context = canvas.getContext('2d');
const subjects = @Html.Raw(Json.Serialize(subjects.ToArray()));
canvas.width = image.offsetWidth;
canvas.height = image.clientHeight; // space for names to hang off
const w = canvas.width;
const h = canvas.height;
// halfwidth, halfheight
const hw = w / 2;
const hh = h / 2;
const colours = ["#96dd3c", "#ceb424", "#cc0a1d", "#c800cc"];
subjects.forEach((s, si) => {
const colour = colours[si % 4];
// Bounding box
const bounds = s.bounds.split(",").map(parseFloat);
const [x1, y1, x2, y2] = bounds.map(n => Math.min(Math.max(n, -1), 1));
const bx = hw - (x2 * hw);
const by = hh - (y2 * hh);
const bw = (x2 - x1) * hw;
const bh = (y2 - y1) * hh;
context.beginPath();
context.lineWidth = 3;
context.strokeStyle = colour;
context.rect(bx, by, bw, bh);
context.stroke();
// Move into relative coordinates from bounding box
context.translate(bx, by);
// Username label
context.font = "16px Lato";
context.fillStyle = colour;
// Text width/height for the label background
const tw = context.measureText(s.username).width;
const th = 24;
// Check if the label will flow off the bottom of the frame
const overflowBottom = (bounds[3] * hh) > (hh - 24);
// Check if the label will flow off the left of the frame
const overflowLeft = (bounds[2] * hw - tw) < (-hw);
// Set alignment
context.textAlign = overflowLeft ? "start" : "end";
// Text x / y
const lx = overflowLeft ? -bw + 6 : -6;
const ly = overflowBottom ? -bh - 6 : 16;
// Label background x / y
const lbx = overflowLeft ? bw - tw - 12 : -2;
const lby = overflowBottom ? bh : -24;
// Draw background
context.fillRect(lbx, lby, tw + 16, th);
// Draw text, rotated back upright (canvas draws rotated 180deg)
context.fillStyle = "white";
context.rotate(Math.PI);
context.fillText(s.username, lx, ly);
// reset transform
context.setTransform(1, 0, 0, 1, 0, 0);
})
}, false);
</script>

View file

@ -0,0 +1,120 @@
@using LBPUnion.ProjectLighthouse
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using Microsoft.EntityFrameworkCore
@model LBPUnion.ProjectLighthouse.Levels.Slot
@{
User? user = (User?)ViewData["User"];
await using Database database = new();
string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool mini = (bool?)ViewData["IsMini"] ?? false;
bool isQueued = false;
bool isHearted = false;
if (user != null)
{
isQueued = await database.QueuedLevels.FirstOrDefaultAsync(h => h.SlotId == Model.SlotId && h.UserId == user.UserId) != null;
isHearted = await database.HeartedLevels.FirstOrDefaultAsync(h => h.SlotId == Model.SlotId && h.UserId == user.UserId) != null;
}
string callbackUrl = (string)ViewData["CallbackUrl"]!;
bool showLink = (bool?)ViewData["ShowLink"] ?? false;
string iconHash = Model.IconHash;
if (string.IsNullOrWhiteSpace(iconHash) || iconHash.StartsWith('g')) iconHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
}
<div class="card">
@{
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 class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
</div>
<div class="cardStats">
@if (!mini)
{
@if (showLink)
{
<h2>
<a href="~/slot/@Model.SlotId">@slotName</a>
</h2>
}
else
{
<h1>
@slotName
</h1>
}
}
else
{
@if (showLink)
{
<h3>
<a href="~/slot/@Model.SlotId">@slotName</a>
</h3>
}
else
{
<h3>
@slotName
</h3>
}
}
<div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>
<i class="blue play icon" title="Plays"></i> <span>@Model.PlaysUnique</span>
<i class="green thumbs up icon" title="Yays"></i> <span>@Model.Thumbsup</span>
<i class="red thumbs down icon" title="Boos"></i> <span>@Model.Thumbsdown</span>
@if (Model.GameVersion == GameVersion.LittleBigPlanet1)
{
<i class="yellow star icon" title="LBP1 Stars"></i>
<span>@Model.RatingLBP1</span>
}
</div>
<p>
<i>Created by <a href="/user/@Model.Creator?.UserId">@Model.Creator?.Username</a> for @Model.GameVersion.ToPrettyString()</i>
</p>
</div>
<div class="cardButtons">
<br>
@if (user != null && !mini)
{
if (isHearted)
{
<a class="ui pink tiny button" href="/slot/@Model.SlotId/unheart?callbackUrl=@callbackUrl" title="Unheart">
<i class="broken heart icon" style="margin: 0"></i>
</a>
}
else
{
<a class="ui pink tiny button" href="/slot/@Model.SlotId/heart?callbackUrl=@callbackUrl" title="Heart">
<i class="heart icon" style="margin: 0"></i>
</a>
}
if (isQueued)
{
<a class="ui yellow tiny button" href="/slot/@Model.SlotId/unqueue?callbackUrl=@callbackUrl" title="Unqueue">
<i class="bell slash icon" style="margin: 0"></i>
</a>
}
else
{
<a class="ui yellow tiny button" href="/slot/@Model.SlotId/queue?callbackUrl=@callbackUrl" title="Queue">
<i class="bell icon" style="margin: 0"></i>
</a>
}
}
</div>
</div>

View file

@ -0,0 +1,37 @@
@model LBPUnion.ProjectLighthouse.PlayerData.Profiles.User
@{
bool showLink = (bool?)ViewData["ShowLink"] ?? false;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
}
<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">
</div>
<div class="cardStats">
@if (showLink)
{
<h2 style="margin-bottom: 2px;">
<a href="~/user/@Model.UserId">@Model.Username</a>
</h2>
}
else
{
<h1 style="margin-bottom: 2px;">
@Model.Username
</h1>
}
<p>
<i>@Model.Status</i>
</p>
<div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>
<i class="blue comment icon" title="Comments"></i> <span>@Model.Comments</span>
<i class="green upload icon" title="Uploaded Levels"></i><span>@Model.UsedSlots</span>
<i class="purple camera icon" title="Uploaded Photos"></i><span>@Model.PhotosByMe</span>
</div>
</div>
</div>

View file

@ -0,0 +1,51 @@
@page "/passwordReset"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Password Reset";
}
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
<script>
function onSubmit(form) {
const passwordInput = document.getElementById("password");
const confirmPasswordInput = document.getElementById("confirmPassword");
const passwordSubmit = document.getElementById("password-submit");
const confirmPasswordSubmit = document.getElementById("confirmPassword-submit");
passwordSubmit.value = sha256(passwordInput.value);
confirmPasswordSubmit.value = sha256(confirmPasswordInput.value);
return true;
}
</script>
@if (!string.IsNullOrWhiteSpace(Model.Error))
{
<div class="ui negative message">
<div class="header">
Uh oh!
</div>
<p>@Model.Error</p>
</div>
}
<form onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()
<div class="ui left labeled input">
<label for="password" class="ui blue label">Password: </label>
<input type="password" id="password">
<input type="hidden" id="password-submit" name="password">
</div><br><br>
<div class="ui left labeled input">
<label for="password" class="ui blue label">Confirm Password: </label>
<input type="password" id="confirmPassword">
<input type="hidden" id="confirmPassword-submit" name="confirmPassword">
</div><br><br><br>
<input type="submit" value="Reset password and continue" id="submit" class="ui green button"><br>
</form>

View file

@ -0,0 +1,56 @@
#nullable enable
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class PasswordResetPage : BaseLayout
{
public PasswordResetPage(Database database) : base(database)
{}
public string? Error { get; private set; }
[UsedImplicitly]
public async Task<IActionResult> OnPost(string password, string confirmPassword)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
if (string.IsNullOrWhiteSpace(password))
{
this.Error = "The password field is required.";
return this.Page();
}
if (password != confirmPassword)
{
this.Error = "Passwords do not match!";
return this.Page();
}
user.Password = CryptoHelper.BCryptHash(password);
user.PasswordResetRequired = false;
await this.Database.SaveChangesAsync();
if (!user.EmailAddressVerified && ServerConfiguration.Instance.Mail.MailEnabled)
return this.Redirect("~/login/sendVerificationEmail");
return this.Redirect("~/");
}
[UsedImplicitly]
public IActionResult OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
return this.Page();
}
}

View file

@ -0,0 +1,13 @@
@page "/passwordResetRequired"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequiredPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Password Reset Required";
}
<p>An administrator has deemed it necessary that you reset your password. Please do so.</p>
<a href="/passwordReset">
<div class="ui blue button">Reset Password</div>
</a>

View file

@ -0,0 +1,24 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class PasswordResetRequiredPage : BaseLayout
{
public PasswordResetRequiredPage(Database database) : base(database)
{}
public bool WasResetRequest { get; private set; }
public IActionResult OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
if (!user.PasswordResetRequired) return this.Redirect("~/passwordReset");
return this.Page();
}
}

View file

@ -0,0 +1,36 @@
@page "/photos/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PhotosPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Photos";
}
<p>There are @Model.PhotoCount total photos!</p>
<form action="/photos/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search photos..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
@foreach (Photo photo in Model.Photos)
{
<div class="ui segment">
@await Html.PartialAsync("Partials/PhotoPartial", photo)
</div>
}
@if (Model.PageNumber != 0)
{
<a href="/photos/@(Model.PageNumber - 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
}
@(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1)
{
<a href="/photos/@(Model.PageNumber + 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
}

View file

@ -0,0 +1,51 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class PhotosPage : BaseLayout
{
public int PageAmount;
public int PageNumber;
public int PhotoCount;
public List<Photo> Photos = new();
public string? SearchValue;
public PhotosPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{
if (string.IsNullOrWhiteSpace(name)) name = "";
this.SearchValue = name.Replace(" ", string.Empty);
this.PhotoCount = await this.Database.Photos.Include
(p => p.Creator)
.CountAsync(p => p.Creator!.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue));
this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.PhotoCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/photos/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Photos = await this.Database.Photos.Include
(p => p.Creator)
.Where(p => p.Creator!.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue))
.OrderByDescending(p => p.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)
.ToListAsync();
return this.Page();
}
}

View file

@ -0,0 +1,108 @@
@page "/register"
@using LBPUnion.ProjectLighthouse.Configuration
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.RegisterForm
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Register";
}
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
<script>
function onSubmit(form) {
const passwordInput = document.getElementById("password");
const confirmPasswordInput = document.getElementById("confirmPassword");
const passwordSubmit = document.getElementById("password-submit");
const confirmPasswordSubmit = document.getElementById("confirm-submit");
passwordSubmit.value = sha256(passwordInput.value);
confirmPasswordSubmit.value = sha256(confirmPasswordInput.value);
return true;
}
</script>
@if (!string.IsNullOrWhiteSpace(Model.Error))
{
<div class="ui negative message">
<div class="header">
Uh oh!
</div>
<p>@Model.Error</p>
</div>
}
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" name="username" id="text" placeholder="Username" pattern="^[a-zA-Z0-9_.-]*$" minlength="3" maxlength="16">
<i class="user icon"></i>
</div>
</div>
@if (ServerConfiguration.Instance.Mail.MailEnabled)
{
<div class="field">
<label>Email address</label>
<div class="ui left icon input">
<input type="email" name="emailAddress" id="emailAddress" placeholder="Email Address">
<i class="mail icon"></i>
</div>
</div>
}
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" id="password" placeholder="Password">
<input type="hidden" name="password" id="password-submit">
<i class="lock icon"></i>
</div>
</div>
<div class="field">
<label>Confirm Password</label>
<div class="ui left icon input">
<input type="password" id="confirmPassword" placeholder="Confirm Password">
<input type="hidden" name="confirmPassword" id="confirm-submit">
<i class="lock icon"></i>
</div>
</div>
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
@await Html.PartialAsync("Partials/CaptchaPartial")
}
<input type="submit" value="Register" id="submit" class="ui green button">
</form>
<br><br>
@if (ServerStatics.IsDebug)
{
<button class="ui red button" onclick="fill()">DEBUG: Fill with everything but email</button>
<script>
const usernameField = document.getElementById("text");
const emailField = document.getElementById("emailAddress");
const passwordField = document.getElementById("password");
const confirmPasswordField = document.getElementById("confirmPassword");
function fill() {
const min = 100;
const max = 99999999;
const rand = Math.floor(Math.random() * (max - min + 1) + min);
usernameField.value = rand.toString();
passwordField.value = rand.toString();
confirmPasswordField.value = rand.toString();
emailField.focus();
}
</script>
}

View file

@ -0,0 +1,98 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class RegisterForm : BaseLayout
{
public RegisterForm(Database database) : base(database)
{}
public string? Error { get; private set; }
[UsedImplicitly]
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
public async Task<IActionResult> OnPost(string username, string password, string confirmPassword, string emailAddress)
{
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound();
if (string.IsNullOrWhiteSpace(username))
{
this.Error = "The username field is blank.";
return this.Page();
}
if (string.IsNullOrWhiteSpace(password))
{
this.Error = "Password field is required.";
return this.Page();
}
if (string.IsNullOrWhiteSpace(emailAddress) && ServerConfiguration.Instance.Mail.MailEnabled)
{
this.Error = "Email address field is required.";
return this.Page();
}
if (password != confirmPassword)
{
this.Error = "Passwords do not match!";
return this.Page();
}
if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null)
{
this.Error = "The username you've chosen is already taken.";
return this.Page();
}
if (ServerConfiguration.Instance.Mail.MailEnabled &&
await this.Database.Users.FirstOrDefaultAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower()) != null)
{
this.Error = "The email address you've chosen is already taken.";
return this.Page();
}
if (!await this.Request.CheckCaptchaValidity())
{
this.Error = "You must complete the captcha correctly.";
return this.Page();
}
User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress);
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = CryptoHelper.GenerateAuthToken(),
};
this.Database.WebTokens.Add(webToken);
await this.Database.SaveChangesAsync();
this.Response.Cookies.Append("LighthouseToken", webToken.UserToken);
if (ServerConfiguration.Instance.Mail.MailEnabled) return this.Redirect("~/login/sendVerificationEmail");
return this.RedirectToPage(nameof(LandingPage));
}
[UsedImplicitly]
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
public IActionResult OnGet()
{
this.Error = string.Empty;
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound();
return this.Page();
}
}

View file

@ -0,0 +1,248 @@
@page "/admin/reports/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Administration.Reports
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.ReportsPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Reports";
}
<p>There are @Model.ReportCount total reports!</p>
<form action="/admin/reports/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search reports..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
<script>
let subjects = [];
let bounds = [];
let canvases = [];
let ctx = [];
let images = [];
</script>
@foreach (GriefReport report in Model.Reports)
{
<div class="ui segment">
<div>
<canvas class="hide-subjects" id="canvas-subjects-@report.ReportId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)">
</canvas>
<img class="hover-region" id="game-image-@report.ReportId" src="/gameAssets/@report.JpegHash" alt="Grief report picture" style="width: 100%; height: auto; border-radius: .28571429rem;">
</div>
<p>
<i>
Report submitted by
<b>
<a href="/user/@report.ReportingPlayerId">@report.ReportingPlayer.Username</a>
</b>
</i>
</p>
<b class="hover-players" id="hover-subjects-2-@report.ReportId">Report contains @report.XmlPlayers.Length @(report.XmlPlayers.Length == 1 ? "player" : "players")</b>
@foreach (ReportPlayer player in report.XmlPlayers)
{
<div id="hover-subjects-@report.ReportId" class="hover-players">
<a href="/">@player.Name</a>
</div>
}
<br>
<div>
<b>Report time: </b>@(DateTimeOffset.FromUnixTimeMilliseconds(report.Timestamp).ToString("R"))
</div>
<div>
<b>Report reason: </b>@report.Type
</div>
<div>
<b>Level ID:</b> @report.LevelId
</div>
<div>
<b>Level type:</b> @report.LevelType
</div>
<div>
<b>Level owner:</b> @report.LevelOwner
</div>
<br>
<a class="ui green small button" href="/admin/report/@report.ReportId/dismiss">
<i class="checkmark icon"></i>
<span>Dismiss</span>
</a>
<a class="ui red small button" href="/admin/report/@report.ReportId/remove">
<i class="trash icon"></i>
<span>Remove all related assets</span>
</a>
</div>
<script>
subjects[@report.ReportId] = @Html.Raw(report.Players)
bounds[@report.ReportId] = @Html.Raw(report.Bounds)
images[@report.ReportId] = document.getElementById("game-image-@report.ReportId")
canvases[@report.ReportId] = document.getElementById("canvas-subjects-@report.ReportId")
canvases[@report.ReportId].width = images[@report.ReportId].offsetWidth;
canvases[@report.ReportId].height = images[@report.ReportId].clientHeight;
ctx[@report.ReportId] = canvases[@report.ReportId].getContext('2d');
</script>
}
<script>
function getReportId(name){
let split = name.split("-");
return split[split.length-1];
}
const colours = ["#96dd3c", "#ceb424", "#cc0a1d", "#c800cc"];
let displayType;
window.addEventListener("load", function () {
document.querySelectorAll(".hover-players").forEach(item => {
item.addEventListener('mouseenter', function () {
let reportId = getReportId(item.id);
const canvas = canvases[reportId];
displayType = 1;
canvas.className = "photo-subjects";
redraw(reportId);
});
});
document.querySelectorAll(".hover-region").forEach(item => {
item.addEventListener('mouseenter', function () {
let reportId = getReportId(item.id);
const canvas = canvases[reportId];
const image = document.getElementById("game-image-" + reportId.toString());
displayType = 0;
canvas.className = "photo-subjects";
canvas.width = image.offsetWidth;
canvas.height = image.clientHeight; // space for names to hang off
redraw(reportId);
});
});
document.querySelectorAll(".hover-region, .hover-players").forEach(item => {
item.addEventListener('mouseleave', function () {
canvases[getReportId(item.id)].className = "photo-subjects hide-subjects";
});
});
}, false);
function redraw(reportId){
let context = ctx[reportId];
let canvas = canvases[reportId];
let image = images[reportId];
context.clearRect(0, 0, canvas.width, canvas.height);
let w = canvas.width;
let h = canvas.height;
// halfwidth, halfheight
const hw = w / 2;
const hh = h / 2;
switch (displayType){
case 0: {
let imageBounds = bounds[reportId];
const x1 = imageBounds.Left;
const x2 = imageBounds.Right;
const y1 = imageBounds.Top;
const y2 = imageBounds.Bottom;
const scaleX = image.naturalWidth / canvas.width;
const scaleY = image.naturalHeight / canvas.height;
const bx = canvas.width-(x2/scaleX);
const by = canvas.height-(y2/scaleY);
const bw = (x2 - x1) / scaleX;
const bh = (y2 - y1) / scaleY;
context.beginPath();
context.globalAlpha = 0.6;
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
context.clearRect(bx, by, bw, bh);
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = "#957a24";
context.rect(bx, by, bw, bh);
context.stroke();
context.globalAlpha = 1.0;
break;
}
case 1: {
let subject = subjects[reportId];
subject.forEach((s, si) => {
const colour = colours[si % 4];
// Bounding box
const x1 = s.Location.Left;
const x2 = s.Location.Right;
const y1 = s.Location.Top;
const y2 = s.Location.Bottom;
const scaleX = image.naturalWidth / canvas.width;
const scaleY = image.naturalHeight / canvas.height;
const bx = canvas.width-(x2/scaleX);
const by = canvas.height-(y2/scaleY);
const bw = (x2 - x1) / scaleX;
const bh = (y2 - y1) / scaleY;
context.beginPath();
context.lineWidth = 3;
context.strokeStyle = colour;
context.rect(bx, by, bw, bh);
context.stroke();
// Move into relative coordinates from bounding box
context.translate(bx, by);
// Username label
context.font = "16px Lato";
context.fillStyle = colour;
// Text width/height for the label background
const tw = context.measureText(s.Name).width;
const th = 24;
// Check if the label will flow off the bottom of the frame
const overflowBottom = (y2+tw - 24) > (canvas.width);
// Check if the label will flow off the left of the frame
const overflowLeft = (x2) < (24);
// Set alignment
context.textAlign = overflowLeft ? "start" : "end";
// Text x / y
const lx = overflowLeft ? -bw + 6 : -6;
const ly = overflowBottom ? -bh - 6 : 16;
// Label background x / y
const lbx = overflowLeft ? bw - tw - 12 : 0;
const lby = overflowBottom ? bh : -24;
// Draw background
context.fillRect(lbx, lby, tw+8, th);
// Draw text, rotated back upright (canvas draws rotated 180deg)
context.fillStyle = "white";
context.rotate(Math.PI);
context.fillText(s.Name, lx, ly);
// reset transform
context.setTransform(1, 0, 0, 1, 0, 0);
});
break;
}
}
}
</script>
@if (Model.PageNumber != 0)
{
<a href="/admin/reports/@(Model.PageNumber - 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
}
@(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1)
{
<a href="/admin/reports/@(Model.PageNumber + 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
}

View file

@ -0,0 +1,66 @@
#nullable enable
using System.Text.Json;
using LBPUnion.ProjectLighthouse.Administration.Reports;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class ReportsPage : BaseLayout
{
public int PageAmount;
public int PageNumber;
public int ReportCount;
public List<GriefReport> Reports = new();
public string SearchValue = "";
public ReportsPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
if (!user.IsAdmin) return this.NotFound();
if (string.IsNullOrWhiteSpace(name)) name = "";
this.SearchValue = name.Replace(" ", string.Empty);
this.ReportCount = await this.Database.Reports.Include(r => r.ReportingPlayer).CountAsync(r => r.ReportingPlayer.Username.Contains(this.SearchValue));
this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.ReportCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount)
return this.Redirect($"/admin/reports/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Reports = await this.Database.Reports.Include(r => r.ReportingPlayer)
.Where(r => r.ReportingPlayer.Username.Contains(this.SearchValue))
.OrderByDescending(r => r.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)
.ToListAsync();
foreach (GriefReport r in this.Reports)
{
r.XmlPlayers = (ReportPlayer[])JsonSerializer.Deserialize(r.Players, typeof(ReportPlayer[]))!;
r.XmlBounds = new Marqee()
{
Rect = (Rectangle)JsonSerializer.Deserialize(r.Bounds, typeof(Rectangle))!,
};
}
return this.Page();
}
}

View file

@ -0,0 +1,14 @@
@page "/login/sendVerificationEmail"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SendVerificationEmailPage
@{
Layout = "Layouts/BaseLayout";
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>
<a href="/login/sendVerificationEmail">
<div class="ui blue button">Resend email</div>
</a>

View file

@ -0,0 +1,61 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
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;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class SendVerificationEmailPage : BaseLayout
{
public SendVerificationEmailPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet()
{
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login");
// `using` weirdness here. I tried to fix it, but I couldn't.
// The user should never see this page once they've been verified, so assert here.
System.Diagnostics.Debug.Assert(!user.EmailAddressVerified);
// Othewise, on a release build, just silently redirect them to the landing page.
#if !DEBUG
if (user.EmailAddressVerified)
{
return this.Redirect("/");
}
#endif
EmailVerificationToken verifyToken = new()
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
};
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");
}
}
}

View file

@ -0,0 +1,29 @@
@page "/login/setEmail"
@using LBPUnion.ProjectLighthouse.Configuration
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SetEmailForm
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Set an Email Address";
}
<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>
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()
@if (ServerConfiguration.Instance.Mail.MailEnabled)
{
<div class="field">
<label>Please type a valid email address and verify it:</label>
<div class="ui left icon input">
<input type="email" name="emailAddress" id="emailAddress" placeholder="Email Address">
<i class="mail icon"></i>
</div>
<input type="hidden" name="token" id="token" value="@Model.EmailToken?.EmailToken">
</div>
}
<input type="submit" value="Verify Email Address" id="submit" class="ui blue button">
</form>

View file

@ -0,0 +1,80 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
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;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class SetEmailForm : BaseLayout
{
public SetEmailForm(Database database) : base(database)
{}
public EmailSetToken? EmailToken;
public async Task<IActionResult> OnGet(string? token = null)
{
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
if (token == null) return this.Redirect("/login");
EmailSetToken? emailToken = await this.Database.EmailSetTokens.FirstOrDefaultAsync(t => t.EmailToken == token);
if (emailToken == null) return this.Redirect("/login");
this.EmailToken = emailToken;
return this.Page();
}
public async Task<IActionResult> OnPost(string emailAddress, string token)
{
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
EmailSetToken? emailToken = await this.Database.EmailSetTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.EmailToken == token);
if (emailToken == null) return this.Redirect("/login");
emailToken.User.EmailAddress = emailAddress;
this.Database.EmailSetTokens.Remove(emailToken);
User user = emailToken.User;
EmailVerificationToken emailVerifyToken = new()
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
};
this.Database.EmailVerificationTokens.Add(emailVerifyToken);
// The user just set their email address. Now, let's grant them a token to proceed with verifying the email.
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = CryptoHelper.GenerateAuthToken(),
};
this.Response.Cookies.Append
(
"LighthouseToken",
webToken.UserToken,
new CookieOptions
{
Expires = DateTimeOffset.Now.AddDays(7),
}
);
Logger.Success($"User {user.Username} (id: {user.UserId}) successfully logged in on web after setting an email address", LogArea.Login);
this.Database.WebTokens.Add(webToken);
await this.Database.SaveChangesAsync();
return this.Redirect("/login/sendVerificationEmail");
}
}

View file

@ -0,0 +1,199 @@
@page "/slot/{id:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData.Reviews
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage
@{
Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false;
Model.Title = Model.Slot?.Name ?? "";
Model.Description = Model.Slot?.Description ?? "";
bool isMobile = this.Request.IsMobile();
}
@await Html.PartialAsync("Partials/SlotCardPartial", Model.Slot, new ViewDataDictionary(ViewData)
{
{
"User", Model.User
},
{
"CallbackUrl", $"~/slot/{Model.Slot?.SlotId}"
},
{
"ShowLink", false
},
{
"IsMobile", Model.Request.IsMobile()
},
})
<br>
<div class="ui grid">
<div class="eight wide column">
<div class="ui blue segment">
<h2>Description</h2>
<p>@HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model.Slot?.Description) ? "This level has no description." : Model.Slot.Description)</p>
</div>
</div>
<div class="eight wide column">
<div class="ui red segment">
<h2>Tags</h2>
@{
string[] authorLabels = Model.Slot?.AuthorLabels.Split(",") ?? new string[]
{};
if (authorLabels.Length == 1) // ..?? ok c#
{
<p>This level has no tags.</p>
}
else
{
foreach (string label in authorLabels.Where(label => !string.IsNullOrEmpty(label)))
{
<div class="ui blue label">@label.Replace("LABEL_", "")</div>
}
}
}
</div>
</div>
<div class="eight wide column">
@await Html.PartialAsync("Partials/CommentsPartial")
</div>
<div class="eight wide column">
<div class="ui purple segment">
<h2>Reviews</h2>
@if (Model.Reviews.Count == 0 && Model.ReviewsEnabled)
{
<p>There are no reviews.</p>
}
else if (!Model.ReviewsEnabled)
{
<b>
<i>Reviews are disabled on this level.</i>
</b>
}
else
{
int count = Model.Reviews.Count;
<p>There @(count == 1 ? "is" : "are") @count review@(count == 1 ? "" : "s").</p>
<div class="ui divider"></div>
}
@for(int i = 0; i < Model.Reviews.Count; i++)
{
Review review = Model.Reviews[i];
string faceHash = (review.Thumb switch {
-1 => review.Reviewer?.BooHash,
0 => review.Reviewer?.MehHash,
1 => review.Reviewer?.YayHash,
_ => throw new ArgumentOutOfRangeException(),
}) ?? "";
if (string.IsNullOrWhiteSpace(faceHash))
{
faceHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
}
string faceAlt = review.Thumb switch {
-1 => "Boo!",
0 => "Meh.",
1 => "Yay!",
_ => throw new ArgumentOutOfRangeException(),
};
int size = isMobile ? 50 : 100;
<div class="card">
<div>
<img class="cardIcon slotCardIcon" src="@ServerConfiguration.Instance.ExternalUrl/gameAssets/@faceHash" alt="@faceAlt" title="@faceAlt" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
</div>
<div class="cardStats">
<h3 style="margin-bottom: 5px;">@review.Reviewer?.Username</h3>
@if (review.Deleted)
{
if (review.DeletedBy == DeletedBy.LevelAuthor)
{
<p>
<i>This review has been deleted by the level author.</i>
</p>
}
else
{
<p>
<i>This review has been deleted by a moderator.</i>
</p>
}
}
else
{
@if (review.Labels.Length > 1)
{
@foreach (string reviewLabel in review.Labels)
{
<div class="ui blue label">@reviewLabel.Replace("LABEL_", "")</div>
}
}
@if (string.IsNullOrWhiteSpace(review.Text))
{
<p>
<i>This review contains no text.</i>
</p>
}
else
{
{
<p>@HttpUtility.HtmlDecode(review.Text)</p>
}
}
}
</div>
</div>
@if (i != Model.Reviews.Count - 1)
{
<div class="ui divider"></div>
}
}
</div>
</div>
</div>
@if (Model.User != null && Model.User.IsAdmin)
{
<div class="ui yellow segment">
<h2>Admin Options</h2>
@if (Model.Slot?.TeamPick ?? false)
{
<a href="/admin/slot/@Model.Slot.SlotId/removeTeamPick">
<div class="ui pink button">
<i class="ribbon icon"></i>
<span>Remove Team Pick</span>
</div>
</a>
}
else
{
<a href="/admin/slot/@Model.Slot?.SlotId/teamPick">
<div class="ui pink button">
<i class="ribbon icon"></i>
<span>Team Pick</span>
</div>
</a>
}
<a href="/admin/slot/@Model.Slot?.SlotId/delete">
<div class="ui red button">
<i class="trash icon"></i>
<span>Delete</span>
</div>
</a>
</div>
}

View file

@ -0,0 +1,70 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Reviews;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class SlotPage : BaseLayout
{
public List<Comment> Comments = new();
public List<Review> Reviews = new();
public readonly bool CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled;
public readonly bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled;
public Slot? Slot;
public SlotPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet([FromRoute] int id)
{
Slot? slot = await this.Database.Slots.Include(s => s.Creator).FirstOrDefaultAsync(s => s.SlotId == id);
if (slot == null) return this.NotFound();
this.Slot = slot;
if (this.CommentsEnabled)
{
this.Comments = await this.Database.Comments.Include(p => p.Poster)
.OrderByDescending(p => p.Timestamp)
.Where(c => c.TargetId == id && c.Type == CommentType.Level)
.Take(50)
.ToListAsync();
}
else
{
this.Comments = new List<Comment>();
}
if (this.ReviewsEnabled)
{
this.Reviews = await this.Database.Reviews.Include(r => r.Reviewer)
.OrderByDescending(r => r.ThumbsUp - r.ThumbsDown)
.ThenByDescending(r => r.Timestamp)
.Where(r => r.SlotId == id)
.Take(50)
.ToListAsync();
}
else
{
this.Reviews = new List<Review>();
}
if (this.User == null) return this.Page();
foreach (Comment c in this.Comments)
{
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r => r.UserId == this.User.UserId && r.TargetId == c.CommentId);
if (reaction != null) c.YourThumb = reaction.Rating;
}
return this.Page();
}
}

View file

@ -0,0 +1,51 @@
@page "/slots/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Levels
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotsPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Levels";
}
<p>There are @Model.SlotCount total levels!</p>
<form action="/slots/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search levels..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
@foreach (Slot slot in Model.Slots)
{
bool isMobile = Model.Request.IsMobile();
<div class="ui segment">
@await Html.PartialAsync("Partials/SlotCardPartial", slot, new ViewDataDictionary(ViewData)
{
{
"User", Model.User
},
{
"CallbackUrl", $"~/slots/{Model.PageNumber}"
},
{
"ShowLink", true
},
{
"IsMobile", isMobile
},
})
</div>
}
@if (Model.PageNumber != 0)
{
<a href="/slots/@(Model.PageNumber - 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
}
@(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1)
{
<a href="/slots/@(Model.PageNumber + 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
}

View file

@ -0,0 +1,79 @@
#nullable enable
using System.Text;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class SlotsPage : BaseLayout
{
public int PageAmount;
public int PageNumber;
public int SlotCount;
public List<Slot> Slots = new();
public string? SearchValue;
public SlotsPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{
if (string.IsNullOrWhiteSpace(name)) name = "";
string? targetAuthor = null;
GameVersion? targetGame = null;
StringBuilder finalSearch = new();
foreach (string part in name.Split(" "))
{
if (part.Contains("by:"))
{
targetAuthor = part.Replace("by:", "");
}
else if (part.Contains("game:"))
{
if (part.Contains('1')) targetGame = GameVersion.LittleBigPlanet1;
else if (part.Contains('2')) targetGame = GameVersion.LittleBigPlanet2;
else if (part.Contains('3')) targetGame = GameVersion.LittleBigPlanet3;
else if (part.Contains('v')) targetGame = GameVersion.LittleBigPlanetVita;
}
else
{
finalSearch.Append(part);
}
}
this.SearchValue = name.Trim();
this.SlotCount = await this.Database.Slots.Include(p => p.Creator)
.Where(p => p.Name.Contains(finalSearch.ToString()))
.Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower())))
.Where(p => targetGame == null || p.GameVersion == targetGame)
.CountAsync();
this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.SlotCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Slots = await this.Database.Slots.Include(p => p.Creator)
.Where(p => p.Name.Contains(finalSearch.ToString()))
.Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower())))
.Where(p => targetGame == null || p.GameVersion == targetGame)
.OrderByDescending(p => p.FirstUploaded)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)
.ToListAsync();
return this.Page();
}
}

View file

@ -0,0 +1,144 @@
@page "/user/{userId:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage
@{
Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false;
Model.Title = Model.ProfileUser!.Username + "'s user page";
Model.Description = Model.ProfileUser!.Biography;
}
@if (Model.ProfileUser.Banned)
{
<div class="ui inverted red segment">
<h2>User is currently banned!</h2>
@if (Model.User != null && Model.User.IsAdmin)
{
<b>Reason:</b>
<span>"@Model.ProfileUser.BannedReason"</span>
<p>
<i>Note: Only you and other admins may view the ban reason.</i>
</p>
<a class="ui inverted button" href="/admin/user/@Model.ProfileUser.UserId/unban">
<i class="ban icon"></i>
<span>Unban User</span>
</a>
}
else
{
<p>For shame...</p>
}
</div>
}
<div class="ui grid">
<div class="eight wide column">
@await Html.PartialAsync("Partials/UserCardPartial", Model.ProfileUser, new ViewDataDictionary(ViewData)
{
{
"ShowLink", false
},
{
"IsMobile", Model.Request.IsMobile()
},
})
</div>
<div class="eight wide right aligned column">
<br>
@if (Model.ProfileUser != Model.User && Model.User != null)
{
if (!Model.IsProfileUserHearted)
{
<a class="ui pink button" href="/user/@Model.ProfileUser.UserId/heart">
<i class="heart icon"></i>
<span>Heart</span>
</a>
}
else
{
<a class="ui pink button" href="/user/@Model.ProfileUser.UserId/unheart">
<i class="heart broken icon"></i>
<span>Unheart</span>
</a>
}
}
@if (Model.ProfileUser == Model.User)
{
<a class="ui blue button" href="/passwordReset">
<i class="key icon"></i>
<span>Reset Password</span>
</a>
}
</div>
<div class="eight wide column">
<div class="ui blue segment">
<h2>Biography</h2>
@if (string.IsNullOrWhiteSpace(Model.ProfileUser.Biography))
{
<p>@Model.ProfileUser.Username hasn't introduced themselves yet</p>
}
else
{
<p>@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p>
}
</div>
</div>
<div class="eight wide column">
<div class="ui red segment">
<h2>Recent Activity</h2>
<p>Coming soon!</p>
</div>
</div>
</div>
@if (Model.Photos != null && Model.Photos.Count != 0)
{
<div class="ui purple segment">
<h2>Most recent photos</h2>
<div class="ui center aligned grid">
@foreach (Photo photo in Model.Photos)
{
<div class="eight wide column">
@await Html.PartialAsync("Partials/PhotoPartial", photo)
</div>
}
</div>
</div>
}
@await Html.PartialAsync("Partials/CommentsPartial")
@if (Model.User != null && Model.User.IsAdmin)
{
<div class="ui yellow segment">
<h2>Admin Options</h2>
@if (!Model.ProfileUser.Banned)
{
<div>
<a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/ban">
<i class="ban icon"></i>
<span>Ban User</span>
</a>
</div>
<div class="ui fitted hidden divider"></div>
}
<div>
<a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/wipePlanets">
<i class="trash alternate icon"></i>
<span>Wipe user's earth decorations</span>
</a>
</div>
<div class="ui fitted hidden divider"></div>
@await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.ProfileUser)
</div>
}

View file

@ -0,0 +1,58 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class UserPage : BaseLayout
{
public List<Comment>? Comments;
public bool CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled;
public bool IsProfileUserHearted;
public List<Photo>? Photos;
public User? ProfileUser;
public UserPage(Database database) : base(database)
{}
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();
this.Photos = await this.Database.Photos.OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync();
if (this.CommentsEnabled)
{
this.Comments = await this.Database.Comments.Include(p => p.Poster)
.OrderByDescending(p => p.Timestamp)
.Where(p => p.TargetId == userId && p.Type == CommentType.Profile)
.Take(50)
.ToListAsync();
}
else
{
this.Comments = new List<Comment>();
}
if (this.User == null) return this.Page();
foreach (Comment c in this.Comments)
{
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r => r.UserId == this.User.UserId && r.TargetId == c.CommentId);
if (reaction != null) c.YourThumb = reaction.Rating;
}
this.IsProfileUserHearted = await this.Database.HeartedProfiles.FirstOrDefaultAsync
(u => u.UserId == this.User.UserId && u.HeartedUserId == this.ProfileUser.UserId) !=
null;
return this.Page();
}
}

View file

@ -0,0 +1,46 @@
@page "/users/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UsersPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Users";
}
<p>There are @Model.UserCount total users.</p>
<form action="/users/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search users..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
@foreach (User user in Model.Users)
{
bool isMobile = Model.Request.IsMobile();
<div class="ui segment">
@await Html.PartialAsync("Partials/UserCardPartial", user, new ViewDataDictionary(ViewData)
{
{
"ShowLink", true
},
{
"IsMobile", isMobile
},
})
</div>
}
@if (Model.PageNumber != 0)
{
<a href="/users/@(Model.PageNumber - 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
}
@(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1)
{
<a href="/users/@(Model.PageNumber + 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
}

View file

@ -0,0 +1,47 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class UsersPage : BaseLayout
{
public int PageAmount;
public int PageNumber;
public int UserCount;
public List<User> Users = new();
public string? SearchValue;
public UsersPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{
if (string.IsNullOrWhiteSpace(name)) name = "";
this.SearchValue = name.Replace(" ", string.Empty);
this.UserCount = await this.Database.Users.CountAsync(u => !u.Banned && u.Username.Contains(this.SearchValue));
this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.UserCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Users = await this.Database.Users.Where(u => !u.Banned && u.Username.Contains(this.SearchValue))
.OrderByDescending(b => b.UserId)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)
.ToListAsync();
return this.Page();
}
}